From 267b71de94f20520de0e733a27bf4ea13b76e81f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 28 Dec 2022 18:39:10 -0800 Subject: [PATCH 001/432] changes( .g_dgl -> private( ._kg_dgl)) --- graphistry/embed_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index 10798d70a3..42743d7537 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -153,13 +153,13 @@ def _build_graph(self, res) -> Plottable: g_dgl.edata[dgl.ETYPE] = r g_dgl.edata["norm"] = dgl.norm_by_dst(g_dgl).unsqueeze(-1) - res.g_dgl = g_dgl + res._kg_dgl = g_dgl return res def _init_model(self, res, batch_size:int, sample_size:int, num_steps:int, device): _, _, _, _, GraphDataLoader, HeteroEmbed, _, _ = lazy_embed_import_dep() - g_iter = SubgraphIterator(res.g_dgl, sample_size, num_steps) + g_iter = SubgraphIterator(res._kg_dgl, sample_size, num_steps) g_dataloader = GraphDataLoader( g_iter, batch_size=batch_size, collate_fn=lambda x: x[0] ) @@ -209,7 +209,7 @@ def _train_embedding(self, res, epochs:int, batch_size:int, lr:float, sample_siz ) model.eval() - res._kg_embeddings = model(res.g_dgl.to(device)).detach() + res._kg_embeddings = model(res._kg_dgl.to(device)).detach() res._embed_model = model if res._eval_flag and res._train_idx is not None: score = res._eval(threshold=0.5) @@ -222,7 +222,7 @@ def _train_embedding(self, res, epochs:int, batch_size:int, lr:float, sample_siz @property def _gcn_node_embeddings(self): _, torch, _, _, _, _, _, _ = lazy_embed_import_dep() - g_dgl = self.g_dgl.to(self._device) + g_dgl = self._kg_dgl.to(self._device) em = self._embed_model(g_dgl).detach() torch.cuda.empty_cache() return em From 4adc90e64d89c2df71c07533776bfd47c12b4601 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 28 Dec 2022 18:43:01 -0800 Subject: [PATCH 002/432] adds clean up sections for jack donations --- demos/ai/OSINT/jack-donations.ipynb | 2644 +++++++++++++++++++++++---- 1 file changed, 2254 insertions(+), 390 deletions(-) diff --git a/demos/ai/OSINT/jack-donations.ipynb b/demos/ai/OSINT/jack-donations.ipynb index 7abddb9f20..1f02d64a2d 100644 --- a/demos/ai/OSINT/jack-donations.ipynb +++ b/demos/ai/OSINT/jack-donations.ipynb @@ -6,30 +6,39 @@ "metadata": {}, "source": [ "________________\n", - "# Jack's money went here. \n", + "# Jack's Money Went Here \n", "\n", - "## Where is twitter likely to lean more and less now that he's leaving? Where will there be matching donations?\n", + "Where is twitter likely to lean more and less now that he's leaving? Where will there be matching donations?\n", "\n", - "Jack Dorsey is pledging over 466 million dollars and wants matching donations. His rational is simple -- billionaires can spare a tithe to help communities and people, and compounded over a few hundred of his closest friends, have a tremendous impact. \n", + "Jack Dorsey is pledging over 466 million dollars and wants matching donations. His rational is simple -- billionaires can spare a tithe to help communities and people, and compounded over a few hundred of his closest friends, have a tremendous impact. What edifice could be built with donations to these entities? What do their service offerings look like when seen as a whole? What are their moving parts?\n", "\n", "This dataset is based off of the tweet https://twitter.com/jack/status/1247616214769086465 which lists pledged organizations and their donation. \n", - "__________________________\n", - "### We will learn how to quickly data science this dataset. We will select feature representations and visualize the resulting graph using UMAP.\n", + "__________________________________________________________________\n", + "\n", + "We will learn how to quickly data science this dataset. We will select feature representations and visualize the resulting graph using UMAP.\n", "\n", "Featurization is the foundation of datascience. Likewise, Graph Thinking requires edges between nodes. Many times the data we have from databases/dataframes is tabular and row like -- with no incling of an edge table. This does *not* have to be an impediment for *Graph Thinking and materialization of datascience workflows*. \n", "\n", "UMAP is a powerful tool that projects complex, heterogeneous data coming from potentially many different distributions, down to lower dimensional embeddings and projections. The embedding estimates similarity between the rows, or nodes of the data, and thus forms a graph. \n", "\n", "Standardizing a feature set across the databases used in every modern company and then sending it to UMAP serves as a powerful graph generation tool. \n", - "____________________________\n", - "Here we demonstrate how to Featurize and use UMAP to generate implicit graphs. The features may then be used in subsequent modeling using your favorite libraries -- sklearn, tensorflow, pytorch[, geometric, lightening, ...], cuGraph, DGL, etc. We demonstrate 4 featurization methods -- (latent embeddings, transformer embeddings, ngrams embeddings, one-hot encodings) that may be mixed and used to make different features for different columns, automatically. \n", + "__________________________________________________________________\n", + "\n", + "Here we demonstrate how to Featurize and use UMAP to generate implicit graphs. The features may then be used in subsequent modeling using your favorite libraries -- sklearn, tensorflow, pytorch[, geometric, lightening, ...], cuGraph, DGL, etc. We demonstrate 4 featurization methods -- \n", + "\n", + "* latent embeddings, \n", + "* transformer embeddings, \n", + "* ngrams embeddings, \n", + "* one-hot encodings\n", "\n", - "Furthermore, when we `g.plot()` the results, it is layed out according to the 2-dimensional UMAP projection of the data -- nearness in that projection represents nearness in the resulting features. We will test this empiracally using the different featurization methods for textual, numeric and categorical data. " + "that may be mixed and used to make different features for different columns, automatically. \n", + "\n", + "Furthermore, when we `g.plot()` the results, it is layed out according to the 2-dimensional UMAP projection of the data -- nearness in that projection represents nearness in the resulting features. We will test this empirically using the different featurization methods for textual, numeric and categorical data. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "a069ef73", "metadata": {}, "outputs": [], @@ -39,17 +48,7 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "97443b1c", - "metadata": {}, - "outputs": [], - "source": [ - "# cd .." - ] - }, - { - "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "b7de987a", "metadata": {}, "outputs": [], @@ -66,7 +65,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "461a22ec", "metadata": {}, "outputs": [], @@ -76,7 +75,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, + "id": "950f6310", + "metadata": {}, + "outputs": [], + "source": [ + "RENDER=False # set to True for inline Graphistry Plots" + ] + }, + { + "cell_type": "code", + "execution_count": 5, "id": "90875a39", "metadata": {}, "outputs": [], @@ -89,32 +98,177 @@ "id": "9acb2823", "metadata": {}, "source": [ - "## Data cleaning\n", + "## Data loading & cleaning\n", "We already added the dataset from the twitter link, downloading a copy (as of May 2022) from the google drive. We need to remove the first few rows to make a valid dataframe. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "0ffe9b64", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
DateAmountCategoryGranteeTwitterLinkWhy?
03/21/2022$2,000,000Social JusticeREFORM Alliance@REFORMhttps://reformalliance.comREFORM Alliance is committed to transforming t...
13/10/2022$1,000,000Crisis ReliefWorld Central Kitchen@WCKitchenhttps://wck.org/World Central Kitchen is serving thousands of ...
23/10/2022$1,000,000Crisis ReliefSunflower of Peace@SunflowerFundhttps://www.sunflowerofpeace.comSunflower of Peace is providing medical and hu...
33/10/2022$1,000,000Crisis ReliefRazom, Inc.@razomforukrainehttps://razomforukraine.orgRazom is supporting Ukrainian people in their ...
43/10/2022$1,000,000Crisis ReliefNova Ukraine@novaukrainehttps://novaukraine.orgNova Ukraine, a Bay Area-based humanitarian no...
\n", + "
" + ], + "text/plain": [ + " Date Amount Category Grantee \\\n", + "0 3/21/2022 $2,000,000 Social Justice REFORM Alliance \n", + "1 3/10/2022 $1,000,000 Crisis Relief World Central Kitchen \n", + "2 3/10/2022 $1,000,000 Crisis Relief Sunflower of Peace \n", + "3 3/10/2022 $1,000,000 Crisis Relief Razom, Inc. \n", + "4 3/10/2022 $1,000,000 Crisis Relief Nova Ukraine \n", + "\n", + " Twitter Link \\\n", + "0 @REFORM https://reformalliance.com \n", + "1 @WCKitchen https://wck.org/ \n", + "2 @SunflowerFund https://www.sunflowerofpeace.com \n", + "3 @razomforukraine https://razomforukraine.org \n", + "4 @novaukraine https://novaukraine.org \n", + "\n", + " Why? \n", + "0 REFORM Alliance is committed to transforming t... \n", + "1 World Central Kitchen is serving thousands of ... \n", + "2 Sunflower of Peace is providing medical and hu... \n", + "3 Razom is supporting Ukrainian people in their ... \n", + "4 Nova Ukraine, a Bay Area-based humanitarian no... " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df = pd.read_csv('https://gist.githubusercontent.com/silkspace/f8d7b8f279a5ffbd710c301fc402ec43/raw/95a722f5c65812322eaf085c1123b58d3ec3da3a/jack_donations.csv')\n", "df = df.fillna('')\n", "columns = df.iloc[3].values \n", "ndf = pd.DataFrame(df[4:].values, columns=columns)\n", - "ndf" + "ndf.head()" + ] + }, + { + "cell_type": "markdown", + "id": "50f59d83", + "metadata": {}, + "source": [ + "Notice that the Category labels are mixed and interwoven. \n", + "We will show how to standardize it without having to do data cleaning or mapping" ] }, { "cell_type": "code", - "execution_count": null, - "id": "e52b4e5d", + "execution_count": 7, + "id": "ac1b493e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "array(['Social Justice', 'Crisis Relief',\n", + " 'COVID-19, Girls Health & Education',\n", + " 'Social Justice, Girls Health & Education', 'COVID-19',\n", + " 'Social Justice, COVID-19', 'Girls Health & Education',\n", + " 'UBI, Social Justice', 'Girls Health & Education, COVID-19',\n", + " 'COVID-19, Social Justice', 'UBI',\n", + " 'COVID-19, Social Justice, Girls Health & Education',\n", + " 'Girls Health & Education; COVID-19', 'COVID-19; Social Justice',\n", + " 'Girls Health & Education; Social Justice',\n", + " 'COVID-19; Girls Health & Education', 'UBI; COVID-19',\n", + " 'COVID-19 & Social Justice',\n", + " 'Social Justice, UBI, Girls Health & Education', 'COVID-19, UBI',\n", + " \"Where it's needed most\", 'COVID-19 '], dtype=object)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "ndf.Category.unique()" + "ndf.Category.unique() # seems like there are 4-6 topics here" ] }, { @@ -122,24 +276,40 @@ "id": "b454348e", "metadata": {}, "source": [ - "# Create the Graph\n", + "# Featurize\n", "\n", - "We will use `g.umap` to featurize and create edges. The details of how UMAP is able to create edges between rows in the data is beyond the scope of this tutorial, however, suffic it to say, it is automatically inferring a network of related entities based off of their column features. \n", + "We will use `g.umap` to featurize and create edges. The details of how UMAP is able to create edges between rows in the data is beyond the scope of this tutorial, however, suffic it to say, it is automatically inferring a network of related entities based off their column features. \n", "\n", "Here is the dataset as graph, \n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "c986ff93", - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "g = graphistry.nodes(ndf).bind(point_title='Category').umap()\n", - "g.plot() # fly around the clusters and click on nodes and edges. " + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (285, 0) in UMAP fit, as it is not one dimensionalOMP: Info #273: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" + ] + }, + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=ae47f85d8eaa4edfa6a3bc0c1124e313&type=arrow&viztoken=41ef5acc-41e3-49e9-99ac-2a57158b31c8&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672009074&info=true&play=0'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g = graphistry.nodes(ndf).umap()\n", + "g.bind(point_title='Grantee').plot(render=RENDER) # fly around the clusters and click on nodes and edges. " ] }, { @@ -147,7 +317,7 @@ "id": "255c8496", "metadata": {}, "source": [ - "## The above featurized every column over the entire datase. Exploring the nodes and their nearest neighbors indeed clusters similar rows -- all in two lines of code!" + "The above featurized every column over the entire datase. Exploring the nodes and their nearest neighbors indeed clusters similar rows -- all in two lines of code!" ] }, { @@ -155,17 +325,39 @@ "id": "d76e628d", "metadata": {}, "source": [ - "# Some light analysis and enrichment \n", + "## Light analysis and enrichment \n", "\n", "Lets convert Amount column into numeric, and then see who is getting what by category and grantee." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "a8ced06c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "0 $2,000,000\n", + "1 $1,000,000\n", + "2 $1,000,000\n", + "3 $1,000,000\n", + "4 $1,000,000\n", + " ... \n", + "280 $13,333\n", + "281 $2,000,000\n", + "282 $1,000,000\n", + "283 $2,100,000\n", + "284 $100,000\n", + "Name: Amount , Length: 285, dtype: object" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "#ndf.columns\n", "ndf[' Amount ']" @@ -173,7 +365,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "8077b2d0", "metadata": {}, "outputs": [], @@ -193,10 +385,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "b0e0c683", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "0 2000000.0\n", + "1 1000000.0\n", + "2 1000000.0\n", + "3 1000000.0\n", + "4 1000000.0\n", + " ... \n", + "280 13333.0\n", + "281 2000000.0\n", + "282 1000000.0\n", + "283 2100000.0\n", + "284 100000.0\n", + "Name: $ amount, Length: 285, dtype: float64" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "ndf['$ amount']" ] @@ -206,17 +420,51 @@ "id": "ac95f782", "metadata": {}, "source": [ - "## Many of these categories are not distinct. But due to data coming in with different notation, it seems distinct. \n", + "Many of these categories are not distinct. But due to data coming in with different notation, it seems distinct. \n", "\n", "We will show in the next section how to deal with this by using the graphistry pipeline to convert the `Category` into a latent target that organizes the labels.\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "6e4fcbaf", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Category\n", + "COVID-19 $153,882,590.0\n", + "COVID-19 $85,019,328.0\n", + "COVID-19 & Social Justice $505,468.0\n", + "COVID-19, Girls Health & Education $4,265,000.0\n", + "COVID-19, Social Justice $1,800,000.0\n", + "COVID-19, Social Justice, Girls Health & Education $250,000.0\n", + "COVID-19, UBI $8,000,000.0\n", + "COVID-19; Girls Health & Education $9,920,000.0\n", + "COVID-19; Social Justice $5,090,080.0\n", + "Crisis Relief $7,500,000.0\n", + "Girls Health & Education $30,300,000.0\n", + "Girls Health & Education, COVID-19 $1,250,000.0\n", + "Girls Health & Education; COVID-19 $12,000,000.0\n", + "Girls Health & Education; Social Justice $2,500,000.0\n", + "Social Justice $84,119,845.0\n", + "Social Justice, COVID-19 $300,000.0\n", + "Social Justice, Girls Health & Education $9,934,000.0\n", + "Social Justice, UBI, Girls Health & Education $1,100,000.0\n", + "UBI $10,210,000.0\n", + "UBI, Social Justice $1,000,000.0\n", + "UBI; COVID-19 $35,000,000.0\n", + "Where it's needed most $3,000,000.0\n", + "Name: $ amount, dtype: object" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "current_funding_by_category = ndf.groupby('Category')['$ amount'].sum()\n", "current_funding_by_category.map(lambda x: '${:3,}'.format(x))" @@ -224,21 +472,66 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "59b71456", "metadata": {}, - "outputs": [], - "source": [ - "fig = plt.figure(figsize=(15,7))\n", - "current_funding_by_category.plot(kind='bar', rot=52)" + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "fig = plt.figure(figsize=(10,5))\n", + "current_funding_by_category.plot(kind='bar', rot=82)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "382780f5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Grantee\n", + "Vital Strategies: Resolve To Save Lives $38,000,000.0\n", + "CORE: Community Organized Relief Effort $30,000,000.0\n", + "Clara Lionel Foundation $28,877,000.0\n", + "Reinvent Stockton Foundation $18,000,000.0\n", + "CARE $16,000,000.0\n", + "Give2SF $15,000,000.0\n", + "Open Research Lab Income Project $15,000,000.0\n", + "REFORM Alliance $12,000,000.0\n", + "World Central Kitchen $11,585,500.0\n", + "Indiana University Foundation $10,025,000.0\n", + "Name: $ amount, dtype: object" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "grantees = ndf.groupby('Grantee')['$ amount'].sum()\n", "grants_sorted = grantees.sort_values()\n", @@ -248,78 +541,123 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "d7d0ff87", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# largest grants\n", - "fig = plt.figure(figsize=(15,7))\n", + "fig = plt.figure(figsize=(10,5))\n", "ax= plt.subplot()\n", - "# ax.set_xticks(range(len(label_list)))\n", - "# ax.set_xticklabels(label_list, rotation=19)\n", "res = grants_sorted[-10:]\n", "\n", - "res.plot(kind='bar', rot=52)" + "res.plot(kind='bar', rot=49)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "14330bfc", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# smallest grants\n", - "fig = plt.figure(figsize=(15,7))\n", + "fig = plt.figure(figsize=(10,5))\n", "ax= plt.subplot()\n", - "# ax.set_xticks(range(len(label_list)))\n", - "# ax.set_xticklabels(label_list, rotation=19)\n", "res = grants_sorted[:10]\n", "\n", - "res.plot(kind='bar', rot = 52)" + "res.plot(kind='bar', rot = 29)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "2ad231a8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'Total Pledged $466,946,311.0'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "'Total Pledged ${:3,}'.format(current_funding_by_category.sum())" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "217026ee", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'Total Pledged $466,946,311.0'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# and this should be the same too\n", "'Total Pledged ${:3,}'.format(grantees.sum())" ] }, - { - "cell_type": "markdown", - "id": "50f59d83", - "metadata": {}, - "source": [ - "## Notice that the Category labels are mixed and interwoven \n", - "We will show how judicious choice of parameters can standardize it without having to do data cleaning or mapping" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ac1b493e", - "metadata": {}, - "outputs": [], - "source": [ - "ndf.Category.unique() # seems like there are 4-6 topics here" - ] - }, { "cell_type": "markdown", "id": "b0565e61", @@ -339,31 +677,12 @@ "____________________________" ] }, - { - "cell_type": "markdown", - "id": "22c6b5c7", - "metadata": {}, - "source": [ - "In the following, we concentrate on the textual `Why?` column as it describes the row/entity in question. Further, we select `y='Category'` as a target variable, and will encode it using a Topic Model as well as standard One-Hot-Encoding.\n", - "\n", - "\n", - "In the following we will show how to encode textual and categorical data using \n", - "\n", - "1) Topic Models\n", - "\n", - "2) Sentence Transformers\n", - "\n", - "3) Ngrams \n", - "\n", - "And see the resulting graphs. We will use the Topic label generated by `y='Category'` to color the graphs, as well as `$ amount` \n" - ] - }, { "cell_type": "markdown", "id": "2255d688", "metadata": {}, "source": [ - "# Topic Model (latent-) features" + "## Topic Model" ] }, { @@ -374,208 +693,609 @@ "We encode the data using Topic Models. This turns the textual features into latent vectors. Likewise, we can do the same for the target data. \n", "\n", "\n", - "Notice that we set `cardinality_threshold_target` very low and `min_words` very high to force featurization as topic models rather than one-hot or topic encoded;\n", + "Notice that we set `cardinality_threshold_target` very low and `min_words` very high to force featurization as topic models rather than one-hot or sbert embeddings;\n", + "\n", "1) encode target using a topic model, and set `n_topics_target` as the dimension of the latent target factorization. This choice is based on the fact that there are really only 4-6 or so distinct categories across the labels, but they are mixed together. The labels are in fact Hierarchical categories. We can use the topic model to find the lowest moments of this Hierarchical classification in the distributional sense. \n", "\n", - "2) and like\n", - "wise for the features `Why?`, and set `n_topics` as the dimension of the latent feature factorization." + "2) Encode the `Why?` column as a `n_topics` -dimensional factorization." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "71ad1fe5", "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (285, 4) in UMAP fit, as it is not one dimensional" + ] + } + ], "source": [ "g = graphistry.nodes(ndf).bind(point_title='Category')\n", "\n", "g2 = g.umap(X=['Why?'], y = ['Category'], \n", - " min_words=50000, # encode as topic model by setting min_words high\n", + " min_words=1e9, # encode as topic model by setting min_words high\n", + " n_topics=42, # latent embedding size of `Why`\n", " n_topics_target=4, # turn categories into a 4dim vector of regressive targets\n", - " n_topics=21, # latent embedding size \n", - " cardinality_threshold_target=2, # make sure that we throw targets into topic model over targets\n", + " cardinality_threshold_target=2, # force topic model over target `Category`\n", + " use_scaler=None,\n", + " use_scaler_target=None\n", " ) " ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "8cb9e6cd", - "metadata": {}, - "outputs": [], - "source": [ - "g2._node_encoder.label_encoder" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b650ef59", - "metadata": {}, - "outputs": [], - "source": [ - "# pretend you have a minibatch of new data -- transform under the fit from the above\n", - "new_df, new_y = ndf.sample(5), ndf.sample(5) # pd.DataFrame({'Category': ndf['Category'].sample(5)})\n", - "a, b = g2.transform(new_df, new_y, kind='nodes')\n", - "a" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dc99ac85", - "metadata": {}, - "outputs": [], - "source": [ - "b" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5076e613", - "metadata": {}, - "outputs": [], - "source": [ - "plt.figure()\n", - "plt.imshow(g2._node_target, aspect='auto', cmap='hot')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d95f4e1b", - "metadata": {}, - "outputs": [], - "source": [ - "g2._node_encoder.label_encoder" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "577b32ab", - "metadata": {}, - "outputs": [], - "source": [ - "g2._node_encoder.y.plot(kind='bar', figsize=(15,7)) # easier to see than before" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cd1e7ffc", - "metadata": {}, - "outputs": [], - "source": [ - "# likewise you can play with how many edges to include using,\n", - "g2 = g2.filter_weighted_edges(scale=0.25) # lower positive values of scale mean closer similarity \n" - ] - }, { "cell_type": "markdown", "id": "93e6ae81", "metadata": {}, "source": [ - "## We have featurized the data and also run UMAP, which projects the features into a 2-dimensional space while generating edges.\n", - "\n", "Plotting the result shows the similarity between entities. It does a good job overall at clustering by topic. Click in and check out some nearby nodes. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "0cdf2370", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=039654b935e6476dbe4a232e28609ae1&type=arrow&viztoken=c8fd440d-2b3a-4fbf-868c-f6c93e141154&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672009084&info=true&play=0'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g2.plot()" + "g2.bind(point_title='Grantee').plot(render=RENDER)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "7a970c06", + "execution_count": 21, + "id": "b650ef59", "metadata": {}, - "outputs": [], - "source": [ - "X = g2._node_features \n", - "X" + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "'SuperVectorizer' object has no attribute 'get_feature_names_in''SuperVectorizer' object has no attribute 'get_feature_names_in'" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Why?: relationships, relationship, trevortextWhy?: thousands, kitchen, kitchensWhy?: multiracial, language, analysisWhy?: foundation, partnership, barbadosWhy?: humanitarian, distributing, distributedWhy?: vulnerable, coordinated, outbreakWhy?: marginalized, globalgiving, emergencyWhy?: sustainable, livelihoods, livelihoodWhy?: healthcare, results, childrenWhy?: movementhub, strengthening, snapshots...Why?: washingtonians, incarceration, restorationWhy?: leadership, confidence, confidentlyWhy?: entrepreneurs, entrepreneurship, tomorrowWhy?: coronavirus, families, primarilyWhy?: california, disproportionately, lgbtiqWhy?: individuals, undiagnosed, disabilitiesWhy?: engineering, criminal, completeWhy?: disparities, nonprofit, socioeconomicWhy?: simultaneously, immediate, richmondWhy?: constitution, employers, nationwide
1030.1402770.1600640.1931840.1548806.8990200.0887430.1616050.1260510.084490143.538295...0.1575010.1233250.1076980.17914319.0654360.1214030.1595800.1324810.2476540.115000
620.2136410.38310415.8832800.1558440.2338540.1972960.2214450.2696280.2038880.218177...68.43184619.1420020.4399620.3541340.3268610.5306093.1059930.3180490.378531234.320552
110.1560880.2123510.2416470.1632090.1522430.1922230.2301880.2146024.3081810.157184...276.3394970.6211900.1706340.1333030.1769930.1870970.2653650.1136090.24968926.072265
2440.1933660.1533920.1227790.1264480.1139650.2102250.2016950.1605180.1611350.175002...0.1297570.1461170.15929330.2629570.10615730.7819270.1228150.11921467.8701430.139393
240.1625940.1303150.1578150.2585911.7916016.2796980.54100645.6672350.22850150.486164...0.1607950.2305750.1673350.60355224.34663632.9227700.2992010.4051750.23685141.980704
\n", + "

5 rows × 42 columns

\n", + "
" + ], + "text/plain": [ + " Why?: relationships, relationship, trevortext \\\n", + "103 0.140277 \n", + "62 0.213641 \n", + "11 0.156088 \n", + "244 0.193366 \n", + "24 0.162594 \n", + "\n", + " Why?: thousands, kitchen, kitchens \\\n", + "103 0.160064 \n", + "62 0.383104 \n", + "11 0.212351 \n", + "244 0.153392 \n", + "24 0.130315 \n", + "\n", + " Why?: multiracial, language, analysis \\\n", + "103 0.193184 \n", + "62 15.883280 \n", + "11 0.241647 \n", + "244 0.122779 \n", + "24 0.157815 \n", + "\n", + " Why?: foundation, partnership, barbados \\\n", + "103 0.154880 \n", + "62 0.155844 \n", + "11 0.163209 \n", + "244 0.126448 \n", + "24 0.258591 \n", + "\n", + " Why?: humanitarian, distributing, distributed \\\n", + "103 6.899020 \n", + "62 0.233854 \n", + "11 0.152243 \n", + "244 0.113965 \n", + "24 1.791601 \n", + "\n", + " Why?: vulnerable, coordinated, outbreak \\\n", + "103 0.088743 \n", + "62 0.197296 \n", + "11 0.192223 \n", + "244 0.210225 \n", + "24 6.279698 \n", + "\n", + " Why?: marginalized, globalgiving, emergency \\\n", + "103 0.161605 \n", + "62 0.221445 \n", + "11 0.230188 \n", + "244 0.201695 \n", + "24 0.541006 \n", + "\n", + " Why?: sustainable, livelihoods, livelihood \\\n", + "103 0.126051 \n", + "62 0.269628 \n", + "11 0.214602 \n", + "244 0.160518 \n", + "24 45.667235 \n", + "\n", + " Why?: healthcare, results, children \\\n", + "103 0.084490 \n", + "62 0.203888 \n", + "11 4.308181 \n", + "244 0.161135 \n", + "24 0.228501 \n", + "\n", + " Why?: movementhub, strengthening, snapshots ... \\\n", + "103 143.538295 ... \n", + "62 0.218177 ... \n", + "11 0.157184 ... \n", + "244 0.175002 ... \n", + "24 50.486164 ... \n", + "\n", + " Why?: washingtonians, incarceration, restoration \\\n", + "103 0.157501 \n", + "62 68.431846 \n", + "11 276.339497 \n", + "244 0.129757 \n", + "24 0.160795 \n", + "\n", + " Why?: leadership, confidence, confidently \\\n", + "103 0.123325 \n", + "62 19.142002 \n", + "11 0.621190 \n", + "244 0.146117 \n", + "24 0.230575 \n", + "\n", + " Why?: entrepreneurs, entrepreneurship, tomorrow \\\n", + "103 0.107698 \n", + "62 0.439962 \n", + "11 0.170634 \n", + "244 0.159293 \n", + "24 0.167335 \n", + "\n", + " Why?: coronavirus, families, primarily \\\n", + "103 0.179143 \n", + "62 0.354134 \n", + "11 0.133303 \n", + "244 30.262957 \n", + "24 0.603552 \n", + "\n", + " Why?: california, disproportionately, lgbtiq \\\n", + "103 19.065436 \n", + "62 0.326861 \n", + "11 0.176993 \n", + "244 0.106157 \n", + "24 24.346636 \n", + "\n", + " Why?: individuals, undiagnosed, disabilities \\\n", + "103 0.121403 \n", + "62 0.530609 \n", + "11 0.187097 \n", + "244 30.781927 \n", + "24 32.922770 \n", + "\n", + " Why?: engineering, criminal, complete \\\n", + "103 0.159580 \n", + "62 3.105993 \n", + "11 0.265365 \n", + "244 0.122815 \n", + "24 0.299201 \n", + "\n", + " Why?: disparities, nonprofit, socioeconomic \\\n", + "103 0.132481 \n", + "62 0.318049 \n", + "11 0.113609 \n", + "244 0.119214 \n", + "24 0.405175 \n", + "\n", + " Why?: simultaneously, immediate, richmond \\\n", + "103 0.247654 \n", + "62 0.378531 \n", + "11 0.249689 \n", + "244 67.870143 \n", + "24 0.236851 \n", + "\n", + " Why?: constitution, employers, nationwide \n", + "103 0.115000 \n", + "62 234.320552 \n", + "11 26.072265 \n", + "244 0.139393 \n", + "24 41.980704 \n", + "\n", + "[5 rows x 42 columns]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# suppose we have a minibatch of new data -- transform under the fit from the above\n", + "new_df = new_y = ndf.sample(5) # pd.DataFrame({'Category': ndf['Category'].sample(5)})\n", + "a, b = g2.transform(new_df, new_y, kind='nodes')\n", + "a" ] }, { "cell_type": "code", - "execution_count": null, - "id": "789b09d8", - "metadata": {}, - "outputs": [], + "execution_count": 22, + "id": "00fc2685", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Category: justice, social, 19Category: crisis, relief, covidCategory: needed, where, mostCategory: education, health, girls
10318.0417990.0500080.0566750.051518
6218.0417990.0500080.0566750.051518
1118.0417990.0500080.0566750.051518
2440.05001010.5489160.0510250.050049
2418.0417990.0500080.0566750.051518
\n", + "
" + ], + "text/plain": [ + " Category: justice, social, 19 Category: crisis, relief, covid \\\n", + "103 18.041799 0.050008 \n", + "62 18.041799 0.050008 \n", + "11 18.041799 0.050008 \n", + "244 0.050010 10.548916 \n", + "24 18.041799 0.050008 \n", + "\n", + " Category: needed, where, most Category: education, health, girls \n", + "103 0.056675 0.051518 \n", + "62 0.056675 0.051518 \n", + "11 0.056675 0.051518 \n", + "244 0.051025 0.050049 \n", + "24 0.056675 0.051518 " + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "y = g2._node_target # we've reduced 22 columns into 5\n", - "y" + "b" ] }, { "cell_type": "code", - "execution_count": null, - "id": "126d5473", + "execution_count": 23, + "id": "cd1e7ffc", "metadata": {}, "outputs": [], "source": [ - "## we can inspect the topics from the column headers\n", - "label_list = y.columns\n", - "label_list" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7396c76b", - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "## and see them across rows of the data\n", - "fig = plt.figure(figsize=(17,10))\n", - "ax = plt.subplot()\n", - "plt.imshow(y, aspect='auto', cmap='hot')\n", - "plt.colorbar()\n", - "plt.ylabel('row number of data')\n", - "ax.set_xticks(range(len(label_list)))\n", - "ax.set_xticklabels(label_list, rotation=39)\n", - "print(f'See the abundance of the data in the latent vector of the corresponding targets')" + "# likewise you can play with how many edges to include using,\n", + "g2 = g2.filter_weighted_edges(scale=0.5) # lower positive values of scale mean closer similarity " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "id": "b9dd69ea", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# find the marginal in the category topic distribution\n", - "y.sum(0).plot(kind='bar', ylabel='support across data', rot=79)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bcf88b65", - "metadata": {}, - "outputs": [], - "source": [ - "## Looking at the above bar chart we may read off the most " + "y = g2._node_target\n", + "y.sum(0).plot(kind='bar', ylabel='support across data', rot=19)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "id": "63b817ae", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--------------------------------------------------\n", + "Topic 1: \t\t\t\t Evidence\n", + "Category: justice, social, 19\n", + "-----------------------------------\n", + "-- Social Justice, 92\n", + "-- COVID-19; Social Justice, 8\n", + "-- COVID-19, Social Justice, 2\n", + "-- Social Justice, COVID-19, 1\n", + "-- UBI, Social Justice, 1\n", + "-- COVID-19 & Social Justice, 1\n", + "\n", + "--------------------------------------------------\n", + "Topic 2: \t\t\t\t Evidence\n", + "Category: crisis, relief, covid\n", + "-----------------------------------\n", + "-- COVID-19, 70\n", + "-- COVID-19 , 55\n", + "-- Crisis Relief, 8\n", + "-- UBI; COVID-19, 3\n", + "-- COVID-19, UBI, 2\n", + "\n", + "--------------------------------------------------\n", + "Topic 3: \t\t\t\t Evidence\n", + "Category: needed, where, most\n", + "-----------------------------------\n", + "-- UBI, 4\n", + "-- Where it's needed most, 1\n", + "\n", + "--------------------------------------------------\n", + "Topic 4: \t\t\t\t Evidence\n", + "Category: education, health, girls\n", + "-----------------------------------\n", + "-- Girls Health & Education, 16\n", + "-- Social Justice, Girls Health & Education, 6\n", + "-- COVID-19, Girls Health & Education, 4\n", + "-- Girls Health & Education; COVID-19, 3\n", + "-- COVID-19; Girls Health & Education, 3\n", + "-- Girls Health & Education, COVID-19, 2\n", + "-- COVID-19, Social Justice, Girls Health & Education, 1\n", + "-- Girls Health & Education; Social Justice, 1\n", + "-- Social Justice, UBI, Girls Health & Education, 1\n" + ] + } + ], "source": [ "# Let's see how the category columns are supported by the data\n", "from collections import Counter\n", @@ -585,7 +1305,7 @@ " top_category = Counter(ndf.loc[indices].Category)\n", " print()\n", " print('-'*50)\n", - " print(f'Topic {topic_number}: \\t\\t\\t\\t Evidence')\n", + " print(f'Topic {topic_number+1}: \\t\\t\\t\\t Evidence')\n", " print(f'{y.columns[topic_number]}')\n", " print('-'*35)\n", " for t, c in top_category.most_common():\n", @@ -597,7 +1317,7 @@ "id": "efe62b1e", "metadata": {}, "source": [ - "### We see that different spellings, spaces, etc or use of ;, , etc map to the same topic. This is a useful way to disambiguate when there are many similar categories without having to do a lot of data cleaning and prep.\n", + "We see that different spellings, spaces, etc or use of ;, , etc map to the same topic. This is a useful way to disambiguate when there are many similar categories without having to do a lot of data cleaning and prep.\n", "\n", "The choice of `n_topics_target` sets the prior on the Dirty_Cat GapEncoder used under the hood" ] @@ -607,24 +1327,15 @@ "id": "530cde56", "metadata": {}, "source": [ - "## Let's add the Category Topic Number as a feature to help us visualize using the Histogram Feature of the Graphistry UI\n", + "_________________________________________________________________________________________\n", + "Let's add the Category Topic Number as a feature to help us visualize using the Histogram Feature of the Graphistry UI\n", "\n", - "This reduces the naive one-hot-encoding of 22 columns down the the number set by the `n_topics_target=5`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "db1b9ea4", - "metadata": {}, - "outputs": [], - "source": [ - "tops" + "This reduces the naive one-hot-encoding of 22 columns down the the number set by the `n_topics_target`" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "5724c75f", "metadata": {}, "outputs": [], @@ -635,10 +1346,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "id": "ad387dc0", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "0 Category: justice, social, 19\n", + "1 Category: crisis, relief, covid\n", + "2 Category: crisis, relief, covid\n", + "3 Category: crisis, relief, covid\n", + "4 Category: crisis, relief, covid\n", + " ... \n", + "280 Category: crisis, relief, covid\n", + "281 Category: crisis, relief, covid\n", + "282 Category: crisis, relief, covid\n", + "283 Category: crisis, relief, covid\n", + "284 Category: crisis, relief, covid\n", + "Name: topic, Length: 285, dtype: object" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "g2._nodes.topic" ] @@ -654,20 +1387,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "5d46b0ab", - "metadata": { - "scrolled": true - }, - "outputs": [], + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=e306d4cb9d9c46e2b451e1739b42fd1f&type=arrow&viztoken=194b1f39-29fa-40e5-b80e-790bf099ecaa&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672009087&info=true&play=0'" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g3 = g2.bind(point_title='topic')\n", - "g3.plot()" + "g2.bind(point_title='Grantee').plot(render=RENDER) # color by `topic` in histogram" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "id": "6c17e7c4", "metadata": {}, "outputs": [], @@ -677,10 +1418,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "id": "c3ad0af4", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "topic\n", + "Category: crisis, relief, covid $289,401,918.0\n", + "Category: justice, social, 19 $92,815,393.0\n", + "Category: education, health, girls $71,519,000.0\n", + "Category: needed, where, most $13,210,000.0\n", + "Name: $ amount, dtype: object" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "topic_sums = ndf.groupby('topic')['$ amount'].sum()\n", "topic_sums.sort_values()[::-1].apply(lambda x : '${:3,}'.format(x))" @@ -691,7 +1448,7 @@ "id": "058f5eef", "metadata": {}, "source": [ - "## hence we have Crisis Relief, Social Justice, Health Education Girls, and UBI occupying the main topics across the target" + "Hence we have Crisis Relief, Social Justice, Health Education Girls, and UBI occupying the main topics across the target" ] }, { @@ -700,8 +1457,8 @@ "metadata": {}, "source": [ "------------------------------------------------------------------------------------------\n", - "# Let's move on to point 2) \n", - "# Sentence Transformer Encodings\n", + "Let's move on to point 2) \n", + "## Sentence Transformer Model\n", "\n", "To trigger the sentence encoder, just lower the `min_words` count (which previously we had set to higher than the number of words across the `Why?` column) to some small value or zero to force encoding any X=[..] columns, since it sets the minimum number of words to consider passing on to the (sentence, ngram) embedding pipelines. \n", "\n", @@ -710,97 +1467,690 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "id": "0c4ceacb", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (285, 7) in UMAP fit, as it is not one dimensional" + ] + } + ], "source": [ - "g2 = g.umap(X = ['Why?', 'Grantee'], y = 'Category', \n", + "g3 = g.umap(X = ['Why?', 'Grantee'], y = 'Category', \n", " min_words=0, \n", " model_name ='paraphrase-MiniLM-L6-v2', \n", " cardinality_threshold_target=2,\n", - " scale=0.6)" + " use_scaler=None,\n", + " use_scaler_target=None,\n", + " scale=0.5)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "id": "e137b52c", "metadata": {}, - "outputs": [], - "source": [ - "g2.search('carbon neutral')[0][['Why?']]" + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Why?
28Climate Justice Alliance (CJA) formed in 2013 ...
27The Climate and Clean Energy Equity Fund (Equi...
138The Richmond Rapid Response Fund (R3F) is a wr...
29The Deep South Center for Environment Justice,...
113For 40 years, Futures Without Violence has pio...
46DC or Nothing, Inc. is a nonprofit organizatio...
134To support Oxfam’s response to the ongoing imp...
54With the support of #StartSmall ALIMA is opera...
161To support the \"AEGIS Study\" Fund to address S...
39The Caribbean Climate Justice Project seeks to...
\n", + "
" + ], + "text/plain": [ + " Why?\n", + "28 Climate Justice Alliance (CJA) formed in 2013 ...\n", + "27 The Climate and Clean Energy Equity Fund (Equi...\n", + "138 The Richmond Rapid Response Fund (R3F) is a wr...\n", + "29 The Deep South Center for Environment Justice,...\n", + "113 For 40 years, Futures Without Violence has pio...\n", + "46 DC or Nothing, Inc. is a nonprofit organizatio...\n", + "134 To support Oxfam’s response to the ongoing imp...\n", + "54 With the support of #StartSmall ALIMA is opera...\n", + "161 To support the \"AEGIS Study\" Fund to address S...\n", + "39 The Caribbean Climate Justice Project seeks to..." + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g3.search('carbon neutral')[0][['Why?']]" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "id": "a222ef95", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'$13,776,250.0'" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "'${:3,}'.format(g2.search('carbon neutral')[0]['$ amount'].sum())" + "# make quick semantic estimates\n", + "'${:3,}'.format(g3.search('carbon neutral')[0]['$ amount'].sum())" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "id": "7bbcec3a", "metadata": {}, - "outputs": [], - "source": [ - "g2.search('sustainable homes and communities')[0][['Why?','$ amount']]#.sum()" + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Why?$ amount
237For the #FirstOfTheMonth rent relief project a...530000.0
184Funds will be used towards their mission of pr...1000000.0
142To support in its efforts to empower people wi...4720000.0
225Supports the 30-day Rent Relief program and fu...200000.0
253Funds support the Navajo Water Project - conne...1000000.0
177To support the COVID-19 Resilience Fund, servi...2000000.0
122For over 60 years, Public Health Solutions (PH...200000.0
226Funding to be used as leverage in negotiations...300000.0
29The Deep South Center for Environment Justice,...300000.0
110Mission Neighborhood Centers (MNC), founded in...1000000.0
\n", + "
" + ], + "text/plain": [ + " Why? $ amount\n", + "237 For the #FirstOfTheMonth rent relief project a... 530000.0\n", + "184 Funds will be used towards their mission of pr... 1000000.0\n", + "142 To support in its efforts to empower people wi... 4720000.0\n", + "225 Supports the 30-day Rent Relief program and fu... 200000.0\n", + "253 Funds support the Navajo Water Project - conne... 1000000.0\n", + "177 To support the COVID-19 Resilience Fund, servi... 2000000.0\n", + "122 For over 60 years, Public Health Solutions (PH... 200000.0\n", + "226 Funding to be used as leverage in negotiations... 300000.0\n", + "29 The Deep South Center for Environment Justice,... 300000.0\n", + "110 Mission Neighborhood Centers (MNC), founded in... 1000000.0" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g3.search('sustainable homes and communities')[0][['Why?','$ amount']]#.sum()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "id": "1cc5bd36", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'$11,250,000.0'" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "'${:3,}'.format(g2.search('sustainable homes and communities')[0]['$ amount'].sum())" + "'${:3,}'.format(g3.search('sustainable homes and communities')[0]['$ amount'].sum())" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "id": "3cc28169", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=6c44a692de41484f9e403eea134435dc&type=arrow&viztoken=479610e9-66b9-483d-a242-5dd476faaa08&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672009105&info=true&play=0'" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# see the queries landscape -- paste url with .plot(render=False)\n", - "g2.search_graph('sustainable homes and communities', scale=0.90, top_n=10).bind(point_title='Why?').plot(render=False)" + "# see the queries landscape -- paste url to see graph if g.plot(render=False)\n", + "g3.search_graph('sustainable homes and communities', scale=0.90, top_n=10).bind(point_title='Grantee').plot(render=RENDER)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 37, "id": "6bf9f793", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "'SuperVectorizer' object has no attribute 'get_feature_names_in'" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Why?_Grantee_0Why?_Grantee_1Why?_Grantee_2Why?_Grantee_3Why?_Grantee_4Why?_Grantee_5Why?_Grantee_6Why?_Grantee_7Why?_Grantee_8Why?_Grantee_9...Why?_Grantee_374Why?_Grantee_375Why?_Grantee_376Why?_Grantee_377Why?_Grantee_378Why?_Grantee_379Why?_Grantee_380Why?_Grantee_381Why?_Grantee_382Why?_Grantee_383
1030.4437810.228207-0.0959600.2334590.0899190.2619630.181132-0.332808-0.1311690.098321...-0.112939-0.0398010.344712-0.1484540.0923070.5542710.7291100.292012-0.2359180.717677
62-0.117390-0.122771-0.457182-0.2898800.0919840.300274-0.013144-0.2189640.0343230.037846...0.2690200.1146750.1340280.085543-0.3067460.0370340.1676200.0815010.109638-0.272466
110.0664930.095128-0.273875-0.1083200.2081530.3931890.2077410.060979-0.1894270.035519...0.117062-0.0895520.2476550.2489770.0262500.2749580.396113-0.1091530.155618-0.087428
2440.2300280.1674710.2090630.0971040.0820540.183794-0.266914-0.023539-0.543348-0.263615...0.0631710.154695-0.307326-0.1970530.221207-0.0982220.054051-0.118295-0.1543150.197405
24-0.1696910.559551-0.129676-0.3666600.267032-0.058975-0.0054590.0622950.0509210.362040...0.1126640.112096-0.1164510.187891-0.1420400.025833-0.6957160.0177720.0102840.043964
\n", + "

5 rows × 384 columns

\n", + "
" + ], + "text/plain": [ + " Why?_Grantee_0 Why?_Grantee_1 Why?_Grantee_2 Why?_Grantee_3 \\\n", + "103 0.443781 0.228207 -0.095960 0.233459 \n", + "62 -0.117390 -0.122771 -0.457182 -0.289880 \n", + "11 0.066493 0.095128 -0.273875 -0.108320 \n", + "244 0.230028 0.167471 0.209063 0.097104 \n", + "24 -0.169691 0.559551 -0.129676 -0.366660 \n", + "\n", + " Why?_Grantee_4 Why?_Grantee_5 Why?_Grantee_6 Why?_Grantee_7 \\\n", + "103 0.089919 0.261963 0.181132 -0.332808 \n", + "62 0.091984 0.300274 -0.013144 -0.218964 \n", + "11 0.208153 0.393189 0.207741 0.060979 \n", + "244 0.082054 0.183794 -0.266914 -0.023539 \n", + "24 0.267032 -0.058975 -0.005459 0.062295 \n", + "\n", + " Why?_Grantee_8 Why?_Grantee_9 ... Why?_Grantee_374 Why?_Grantee_375 \\\n", + "103 -0.131169 0.098321 ... -0.112939 -0.039801 \n", + "62 0.034323 0.037846 ... 0.269020 0.114675 \n", + "11 -0.189427 0.035519 ... 0.117062 -0.089552 \n", + "244 -0.543348 -0.263615 ... 0.063171 0.154695 \n", + "24 0.050921 0.362040 ... 0.112664 0.112096 \n", + "\n", + " Why?_Grantee_376 Why?_Grantee_377 Why?_Grantee_378 Why?_Grantee_379 \\\n", + "103 0.344712 -0.148454 0.092307 0.554271 \n", + "62 0.134028 0.085543 -0.306746 0.037034 \n", + "11 0.247655 0.248977 0.026250 0.274958 \n", + "244 -0.307326 -0.197053 0.221207 -0.098222 \n", + "24 -0.116451 0.187891 -0.142040 0.025833 \n", + "\n", + " Why?_Grantee_380 Why?_Grantee_381 Why?_Grantee_382 Why?_Grantee_383 \n", + "103 0.729110 0.292012 -0.235918 0.717677 \n", + "62 0.167620 0.081501 0.109638 -0.272466 \n", + "11 0.396113 -0.109153 0.155618 -0.087428 \n", + "244 0.054051 -0.118295 -0.154315 0.197405 \n", + "24 -0.695716 0.017772 0.010284 0.043964 \n", + "\n", + "[5 rows x 384 columns]" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# or transform on new data as before\n", - "a, b = g2.transform(new_df, new_y, kind='nodes')\n", + "a, b = g3.transform(new_df, new_y, kind='nodes')\n", "a" ] }, + { + "cell_type": "code", + "execution_count": 38, + "id": "142b85db", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Category: covid, ubi, 19Category: 19, it, ubiCategory: justice, social, mostCategory: 19, it, ubiCategory: education, health, girlsCategory: crisis, relief, neededCategory: ubi, 19, it
1030.0500030.09846017.9763740.0509140.0515390.0501210.072589
620.0500030.09846017.9763740.0509140.0515390.0501210.072589
110.0500030.09846017.9763740.0509140.0515390.0501210.072589
24410.5175000.0683720.0500040.0640940.0500200.0500030.050007
240.0500030.09846017.9763740.0509140.0515390.0501210.072589
\n", + "
" + ], + "text/plain": [ + " Category: covid, ubi, 19 Category: 19, it, ubi \\\n", + "103 0.050003 0.098460 \n", + "62 0.050003 0.098460 \n", + "11 0.050003 0.098460 \n", + "244 10.517500 0.068372 \n", + "24 0.050003 0.098460 \n", + "\n", + " Category: justice, social, most Category: 19, it, ubi \\\n", + "103 17.976374 0.050914 \n", + "62 17.976374 0.050914 \n", + "11 17.976374 0.050914 \n", + "244 0.050004 0.064094 \n", + "24 17.976374 0.050914 \n", + "\n", + " Category: education, health, girls Category: crisis, relief, needed \\\n", + "103 0.051539 0.050121 \n", + "62 0.051539 0.050121 \n", + "11 0.051539 0.050121 \n", + "244 0.050020 0.050003 \n", + "24 0.051539 0.050121 \n", + "\n", + " Category: ubi, 19, it \n", + "103 0.072589 \n", + "62 0.072589 \n", + "11 0.072589 \n", + "244 0.050007 \n", + "24 0.072589 " + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "b" + ] + }, { "cell_type": "markdown", "id": "5a3033a4", "metadata": {}, "source": [ - "## Clicking around to nearest neighbors demonstrates good semantic similarity, as seen by the Paraphrase Model `paraphrase-MiniLM-L6-v2`" + "Clicking around to nearest neighbors demonstrates good semantic similarity, as seen by the Paraphrase Model `paraphrase-MiniLM-L6-v2`" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "id": "9d33ea95", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=51fe80f88c464b5eb049dd382cfb9b46&type=arrow&viztoken=ab98054f-1141-4657-b76d-9a98b4753917&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672009108&info=true&play=0'" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g2.plot()" + "g3.bind(point_title='Grantee').plot(render=RENDER)" ] }, { @@ -808,70 +2158,81 @@ "id": "7cbb210c", "metadata": {}, "source": [ - "## Suppose we wanted to add the Grantee column as a feature: \n", - "To include it in the sentence transformer model, reduce the` min_words` threshold to include it. If we want the column `Grantee` to be encoded as a topic model, set `min_words` to between the average of `Why?` (higher) and `Grantee` (lower) and `$ amount` (which is just 1). This may seem a bit sloppy as an API, nevertheless useful across many datasets since if a column is truly categorical, its cardinality is usually well under that of a truly textual feature. Moreover, if you want all columns to be textually encoded, set `min_words=0`. " + "Suppose we wanted to add the `$ amount` column as a feature: \n", + "\n", + "To include it in the sentence transformer model, reduce the` min_words` threshold to include it. If we want the column `Grantee` to be encoded as a topic model, set `min_words` to between the average of `Why?` (higher) and `Grantee` (lower). It nevertheless is useful across many datasets since if a column is truly categorical, its cardinality is usually well under that of a truly textual feature. Moreover, if you want all columns to be textually encoded, set `min_words=0`. \n", + "\n", + "The `$ amount` column will be passed in and scaled according to `use_scaler`, while `use_scaler_target` selects how to scale targets \n", + "\n", + "(exercise: `use_scaler_target='kbins'` to see the difference in `g._node_target` \n", + "\n", + "or scale the dataframe directly (this transforms the batch dataframe)\n", + "\n", + "`a, b = g.scale(ndf, ydf=ndf, 'nodes', use_scaler_target='kbins', n_bins=9))` " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "id": "4ef2870c", "metadata": {}, - "outputs": [], - "source": [ - "g2 = g.umap(X = ['Why?', 'Grantee', '$ amount'], y = 'Category',\n", - " min_words=2,\n", + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (285, 22) in UMAP fit, as it is not one dimensional" + ] + } + ], + "source": [ + "g3 = g.umap(X = ['Why?', 'Grantee', '$ amount'], y = 'Category',\n", + " min_words=2, # don't set to zero or it will stringify the `$ amount`\n", " model_name ='paraphrase-MiniLM-L6-v2',\n", " use_scaler=None,\n", - " ) " + " )" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 41, "id": "97bdaa46", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "['Why?', 'Grantee']" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g2._node_encoder.text_cols" + "g3._node_encoder.text_cols" ] }, { "cell_type": "code", - "execution_count": null, - "id": "05b61370", - "metadata": {}, - "outputs": [], - "source": [ - "# just for fun, can we find outliers (which we know will be influenced by the numeric $ amount)\n", - "from graphistry.outliers import detect_outliers\n", - "\n", - "# organized by amount\n", - "embedding = g2._xy\n", - "clfs, ax, fig = detect_outliers(embedding.values, name='Donations', contamination=0.3)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b608b9cb", - "metadata": {}, - "outputs": [], - "source": [ - "# the different models\n", - "clfs" - ] - }, - { - "cell_type": "code", - "execution_count": null, + "execution_count": 42, "id": "33f3bc17", - "metadata": { - "scrolled": false - }, - "outputs": [], + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=22d84e2ef2de4cb4830ea1c6c9dc2f70&type=arrow&viztoken=b5067b0e-4c9b-4341-8c41-ce05998aacd1&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672009127&info=true&play=0'" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g2.plot() # color/size the noded by `$ amount`" + "g3.plot(render=RENDER) # color/size the noded by `$ amount`, mimics graph above as they use the same embedding xys" ] }, { @@ -879,7 +2240,9 @@ "id": "f014b4e0", "metadata": {}, "source": [ - "# Lastly, suppose we want a plain Ngrams model matrix, and for a change, one-hot-encode the target `Category`\n", + "## NGRAMS model\n", + "\n", + "Lastly, suppose we want a plain Ngrams model matrix, and for a change, one-hot-encode the target `Category`\n", "\n", "Set `use_ngrams = True`\n", "and set the `cardinality_threshold_target` > cardinality(`Category`).\n", @@ -889,84 +2252,585 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 43, "id": "8c1a588c", "metadata": {}, - "outputs": [], - "source": [ - "g3 = g.umap(X = ['Why?', 'Grantee'], y = 'Category', \n", + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (285, 22) in UMAP fit, as it is not one dimensional" + ] + } + ], + "source": [ + "g4 = g.umap(X = ['Why?', 'Grantee'], y = 'Category', \n", " use_ngrams=True, \n", " ngram_range=(1,3), \n", " min_df=2, \n", " max_df=0.3,\n", - " cardinality_threshold_target=400\n", - " ) # this will one-hot-encode the target, as we have less than 400 total `categories`" + " use_scaler=None,\n", + " cardinality_threshold_target=400 # this will one-hot-encode the target, \n", + " # as we have less than 400 total `categories`\n", + " )" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 44, "id": "e1e2683c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=aeb09ebc8a914fe8851b7bc4ea255c9c&type=arrow&viztoken=f768c875-ee7d-4cea-b514-a3c8ba0d59bc&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672009131&info=true&play=0'" + ] + }, + "execution_count": 44, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g3.bind(point_title='Category').plot()" + "g4.bind(point_title='Category').plot(render=RENDER) # umap-ing ngrams is not as useful as sentence embeddings as you may visually see, however they can be useful graphs nonetheless. Press `play` in the UI." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 45, "id": "d570f001", "metadata": {}, - "outputs": [], - "source": [ - "g3._node_features # a standard tfidf ngrams matrix" + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
reformalliancecommittedcriminaljusticesystemthroughoutunitedstatesby...mayor officefor homelessmatchhospital2mgo towardsgrant 2mwill go towardstotal grant 2mclinics
00.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
10.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
20.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
30.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
40.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
..................................................................
2800.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
2810.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
2820.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
2830.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
2840.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.0
\n", + "

285 rows × 3889 columns

\n", + "
" + ], + "text/plain": [ + " reform alliance committed criminal justice system throughout \\\n", + "0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + ".. ... ... ... ... ... ... ... \n", + "280 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "281 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "282 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "283 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "284 0.0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "\n", + " united states by ... mayor office for homeless match hospital \\\n", + "0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "1 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "2 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "4 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + ".. ... ... ... ... ... ... ... ... \n", + "280 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "281 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "282 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "283 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "284 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 \n", + "\n", + " 2m go towards grant 2m will go towards total grant 2m clinics \n", + "0 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "1 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "2 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "3 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "4 0.0 0.0 0.0 0.0 0.0 0.0 \n", + ".. ... ... ... ... ... ... \n", + "280 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "281 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "282 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "283 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "284 0.0 0.0 0.0 0.0 0.0 0.0 \n", + "\n", + "[285 rows x 3889 columns]" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g4._node_features # a standard tfidf ngrams matrix" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 46, "id": "338010f2", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Pipeline(steps=[('vect',\n", + " CountVectorizer(max_df=0.3, min_df=2, ngram_range=(1, 3))),\n", + " ('tfidf', TfidfTransformer())])" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g3._node_encoder.text_model #sklearn pipeline " + "g4._node_encoder.text_model #sklearn pipeline " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 47, "id": "e7582131", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "3889" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "## vocab size\n", - "len(g3._node_encoder.text_model[0].vocabulary_)" + "len(g4._node_encoder.text_model[0].vocabulary_)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 48, "id": "9e691b4d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "'SuperVectorizer' object has no attribute 'get_feature_names_in'" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xy
1036.100185-1.109381
626.937139-2.348813
116.991597-2.461275
2445.651282-1.004279
245.893190-3.356526
\n", + "
" + ], + "text/plain": [ + " x y\n", + "103 6.100185 -1.109381\n", + "62 6.937139 -2.348813\n", + "11 6.991597 -2.461275\n", + "244 5.651282 -1.004279\n", + "24 5.893190 -3.356526" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# or transform new data: \n", - "emb, a, b = g2.transform_umap(new_df, new_y, kind='nodes')\n", + "emb, a, b = g4.transform_umap(new_df, new_y, kind='nodes')\n", "emb" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 49, "id": "5bc7b2c0", - "metadata": { - "scrolled": false - }, - "outputs": [], + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Naive Indicator Variables\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# we include the naive indicator variable for completeness.\n", - "y = g3._node_target\n", + "y = g4._node_target\n", "label_list = b.columns\n", "\n", "fig = plt.figure(figsize=(17,10))\n", @@ -981,22 +2845,22 @@ }, { "cell_type": "markdown", - "id": "ec83a920", + "id": "22596c6d", "metadata": {}, "source": [ "# Contributions\n", "\n", - "We've seen how we may pull in tabular data that exists in the wild and quickly make features and graphs that allow semantic and topological exploration and traversals. \n", + "Input tabular data that exists in the wild and quickly make features and graphs that allow semantic and topological exploration and traversals. \n", "\n", - "In this way one can quickly track a variety of datasets and (in this case) gauge growth, investment, and promise fullfillment and transparently using Graph Thinking and analysis.\n", + "Quickly track a variety of datasets and gauge growth, investment, and promise fullfillment and transparently using Graph Thinking and Analysis. In Jack's case, we see the possibility of a multibillion dollar edific erected around Covid-19, Girls Education and Social Justice. Further downstream modeling might tell us what such an edific is able to manufacture as a force for Good.\n", "\n", - "Encoding text, categorical, and numeric features while exploring the relationships can be time consuming tasks. We hope that Graphistry[ai] demonstrates an exciting and visually compelling way to explore Graph Data. \n", + "Encoding text, categorical, and numeric features while exploring the relationships can be time consuming tasks. \n", "\n", - "Now you can mix and match features, augment it with more columns via enrichment, and pivot large amounts of data using natural language search, all using a few lines of code. The features produced may then be used in downstream models, whose outputs could be added and the entire process repeated.\n", + "PyGraphistry[ai] demonstrates an exciting and visually accelerated way to explore Graph Data. \n", "\n", - "Let us know what you think!\n", + "It allows quick Mix and Match featurization models and types, while pivoting on large amounts of data using natural language search, in just a few lines of code. The resulting features may then be used in downstream models.\n", "\n", - "Join our Slack: Graphistry-Community\n" + "Join our Slack: Graphistry-Community" ] }, { From 3d21f8edfce250da71e7642a3f51d573254c847e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 28 Dec 2022 19:01:15 -0800 Subject: [PATCH 003/432] fix typo --- demos/ai/Introduction/Ask-HackerNews-Demo.ipynb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/demos/ai/Introduction/Ask-HackerNews-Demo.ipynb b/demos/ai/Introduction/Ask-HackerNews-Demo.ipynb index 47e010af9a..99cc397dae 100644 --- a/demos/ai/Introduction/Ask-HackerNews-Demo.ipynb +++ b/demos/ai/Introduction/Ask-HackerNews-Demo.ipynb @@ -5,7 +5,7 @@ "id": "c39da4a9", "metadata": {}, "source": [ - "# Hello PyGraphistry[ai] - HackerNews visual semantic search with UMAP & BERT and \n", + "# Hello PyGraphistry[ai] - HackerNews visual semantic search with UMAP & BERT.\n", "\n", "`PyGraphistry[ai]` can quickly create visual graph search interfaces for structured text. It automates much of the work in cleaning, connecting, encoding, searching, and visualing graph data. The result is increasing the *time to graph* and overall results in as little as one line of code.\n", "\n", @@ -244,9 +244,7 @@ "cell_type": "code", "execution_count": null, "id": "f43b7806", - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "# visualize the results where we prune edges using the `filter_weighted_edges` method\n", @@ -278,9 +276,7 @@ "cell_type": "code", "execution_count": null, "id": "85cf9c06", - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "# Query semantically instead of strict keyword matching\n", @@ -502,9 +498,7 @@ "cell_type": "code", "execution_count": null, "id": "45d3a37a", - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "# see the edge features which are shape (n_edges, n_nodes + weight)\n", From 5b73c37a1799fd756d2d7c2228dd05c48e8904d8 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 13:18:03 -0800 Subject: [PATCH 004/432] adds ModelDict and removes build_index call from demo notebook --- demos/ai/cyber/cyber-redteam-umap-demo.ipynb | 2685 ++++++++++++++++-- 1 file changed, 2487 insertions(+), 198 deletions(-) diff --git a/demos/ai/cyber/cyber-redteam-umap-demo.ipynb b/demos/ai/cyber/cyber-redteam-umap-demo.ipynb index 9ed84b308b..9e30bb5dff 100644 --- a/demos/ai/cyber/cyber-redteam-umap-demo.ipynb +++ b/demos/ai/cyber/cyber-redteam-umap-demo.ipynb @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "f9de6fd3-b87b-4dc4-8d1c-b8f3feceb5e6", "metadata": {}, "outputs": [], @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "0215906c", "metadata": {}, "outputs": [], @@ -31,6 +31,8 @@ "import pandas as pd\n", "import graphistry\n", "\n", + "from graphistry.features import topic_model, search_model, ModelDict\n", + "\n", "import os\n", "from joblib import load, dump\n", "from collections import Counter\n", @@ -44,7 +46,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, + "id": "8e1747b9-c903-4398-9aa0-b52b69fce021", + "metadata": {}, + "outputs": [], + "source": [ + "np.random.seed(137)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6d2669fd-6164-4376-81bd-79c6c6f4112f", + "metadata": {}, + "outputs": [], + "source": [ + "RENDER = False # set to True to render Graphistry UI inline" + ] + }, + { + "cell_type": "code", + "execution_count": 5, "id": "59e1cc0b", "metadata": {}, "outputs": [], @@ -74,7 +96,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "fe6e61b0", "metadata": {}, "outputs": [], @@ -103,10 +125,153 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "efe68cf8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
timesrc_domaindst_domainsrc_computerdst_computerauth_typelogontypeauthentication_orientationsuccess_or_failureREDfeatsfeats2
30526246155805C7048$@DOM1C7048$@DOM1C7048TGT??TGSSuccess0.0C7048 TGT ? ?C7048 TGT
592820137690C15034$@DOM1C15034$@DOM1C15034C467??TGSSuccess0.0C15034 C467 ? ?C15034 C467
21160461116992U2075@DOM1U2075@DOM1C529C529?NetworkLogOffSuccess0.0C529 C529 ? NetworkC529 C529
218232822019C3547$@DOM1C3547$@DOM1C457C457?NetworkLogOffSuccess0.0C457 C457 ? NetworkC457 C457
28495743145572C567$@DOM1C567$@DOM1C574C523KerberosNetworkLogOnSuccess0.0C574 C523 Kerberos NetworkC574 C523
\n", + "
" + ], + "text/plain": [ + " time src_domain dst_domain src_computer dst_computer \\\n", + "30526246 155805 C7048$@DOM1 C7048$@DOM1 C7048 TGT \n", + "5928201 37690 C15034$@DOM1 C15034$@DOM1 C15034 C467 \n", + "21160461 116992 U2075@DOM1 U2075@DOM1 C529 C529 \n", + "2182328 22019 C3547$@DOM1 C3547$@DOM1 C457 C457 \n", + "28495743 145572 C567$@DOM1 C567$@DOM1 C574 C523 \n", + "\n", + " auth_type logontype authentication_orientation success_or_failure \\\n", + "30526246 ? ? TGS Success \n", + "5928201 ? ? TGS Success \n", + "21160461 ? Network LogOff Success \n", + "2182328 ? Network LogOff Success \n", + "28495743 Kerberos Network LogOn Success \n", + "\n", + " RED feats feats2 \n", + "30526246 0.0 C7048 TGT ? ? C7048 TGT \n", + "5928201 0.0 C15034 C467 ? ? C15034 C467 \n", + "21160461 0.0 C529 C529 ? Network C529 C529 \n", + "2182328 0.0 C457 C457 ? Network C457 C457 \n", + "28495743 0.0 C574 C523 Kerberos Network C574 C523 " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# small sample (get almost equivalent results without overheating computer over the 1.6B events in the full dataset)\n", "df = pd.read_csv('https://gist.githubusercontent.com/silkspace/c7b50d0c03dc59f63c48d68d696958ff/raw/31d918267f86f8252d42d2e9597ba6fc03fcdac2/redteam_50k.csv', index_col=0)\n", @@ -115,17 +280,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "03610297", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(50000, 12)\n" + ] + } + ], "source": [ "print(df.shape) # -> 50k" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "66c5126e", "metadata": {}, "outputs": [], @@ -134,6 +307,17 @@ "red_team = pd.read_csv('https://gist.githubusercontent.com/silkspace/5cf5a94b9ac4b4ffe38904f20d93edb1/raw/888dabd86f88ea747cf9ff5f6c44725e21536465/redteam_labels.csv', index_col=0)" ] }, + { + "cell_type": "code", + "execution_count": 10, + "id": "7b31d2b0-b123-4f7c-9157-03accce6a6c7", + "metadata": {}, + "outputs": [], + "source": [ + "# for later\n", + "red_team['feats2'] = red_team.feats" + ] + }, { "cell_type": "markdown", "id": "3c6615aa", @@ -146,10 +330,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "3641d3b5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(19013, 12)\n" + ] + } + ], "source": [ "process = True \n", "# makes a combined feature we can use for topic modeling!\n", @@ -159,13 +351,21 @@ " # and one of just computer to computer \n", " df['feats2'] = df.src_computer + ' ' + df.dst_computer\n", " ndf = df.drop_duplicates(subset=['feats'])\n", - " ndf.to_parquet('../data/auth-50k-feats-one-column.parquet')\n", + " ndf.to_parquet('auth-feats-one-column.parquet')\n", "else:\n", - " ndf = pd.read_parquet('../data/auth-50k-feats-one-column.parquet')\n", + " ndf = pd.read_parquet('auth-feats-one-column.parquet')\n", " \n", "print(ndf.shape)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1c15b72-a355-48d5-9a4e-31dbb6a47b06", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", "id": "32d1755d", @@ -177,10 +377,289 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "d67c86b8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
indextimesrc_domainsrc_computerdst_computerfeatsREDfeats2dst_domainauth_typelogontypeauthentication_orientationsuccess_or_failure
00150885U620@DOM1C17693C1003C17693 C10031.0C17693 C1003NaNNaNNaNNaNNaN
11151036U748@DOM1C17693C305C17693 C3051.0C17693 C305NaNNaNNaNNaNNaN
22151648U748@DOM1C17693C728C17693 C7281.0C17693 C728NaNNaNNaNNaNNaN
33151993U6115@DOM1C17693C1173C17693 C11731.0C17693 C1173NaNNaNNaNNaNNaN
44153792U636@DOM1C17693C294C17693 C2941.0C17693 C294NaNNaNNaNNaNNaN
..........................................
19008846310748263C11843$@DOM1C11843C528C11843 C528 Kerberos Network0.0C11843 C528C11843$@DOM1KerberosNetworkLogOnSuccess
190091439463077937C8470$@DOM1C8470C528C8470 C528 NTLM Network0.0C8470 C528C8470$@DOM1NTLMNetworkLogOnSuccess
1901033398153173300C716$@DOM1C716C716C716 C716 ? ?0.0C716 C716C716$@DOM1??AuthMapSuccess
1901118353851102472U7365@DOM1C16126C586C16126 C586 ? ?0.0C16126 C586U7365@DOM1??TGSSuccess
1901227372458141156NETWORK SERVICE@C6215C6215C6215C6215 C6215 Negotiate Service0.0C6215 C6215NETWORK SERVICE@C6215NegotiateServiceLogOnSuccess
\n", + "

19762 rows × 13 columns

\n", + "
" + ], + "text/plain": [ + " index time src_domain src_computer dst_computer \\\n", + "0 0 150885 U620@DOM1 C17693 C1003 \n", + "1 1 151036 U748@DOM1 C17693 C305 \n", + "2 2 151648 U748@DOM1 C17693 C728 \n", + "3 3 151993 U6115@DOM1 C17693 C1173 \n", + "4 4 153792 U636@DOM1 C17693 C294 \n", + "... ... ... ... ... ... \n", + "19008 8463107 48263 C11843$@DOM1 C11843 C528 \n", + "19009 14394630 77937 C8470$@DOM1 C8470 C528 \n", + "19010 33398153 173300 C716$@DOM1 C716 C716 \n", + "19011 18353851 102472 U7365@DOM1 C16126 C586 \n", + "19012 27372458 141156 NETWORK SERVICE@C6215 C6215 C6215 \n", + "\n", + " feats RED feats2 \\\n", + "0 C17693 C1003 1.0 C17693 C1003 \n", + "1 C17693 C305 1.0 C17693 C305 \n", + "2 C17693 C728 1.0 C17693 C728 \n", + "3 C17693 C1173 1.0 C17693 C1173 \n", + "4 C17693 C294 1.0 C17693 C294 \n", + "... ... ... ... \n", + "19008 C11843 C528 Kerberos Network 0.0 C11843 C528 \n", + "19009 C8470 C528 NTLM Network 0.0 C8470 C528 \n", + "19010 C716 C716 ? ? 0.0 C716 C716 \n", + "19011 C16126 C586 ? ? 0.0 C16126 C586 \n", + "19012 C6215 C6215 Negotiate Service 0.0 C6215 C6215 \n", + "\n", + " dst_domain auth_type logontype authentication_orientation \\\n", + "0 NaN NaN NaN NaN \n", + "1 NaN NaN NaN NaN \n", + "2 NaN NaN NaN NaN \n", + "3 NaN NaN NaN NaN \n", + "4 NaN NaN NaN NaN \n", + "... ... ... ... ... \n", + "19008 C11843$@DOM1 Kerberos Network LogOn \n", + "19009 C8470$@DOM1 NTLM Network LogOn \n", + "19010 C716$@DOM1 ? ? AuthMap \n", + "19011 U7365@DOM1 ? ? TGS \n", + "19012 NETWORK SERVICE@C6215 Negotiate Service LogOn \n", + "\n", + " success_or_failure \n", + "0 NaN \n", + "1 NaN \n", + "2 NaN \n", + "3 NaN \n", + "4 NaN \n", + "... ... \n", + "19008 Success \n", + "19009 Success \n", + "19010 Success \n", + "19011 Success \n", + "19012 Success \n", + "\n", + "[19762 rows x 13 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# make a subsampled dataframe with the anom red-team data at top...so we can keep track.\n", "# we don't need the full `df`, only the unique entries of 'feats' in `ndf` for \n", @@ -192,7 +671,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "5f62b7b5", "metadata": {}, "outputs": [], @@ -203,10 +682,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "5ffd6aac", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "749.0" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# total number of red team events\n", "tdf.RED.sum()" @@ -222,7 +712,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "72c53f98", "metadata": {}, "outputs": [], @@ -289,30 +779,165 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, + "id": "504781dc-9fbe-467c-9b4d-2e907133cfb7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "_________________________________________________________________\n", + "\n", + "A topic model for computer to computer + metadata cyber auth logs\n", + "_________________________________________________________________\n", + "\n", + "Updated: {'n_topics': 32, 'X': ['feats']}\n", + "_________________________________________________________________\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "{'kind': 'nodes', 'use_scaler': None, 'use_scaler_target': None, 'cardinality_threshold': 2, 'cardinality_threshold_target': 2, 'n_topics': 32, 'n_topics_target': 10, 'multilabel': False, 'embedding': False, 'use_ngrams': False, 'ngram_range': (1, 3), 'max_df': 0.2, 'min_df': 3, 'min_words': 40000000.0, 'model_name': 'sentence-transformers/msmarco-distilbert-base-v2', 'impute': 'median', 'n_quantiles': 100, 'output_distribution': 'normal', 'quantile_range': (25, 75), 'n_bins': 10, 'encode': 'ordinal', 'strategy': 'uniform', 'similarity': None, 'categories': 'auto', 'keep_n_decimals': 5, 'remove_node_column': True, 'inplace': False, 'feature_engine': 'auto', 'memoize': True, 'X': ['feats']}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# this is a convienence method for setting parameters in `g.featurize()/umap()` -- just a verbose dictionary\n", + "cyber_model = ModelDict('A topic model for computer to computer + metadata cyber auth logs', **topic_model)\n", + "\n", + "cyber_model.update(dict(n_topics=32, X=['feats'])) # name the column to featurize, which we lumped into `feats`\n", + "\n", + "cyber_model" + ] + }, + { + "cell_type": "code", + "execution_count": 17, "id": "6909cc90", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (19762, 0) in UMAP fit, as it is not one dimensionalOMP: Info #273: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------\n", + "cluster: 0\n", + " red 2.59% or 95.0 out of 3665\n", + "--------------------\n", + "cluster: 27\n", + " red 0.41% or 3.0 out of 724\n", + "--------------------\n", + "cluster: 26\n", + " red 0.38% or 1.0 out of 260\n", + "--------------------\n", + "cluster: 10\n", + " red 0.43% or 1.0 out of 234\n", + "--------------------\n", + "cluster: 1\n", + " red 94.44% or 119.0 out of 126\n", + "--------------------\n", + "cluster: 6\n", + " red 95.06% or 77.0 out of 81\n", + "--------------------\n", + "cluster: 9\n", + " red 84.42% or 65.0 out of 77\n", + "--------------------\n", + "cluster: 5\n", + " red 96.61% or 57.0 out of 59\n", + "--------------------\n", + "cluster: 7\n", + " red 91.84% or 45.0 out of 49\n", + "--------------------\n", + "cluster: 14\n", + " red 92.11% or 35.0 out of 38\n", + "--------------------\n", + "cluster: 3\n", + " red 94.59% or 35.0 out of 37\n", + "--------------------\n", + "cluster: 22\n", + " red 100.00% or 27.0 out of 27\n", + "--------------------\n", + "cluster: 4\n", + " red 84.00% or 21.0 out of 25\n", + "--------------------\n", + "cluster: 23\n", + " red 100.00% or 24.0 out of 24\n", + "--------------------\n", + "cluster: 8\n", + " red 100.00% or 23.0 out of 23\n", + "--------------------\n", + "cluster: 20\n", + " red 81.25% or 13.0 out of 16\n", + "--------------------\n", + "cluster: 13\n", + " red 93.33% or 14.0 out of 15\n", + "--------------------\n", + "cluster: 16\n", + " red 100.00% or 15.0 out of 15\n", + "--------------------\n", + "cluster: 2\n", + " red 100.00% or 14.0 out of 14\n", + "--------------------\n", + "cluster: 25\n", + " red 100.00% or 13.0 out of 13\n", + "--------------------\n", + "cluster: 11\n", + " red 100.00% or 11.0 out of 11\n", + "--------------------\n", + "cluster: 18\n", + " red 100.00% or 9.0 out of 9\n", + "--------------------\n", + "cluster: 15\n", + " red 100.00% or 6.0 out of 6\n", + "--------------------\n", + "cluster: 24\n", + " red 100.00% or 6.0 out of 6\n", + "--------------------\n", + "cluster: 12\n", + " red 100.00% or 5.0 out of 5\n", + "--------------------\n", + "cluster: 17\n", + " red 100.00% or 5.0 out of 5\n", + "--------------------\n", + "cluster: 19\n", + " red 100.00% or 5.0 out of 5\n", + "--------------------\n", + "cluster: 21\n", + " red 100.00% or 5.0 out of 5\n", + "CPU times: user 3min 16s, sys: 32 s, total: 3min 48s\n", + "Wall time: 1min 57s\n" + ] + } + ], "source": [ "%%time\n", "process = True # set to false after it's run for ease of speed\n", "if process:\n", - " g = graphistry.nodes(tdf, 'node')\n", - " g5 = g.umap(X=['feats'], \n", - " min_words=1000000, # force high so that we don't use Sentence Transformers\n", - " cardinality_threshold=4, # set low so we force topic model\n", - " n_topics=32, # number of topics\n", - " use_scaler=None,\n", - " use_scaler_target=None\n", - " )\n", + " # ##################################\n", + " g = graphistry.nodes(tdf, 'node') # two lines does the heavy lifting\n", + " g5 = g.umap(**cyber_model)\n", + " # #########################\n", " \n", " g5, dbscan, cluster_confidences = enrich(g5)\n", "\n", - " g5.build_index()\n", - " g5.save_search_instance('../data/auth-feat-topic.search')\n", + " g5.save_search_instance('auth-feat-topic.search')\n", "else:\n", " g = graphistry.bind()\n", - " g5 = g.load_search_instance('../data/auth-feat-topic.search')\n", + " g5 = g.load_search_instance('auth-feat-topic.search')\n", " g5, dbscan, cluster_confidences = enrich(g5)\n" ] }, @@ -321,26 +946,516 @@ "id": "54c13cba-bc36-4d49-8e7a-7dc05b27610a", "metadata": {}, "source": [ - "## Plot it\n", - "Color by `confidence` and hover over `red` team histogram to see where events occur" + "## Plot Graph\n", + "Color by `confidence` and hover over `red` team histogram to see where events occur. Alternatively, color by `cluster` assignment" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "id": "279fef41", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=77ee7b6a4daa4539a93f0c60e34934c0&type=arrow&viztoken=4d612b44-1558-4398-8964-744cf5c9c632&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672345808&info=true&play=0'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g5.name('auth 50k topic feats no target').plot(render=False)" + "g5.name('auth topic feats no target').plot(render=RENDER)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "79ece955", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
feats: c9990, c9994, c9997feats: kerberos, u1, u7feats: c528, c5252, c5281feats: c612, c6121, c6125feats: c2446, c2444, c24464feats: c13713, c13130, c13134feats: c586, c5866, c5864feats: c467, c4674, c4667feats: unlock, c1111, c11114feats: c625, c6257, c6255...feats: c5299, c529, c5294feats: c16616, c16168, c16663feats: microsoft_authentication_package_v1_0, microsoft_authentication_package_v1_, microsoft_authentication_package_v1feats: c1065, c10658, c10652feats: cachedinteractive, remoteinteractive, interactivefeats: c3888, c3884, u608feats: c2327, c2323, c2727feats: negotiate, service, c14514feats: c8282, c8280, c8289feats: c1964, c1968, c25685
00.0529920.0500290.0514130.0514030.0500190.0612120.0514190.0504630.0573040.051460...0.0513920.0639230.0502670.1259011.2587630.0524620.0515190.0511450.0519620.054784
11.6098710.0500300.0514200.0514100.0500160.0914860.0514260.0504650.0610340.051467...0.0513990.0690570.0502850.0620531.3217390.0531820.0515270.0500770.0519720.057155
20.0519750.0500300.5577470.0543090.0500160.0701150.0514570.0504750.0609110.051500...0.0514290.0688270.0502900.0619311.3859450.0532100.0530940.0500770.0596950.057138
30.0520890.0500320.0515340.0515230.0500230.0690800.0515410.0505013.7819850.051586...0.0515110.0745020.0502960.0899172.5851250.0529920.0516500.0517860.0521320.056803
41.6125390.0500310.0514820.0514720.0500160.0703620.0514880.0504850.0610270.051532...0.5660140.0690570.0502950.0620661.3220350.0532630.0733810.0500770.0520590.057233
..................................................................
190080.05185622.4777297.1830050.0513550.0500150.0613630.0650230.0504472.8514670.051411...0.0800291.2553180.0563170.0565650.0570330.0669030.0514680.0546991.6110460.054593
190090.0519610.0770695.7110640.0514310.0500160.0514970.0696930.0504720.0508320.051490...0.0931340.0515010.0553220.0511991.5924400.0579840.0515500.0570413.2042790.051984
190100.0523280.0500350.0517080.0516964.0515620.0517750.9875260.0505570.0509830.051765...0.0516826.6525120.0503030.0514190.0835640.0522650.0518370.0500060.0523770.054756
190110.0520750.0500320.0581360.7985984.0513700.0549017.3887750.0504980.0528610.051575...0.0579752.6554570.0502780.0533640.0535340.0523060.0516390.0500210.0521180.054182
190120.0522230.0559720.0516310.0640400.0500180.0516950.0516380.0505321.0696967.039859...0.0516070.0516980.0519440.0920370.0513860.0521620.05175526.0603540.0522690.052257
\n", + "

19762 rows × 32 columns

\n", + "
" + ], + "text/plain": [ + " feats: c9990, c9994, c9997 feats: kerberos, u1, u7 \\\n", + "0 0.052992 0.050029 \n", + "1 1.609871 0.050030 \n", + "2 0.051975 0.050030 \n", + "3 0.052089 0.050032 \n", + "4 1.612539 0.050031 \n", + "... ... ... \n", + "19008 0.051856 22.477729 \n", + "19009 0.051961 0.077069 \n", + "19010 0.052328 0.050035 \n", + "19011 0.052075 0.050032 \n", + "19012 0.052223 0.055972 \n", + "\n", + " feats: c528, c5252, c5281 feats: c612, c6121, c6125 \\\n", + "0 0.051413 0.051403 \n", + "1 0.051420 0.051410 \n", + "2 0.557747 0.054309 \n", + "3 0.051534 0.051523 \n", + "4 0.051482 0.051472 \n", + "... ... ... \n", + "19008 7.183005 0.051355 \n", + "19009 5.711064 0.051431 \n", + "19010 0.051708 0.051696 \n", + "19011 0.058136 0.798598 \n", + "19012 0.051631 0.064040 \n", + "\n", + " feats: c2446, c2444, c24464 feats: c13713, c13130, c13134 \\\n", + "0 0.050019 0.061212 \n", + "1 0.050016 0.091486 \n", + "2 0.050016 0.070115 \n", + "3 0.050023 0.069080 \n", + "4 0.050016 0.070362 \n", + "... ... ... \n", + "19008 0.050015 0.061363 \n", + "19009 0.050016 0.051497 \n", + "19010 4.051562 0.051775 \n", + "19011 4.051370 0.054901 \n", + "19012 0.050018 0.051695 \n", + "\n", + " feats: c586, c5866, c5864 feats: c467, c4674, c4667 \\\n", + "0 0.051419 0.050463 \n", + "1 0.051426 0.050465 \n", + "2 0.051457 0.050475 \n", + "3 0.051541 0.050501 \n", + "4 0.051488 0.050485 \n", + "... ... ... \n", + "19008 0.065023 0.050447 \n", + "19009 0.069693 0.050472 \n", + "19010 0.987526 0.050557 \n", + "19011 7.388775 0.050498 \n", + "19012 0.051638 0.050532 \n", + "\n", + " feats: unlock, c1111, c11114 feats: c625, c6257, c6255 ... \\\n", + "0 0.057304 0.051460 ... \n", + "1 0.061034 0.051467 ... \n", + "2 0.060911 0.051500 ... \n", + "3 3.781985 0.051586 ... \n", + "4 0.061027 0.051532 ... \n", + "... ... ... ... \n", + "19008 2.851467 0.051411 ... \n", + "19009 0.050832 0.051490 ... \n", + "19010 0.050983 0.051765 ... \n", + "19011 0.052861 0.051575 ... \n", + "19012 1.069696 7.039859 ... \n", + "\n", + " feats: c5299, c529, c5294 feats: c16616, c16168, c16663 \\\n", + "0 0.051392 0.063923 \n", + "1 0.051399 0.069057 \n", + "2 0.051429 0.068827 \n", + "3 0.051511 0.074502 \n", + "4 0.566014 0.069057 \n", + "... ... ... \n", + "19008 0.080029 1.255318 \n", + "19009 0.093134 0.051501 \n", + "19010 0.051682 6.652512 \n", + "19011 0.057975 2.655457 \n", + "19012 0.051607 0.051698 \n", + "\n", + " feats: microsoft_authentication_package_v1_0, microsoft_authentication_package_v1_, microsoft_authentication_package_v1 \\\n", + "0 0.050267 \n", + "1 0.050285 \n", + "2 0.050290 \n", + "3 0.050296 \n", + "4 0.050295 \n", + "... ... \n", + "19008 0.056317 \n", + "19009 0.055322 \n", + "19010 0.050303 \n", + "19011 0.050278 \n", + "19012 0.051944 \n", + "\n", + " feats: c1065, c10658, c10652 \\\n", + "0 0.125901 \n", + "1 0.062053 \n", + "2 0.061931 \n", + "3 0.089917 \n", + "4 0.062066 \n", + "... ... \n", + "19008 0.056565 \n", + "19009 0.051199 \n", + "19010 0.051419 \n", + "19011 0.053364 \n", + "19012 0.092037 \n", + "\n", + " feats: cachedinteractive, remoteinteractive, interactive \\\n", + "0 1.258763 \n", + "1 1.321739 \n", + "2 1.385945 \n", + "3 2.585125 \n", + "4 1.322035 \n", + "... ... \n", + "19008 0.057033 \n", + "19009 1.592440 \n", + "19010 0.083564 \n", + "19011 0.053534 \n", + "19012 0.051386 \n", + "\n", + " feats: c3888, c3884, u608 feats: c2327, c2323, c2727 \\\n", + "0 0.052462 0.051519 \n", + "1 0.053182 0.051527 \n", + "2 0.053210 0.053094 \n", + "3 0.052992 0.051650 \n", + "4 0.053263 0.073381 \n", + "... ... ... \n", + "19008 0.066903 0.051468 \n", + "19009 0.057984 0.051550 \n", + "19010 0.052265 0.051837 \n", + "19011 0.052306 0.051639 \n", + "19012 0.052162 0.051755 \n", + "\n", + " feats: negotiate, service, c14514 feats: c8282, c8280, c8289 \\\n", + "0 0.051145 0.051962 \n", + "1 0.050077 0.051972 \n", + "2 0.050077 0.059695 \n", + "3 0.051786 0.052132 \n", + "4 0.050077 0.052059 \n", + "... ... ... \n", + "19008 0.054699 1.611046 \n", + "19009 0.057041 3.204279 \n", + "19010 0.050006 0.052377 \n", + "19011 0.050021 0.052118 \n", + "19012 26.060354 0.052269 \n", + "\n", + " feats: c1964, c1968, c25685 \n", + "0 0.054784 \n", + "1 0.057155 \n", + "2 0.057138 \n", + "3 0.056803 \n", + "4 0.057233 \n", + "... ... \n", + "19008 0.054593 \n", + "19009 0.051984 \n", + "19010 0.054756 \n", + "19011 0.054182 \n", + "19012 0.052257 \n", + "\n", + "[19762 rows x 32 columns]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# see how the model has organized features\n", "X = g5._node_features\n", @@ -352,9 +1467,9 @@ "id": "632d6d0f-8212-4f4a-a920-7600d7456351", "metadata": {}, "source": [ - "## Put model into Predict Mode\n", + "## Predict/Online Mode\n", "\n", - "Once a model is fit, can predict on new batches as we demonstrate here\n", + "Once a model is fit, predict on new batches as we demonstrate here\n", "\n", "There are two main methods\n", "\n", @@ -367,10 +1482,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "7b44d418", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "'SuperVectorizer' object has no attribute 'get_feature_names_in''SuperVectorizer' object has no attribute 'get_feature_names_in'" + ] + } + ], "source": [ "# first sample a batch from the normal data (auth=df)\n", "emb_normal, xp_normal, _ = g5.transform_umap(df.sample(200), None, kind='nodes')\n", @@ -380,10 +1503,118 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "d0aebbbc", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xy
09.2325930.724252
15.324008-8.997888
210.624950-0.399632
39.591936-0.037859
413.842589-3.487622
.........
1900810.19344112.514707
190094.766062-1.102680
190109.568494-1.873951
1901111.638880-0.451751
190123.685098-6.050752
\n", + "

19762 rows × 2 columns

\n", + "
" + ], + "text/plain": [ + " x y\n", + "0 9.232593 0.724252\n", + "1 5.324008 -8.997888\n", + "2 10.624950 -0.399632\n", + "3 9.591936 -0.037859\n", + "4 13.842589 -3.487622\n", + "... ... ...\n", + "19008 10.193441 12.514707\n", + "19009 4.766062 -1.102680\n", + "19010 9.568494 -1.873951\n", + "19011 11.638880 -0.451751\n", + "19012 3.685098 -6.050752\n", + "\n", + "[19762 rows x 2 columns]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# all emb's have this form\n", "g5._node_embedding" @@ -391,16 +1622,40 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "8a8d5aa9", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# scatter to see how well it does.\n", "plt.figure(figsize=(10,7))\n", - "plt.scatter(g5._node_embedding.x, g5._node_embedding.y , c='b') # the totality of the fit data\n", + "plt.scatter(g5._node_embedding.x, g5._node_embedding.y , c='b', s=60, alpha=0.5) # the totality of the fit data\n", "plt.scatter(emb_normal.x, emb_normal.y, c='g') # batch of new data\n", - "plt.scatter(emb_red.x, emb_red.y, c='r') # red labels to show good cluster seperation" + "plt.scatter(emb_red.x, emb_red.y, c='r') # red labels to show good cluster seperation\n", + "plt.scatter(emb_normal.x, emb_normal.y, c='g') # batch of new data, to see if they occlude " ] }, { @@ -410,17 +1665,25 @@ "source": [ "## 96% Reduction in Alerts\n", "\n", - "This indicates a huge reduction in the search space needed \n", + "This indicates a huge reduction in the search space needed.\n", "\n", - "Since we have clear cluster assignments along with (post facto) confidences of known anomalous activity, we can reduce the search space on new events (via Kafka, Splunk, etc)" + "Since we have clear cluster assignments along with (post facto) confidences of known anomalous activity, we can reduce the search space on new events (gotten via Kafka, Splunk, etc)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "14d207db-9a58-45a3-9876-058632389f17", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "93.92%\n" + ] + } + ], "source": [ "# percent of RED team labels we get with 10% confidence or above\n", "p = cluster_confidences[cluster_confidences.confidence>0.1].n_red.sum()/cluster_confidences[cluster_confidences.confidence>0.1].total_in_cluster.sum()\n", @@ -429,21 +1692,40 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "id": "755a3f27-935d-4ba8-96cb-cbff11fdf00e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "19071" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# number of data points not to consider (and it's more if we look at df proper!)\n", + "# number of data points *not* to consider (and it's more if we look at df proper!)\n", "cluster_confidences[cluster_confidences.confidence<0.1].total_in_cluster.sum()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "id": "5fd1cc50-0900-4694-8400-c426e314ec2e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Alert Reduction 96.50%\n" + ] + } + ], "source": [ "p = cluster_confidences[cluster_confidences.confidence<0.1].total_in_cluster.sum()/cluster_confidences.total_in_cluster.sum()\n", "print(f'Alert Reduction {100*p:.2f}%')" @@ -451,10 +1733,30 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "0ee508a5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "plt.figure(figsize=(10,7))\n", "plt.plot(np.cumsum([k[2] for k in cluster_confidences.values]))\n", @@ -477,29 +1779,232 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "id": "e0c6a16d-a899-43b6-a7ba-75b45f855a78", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------\n", + "cluster: 2\n", + " red 99.63% or 267.0 out of 268\n", + "--------------------\n", + "cluster: 6\n", + " red 100.00% or 58.0 out of 58\n", + "--------------------\n", + "cluster: 4\n", + " red 97.22% or 35.0 out of 36\n", + "--------------------\n", + "cluster: 8\n", + " red 97.14% or 34.0 out of 35\n", + "--------------------\n", + "cluster: 16\n", + " red 100.00% or 34.0 out of 34\n", + "--------------------\n", + "cluster: 11\n", + " red 100.00% or 31.0 out of 31\n", + "--------------------\n", + "cluster: 24\n", + " red 100.00% or 27.0 out of 27\n", + "--------------------\n", + "cluster: 25\n", + " red 100.00% or 24.0 out of 24\n", + "--------------------\n", + "cluster: 3\n", + " red 100.00% or 19.0 out of 19\n", + "--------------------\n", + "cluster: 7\n", + " red 100.00% or 18.0 out of 18\n", + "--------------------\n", + "cluster: 17\n", + " red 100.00% or 18.0 out of 18\n", + "--------------------\n", + "cluster: 0\n", + " red 100.00% or 17.0 out of 17\n", + "--------------------\n", + "cluster: 12\n", + " red 100.00% or 17.0 out of 17\n", + "--------------------\n", + "cluster: 18\n", + " red 94.12% or 16.0 out of 17\n", + "--------------------\n", + "cluster: 26\n", + " red 100.00% or 17.0 out of 17\n", + "--------------------\n", + "cluster: 14\n", + " red 100.00% or 15.0 out of 15\n", + "--------------------\n", + "cluster: 5\n", + " red 100.00% or 14.0 out of 14\n", + "--------------------\n", + "cluster: 10\n", + " red 100.00% or 14.0 out of 14\n", + "--------------------\n", + "cluster: 1\n", + " red 100.00% or 13.0 out of 13\n", + "--------------------\n", + "cluster: 13\n", + " red 100.00% or 9.0 out of 9\n", + "--------------------\n", + "cluster: 19\n", + " red 100.00% or 9.0 out of 9\n", + "--------------------\n", + "cluster: 15\n", + " red 100.00% or 8.0 out of 8\n", + "--------------------\n", + "cluster: 21\n", + " red 100.00% or 8.0 out of 8\n", + "--------------------\n", + "cluster: 23\n", + " red 87.50% or 7.0 out of 8\n", + "--------------------\n", + "cluster: -1\n", + " red 71.43% or 5.0 out of 7\n", + "--------------------\n", + "cluster: 9\n", + " red 100.00% or 5.0 out of 5\n", + "--------------------\n", + "cluster: 20\n", + " red 100.00% or 5.0 out of 5\n", + "--------------------\n", + "cluster: 22\n", + " red 100.00% or 5.0 out of 5\n", + "CPU times: user 2min 56s, sys: 33.1 s, total: 3min 29s\n", + "Wall time: 1min 24s\n" + ] + } + ], "source": [ "%%time\n", "process = True\n", "if process:\n", + " # ################################## # an example of setting features explicitly, could use ModelDict \n", " g = graphistry.nodes(tdf, 'node')\n", " g6 = g.umap(X=['feats'], y =['RED'], \n", - " min_words=100000, \n", - " cardinality_threshold=2, \n", + " min_words=100000, # set high to bypass sbert encoding\n", + " cardinality_threshold=2, # set low to force topic modeling\n", " n_topics=32,\n", - " use_scaler_target=None)\n", + " use_scaler_target=None) # keep labels unscaled\n", + " # ##################################\n", + " \n", " g6, dbscan6, cluster_confidences6 = enrich(g6)\n", - " g6.build_index()\n", - " g6.save_search_instance('../data/auth-feat-supervised-topic.search')\n", + " \n", + " g6.save_search_instance('auth-feat-supervised-topic.search')\n", "else:\n", " g = graphistry.bind()\n", - " g6 = g.load_search_instance('../data/auth-feat-supervised-topic.search')\n", + " g6 = g.load_search_instance('auth-feat-supervised-topic.search')\n", + " \n", " g6, dbscan6, cluster_confidences6 = enrich(g6)\n" ] }, + { + "cell_type": "code", + "execution_count": 28, + "id": "a98ef657-5307-41d9-ae31-79c1794b3728", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
RED
01
11
21
31
41
......
190080
190090
190100
190110
190120
\n", + "

19762 rows × 1 columns

\n", + "
" + ], + "text/plain": [ + " RED\n", + "0 1\n", + "1 1\n", + "2 1\n", + "3 1\n", + "4 1\n", + "... ...\n", + "19008 0\n", + "19009 0\n", + "19010 0\n", + "19011 0\n", + "19012 0\n", + "\n", + "[19762 rows x 1 columns]" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g6._node_target.astype(int)" + ] + }, { "cell_type": "markdown", "id": "0cc72ab4-c0da-4541-b32b-aa771d6e510f", @@ -508,17 +2013,28 @@ }, "source": [ "### Plot\n", - "Color by `confidence` and hover over `red` team histogram to see where events occur" + "Color by `confidence` and hover over `red` team histogram to see where events occur. Alternatively, color by `cluster` assignment" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "id": "16e09a7d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=426dd5f70ceb45f9bb8e8b8ac45a85ac&type=arrow&viztoken=bfeae91e-2f9e-4e1c-90a4-c968aed1a68e&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672345903&info=true&play=0'" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g6.name('auth 50k topic with supervised umap').plot(render=False)" + "g6.name('auth topic with supervised umap').plot(render=RENDER)" ] }, { @@ -532,53 +2048,633 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "id": "099b9d38", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (19762, 0) in UMAP fit, as it is not one dimensional" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------\n", + "cluster: 2\n", + " red 0.37% or 48.0 out of 12839\n", + "--------------------\n", + "cluster: 4\n", + " red 0.72% or 16.0 out of 2222\n", + "--------------------\n", + "cluster: 30\n", + " red 0.21% or 3.0 out of 1435\n", + "--------------------\n", + "cluster: 3\n", + " red 98.61% or 71.0 out of 72\n", + "--------------------\n", + "cluster: 10\n", + " red 100.00% or 51.0 out of 51\n", + "--------------------\n", + "cluster: 11\n", + " red 98.00% or 49.0 out of 50\n", + "--------------------\n", + "cluster: 14\n", + " red 97.67% or 42.0 out of 43\n", + "--------------------\n", + "cluster: 6\n", + " red 97.50% or 39.0 out of 40\n", + "--------------------\n", + "cluster: 12\n", + " red 97.44% or 38.0 out of 39\n", + "--------------------\n", + "cluster: 9\n", + " red 100.00% or 36.0 out of 36\n", + "--------------------\n", + "cluster: 0\n", + " red 100.00% or 33.0 out of 33\n", + "--------------------\n", + "cluster: 1\n", + " red 96.88% or 31.0 out of 32\n", + "--------------------\n", + "cluster: 18\n", + " red 100.00% or 30.0 out of 30\n", + "--------------------\n", + "cluster: 8\n", + " red 96.55% or 28.0 out of 29\n", + "--------------------\n", + "cluster: 20\n", + " red 96.43% or 27.0 out of 28\n", + "--------------------\n", + "cluster: 26\n", + " red 100.00% or 27.0 out of 27\n", + "--------------------\n", + "cluster: 29\n", + " red 96.00% or 24.0 out of 25\n", + "--------------------\n", + "cluster: 19\n", + " red 90.00% or 18.0 out of 20\n", + "--------------------\n", + "cluster: 5\n", + " red 100.00% or 17.0 out of 17\n", + "--------------------\n", + "cluster: 21\n", + " red 100.00% or 17.0 out of 17\n", + "--------------------\n", + "cluster: 25\n", + " red 100.00% or 13.0 out of 13\n", + "--------------------\n", + "cluster: 24\n", + " red 100.00% or 11.0 out of 11\n", + "--------------------\n", + "cluster: 16\n", + " red 100.00% or 10.0 out of 10\n", + "--------------------\n", + "cluster: 23\n", + " red 100.00% or 10.0 out of 10\n", + "--------------------\n", + "cluster: 7\n", + " red 100.00% or 9.0 out of 9\n", + "--------------------\n", + "cluster: 13\n", + " red 100.00% or 9.0 out of 9\n", + "--------------------\n", + "cluster: 15\n", + " red 88.89% or 8.0 out of 9\n", + "--------------------\n", + "cluster: 17\n", + " red 87.50% or 7.0 out of 8\n", + "--------------------\n", + "cluster: 27\n", + " red 100.00% or 8.0 out of 8\n", + "--------------------\n", + "cluster: 28\n", + " red 100.00% or 8.0 out of 8\n", + "--------------------\n", + "cluster: 31\n", + " red 85.71% or 6.0 out of 7\n", + "--------------------\n", + "cluster: 22\n", + " red 100.00% or 5.0 out of 5\n", + "CPU times: user 2min 39s, sys: 31.4 s, total: 3min 10s\n", + "Wall time: 1min 14s\n" + ] + } + ], "source": [ "%%time\n", "process = True\n", "if process:\n", + " # #####################################\n", " g = graphistry.nodes(tdf, 'node')\n", " g7 = g.umap(X=['feats2'], #y =['RED'], \n", " min_words=100000, \n", " cardinality_threshold=2, \n", " n_topics=32,\n", " use_scaler_target=None)\n", + " # ###################################\n", " g7, dbscan7, cluster_confidences7 = enrich(g7)\n", " g7.build_index()\n", - " g7.save_search_instance('../data/auth-feat-just-ip-topic.search')\n", + " g7.save_search_instance('auth-just-ip-topic.search')\n", "else:\n", - " g7 = graphistry.bind().load_search_instance('../data/auth-feat-just-ip-topic.search')\n", + " g7 = graphistry.bind().load_search_instance('auth-just-ip-topic.search')\n", " g7, dbscan7, cluster_confidences7 = enrich(g7)\n" ] }, { "cell_type": "markdown", "id": "836883cb-bc66-4a40-9ca8-f01fd38b6f2a", - "metadata": {}, + "metadata": { + "tags": [] + }, "source": [ "### Plot\n", - "Color by `confidence` and hover over `red` team histogram to see where events occur" + "Color by `confidence` and hover over `red` team histogram to see where events occur. Alternatively, color by `cluster` assignment" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "id": "c1e586a3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=b85fc0d43e884508ad22d4a1e5daa03b&type=arrow&viztoken=75f5f3cc-b52a-4488-b0eb-9b9941208629&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672345982&info=true&play=0'" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g7.name('auth 50k topic only ips no supervision').plot(render=False)" + "g7.name('auth topic ips-ips only, no supervision').plot(render=RENDER)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "id": "5f93d747", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
feats2: c10555, c8555, c1055feats2: c12222, c12226, c1227feats2: c6665, c6667, c6653feats2: c7703, c7701, c7707feats2: c1992, c1922, c19932feats2: c625, c612, c6125feats2: c9028, c9904, c9283feats2: c3073, c3037, c3074feats2: c2106, c2626, c4210feats2: c11196, c11918, c1111...feats2: c4448, c4444, c4487feats2: c17981, c2980, c1798feats2: c4777, c14777, c4787feats2: c7554, c7519, c5151feats2: c10000, c10008, c10003feats2: c25240, c2524, c2456feats2: c809, c5099, c5809feats2: c1065, c10658, c10656feats2: c1550, c15034, c15615feats2: c8182, c8882, c8889
0-0.34853-0.29446-0.34437-0.31798-0.34102-0.46182-0.33386-0.32362-0.31948-0.32044...-0.36251-0.36984-0.32987-0.286723.88640-0.36522-0.34670-0.15082-0.38757-0.32410
10.09896-0.29610-0.34437-0.31795-0.34092-0.46180-0.333832.52748-0.32073-0.32158...-0.36238-0.36982-0.32984-0.28669-0.29198-0.36519-0.34666-0.32204-0.38862-0.32407
2-0.34853-0.29417-0.344373.43709-0.33256-0.46182-0.00576-0.32411-0.32076-0.32092...-0.36235-0.36985-0.32988-0.28673-0.29119-0.36523-0.34668-0.32147-0.38820-0.32411
3-0.34854-0.29143-0.34437-0.31800-0.34095-0.46183-0.333880.60698-0.320772.87353...-0.36240-0.36985-0.32989-0.28674-0.28688-0.36524-0.34671-0.29361-0.38584-0.32412
4-0.34850-0.27595-0.34437-0.31795-0.34093-0.46180-0.32521-0.324080.41376-0.32181...-0.362390.51644-0.32984-0.28668-0.29225-0.36184-0.34668-0.322240.57418-0.32407
..................................................................
19008-0.34820-0.29900-0.34437-0.31436-0.34085-0.461580.82235-0.32377-0.320492.46634...-0.36230-0.36955-0.32952-0.28627-0.29503-0.36480-0.34654-0.32422-0.39023-0.28972
19009-0.34816-0.30263-0.344370.18120-0.34109-0.461540.81189-0.32372-0.32045-0.32738...-0.36260-0.369512.52929-0.28621-0.29878-0.36475-0.34650-0.32700-0.392510.15501
19010-0.34800-0.30251-0.344376.66342-0.34094-0.46143-0.33312-0.32356-0.32032-0.32727...-0.36242-0.36381-0.32929-0.28598-0.298670.69742-0.34635-0.32689-0.39238-0.32350
19011-0.348171.28021-0.34437-0.31753-0.340740.23228-0.33337-0.323740.70051-0.32308...-0.36216-0.36952-0.32948-0.28623-0.293811.22123-0.34651-0.32332-0.38951-0.32370
190122.91613-0.30214-0.34437-0.31754-0.339192.08866-0.33338-0.323752.35113-0.32740...-0.362630.56537-0.32949-0.28624-0.29880-0.36478-0.34652-0.327010.50155-0.32371
\n", + "

19762 rows × 32 columns

\n", + "
" + ], + "text/plain": [ + " feats2: c10555, c8555, c1055 feats2: c12222, c12226, c1227 \\\n", + "0 -0.34853 -0.29446 \n", + "1 0.09896 -0.29610 \n", + "2 -0.34853 -0.29417 \n", + "3 -0.34854 -0.29143 \n", + "4 -0.34850 -0.27595 \n", + "... ... ... \n", + "19008 -0.34820 -0.29900 \n", + "19009 -0.34816 -0.30263 \n", + "19010 -0.34800 -0.30251 \n", + "19011 -0.34817 1.28021 \n", + "19012 2.91613 -0.30214 \n", + "\n", + " feats2: c6665, c6667, c6653 feats2: c7703, c7701, c7707 \\\n", + "0 -0.34437 -0.31798 \n", + "1 -0.34437 -0.31795 \n", + "2 -0.34437 3.43709 \n", + "3 -0.34437 -0.31800 \n", + "4 -0.34437 -0.31795 \n", + "... ... ... \n", + "19008 -0.34437 -0.31436 \n", + "19009 -0.34437 0.18120 \n", + "19010 -0.34437 6.66342 \n", + "19011 -0.34437 -0.31753 \n", + "19012 -0.34437 -0.31754 \n", + "\n", + " feats2: c1992, c1922, c19932 feats2: c625, c612, c6125 \\\n", + "0 -0.34102 -0.46182 \n", + "1 -0.34092 -0.46180 \n", + "2 -0.33256 -0.46182 \n", + "3 -0.34095 -0.46183 \n", + "4 -0.34093 -0.46180 \n", + "... ... ... \n", + "19008 -0.34085 -0.46158 \n", + "19009 -0.34109 -0.46154 \n", + "19010 -0.34094 -0.46143 \n", + "19011 -0.34074 0.23228 \n", + "19012 -0.33919 2.08866 \n", + "\n", + " feats2: c9028, c9904, c9283 feats2: c3073, c3037, c3074 \\\n", + "0 -0.33386 -0.32362 \n", + "1 -0.33383 2.52748 \n", + "2 -0.00576 -0.32411 \n", + "3 -0.33388 0.60698 \n", + "4 -0.32521 -0.32408 \n", + "... ... ... \n", + "19008 0.82235 -0.32377 \n", + "19009 0.81189 -0.32372 \n", + "19010 -0.33312 -0.32356 \n", + "19011 -0.33337 -0.32374 \n", + "19012 -0.33338 -0.32375 \n", + "\n", + " feats2: c2106, c2626, c4210 feats2: c11196, c11918, c1111 ... \\\n", + "0 -0.31948 -0.32044 ... \n", + "1 -0.32073 -0.32158 ... \n", + "2 -0.32076 -0.32092 ... \n", + "3 -0.32077 2.87353 ... \n", + "4 0.41376 -0.32181 ... \n", + "... ... ... ... \n", + "19008 -0.32049 2.46634 ... \n", + "19009 -0.32045 -0.32738 ... \n", + "19010 -0.32032 -0.32727 ... \n", + "19011 0.70051 -0.32308 ... \n", + "19012 2.35113 -0.32740 ... \n", + "\n", + " feats2: c4448, c4444, c4487 feats2: c17981, c2980, c1798 \\\n", + "0 -0.36251 -0.36984 \n", + "1 -0.36238 -0.36982 \n", + "2 -0.36235 -0.36985 \n", + "3 -0.36240 -0.36985 \n", + "4 -0.36239 0.51644 \n", + "... ... ... \n", + "19008 -0.36230 -0.36955 \n", + "19009 -0.36260 -0.36951 \n", + "19010 -0.36242 -0.36381 \n", + "19011 -0.36216 -0.36952 \n", + "19012 -0.36263 0.56537 \n", + "\n", + " feats2: c4777, c14777, c4787 feats2: c7554, c7519, c5151 \\\n", + "0 -0.32987 -0.28672 \n", + "1 -0.32984 -0.28669 \n", + "2 -0.32988 -0.28673 \n", + "3 -0.32989 -0.28674 \n", + "4 -0.32984 -0.28668 \n", + "... ... ... \n", + "19008 -0.32952 -0.28627 \n", + "19009 2.52929 -0.28621 \n", + "19010 -0.32929 -0.28598 \n", + "19011 -0.32948 -0.28623 \n", + "19012 -0.32949 -0.28624 \n", + "\n", + " feats2: c10000, c10008, c10003 feats2: c25240, c2524, c2456 \\\n", + "0 3.88640 -0.36522 \n", + "1 -0.29198 -0.36519 \n", + "2 -0.29119 -0.36523 \n", + "3 -0.28688 -0.36524 \n", + "4 -0.29225 -0.36184 \n", + "... ... ... \n", + "19008 -0.29503 -0.36480 \n", + "19009 -0.29878 -0.36475 \n", + "19010 -0.29867 0.69742 \n", + "19011 -0.29381 1.22123 \n", + "19012 -0.29880 -0.36478 \n", + "\n", + " feats2: c809, c5099, c5809 feats2: c1065, c10658, c10656 \\\n", + "0 -0.34670 -0.15082 \n", + "1 -0.34666 -0.32204 \n", + "2 -0.34668 -0.32147 \n", + "3 -0.34671 -0.29361 \n", + "4 -0.34668 -0.32224 \n", + "... ... ... \n", + "19008 -0.34654 -0.32422 \n", + "19009 -0.34650 -0.32700 \n", + "19010 -0.34635 -0.32689 \n", + "19011 -0.34651 -0.32332 \n", + "19012 -0.34652 -0.32701 \n", + "\n", + " feats2: c1550, c15034, c15615 feats2: c8182, c8882, c8889 \n", + "0 -0.38757 -0.32410 \n", + "1 -0.38862 -0.32407 \n", + "2 -0.38820 -0.32411 \n", + "3 -0.38584 -0.32412 \n", + "4 0.57418 -0.32407 \n", + "... ... ... \n", + "19008 -0.39023 -0.28972 \n", + "19009 -0.39251 0.15501 \n", + "19010 -0.39238 -0.32350 \n", + "19011 -0.38951 -0.32370 \n", + "19012 0.50155 -0.32371 \n", + "\n", + "[19762 rows x 32 columns]" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "X = g7._get_feature('nodes')\n", "X" @@ -590,14 +2686,18 @@ "metadata": {}, "source": [ "# Conditional Probability\n", - "Let's see if can give us good histograms to tease out red team nodes? This is to baseline the above UMAP models, and we find in retrospect, UMAP wins." + "Let's see if conditiona probability of computer to computer connections can give us good histograms to tease out red team nodes? This is to baseline the above UMAP models, and we find in retrospect, UMAP wins. \n", + "\n", + "The conditional graph is however useful to see aggregate behavior, and coloring by 'red' team shows topology of Infection" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "id": "2d6f58dd", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "g = graphistry.edges(tdf, \"src_computer\", \"dst_computer\")" @@ -605,49 +2705,159 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "54b83f83", - "metadata": {}, + "execution_count": 34, + "id": "f3b44db2-b34e-4398-8c5a-7a10bbe5d681", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "def conditional_probability(x, given, df):\n", - " \"\"\"conditional probability function over categorical variables\n", - " p(x|given) = p(x,given)/p(given)\n", - " \n", - " Args:\n", - " x: the column variable of interest given the column 'given'\n", - " given: the variabe to fix constant\n", - " df: dataframe with columns [given, x]\n", - " Returns:\n", - " pd.DataFrame: the conditional probability of x given the column 'given'\n", - " \"\"\"\n", - " return df.groupby([given])[x].apply(lambda g: g.value_counts()/len(g))\n" + "x='dst_computer'\n", + "given='src_computer'\n", + "cg = g.conditional_graph(x, given, kind='edges')" ] }, { "cell_type": "code", - "execution_count": null, - "id": "fd738336", + "execution_count": 35, + "id": "3b2af6a2-4f10-4707-beb8-4f3447d3e3b8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
src_computerdst_computer_probs
0C1C6121.000000
1C10C100.333333
2C10C29970.333333
3C10C107180.333333
4C100C5280.500000
............
17831C9990C5280.250000
17832C9992C5861.000000
17833C9994C99941.000000
17834C9997C5860.500000
17835C9997C6250.500000
\n", + "

17836 rows × 3 columns

\n", + "
" + ], + "text/plain": [ + " src_computer dst_computer _probs\n", + "0 C1 C612 1.000000\n", + "1 C10 C10 0.333333\n", + "2 C10 C2997 0.333333\n", + "3 C10 C10718 0.333333\n", + "4 C100 C528 0.500000\n", + "... ... ... ...\n", + "17831 C9990 C528 0.250000\n", + "17832 C9992 C586 1.000000\n", + "17833 C9994 C9994 1.000000\n", + "17834 C9997 C586 0.500000\n", + "17835 C9997 C625 0.500000\n", + "\n", + "[17836 rows x 3 columns]" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "x='dst_computer'\n", - "given='src_computer'\n", - "condprobs = conditional_probability(x, given, df=tdf)\n", - "\n", - "cprob = pd.DataFrame(list(condprobs.index), columns=[given, x])\n", - "cprob['_probs'] = condprobs.values" + "# the new edge dataframe assess conditiona prob of computer-to-computer connection\n", + "cprob = cg._edges\n", + "cprob" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "id": "5258aee1", "metadata": {}, "outputs": [], "source": [ - "# now enrich the edges dataframe with the redteam data\n", - "# since cprobs lost those labels during the function cal\n", + "# enrich the edges dataframe with the redteam data\n", + "# since cprobs lost those labels during the function call\n", "indx = cprob.src_computer.isin(red_team.src_computer) & cprob.dst_computer.isin(red_team.dst_computer)\n", "cprob.loc[indx, 'red'] = 1\n", "cprob.loc[~indx, 'red'] = 0" @@ -655,117 +2865,196 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "9b3af1cd-6423-4484-8b99-81fad821f118", + "execution_count": 37, + "id": "7ff921fc-3ecd-4404-acd7-8db943a4ebcc", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
src_computerdst_computer_probsred
0C1C6121.0000000.0
1C10C100.3333330.0
2C10C29970.3333330.0
3C10C107180.3333330.0
4C100C5280.5000000.0
...............
17831C9990C5280.2500000.0
17832C9992C5861.0000000.0
17833C9994C99941.0000000.0
17834C9997C5860.5000000.0
17835C9997C6250.5000000.0
\n", + "

17836 rows × 4 columns

\n", + "
" + ], + "text/plain": [ + " src_computer dst_computer _probs red\n", + "0 C1 C612 1.000000 0.0\n", + "1 C10 C10 0.333333 0.0\n", + "2 C10 C2997 0.333333 0.0\n", + "3 C10 C10718 0.333333 0.0\n", + "4 C100 C528 0.500000 0.0\n", + "... ... ... ... ...\n", + "17831 C9990 C528 0.250000 0.0\n", + "17832 C9992 C586 1.000000 0.0\n", + "17833 C9994 C9994 1.000000 0.0\n", + "17834 C9997 C586 0.500000 0.0\n", + "17835 C9997 C625 0.500000 0.0\n", + "\n", + "[17836 rows x 4 columns]" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# full condprob graph \n", - "cg = graphistry.edges(cprob, x, given).bind(edge_weight='_probs')\n", - "cg.plot(render=False)" - ] - }, - { - "cell_type": "markdown", - "id": "42fb3dff", - "metadata": {}, - "source": [ - "## Learning\n", - "The conditional graph shows that most of the edge probabilities are between 4e-7 and 0.03, whose bucket contains most events. Thus the chances of finding the red team edges are ~ 1e-4 -- slim indeed. UMAP wins." - ] - }, - { - "cell_type": "markdown", - "id": "9d2cd536", - "metadata": {}, - "source": [ - "Likewise the transpose conditional is even worse \n", - "with prob_detection ~ 6e-5" + "cprob" ] }, { "cell_type": "code", - "execution_count": null, - "id": "18eafcff", + "execution_count": 38, + "id": "b4b10152-cac9-4497-b016-dd67b54cdcf2", "metadata": {}, "outputs": [], "source": [ - "# let's repeat but with reverse conditional\n", - "x='src_computer'\n", - "given='dst_computer'\n", - "condprobs2 = conditional_probability(x, given, df=tdf)\n", - "\n", - "cprob2 = pd.DataFrame(list(condprobs2.index), columns=[given, x])\n", - "cprob2['_probs'] = condprobs2.values" + "# add edges back to graphistry instance\n", + "cg._edges = cprob" ] }, { "cell_type": "code", - "execution_count": null, - "id": "74913e34", + "execution_count": 39, + "id": "9b3af1cd-6423-4484-8b99-81fad821f118", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'https://hub.graphistry.com/graph/graph.html?dataset=5e532500da8d459d8a3ef7832a6d6d9a&type=arrow&viztoken=4e311702-17ef-4563-b060-6d631e4a4101&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672345993&info=true'" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# now enrich the edges dataframe with the redteam data\n", - "indx = cprob2.src_computer.isin(red_team.src_computer) & cprob2.dst_computer.isin(red_team.dst_computer)\n", - "cprob2.loc[indx, 'red'] = 1\n", - "cprob2.loc[~indx, 'red'] = 0" + "# full condprob graph\n", + "cg.plot(render=RENDER)" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "22f4ac54", - "metadata": {}, - "outputs": [], - "source": [ - "cg2 = graphistry.edges(cprob2, x, given).bind(edge_weight='_probs')\n", - "cg2.plot(render=False)\n", - "# same conclusion as above..." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "db832e1c", + "cell_type": "markdown", + "id": "42fb3dff", "metadata": {}, - "outputs": [], "source": [ - "# # let's see the probs better:\n", - "# for src in red_team.src_computer.unique():\n", - "# for dst in red_team.dst_computer.unique():\n", - "# if dst in condprobs[src]:\n", - "# print('-'*30)\n", - "# print(f'given src {src} -> dst {dst}')\n", - "# print('-'*10)\n", - "# print(f' {condprobs[src][dst]*100:.2f}%')\n", - "# print()" + "## Learning\n", + "The conditional graph shows that most of the edge probabilities are between 4e-7 and 0.03, whose bucket contains most of the events. Thus the chances of finding the red team edges are ~ 1e-4 -- slim indeed. UMAP wins." ] }, { - "cell_type": "code", - "execution_count": null, - "id": "21f51de6", + "cell_type": "markdown", + "id": "9d2cd536", "metadata": {}, - "outputs": [], "source": [ - "# for dst in red_team.dst_computer.unique():\n", - "# for src in red_team.src_computer.unique():\n", - "# if src in condprobs2[dst]:\n", - "# print('-'*20)\n", - "# print(f'given dst {dst} -> src {src}')\n", - "# print('-'*10)\n", - "# print(f' {condprobs2[dst][src]*100:.2f}%')\n", - "# print()" + "Likewise the transpose conditional is even worse \n", + "with prob_detection ~ 6e-5" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c3a008f6-75ed-4045-b13c-494cb015d185", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 1944afac08cda822a0bdf098b405bf37da881451 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 13:19:11 -0800 Subject: [PATCH 005/432] feat(adds cpu and gpu support for DBSCAN, adds constants, adds DBSCAN Mixin) --- graphistry/constants.py | 5 +++++ graphistry/plotter.py | 4 +++- graphistry/umap_utils.py | 23 ++++++++++++----------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/graphistry/constants.py b/graphistry/constants.py index 1e9f862e92..c7c2bf9bfa 100644 --- a/graphistry/constants.py +++ b/graphistry/constants.py @@ -17,12 +17,17 @@ # ############################################################### # consistent clf pipelining and constructor methods across files DGL_GRAPH = "DGL_graph" +KG_GRAPH = '_kg_graph' FEATURE = "feature" TARGET = "target" LABEL = "label" LABEL_NODES = "node_label" LABEL_EDGES = "edge_label" +# ENGINES +CUML = 'cuml' +UMAP_LEARN = 'umap_learn' + TRAIN_MASK = "train_mask" TEST_MASK = "test_mask" diff --git a/graphistry/plotter.py b/graphistry/plotter.py index 3ed7c06118..00f32dfdeb 100644 --- a/graphistry/plotter.py +++ b/graphistry/plotter.py @@ -8,13 +8,14 @@ from .embed_utils import HeterographEmbedModuleMixin # type: ignore from .text_utils import SearchToGraphMixin # type: ignore from .compute.conditional import ConditionalMixin # type: ignore +from .compute.cluster import ClusterMixin # type: ignore mixins = ([ CosmosMixin, NeptuneMixin, GremlinMixin, HeterographEmbedModuleMixin, SearchToGraphMixin, - DGLGraphMixin, + DGLGraphMixin, ClusterMixin, UMAPMixin, FeatureMixin, ConditionalMixin, LayoutsMixin, @@ -32,6 +33,7 @@ def __init__(self, *args, **kwargs): ConditionalMixin.__init__(self, *args, **kwargs) FeatureMixin.__init__(self, *args, **kwargs) UMAPMixin.__init__(self, *args, **kwargs) + ClusterMixin.__init__(self, *args, **kwargs) DGLGraphMixin.__init__(self, *args, **kwargs) SearchToGraphMixin.__init__(self, *args, **kwargs) HeterographEmbedModuleMixin.__init__(self, *args, **kwargs) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 5cd092f29d..eff2c7ae1a 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -5,6 +5,7 @@ import pandas as pd from . import constants as config +from .constants import CUML, UMAP_LEARN from .feature_utils import (FeatureMixin, Literal, XSymbolic, YSymbolic, prune_weighted_edges_df_and_relabel_nodes, resolve_feature_engine) @@ -73,22 +74,22 @@ def is_legacy_cuml(): return False -UMAPEngineConcrete = Literal["cuml", "umap_learn"] +UMAPEngineConcrete = Literal[CUML, UMAP_LEARN] UMAPEngine = Literal[UMAPEngineConcrete, "auto"] def resolve_umap_engine( engine: UMAPEngine, ) -> UMAPEngineConcrete: # noqa - if engine in ["cuml", "umap_learn"]: + if engine in [CUML, UMAP_LEARN]: return engine # type: ignore if engine in ["auto"]: has_cuml_dependancy_, _, cuml = lazy_cuml_import_has_dependancy() if has_cuml_dependancy_: - return "cuml" + return CUML has_umap_dependancy_, _, _ = lazy_umap_import_has_dependancy() if has_umap_dependancy_: - return "umap_learn" + return UMAP_LEARN raise ValueError( # noqa f'engine expected to be "auto", ' @@ -180,9 +181,9 @@ def umap_lazy_init( ): engine_resolved = resolve_umap_engine(engine) # FIXME remove as set_new_kwargs will always replace? - if engine_resolved == "umap_learn": + if engine_resolved == UMAP_LEARN: _, _, umap_engine = lazy_umap_import_has_dependancy() - elif engine_resolved == "cuml": + elif engine_resolved == CUML: _, _, umap_engine = lazy_cuml_import_has_dependancy() else: raise ValueError( @@ -193,7 +194,7 @@ def umap_lazy_init( umap_kwargs = dict( { "n_components": n_components, - **({"metric": metric} if engine_resolved == "umap_learn" else {}), + **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), "n_neighbors": n_neighbors, "min_dist": min_dist, "spread": spread, @@ -238,7 +239,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info("-" * 90) logger.info(f"Starting UMAP-ing data of shape {X.shape}") - if self.engine == "cuml" and is_legacy_cuml(): + if self.engine == CUML and is_legacy_cuml(): from cuml.neighbors import NearestNeighbors knn = NearestNeighbors(n_neighbors=self.n_neighbors) @@ -431,9 +432,9 @@ def umap( default True. :return: self, with attributes set with new data """ - if engine == "umap_learn": + if engine == UMAP_LEARN: assert_imported() - elif engine == "cuml": + elif engine == CUML: assert_imported_cuml() umap_kwargs = dict( @@ -563,7 +564,7 @@ def umap( res, kind, encode_position, encode_weight, play ) # noqa: E501 - if res.engine == "cuml" and is_legacy_cuml(): + if res.engine == CUML and is_legacy_cuml(): res = res.prune_self_edges() if not inplace: From a07a59d18bca79a3059d984ece6a8c8725cce609 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 13:37:11 -0800 Subject: [PATCH 006/432] adds arguments to PlotterBase, typing coerced to fstring --- graphistry/PlotterBase.py | 6 ++++++ graphistry/umap_utils.py | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 7d22469d8f..81591d8e43 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -165,6 +165,7 @@ def __init__(self, *args, **kwargs): self._bolt_driver : any = None self._tigergraph : any = None + # feature engineering self._node_embedding = None self._node_encoder = None self._node_features = None @@ -190,6 +191,7 @@ def __init__(self, *args, **kwargs): self._weighted_edges_df_from_edges = None self._xy = None + # the fit umap instance self._umap = None self._adjacency = None @@ -201,6 +203,10 @@ def __init__(self, *args, **kwargs): self._use_feat: bool = False self._triplets: Optional[List] = None self._kg_embed_dim: int = 128 + + # Dbscan + self._node_dbscan = None # the fit dbscan instance + self._edge_dbscan = None def __repr__(self): diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index eff2c7ae1a..50d604c348 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -74,7 +74,7 @@ def is_legacy_cuml(): return False -UMAPEngineConcrete = Literal[CUML, UMAP_LEARN] +UMAPEngineConcrete = Literal[f'{CUML}', f'{UMAP_LEARN}'] UMAPEngine = Literal[UMAPEngineConcrete, "auto"] @@ -105,7 +105,6 @@ def resolve_umap_engine( "n_components": 2, "metric": "hellinger", # info metric, can't use on # textual encodings since they contain negative values... - # unless scaling min max etc "n_neighbors": 15, "min_dist": 0.3, "verbose": True, From cc50bb484c18990f45a21b283818fbb201410198 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 13:59:27 -0800 Subject: [PATCH 007/432] adds cluster.py --- graphistry/compute/cluster.py | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 graphistry/compute/cluster.py diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py new file mode 100644 index 0000000000..a4899abf7f --- /dev/null +++ b/graphistry/compute/cluster.py @@ -0,0 +1,81 @@ +import logging +import pandas as pd +from typing import Any, List, Union, TYPE_CHECKING +from typing_extensions import Literal +from collections import Counter + +from graphistry.Engine import Engine +from graphistry.Plottable import Plottable +from graphistry.constants import CUML, UMAP_LEARN # noqa type: ignore + +logger = logging.getLogger("compute.cluster") + +if TYPE_CHECKING: + MIXIN_BASE = Plottable +else: + MIXIN_BASE = object + + +def cluster(g, dbscan, kind='nodes'): + """ + Fits clustering on UMAP embeddings + """ + if kind=='nodes': + df = g._node_embedding + elif kind=='edges': + df = g._edge_embedding + else: + raise ValueError('kind must be one of nodes or edges') + + dbscan.fit(df) + labels = dbscan.labels_ + + if kind=='nodes': + g._nodes['_cluster'] = labels + elif kind=='edges': + g._edges['_cluster'] = labels + else: + raise ValueError('kind must be one of nodes or edges') + + kind = 'node' if kind=='nodes' else 'edge' + setattr(g, f'_{kind}_dbscan', dbscan) + + return g + +class ClusterMixin(MIXIN_BASE): + def __init__(self, *args, **kwargs): + pass + + def _cluster_dbscan(self, res, kind, eps, min_samples, **kwargs): + """ + DBSCAN clustering on cpu or gpu infered by umap's .engine flag + """ + if self.engine == UMAP_LEARN: + try: + from sklearn.cluster import DBSCAN + except ImportError: + raise ImportError('Please install sklearn') + + elif self.engine == CUML: + try: + from cuml import DBSCAN as cuDBSCAN + except ImportError: + raise ImportError('Please install cuml') + else: + raise ValueError(f'engine must be one of {UMAP_LEARN} or {CUML}') + + dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if self.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) + res = cluster(res, dbscan, kind=kind) + + return res + + def dbscan(self, kind='nodes', eps: float = 1., min_samples: int = 1, **kwargs): + """DBSCAN clustering + """ + res = self.bind() + res = self._cluster_dbscan(res, kind=kind, eps=eps, min_samples=min_samples, **kwargs) + + return res + + def _is_cudf(self, df): + return 'cudf' in str(type(df)) From b9df147127244ba7d796aae7f5bfa4f595cd5269 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 15:05:50 -0800 Subject: [PATCH 008/432] testing linteer --- graphistry/umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 50d604c348..6b890ef3a2 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -74,7 +74,7 @@ def is_legacy_cuml(): return False -UMAPEngineConcrete = Literal[f'{CUML}', f'{UMAP_LEARN}'] +UMAPEngineConcrete = Literal['cuml', 'umap_learn'] UMAPEngine = Literal[UMAPEngineConcrete, "auto"] From 070a39139f250c2783da944b7cb3964afeb5d46e Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 15:28:03 -0800 Subject: [PATCH 009/432] lint --- graphistry/compute/cluster.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index a4899abf7f..7892f775d4 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -20,9 +20,9 @@ def cluster(g, dbscan, kind='nodes'): """ Fits clustering on UMAP embeddings """ - if kind=='nodes': + if kind == 'nodes': df = g._node_embedding - elif kind=='edges': + elif kind == 'edges': df = g._edge_embedding else: raise ValueError('kind must be one of nodes or edges') @@ -30,14 +30,14 @@ def cluster(g, dbscan, kind='nodes'): dbscan.fit(df) labels = dbscan.labels_ - if kind=='nodes': + if kind == 'nodes': g._nodes['_cluster'] = labels - elif kind=='edges': + elif kind == 'edges': g._edges['_cluster'] = labels else: raise ValueError('kind must be one of nodes or edges') - kind = 'node' if kind=='nodes' else 'edge' + kind = 'node' if kind == 'nodes' else 'edge' setattr(g, f'_{kind}_dbscan', dbscan) return g From 0e466ec10690300e3ec2a79c1a37dfb92e7348e0 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 15:34:49 -0800 Subject: [PATCH 010/432] lint --- graphistry/umap_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 6b890ef3a2..25fc003e3a 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -86,10 +86,10 @@ def resolve_umap_engine( if engine in ["auto"]: has_cuml_dependancy_, _, cuml = lazy_cuml_import_has_dependancy() if has_cuml_dependancy_: - return CUML + return 'cuml' has_umap_dependancy_, _, _ = lazy_umap_import_has_dependancy() if has_umap_dependancy_: - return UMAP_LEARN + return 'umap_learn' raise ValueError( # noqa f'engine expected to be "auto", ' From 85941a7adddabf80ed94684201365072d803665a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 15:39:06 -0800 Subject: [PATCH 011/432] adds conf pyclass --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 319295df56..5797134b6a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -53,6 +53,7 @@ ('py:class', 'graphistry.layouts.LayoutsMixin'), ('py:class', 'graphistry.compute.ComputeMixin'), ('py:class', 'graphistry.compute.conditional.ConditionalMixin'), + ('py:class', 'graphistry.compute.cluster.ClusterMixin'), ('py:class', 'graphistry.Plottable.Plottable'), ('py:class', 'graphistry.feature_utils.FeatureMixin'), ('py:class', 'graphistry.dgl_utils.DGLGraphMixin'), From bb261fa710bd9246df846e564ab3b847d6deeac1 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 22:33:26 -0800 Subject: [PATCH 012/432] feat(support for dbscan over feature cols or umap embeddings) --- graphistry/compute/cluster.py | 81 ++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 7892f775d4..d3bb1adbd5 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -14,11 +14,32 @@ MIXIN_BASE = Plottable else: MIXIN_BASE = object + + +def lazy_dbscan_import_has_dependency(): + has_min_dependency = True + DBSCAN = None + try: + from sklearn.cluster import DBSCAN + except ImportError: + has_min_dependency = False + logger.warning('Please install sklearn for CPU DBSCAN') + + has_cuml_dependency = True + cuDBSCAN = None + try: + from cuml import DBSCAN as cuDBSCAN + except ImportError: + has_cuml_dependency = False + logger.warning('Please install cuml for GPU DBSCAN') + + return has_min_dependency, DBSCAN, has_cuml_dependency, cuDBSCAN + -def cluster(g, dbscan, kind='nodes'): +def get_umap_embedding_df(g, kind='nodes'): """ - Fits clustering on UMAP embeddings + Returns a dataframe with the UMAP embeddings from the graphistry graph """ if kind == 'nodes': df = g._node_embedding @@ -27,6 +48,30 @@ def cluster(g, dbscan, kind='nodes'): else: raise ValueError('kind must be one of nodes or edges') + return df + +def cluster(g, dbscan, kind='nodes', cols=None, umap=True): + """ + Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe + + args: + g: graphistry graph + kind: 'nodes' or 'edges' + cols: list of columns to use for clustering given `g.featurize` has been run + umap: whether to use UMAP embeddings or features dataframe + """ + + if cols is None: + df = g._get_feature(kind) + else: + df = g.get_features_by_cols(cols, kind) + + if umap and cols is None: + df = get_umap_embedding_df(g, kind) + + print(df.head()) + + dbscan.fit(df) labels = dbscan.labels_ @@ -35,7 +80,7 @@ def cluster(g, dbscan, kind='nodes'): elif kind == 'edges': g._edges['_cluster'] = labels else: - raise ValueError('kind must be one of nodes or edges') + raise ValueError('kind must be one of `nodes` or `edges`') kind = 'node' if kind == 'nodes' else 'edge' setattr(g, f'_{kind}_dbscan', dbscan) @@ -46,34 +91,28 @@ class ClusterMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): pass - def _cluster_dbscan(self, res, kind, eps, min_samples, **kwargs): + def _cluster_dbscan(self, res, kind, cols, umap, eps, min_samples, **kwargs): """ DBSCAN clustering on cpu or gpu infered by umap's .engine flag """ - if self.engine == UMAP_LEARN: - try: - from sklearn.cluster import DBSCAN - except ImportError: - raise ImportError('Please install sklearn') - - elif self.engine == CUML: - try: - from cuml import DBSCAN as cuDBSCAN - except ImportError: - raise ImportError('Please install cuml') - else: - raise ValueError(f'engine must be one of {UMAP_LEARN} or {CUML}') + _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if self.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) - res = cluster(res, dbscan, kind=kind) + res = cluster(res, dbscan, kind=kind, cols=cols, umap=umap) return res - def dbscan(self, kind='nodes', eps: float = 1., min_samples: int = 1, **kwargs): - """DBSCAN clustering + + def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_samples: int = 1, **kwargs): + """DBSCAN clustering on cpu or gpu infered by umap's .engine flag + + Args: + kind: 'nodes' or 'edges' + cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip172', 'location', 'asn', 'warnings'] + umap: whether to use UMAP embeddings or features dataframe """ res = self.bind() - res = self._cluster_dbscan(res, kind=kind, eps=eps, min_samples=min_samples, **kwargs) + res = res._cluster_dbscan(res, kind=kind, cols=cols, umap=umap, eps=eps, min_samples=min_samples, **kwargs) return res From ef0e9b2ecd55edf392e11212108c601ea1e42370 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 23:20:28 -0800 Subject: [PATCH 013/432] adds streamline methods between mixins --- graphistry/compute/ComputeMixin.py | 3 +++ graphistry/compute/cluster.py | 35 +++++++++++++++--------------- graphistry/umap_utils.py | 8 +++++++ 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/graphistry/compute/ComputeMixin.py b/graphistry/compute/ComputeMixin.py index 8fd9895b95..7a9b2f71c7 100644 --- a/graphistry/compute/ComputeMixin.py +++ b/graphistry/compute/ComputeMixin.py @@ -347,6 +347,9 @@ def collapse( :param node: start `node` to begin traversal :param attribute: the given `attribute` to collapse over within `column` :param column: the `column` of nodes DataFrame that contains `attribute` to collapse over + :param self_edges: whether to include self edges in the collapsed graph + :param unwrap: whether to unwrap the collapsed graph into a single node + :param verbose: whether to print out collapse summary information :returns:A new Graphistry instance with nodes and edges DataFrame containing collapsed nodes and edges given by column attribute -- nodes and edges DataFrames contain six new columns `collapse_{node | edges}` and `final_{node | edges}`, while original (node, src, dst) columns are left untouched :rtype: Plottable diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index d3bb1adbd5..85f69aa01c 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -23,7 +23,7 @@ def lazy_dbscan_import_has_dependency(): from sklearn.cluster import DBSCAN except ImportError: has_min_dependency = False - logger.warning('Please install sklearn for CPU DBSCAN') + logger.info('Please install sklearn for CPU DBSCAN') has_cuml_dependency = True cuDBSCAN = None @@ -31,24 +31,11 @@ def lazy_dbscan_import_has_dependency(): from cuml import DBSCAN as cuDBSCAN except ImportError: has_cuml_dependency = False - logger.warning('Please install cuml for GPU DBSCAN') + logger.info('Please install cuml for GPU DBSCAN') return has_min_dependency, DBSCAN, has_cuml_dependency, cuDBSCAN - - -def get_umap_embedding_df(g, kind='nodes'): - """ - Returns a dataframe with the UMAP embeddings from the graphistry graph - """ - if kind == 'nodes': - df = g._node_embedding - elif kind == 'edges': - df = g._edge_embedding - else: - raise ValueError('kind must be one of nodes or edges') - - return df + def cluster(g, dbscan, kind='nodes', cols=None, umap=True): """ @@ -67,9 +54,9 @@ def cluster(g, dbscan, kind='nodes', cols=None, umap=True): df = g.get_features_by_cols(cols, kind) if umap and cols is None: - df = get_umap_embedding_df(g, kind) + df = g._get_embedding(kind) - print(df.head()) + #print(df.head()) dbscan.fit(df) @@ -106,10 +93,22 @@ def _cluster_dbscan(self, res, kind, cols, umap, eps, min_samples, **kwargs): def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_samples: int = 1, **kwargs): """DBSCAN clustering on cpu or gpu infered by umap's .engine flag + g2 = g.featurize().dbscan(kind='nodes', cols=None, umap=True, eps=1., min_samples=1, **kwargs) + print(g2._nodes['_cluster']) + + # cluster by 'ip172' and 'location', for example + g2 = g.featurize().dbscan(cols=['column_attribute1', 'column_attribute2'], **kwargs) + + # cluster by UMAP embeddings + g2 = g.umap().dbscan() + Args: kind: 'nodes' or 'edges' cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip172', 'location', 'asn', 'warnings'] umap: whether to use UMAP embeddings or features dataframe + eps: The maximum distance between two samples for them to be considered as in the same neighborhood. + min_samples: The number of samples (or total integer weight) in a neighborhood for a point to be considered as a core point. This includes the point itself. + """ res = self.bind() res = res._cluster_dbscan(res, kind=kind, cols=cols, umap=umap, eps=eps, min_samples=min_samples, **kwargs) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 25fc003e3a..873c194df6 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -229,6 +229,14 @@ def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): "as it is not one dimensional" ) return None + + def _get_embedding(self, kind='nodes'): + if kind == 'nodes': + return self._node_embedding + elif kind == 'edges': + return self._edge_embedding + else: + raise ValueError('kind must be one of nodes or edges') def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: From e16c186a859a48b542fe7686c65eda37dc3382a5 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 23:21:07 -0800 Subject: [PATCH 014/432] adds tests --- graphistry/tests/test_cluster.py | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 graphistry/tests/test_cluster.py diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py new file mode 100644 index 0000000000..a6ee965ced --- /dev/null +++ b/graphistry/tests/test_cluster.py @@ -0,0 +1,51 @@ +import pandas as pd +import unittest +import pytest +import graphistry +import pandas as pd + + +from graphistry.compute.cluster import lazy_dbscan_import_has_dependency + +has_dbscan, _, has_gpu_dbscan, _ = lazy_dbscan_import_has_dependency() + + +ndf = edf = pd.DataFrame({'src': [1, 2, 3], 'dst': [4, 5, 6]}) +edf_umap = pd.DataFrame({'src': [1, 2, 3], 'dst': [4, 5, 6], 'x': [1, 2, 3], 'y': [4, 5, 6]}) + +node_embedding = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]}) +edge_embedding = node_embedding + +class TestComputeCluster(unittest.TestCase): + + @pytest.mark.skipif(not has_dbscan, reason="requires DGL dependencies") + def test_umap_node_cluster(self): + g = graphistry.nodes(ndf) + g = g.umap(kind='nodes').dbscan(kind='nodes') + self.assertTrue('_cluster' in g._nodes) + self.assertTrue(g._node_dbscan is not None) + + @pytest.mark.skipif(not has_dbscan, reason="requires DGL dependencies") + def test_umap_edge_cluster(self): + g = graphistry.bind(source='src', destination='dst').edges(edf) + g = g.umap(kind='edges').dbscan(kind='edges') + self.assertTrue('_cluster' in g._edges) + self.assertTrue(g._edge_dbscan is not None) + + @pytest.mark.skipif(not has_dbscan, reason="requires DGL dependencies") + def test_featurize_edge_cluster(self): + g = graphistry.bind(source='src', destination='dst').edges(edf).nodes(ndf) + for kind in ['nodes', 'edges']: + g = g.featurize(kind=kind).dbscan(kind=kind) + if kind=='nodes': + self.assertTrue(g._node_dbscan is not None) + self.assertTrue('_cluster' in g._nodes) + else: + self.assertTrue(g._edge_dbscan is not None) + self.assertTrue('_cluster' in g._edges) + + + +if __name__=='__main__': + unittest.main() + \ No newline at end of file From 46c0a4586daba09cd8ea7b97f3c01f9ae9138c9a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 23:37:28 -0800 Subject: [PATCH 015/432] adds check if umap has been fit so that g.featurize().dbscan() chains properly on argument defaults --- graphistry/compute/cluster.py | 3 +-- graphistry/tests/test_cluster.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 85f69aa01c..4dbaee9522 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -53,12 +53,11 @@ def cluster(g, dbscan, kind='nodes', cols=None, umap=True): else: df = g.get_features_by_cols(cols, kind) - if umap and cols is None: + if umap and cols is None and g._umap is not None: df = g._get_embedding(kind) #print(df.head()) - dbscan.fit(df) labels = dbscan.labels_ diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index a6ee965ced..1152b5c5c3 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -2,7 +2,6 @@ import unittest import pytest import graphistry -import pandas as pd from graphistry.compute.cluster import lazy_dbscan_import_has_dependency @@ -37,7 +36,7 @@ def test_featurize_edge_cluster(self): g = graphistry.bind(source='src', destination='dst').edges(edf).nodes(ndf) for kind in ['nodes', 'edges']: g = g.featurize(kind=kind).dbscan(kind=kind) - if kind=='nodes': + if kind == 'nodes': self.assertTrue(g._node_dbscan is not None) self.assertTrue('_cluster' in g._nodes) else: @@ -46,6 +45,7 @@ def test_featurize_edge_cluster(self): -if __name__=='__main__': +if __name__ == '__main__': unittest.main() + \ No newline at end of file From 665c130357a0a61eb8518c05358931e456000aa5 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 23:39:18 -0800 Subject: [PATCH 016/432] lint --- graphistry/tests/test_cluster.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index 1152b5c5c3..609656a8e6 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -44,7 +44,6 @@ def test_featurize_edge_cluster(self): self.assertTrue('_cluster' in g._edges) - if __name__ == '__main__': unittest.main() From 7be92021be7771cd056de908f65b2fe5df77d691 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 23:40:54 -0800 Subject: [PATCH 017/432] lint --- graphistry/tests/test_cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index 609656a8e6..11b773af4c 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -46,5 +46,5 @@ def test_featurize_edge_cluster(self): if __name__ == '__main__': unittest.main() + - \ No newline at end of file From 2b6d966b4b0755b7419a8463a49537186d371901 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Dec 2022 23:42:28 -0800 Subject: [PATCH 018/432] lint --- graphistry/tests/test_cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index 11b773af4c..d640283613 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -47,4 +47,4 @@ def test_featurize_edge_cluster(self): if __name__ == '__main__': unittest.main() - +1 From bc813a2190335fe7aef44305178317529c6820d6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 00:18:22 -0800 Subject: [PATCH 019/432] feat(resolves cpu gpu Literal), fixes .featurize().dbscan() --- .github/workflows/ci.yml | 5 ++++ graphistry/compute/cluster.py | 25 ++++++++++++++-- graphistry/tests/test_cluster.py | 49 ++++++++++++++------------------ 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41fdee0554..61a1447280 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,6 +199,11 @@ jobs: source pygraphistry/bin/activate ./bin/typecheck.sh + - name: Full dbscan tests (rich featurize) + run: | + source pygraphistry/bin/activate + ./bin/test-dbscan.sh + - name: Full feature tests (rich featurize) run: | source pygraphistry/bin/activate diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 4dbaee9522..aa910afb32 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -14,6 +14,9 @@ MIXIN_BASE = Plottable else: MIXIN_BASE = object + +DBSCANEngineConcrete = Literal['cuml', 'umap_learn'] +DBSCANEngine = Literal[DBSCANEngineConcrete, "auto"] def lazy_dbscan_import_has_dependency(): @@ -36,6 +39,25 @@ def lazy_dbscan_import_has_dependency(): return has_min_dependency, DBSCAN, has_cuml_dependency, cuDBSCAN + +def resolve_cpu_gpu_engine( + engine: DBSCANEngine, +) -> DBSCANEngineConcrete: # noqa + if engine in [CUML, UMAP_LEARN]: + return engine # type: ignore + if engine in ["auto"]: + has_min_dependency, _, has_cuml_dependency, _ = lazy_dbscan_import_has_dependency() + if has_cuml_dependency: + return CUML + if has_min_dependency: + return UMAP_LEARN + + raise ValueError( # noqa + f'engine expected to be "auto", ' + '"umap_learn", or "cuml" ' + f"but received: {engine} :: {type(engine)}" + ) + def cluster(g, dbscan, kind='nodes', cols=None, umap=True): """ @@ -56,8 +78,6 @@ def cluster(g, dbscan, kind='nodes', cols=None, umap=True): if umap and cols is None and g._umap is not None: df = g._get_embedding(kind) - #print(df.head()) - dbscan.fit(df) labels = dbscan.labels_ @@ -82,6 +102,7 @@ def _cluster_dbscan(self, res, kind, cols, umap, eps, min_samples, **kwargs): DBSCAN clustering on cpu or gpu infered by umap's .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() + self.engine = resolve_cpu_gpu_engine("auto") dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if self.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) res = cluster(res, dbscan, kind=kind, cols=cols, umap=umap) diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index d640283613..593c088079 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -9,39 +9,32 @@ has_dbscan, _, has_gpu_dbscan, _ = lazy_dbscan_import_has_dependency() -ndf = edf = pd.DataFrame({'src': [1, 2, 3], 'dst': [4, 5, 6]}) -edf_umap = pd.DataFrame({'src': [1, 2, 3], 'dst': [4, 5, 6], 'x': [1, 2, 3], 'y': [4, 5, 6]}) - -node_embedding = pd.DataFrame({'x': [1, 2, 3], 'y': [4, 5, 6]}) -edge_embedding = node_embedding +ndf = edf = pd.DataFrame({'src': [1, 2, 3, 4], 'dst': [4, 5, 6, 1]}) class TestComputeCluster(unittest.TestCase): - @pytest.mark.skipif(not has_dbscan, reason="requires DGL dependencies") - def test_umap_node_cluster(self): - g = graphistry.nodes(ndf) - g = g.umap(kind='nodes').dbscan(kind='nodes') - self.assertTrue('_cluster' in g._nodes) - self.assertTrue(g._node_dbscan is not None) - - @pytest.mark.skipif(not has_dbscan, reason="requires DGL dependencies") - def test_umap_edge_cluster(self): - g = graphistry.bind(source='src', destination='dst').edges(edf) - g = g.umap(kind='edges').dbscan(kind='edges') - self.assertTrue('_cluster' in g._edges) - self.assertTrue(g._edge_dbscan is not None) - - @pytest.mark.skipif(not has_dbscan, reason="requires DGL dependencies") + def _condition(self, g, kind): + if kind == 'nodes': + self.assertTrue(g._node_dbscan is not None) + self.assertTrue('_cluster' in g._nodes) + else: + self.assertTrue(g._edge_dbscan is not None) + self.assertTrue('_cluster' in g._edges) + + @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") + def test_umap_cluster(self): + for kind in ['nodes', 'edges']: + g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') + g = g.umap(kind=kind, n_topics=2).dbscan(kind=kind) + self._condition(g, kind) + + + @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") def test_featurize_edge_cluster(self): - g = graphistry.bind(source='src', destination='dst').edges(edf).nodes(ndf) + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf) for kind in ['nodes', 'edges']: - g = g.featurize(kind=kind).dbscan(kind=kind) - if kind == 'nodes': - self.assertTrue(g._node_dbscan is not None) - self.assertTrue('_cluster' in g._nodes) - else: - self.assertTrue(g._edge_dbscan is not None) - self.assertTrue('_cluster' in g._edges) + g = g.featurize(kind=kind, n_topics=2).dbscan(kind=kind) + self._condition(g, kind) if __name__ == '__main__': From 49c78f6a4f2aea6d1daaa11e45e22829c37abeaa Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 00:22:12 -0800 Subject: [PATCH 020/432] lint --- graphistry/compute/cluster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index aa910afb32..0eb27fd497 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -48,9 +48,9 @@ def resolve_cpu_gpu_engine( if engine in ["auto"]: has_min_dependency, _, has_cuml_dependency, _ = lazy_dbscan_import_has_dependency() if has_cuml_dependency: - return CUML + return 'cuml' if has_min_dependency: - return UMAP_LEARN + return 'umap_learn' raise ValueError( # noqa f'engine expected to be "auto", ' From 5bf60428d6ab0d36038427776f035fca7341bac9 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 00:28:41 -0800 Subject: [PATCH 021/432] adds bin/test-dscan.sh --- bin/test-dbscan.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 bin/test-dbscan.sh diff --git a/bin/test-dbscan.sh b/bin/test-dbscan.sh new file mode 100644 index 0000000000..c1b6ebfc2e --- /dev/null +++ b/bin/test-dbscan.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -ex + +# Run from project root +# - Args get passed to pytest phase +# Non-zero exit code on fail + +# Assume [umap-learn,test] + +python -m pytest --version + +python -B -m pytest -vv \ + graphistry/tests/test_cluster.py + +#chmod +x bin/test-embed.sh \ No newline at end of file From c4dcc1b6948c20b12a02ad9ab49ecf7761fbb23c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 00:42:00 -0800 Subject: [PATCH 022/432] chmod bin/test-dbscan.sh, adds doc --- bin/test-dbscan.sh | 0 graphistry/compute/cluster.py | 9 +++++---- 2 files changed, 5 insertions(+), 4 deletions(-) mode change 100644 => 100755 bin/test-dbscan.sh diff --git a/bin/test-dbscan.sh b/bin/test-dbscan.sh old mode 100644 new mode 100755 diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 0eb27fd497..4152bff285 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -102,21 +102,22 @@ def _cluster_dbscan(self, res, kind, cols, umap, eps, min_samples, **kwargs): DBSCAN clustering on cpu or gpu infered by umap's .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() - self.engine = resolve_cpu_gpu_engine("auto") - dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if self.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) + res.engine = resolve_cpu_gpu_engine("auto") + + dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if res.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) res = cluster(res, dbscan, kind=kind, cols=cols, umap=umap) return res def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_samples: int = 1, **kwargs): - """DBSCAN clustering on cpu or gpu infered by umap's .engine flag + """DBSCAN clustering on cpu or gpu infered automatically g2 = g.featurize().dbscan(kind='nodes', cols=None, umap=True, eps=1., min_samples=1, **kwargs) print(g2._nodes['_cluster']) - # cluster by 'ip172' and 'location', for example + # cluster by 'column_attribute1=ip172' and 'column_attribute2=location', for example g2 = g.featurize().dbscan(cols=['column_attribute1', 'column_attribute2'], **kwargs) # cluster by UMAP embeddings From 278121b3b453a052c5f539f84b359b20e554f317 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 09:09:47 -0800 Subject: [PATCH 023/432] feat(adds dbscan within umap call), adds test --- graphistry/compute/cluster.py | 2 +- graphistry/tests/test_cluster.py | 16 +++++++++------- graphistry/umap_utils.py | 19 +++++++++++-------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 4152bff285..9a3fd7c05a 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -128,7 +128,7 @@ def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_ cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip172', 'location', 'asn', 'warnings'] umap: whether to use UMAP embeddings or features dataframe eps: The maximum distance between two samples for them to be considered as in the same neighborhood. - min_samples: The number of samples (or total integer weight) in a neighborhood for a point to be considered as a core point. This includes the point itself. + min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. """ res = self.bind() diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index 593c088079..d2fc31ca66 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -23,15 +23,17 @@ def _condition(self, g, kind): @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") def test_umap_cluster(self): + g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') for kind in ['nodes', 'edges']: - g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') - g = g.umap(kind=kind, n_topics=2).dbscan(kind=kind) - self._condition(g, kind) - + g2 = g.umap(kind=kind, n_topics=2, dbscan=False).dbscan(kind=kind) + self._condition(g2, kind) + g3 = g.umap(kind=kind, n_topics=2, dbscan=True) + self._condition(g3, kind) + self.assertEqual(g2._nodes['_cluster'].tolist(), g3._nodes['_cluster'].tolist()) @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") - def test_featurize_edge_cluster(self): - g = graphistry.edges(edf, 'src', 'dst').nodes(ndf) + def test_featurize_cluster(self): + g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') for kind in ['nodes', 'edges']: g = g.featurize(kind=kind, n_topics=2).dbscan(kind=kind) self._condition(g, kind) @@ -40,4 +42,4 @@ def test_featurize_edge_cluster(self): if __name__ == '__main__': unittest.main() -1 + diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 873c194df6..99dfa5c891 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -84,7 +84,7 @@ def resolve_umap_engine( if engine in [CUML, UMAP_LEARN]: return engine # type: ignore if engine in ["auto"]: - has_cuml_dependancy_, _, cuml = lazy_cuml_import_has_dependancy() + has_cuml_dependancy_, _, _ = lazy_cuml_import_has_dependancy() if has_cuml_dependancy_: return 'cuml' has_umap_dependancy_, _, _ = lazy_umap_import_has_dependancy() @@ -390,6 +390,7 @@ def umap( play: Optional[int] = 0, encode_position: bool = True, encode_weight: bool = True, + dbscan: bool = True, engine: UMAPEngine = "auto", inplace: bool = False, feature_engine: str = "auto", @@ -411,8 +412,8 @@ def umap( implicit UMAP, default True. :param encode_position: whether to set default plotting bindings -- positions x,y from umap for .plot() - :param X: either an ndarray of features, or column names to featurize - :param y: either an ndarray of targets, or column names to featurize + :param X: either a dataframe ndarray of features, or column names to featurize + :param y: either an dataframe ndarray of targets, or column names to featurize targets :param scale: multiplicative scale for pruning weighted edge DataFrame gotten from UMAP, between [0, ..) with high end meaning keep @@ -432,9 +433,10 @@ def umap( en/latest/parameters.html] documentation for more. :param suffix: optional suffix to add to x, y attributes of umap. :param play: Graphistry play parameter, default 0, how much to evolve - the network during clustering + the network during clustering. 0 preserves the original UMAP layout. + :param dbscan: whether to run DBSCAN on the UMAP embedding, default True. :param engine: selects which engine to use to calculate UMAP: - NotImplemented yet, default UMAP-LEARN + default "auto" will use cuML if available, otherwise UMAP-LEARN. :param memoize: whether to memoize the results of this method, default True. :return: self, with attributes set with new data @@ -463,7 +465,6 @@ def umap( res = self.bind() res.umap_lazy_init(engine=engine, suffix=suffix) - # res.suffix = suffix logger.debug("umap input X :: %s", X) logger.debug("umap input y :: %s", y) @@ -471,8 +472,7 @@ def umap( featurize_kwargs = self._set_features( res, X, y, kind, feature_engine, {**featurize_kwargs, "memoize": memoize} ) - # umap_kwargs = {**umap_kwargs, - # 'featurize_kwargs': featurize_kwargs or {}} + if kind == "nodes": if res._node is None: @@ -574,6 +574,9 @@ def umap( if res.engine == CUML and is_legacy_cuml(): res = res.prune_self_edges() + if dbscan: + res = res.dbscan(kind=kind, umap=True) + if not inplace: return res From 5f568943b4f68346b2d4a7b5ea7bea0e30c088c5 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 09:18:46 -0800 Subject: [PATCH 024/432] lint --- graphistry/compute/cluster.py | 15 ++++++++++++--- graphistry/tests/test_cluster.py | 3 +-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 9a3fd7c05a..2a5ac4f218 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -114,14 +114,23 @@ def _cluster_dbscan(self, res, kind, cols, umap, eps, min_samples, **kwargs): def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_samples: int = 1, **kwargs): """DBSCAN clustering on cpu or gpu infered automatically - g2 = g.featurize().dbscan(kind='nodes', cols=None, umap=True, eps=1., min_samples=1, **kwargs) + Examples: + # cluster by feature embeddings + g2 = g.featurize().dbscan(kind='nodes', eps=1., min_samples=1, **kwargs) print(g2._nodes['_cluster']) - # cluster by 'column_attribute1=ip172' and 'column_attribute2=location', for example - g2 = g.featurize().dbscan(cols=['column_attribute1', 'column_attribute2'], **kwargs) + # cluster by a given set of feature column attributes + # 'column_attribute1=ip_172' and 'column_attribute2=location', for example + g2 = g.featurize().dbscan(cols=['ip_172', 'location'], **kwargs) # cluster by UMAP embeddings g2 = g.umap().dbscan() + + # dbscan with fixed parameters is default in umap + g2 = g.umap(dbscan=True) + + # with greater control over parameters via chaining, + g2 = g.umap().dbscan(eps=1.0, min_samples=2, **kwargs) Args: kind: 'nodes' or 'edges' diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index d2fc31ca66..b98fe77bc8 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -41,5 +41,4 @@ def test_featurize_cluster(self): if __name__ == '__main__': unittest.main() - - + \ No newline at end of file From a336217f2128b4b5460c0599742d36199600b0a0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 09:21:44 -0800 Subject: [PATCH 025/432] lint --- graphistry/tests/test_cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index b98fe77bc8..4c39dced35 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -41,4 +41,4 @@ def test_featurize_cluster(self): if __name__ == '__main__': unittest.main() - \ No newline at end of file + From c4f6cffe1d28e5e2fbbdfdfd58c3030798a870de Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 09:25:33 -0800 Subject: [PATCH 026/432] lint --- graphistry/umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 99dfa5c891..fda5a96060 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -575,7 +575,7 @@ def umap( res = res.prune_self_edges() if dbscan: - res = res.dbscan(kind=kind, umap=True) + res = res.dbscan(kind=kind, umap=True) # type: ignore if not inplace: return res From 12c2ea76895bc8e591e662f2e940bff9f3358153 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 09:36:24 -0800 Subject: [PATCH 027/432] adds dbscan=False flag in umap tests --- bin/test-dbscan.sh | 2 +- graphistry/tests/test_umap_utils.py | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/bin/test-dbscan.sh b/bin/test-dbscan.sh index c1b6ebfc2e..0bc204cbdc 100755 --- a/bin/test-dbscan.sh +++ b/bin/test-dbscan.sh @@ -12,4 +12,4 @@ python -m pytest --version python -B -m pytest -vv \ graphistry/tests/test_cluster.py -#chmod +x bin/test-embed.sh \ No newline at end of file +#chmod +x bin/test-dbscan.sh \ No newline at end of file diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index ca0c3897ba..218ab8d24a 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -102,14 +102,6 @@ def setUp(self): self.EMBe = g2._edge_embedding self.embe, self.xe, self.ye = g2.transform_umap(edge_df22, ydf=edge2_target_df, kind='edges') - # @pytest.mark.skipif(not has_dependancy, reason="requires umap feature dependencies") - # def test_allclose_fit_transform_on_same_data(self): - # check_allclose_fit_transform_on_same_data(self.X, self.x, self.Y, self.y) - # check_allclose_fit_transform_on_same_data(self.Xe, self.xe, self.Ye, self.ye) - - # check_allclose_fit_transform_on_same_data(self.EMB, self.emb, None, None) - # check_allclose_fit_transform_on_same_data(self.EMBe, self.embe, None, None) - @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_columns_match(self): assert all(self.X.columns == self.x.columns), 'Node Feature Columns do not match' @@ -195,6 +187,7 @@ def _test_umap(self, g, use_cols, targets, name, kind, df): model_name=model_avg_name, feature_engine=feature_engine, n_neighbors=2, + dbscan=False ) self.cases_test_graph(g2, kind=kind, df=df) @@ -272,7 +265,8 @@ def _test_umap(self, g, use_cols, targets, name, kind, df): engine='umap_learn', cardinality_threshold=cardinality, cardinality_threshold_target=cardinality, - n_neighbors=3) + n_neighbors=3, + dbscan=False) self.cases_test_graph(g2, kind=kind, df=df) From e24efcf0feb4522a6930f13322396fb77309eb84 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 09:54:38 -0800 Subject: [PATCH 028/432] fix umap tests --- graphistry/tests/test_umap_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 218ab8d24a..b0aef2432c 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -321,12 +321,12 @@ def test_edge_umap(self): ) def test_chaining_nodes(self): g = graphistry.nodes(ndf_reddit) - g2 = g.umap() + g2 = g.umap(dbscan=False) logger.debug('======= g.umap() done ======') g3a = g2.featurize() logger.debug('======= g3a.featurize() done ======') - g3 = g3a.umap() + g3 = g3a.umap(dbscan=False) logger.debug('======= g3.umap() done ======') assert g2._node_features.shape == g3._node_features.shape # since g3 has feature params with x and y. @@ -346,8 +346,8 @@ def test_chaining_edges(self): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) - g2 = g.umap(kind='edges') - g3 = g.featurize(kind='edges').umap(kind='edges') + g2 = g.umap(kind='edges', dbscan=False) + g3 = g.featurize(kind='edges').umap(kind='edges', dbscan=False) assert all(g2._feature_params['edges']['X'] == g3._feature_params['edges']['X']) assert all(g2._feature_params['edges']['y'] == g3._feature_params['edges']['y']) # None From f8337e9184aa1b40ed507b0e22e4b24a55f29048 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 11:04:18 -0800 Subject: [PATCH 029/432] doc(adds readme and doc edits) --- README.md | 30 +++++++++++++++++++++++++++ graphistry/compute/cluster.py | 39 ++++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 00ed8c5e2f..180d8f099e 100644 --- a/README.md +++ b/README.md @@ -554,6 +554,36 @@ See `help(g.search_graph)` for options See `help(g.embed)`, `help(g.predict_links)` , `help(g.predict_links_all)` for options +### DBSCAN + +* Enrich UMAP embeddings or featurization dataframe with GPU or CPU DBSCAN + + ```python + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') + + # cluster by UMAP embeddings + kind = 'nodes' | 'edges' + g2 = g.umap(kind=kind).dbscan(kind=kind) + print(g2._nodes['_cluster']) | print(g2._edges['_cluster']) + + # dbscan with fixed parameters is default in umap + g2 = g.umap(dbscan=True) + + # and with greater control over parameters via chaining, + g2 = g.umap().dbscan(eps=1.2, min_samples=2, **kwargs) + + # cluster by feature embeddings + g2 = g.featurize().dbscan(**kwargs) + + # cluster by a given set of feature column attributes + g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], **kwargs) + + # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) + g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) + g2.plot() # color by `_cluster` + ``` + + ### Quickly configurable diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 2a5ac4f218..6ba1d91071 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -115,26 +115,37 @@ def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_ """DBSCAN clustering on cpu or gpu infered automatically Examples: - # cluster by feature embeddings - g2 = g.featurize().dbscan(kind='nodes', eps=1., min_samples=1, **kwargs) - print(g2._nodes['_cluster']) - - # cluster by a given set of feature column attributes - # 'column_attribute1=ip_172' and 'column_attribute2=location', for example - g2 = g.featurize().dbscan(cols=['ip_172', 'location'], **kwargs) + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') # cluster by UMAP embeddings - g2 = g.umap().dbscan() - + kind = 'nodes' | 'edges' + g2 = g.umap(kind=kind).dbscan(kind=kind) + print(g2._nodes['_cluster']) | print(g2._edges['_cluster']) + # dbscan with fixed parameters is default in umap g2 = g.umap(dbscan=True) - # with greater control over parameters via chaining, - g2 = g.umap().dbscan(eps=1.0, min_samples=2, **kwargs) + # and with greater control over parameters via chaining, + g2 = g.umap().dbscan(eps=1.2, min_samples=2, **kwargs) + + # cluster by feature embeddings + g2 = g.featurize().dbscan(**kwargs) + + # cluster by a given set of feature column attributes + g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], **kwargs) + + # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) + g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) + + g2.plot() # color by `_cluster` + Useful: + Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc. + see https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb + Args: kind: 'nodes' or 'edges' - cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip172', 'location', 'asn', 'warnings'] + cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] umap: whether to use UMAP embeddings or features dataframe eps: The maximum distance between two samples for them to be considered as in the same neighborhood. min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. @@ -145,5 +156,5 @@ def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_ return res - def _is_cudf(self, df): - return 'cudf' in str(type(df)) + # def _is_cudf(self, df): + # return 'cudf' in str(type(df)) From afd3a7e2335a3c09f9a97cb9c5104e92afeb0c1e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 11:13:18 -0800 Subject: [PATCH 030/432] doc(adds more to Readme) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 180d8f099e..53ed6e5293 100644 --- a/README.md +++ b/README.md @@ -521,7 +521,7 @@ See `help(g.search_graph)` for options relation=['relationship_1', 'relationship_4', ..], destination=['entity_l', 'entity_m', ..], threshold=0.9, # score threshold - return_dataframe=False) # set to `True` to return dataframe, or just access via `g5._edges` + return_dataframe=False) # set to `True` to return dataframe, or just access via `g4._edges` ``` * Detect Anamolous Behavior (example use cases such as Cyber, Fraud, etc) @@ -583,7 +583,7 @@ See `help(g.embed)`, `help(g.predict_links)` , `help(g.predict_links_all)` for o g2.plot() # color by `_cluster` ``` - +See `help(g.dbscan)` for options ### Quickly configurable From 44827ea66bb2d5d11b8956cc36e5b6742ead1ab4 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 11:15:07 -0800 Subject: [PATCH 031/432] indent --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 53ed6e5293..4c628e343c 100644 --- a/README.md +++ b/README.md @@ -559,28 +559,28 @@ See `help(g.embed)`, `help(g.predict_links)` , `help(g.predict_links_all)` for o * Enrich UMAP embeddings or featurization dataframe with GPU or CPU DBSCAN ```python - g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') - - # cluster by UMAP embeddings - kind = 'nodes' | 'edges' - g2 = g.umap(kind=kind).dbscan(kind=kind) - print(g2._nodes['_cluster']) | print(g2._edges['_cluster']) - - # dbscan with fixed parameters is default in umap - g2 = g.umap(dbscan=True) - - # and with greater control over parameters via chaining, - g2 = g.umap().dbscan(eps=1.2, min_samples=2, **kwargs) - - # cluster by feature embeddings - g2 = g.featurize().dbscan(**kwargs) - - # cluster by a given set of feature column attributes - g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], **kwargs) - - # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) - g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) - g2.plot() # color by `_cluster` + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') + + # cluster by UMAP embeddings + kind = 'nodes' | 'edges' + g2 = g.umap(kind=kind).dbscan(kind=kind) + print(g2._nodes['_cluster']) | print(g2._edges['_cluster']) + + # dbscan with fixed parameters is default in umap + g2 = g.umap(dbscan=True) + + # and with greater control over parameters via chaining, + g2 = g.umap().dbscan(eps=1.2, min_samples=2, **kwargs) + + # cluster by feature embeddings + g2 = g.featurize().dbscan(**kwargs) + + # cluster by a given set of feature column attributes + g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], **kwargs) + + # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) + g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) + g2.plot() # color by `_cluster` ``` See `help(g.dbscan)` for options From 53e1f0cd7a5d3601a806d87bc00ff07363844d0a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 11:30:13 -0800 Subject: [PATCH 032/432] doc(CHANGELOG and README) --- CHANGELOG.md | 6 ++++++ README.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b29546b8..621e3ad90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Development] +### Added +* AI: DBSCAN -- `g.featurize().dbscan()` and `g.umap().dbscan()` with options to use UMAP embedding, feature matrix, or subset of feature matrix via `g.dbscan(cols=[...])` +* AI: Demo cleanup using ModelDict & new features +* AI: moves public `g.g_dgl` from KG `embed` method to private method `g._kg_dgl` +* Tests: dbscan tests + ### Added * AI: Easy import of featurization kwargs for `g.umap(**kwargs)` and `g.featurize(**kwargs)` * AI: `g.get_features_by_cols` returns featurized submatrix with `col_part` in their columns diff --git a/README.md b/README.md index 4c628e343c..f3f64aea83 100644 --- a/README.md +++ b/README.md @@ -482,7 +482,7 @@ GNN support is rapidly evolving, please contact the team directly or on Slack fo results_df, query_vector = g2.search('my natural language query', ...) - print(results_df[['_distance', 'text_col_1', ..., 'text_col_n']]) #sorted by relevancy + print(results_df[['_distance', ..., 'text_col_n']]) #sorted by relevancy # or see graph of matching entities and similarity edges (or optional original edges) g2.search_graph('my natural language query', ...).plot() From 26eb4c51e2e9bfefb166d9b468108fdd76cc786a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Dec 2022 11:39:06 -0800 Subject: [PATCH 033/432] doc(changelog) --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 621e3ad90b..119150b9c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Development] +### Changed +* AI: moves public `g.g_dgl` from KG `embed` method to private method `g._kg_dgl` + ### Added * AI: DBSCAN -- `g.featurize().dbscan()` and `g.umap().dbscan()` with options to use UMAP embedding, feature matrix, or subset of feature matrix via `g.dbscan(cols=[...])` * AI: Demo cleanup using ModelDict & new features -* AI: moves public `g.g_dgl` from KG `embed` method to private method `g._kg_dgl` * Tests: dbscan tests - -### Added * AI: Easy import of featurization kwargs for `g.umap(**kwargs)` and `g.featurize(**kwargs)` * AI: `g.get_features_by_cols` returns featurized submatrix with `col_part` in their columns * AI: `g.conditional_graph` and `g.conditional_probs` assessing conditional probs and graph From 8b94be9496bffccd4d39ea083715742ca2ee774a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 Jan 2023 00:54:46 -0800 Subject: [PATCH 034/432] feat(large refactor of transform methods from featurize, umap, and dbscan, adding `infer_graph` nearest graph interpolation in umap or featurize coordinates) --- graphistry/ai_utils.py | 153 +++++++++++++++++++++++++++++++++- graphistry/compute/cluster.py | 141 ++++++++++++++++++++++++++----- graphistry/feature_utils.py | 49 ++++++----- graphistry/umap_utils.py | 17 ++-- 4 files changed, 313 insertions(+), 47 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 1b18dd4404..e46922b916 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -1,9 +1,12 @@ import pandas as pd +import numpy as np import graphistry -from .util import setup_logger -logger = setup_logger(__name__) +from .constants import N_TREES, DISTANCE +from logging import getLogger + +logger = getLogger(__name__) # ################################################################################################# @@ -127,3 +130,149 @@ def get_graphistry_from_milieu_search( ntdf = ndf[ndf[node_col].isin(gcols)] g = graphistry.edges(tdf, src, dst).nodes(ntdf, node_col) return g + +# ######################################################################################################################### +# +# Graphistry Vector Search Index +# +########################################################################################################################## + +def build_annoy_index(X, angular, n_trees=None): + """ Builds an Annoy Index for fast vector search + + Args: + X (_type_): _description_ + angular (_type_): _description_ + n_trees (_type_, optional): _description_. Defaults to None. + + Returns: + _type_: _description_ + """ + from annoy import AnnoyIndex # type: ignore + + logger.info(f"Building Index of size {X.shape}") + + if angular: + logger.info('-using angular metric') + metric = 'angular' + else: + logger.info('-using euclidean metric') + metric = 'euclidean' + + search_index = AnnoyIndex(X.shape[1], metric) + # Add all the feature vectors to the search index + for i in range(len(X)): + search_index.add_item(i, X.values[i]) + if n_trees is None: + n_trees = N_TREES + + logger.info(f'-building index with {n_trees} trees') + search_index.build(n_trees) + return search_index + +def query_by_vector(vect, df, search_index, top_n): + indices, distances = search_index.get_nns_by_vector( + vect.values[0], top_n, include_distances=True + ) + + results = df.iloc[indices] + results[DISTANCE] = distances + results = results.sort_values(by=[DISTANCE]) + + return results + + + + + +# ######################################################################################################################### +# +# Graphistry Graph Inference +# +########################################################################################################################## + + +def infer_graph(res, emb, X, y, df, use_umap, eps=1, sample=None): + """ + Infer a graph from a graphistry object + + args: + res: graphistry object + df: outside minibatch dataframe to add to existing graph + X: minibatch transformed dataframe + emb: minibatch UMAP embedding + kind: 'nodes' or 'edges' + eps: distance threshold for a minibatchh point to cluster to existing graph + n_nearest: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. + """ + # if we have node features in this mini_batch, we can + if use_umap and emb is not None: #would conflict if node_features is of shape (n, 2) + X_ = res._node_embedding + W = emb + else: + X_ = res._node_features + W = X + Y = res._node_target + + assert df.shape[0] == X.shape[0], 'minibatches df and X must have same number of rows since f(df) = X' + + new_edges = [] + # if umap, need to add '_n' as node id to df, adding new indices to existing graph + df['_n'] = range(X.shape[0], X.shape[0]+df.shape[0]) + df['_batch'] = 1 # 1 for minibatch, 0 for existing graph + node = res._node + NDF = res._nodes + NDF['_batch'] = 0 + EDF = res._edges + EDF['_batch'] = 0 + src = res._source + dst = res._destination + + old_edges = [] + old_nodes = [] + mdists=[] + + #vsearch = build_search_index(X_, angular=False) + + for i in range(W.shape[0]): + record_df = df.iloc[i, :] + diff = X_ - W.iloc[i, :] + dist = np.linalg.norm(diff, axis=1) # Euclidean distance + + mdist = dist.mean() + mdists.append(mdist) + for nn in np.where(dist < eps)[0]: + this_ndf = NDF.iloc[nn, :] + if sample: + local_edges = EDF[(EDF[src] == this_ndf[node]) | (EDF[dst] == this_ndf[node])] + if not local_edges.empty: + old_edges.append(local_edges.sample(sample, replace=True)) + new_edges.append([this_ndf[node], record_df[node], 1, 1]) + old_nodes.append(this_ndf) + + print('mean dist', np.mean(mdist)) + + new_edges = pd.DataFrame(new_edges, columns=[src, dst, '_weight', '_batch']) + if sample: + new_edges = pd.concat([new_edges, pd.concat(old_edges, axis=0).assign(_batch=0)], axis=0) + new_edges = new_edges.drop_duplicates() + + old_nodes = pd.DataFrame(old_nodes).drop_duplicates(subset=[node]) + old_emb = X_.loc[old_nodes.index] + new_emb = pd.concat([W, old_emb], axis=0) + + new_nodes = pd.concat([df, old_nodes], axis=0)#.reset_index(drop=True) # append minibatch at top + + # ######################################################### + g = res.nodes(new_nodes, node).edges(new_edges, src, dst) + + if use_umap: + g._node_embedding = new_emb + g._node_features = X_ + else: + g._node_features = new_emb + g._node_embedding = X_ + + g._node_targets = pd.concat([y, Y.loc[old_nodes.index]]) if y is not None else Y + + return g \ No newline at end of file diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 6ba1d91071..6f6cb99f7c 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -1,5 +1,7 @@ import logging import pandas as pd +import numpy as np + from typing import Any, List, Union, TYPE_CHECKING from typing_extensions import Literal from collections import Counter @@ -7,6 +9,9 @@ from graphistry.Engine import Engine from graphistry.Plottable import Plottable from graphistry.constants import CUML, UMAP_LEARN # noqa type: ignore +from graphistry.features import ModelDict +from graphistry.feature_utils import get_matrix_by_column_parts +from graphistry.ai_utils import infer_graph logger = logging.getLogger("compute.cluster") @@ -59,7 +64,22 @@ def resolve_cpu_gpu_engine( ) -def cluster(g, dbscan, kind='nodes', cols=None, umap=True): +def get_model_matrix(g, kind, cols, umap): + assert kind in ['nodes', 'edges'] + assert hasattr(g, '_node_encoder') if kind == 'nodes' else hasattr(g, '_edge_encoder') + + if cols is None: + df = g._get_feature(kind) + else: + df = g.get_features_by_cols(cols, kind) + + if umap and cols is None and g._umap is not None: + df = g._get_embedding(kind) + + return df + + +def dbscan_fit(g, dbscan, kind='nodes', cols=None, umap=True): """ Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe @@ -69,22 +89,15 @@ def cluster(g, dbscan, kind='nodes', cols=None, umap=True): cols: list of columns to use for clustering given `g.featurize` has been run umap: whether to use UMAP embeddings or features dataframe """ - - if cols is None: - df = g._get_feature(kind) - else: - df = g.get_features_by_cols(cols, kind) + df = get_model_matrix(g, kind, cols, umap) - if umap and cols is None and g._umap is not None: - df = g._get_embedding(kind) - dbscan.fit(df) labels = dbscan.labels_ if kind == 'nodes': - g._nodes['_cluster'] = labels + g._nodes = g._nodes.assign(_dbscan = labels) elif kind == 'edges': - g._edges['_cluster'] = labels + g._nodes = g._edges.assign(_dbscan = labels) else: raise ValueError('kind must be one of `nodes` or `edges`') @@ -93,20 +106,53 @@ def cluster(g, dbscan, kind='nodes', cols=None, umap=True): return g + + +def dbscan_predict(X: pd.DataFrame, model): + """ + DBSCAN has no predict per se, so we reverse engineer one here + from https://stackoverflow.com/questions/27822752/scikit-learn-predicting-new-points-with-dbscan + """ + n_samples = X.shape[0] + + y_new = np.ones(shape=n_samples, dtype=int) * -1 + + for i in range(n_samples): + diff = model.components_ - X.iloc[i, :].values # NumPy broadcasting + + dist = np.linalg.norm(diff, axis=1) # Euclidean distance + + shortest_dist_idx = np.argmin(dist) + + if dist[shortest_dist_idx] < model.eps: + y_new[i] = model.labels_[model.core_sample_indices_[shortest_dist_idx]] + + return y_new + +def dbscan_predict2(g, kind='nodes', cols=None, umap=True): + X = g._get_feature(kind) + dbscan = g._node_dbscan if kind == 'nodes' else g._edge_dbscan + + preds = dbscan_predict(X, dbscan) + return X, preds + + class ClusterMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): pass def _cluster_dbscan(self, res, kind, cols, umap, eps, min_samples, **kwargs): """ - DBSCAN clustering on cpu or gpu infered by umap's .engine flag + DBSCAN clustering on cpu or gpu infered by .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() res.engine = resolve_cpu_gpu_engine("auto") + res._kwargs_dbscan = ModelDict('latest dbscan kwargs', kind=kind, cols=cols, umap=umap, eps=eps, min_samples=min_samples, **kwargs) dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if res.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) - res = cluster(res, dbscan, kind=kind, cols=cols, umap=umap) + + res = dbscan_fit(res, dbscan, kind=kind, cols=cols, umap=umap) return res @@ -140,21 +186,76 @@ def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_ g2.plot() # color by `_cluster` Useful: - Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc. - see https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb + Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, + as well as assessing metrics per cluster, e.g. + https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb Args: kind: 'nodes' or 'edges' - cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] + cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by + fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] umap: whether to use UMAP embeddings or features dataframe eps: The maximum distance between two samples for them to be considered as in the same neighborhood. - min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. + min_samples: The number of samples in a neighborhood for a point to be considered as a core point. + This includes the point itself. """ res = self.bind() res = res._cluster_dbscan(res, kind=kind, cols=cols, umap=umap, eps=eps, min_samples=min_samples, **kwargs) return res - - # def _is_cudf(self, df): - # return 'cudf' in str(type(df)) + + def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd.DataFrame: + """Transforms a dataframe to one with a new column '_cluster' containing the DBSCAN cluster labels + and returns feature[cols] or UMAP embedding + Examples: + fit: + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') + g2 = g.featurize().dbscan() + + predict: + labels = g2.transform_dbscan(ndf) + + """ + + res = self.bind() + if hasattr(res, '_kwargs_dbscan'): + # Assume that we are transforming to last fit of dbscan + cols = res._kwargs_dbscan['cols'] + umap = res._kwargs_dbscan['umap'] + + dbscan = res._node_dbscan if kind == 'nodes' else res._edge_dbscan + + emb = None + if umap: + emb, X, y = res.transform_umap(df, ydf, kind=kind, return_graph=False) + else: + X, _ = res.transform(df, ydf, kind=kind) + if cols is not None: + X = get_matrix_by_column_parts(X, cols) + + if umap: + X_ = emb + else: + X_ = X + + labels = dbscan_predict(X_, dbscan) + df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) + return emb, X, y, df + else: + raise Exception('No dbscan model found. Please run `g.dbscan()` first') + + def transform_dbscan(self, df, y=None, eps=30, use_umap_embedding=True, n_nearest=None, kind='nodes', return_graph=True): + """Transforms a dataframe to one with a new column '_cluster' containing the DBSCAN cluster labels on the minibatch + if return_graph is True, then a graph is returned with the minibatch added to the existing graph + if return_graph is False, then the enriched minibatch dataframe, features, and UMAP embedding are returned + + """ + emb, X, y, df = self._transform_dbscan(df, y, kind=kind) + if return_graph: + res = self.bind() + g = infer_graph(res, emb, X, y, df, use_umap=use_umap_embedding, eps=eps, sample=n_nearest) + return g + return emb, X, df + + diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index ba6227da29..16b6671201 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1815,6 +1815,19 @@ def reuse_featurization( memoize=memoize, ) +def get_matrix_by_column_part(X: pd.DataFrame, column_part: str) -> pd.DataFrame: + """Get the feature matrix by column part existing in column names.""" + transformed_columns = X.columns[X.columns.map(lambda x: True if column_part in x else False)] # type: ignore + return X[transformed_columns] + +def get_matrix_by_column_parts(X: pd.DataFrame, column_parts: Union[list, str]) -> pd.DataFrame: + """Get the feature matrix by column parts list existing in column names.""" + if isinstance(column_parts, str): + column_parts = [column_parts] + res = pd.concat([get_matrix_by_column_part(X, column_part) for column_part in column_parts], axis=1) # type: ignore + res = res.loc[:, ~res.columns.duplicated()] # type: ignore + return res + class FeatureMixin(MIXIN_BASE): """ @@ -2109,15 +2122,21 @@ def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): "before being able to transform data" ) - def transform(self, df, ydf, kind): + def transform(self, df, ydf=None, kind='nodes', return_graph=False, eps=1, n_nearest=None): """Transform new data""" if kind == "nodes": - return self._transform("_node_encoder", df, ydf) + X, y = self._transform("_node_encoder", df, ydf) elif kind == "edges": - return self._transform("_edge_encoder", df, ydf) + X, y = self._transform("_edge_encoder", df, ydf) else: logger.debug("kind must be one of `nodes`," f"`edges`, found {kind}") + if return_graph: + res = self.bind() + emb = None + g = infer_graph(res, emb, X, y, df, use_umap=False, eps=eps, n_nearest=n_nearest) + return g + return X, y def scale( self, @@ -2600,18 +2619,8 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( memoize=memoize, ) - def _features_by_col(self, column_part: str, kind: str): - if kind == 'nodes' and hasattr(self, '_node_features'): - X = self._node_features - elif kind == 'edges' and hasattr(self, '_edge_features'): - X = self._edge_features - else: - raise ValueError('make sure to call `featurize` or `umap` before calling `get_features_by_cols`') - - transformed_columns = X.columns[X.columns.map(lambda x: True if column_part in x else False)] # type: ignore - return X[transformed_columns] # type: ignore - def get_features_by_cols(self, columns: Union[List, str], kind: str = 'nodes'): + def get_features_by_cols(self, columns: Union[List, str], kind: str = 'nodes', target=False): """Returns feature matrix with only the columns that contain the string `column_part` in their name. `X = g.get_features_by_cols(['feature1', 'feature2'])` @@ -2627,12 +2636,14 @@ def get_features_by_cols(self, columns: Union[List, str], kind: str = 'nodes'): columns (Union[List, str]): list of column names or a single column name that may exist in columns of the feature matrix. kind (str, optional): Node or Edge features. Defaults to 'nodes'. + target (bool, optional): If True, returns the target matrix. Defaults to False. Returns: pd.DataFrame: feature matrix with only the columns that contain the string `column_part` in their name. """ - if isinstance(columns, str): - columns = [columns] - X = pd.concat([self._features_by_col(col, kind=kind) for col in columns], axis=1) # type: ignore - X = X.loc[:, ~X.columns.duplicated()] # type: ignore - return X + if target: + X = self._get_target(kind) + else: + X = self._get_feature(kind) + + return get_matrix_by_column_parts(X, columns) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index fda5a96060..6394ac69ff 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -11,6 +11,7 @@ resolve_feature_engine) from .PlotterBase import Plottable, WeakValueDictionary from .util import check_set_memoize, setup_logger +from .ai_utils import infer_graph logger = setup_logger(name=__name__, verbose=config.VERBOSE) @@ -268,7 +269,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info(f" - or {X.shape[0]/mins:.2f} rows per minute") return self - def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): + def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: raise ValueError("UMAP is not initialized") self.umap_fit(X, y) @@ -277,15 +278,19 @@ def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = Non return emb def transform_umap( # noqa: E303 - self, df: pd.DataFrame, ydf: pd.DataFrame, kind: str = "nodes" - ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: + self, df: pd.DataFrame, ydf=None, kind: str = "nodes", use_umap=True, eps=1, n_nearest=None, return_graph=True + ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: try: - logger.debug(f"Going into Transform umap {df.shape}, {ydf.shape}") + logger.debug(f"Going into Transform umap {df.shape}") except: pass x, y = self.transform(df, ydf, kind=kind) emb = self._umap.transform(x) # type: ignore emb = self._bundle_embedding(emb, index=df.index) + if return_graph: + res = self.bind() + g = infer_graph(res, df, x, emb, y, use_umap=use_umap, eps=eps, sample=n_nearest) + return g return emb, x, y def _bundle_embedding(self, emb, index): @@ -331,7 +336,7 @@ def _process_umap( fresh_res._umap = old_res._umap # this saves the day! return fresh_res - emb = res.umap_fit_transform(X_, y_) + emb = res._umap_fit_transform(X_, y_) res._xy = emb return res @@ -549,7 +554,7 @@ def umap( ) if X is not None and isinstance(X, pd.DataFrame): logger.info("New Matrix `X` passed in for UMAP-ing") - xy = res.umap_fit_transform(X, y) + xy = res._umap_fit_transform(X, y) res._xy = xy res._weighted_edges_df = prune_weighted_edges_df_and_relabel_nodes( res._weighted_edges_df, scale=scale From 52a29a8644e08fcedfb96c5a4a388a54d9dedda7 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 Jan 2023 00:58:33 -0800 Subject: [PATCH 035/432] typo --- graphistry/ai_utils.py | 4 +--- graphistry/feature_utils.py | 1 + graphistry/text_utils.py | 48 +++++++++++-------------------------- 3 files changed, 16 insertions(+), 37 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index e46922b916..944fa535ca 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -170,6 +170,7 @@ def build_annoy_index(X, angular, n_trees=None): search_index.build(n_trees) return search_index + def query_by_vector(vect, df, search_index, top_n): indices, distances = search_index.get_nns_by_vector( vect.values[0], top_n, include_distances=True @@ -182,9 +183,6 @@ def query_by_vector(vect, df, search_index, top_n): return results - - - # ######################################################################################################################### # # Graphistry Graph Inference diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 16b6671201..02e3e658b2 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -23,6 +23,7 @@ from . import constants as config from .PlotterBase import WeakValueDictionary, Plottable from .util import setup_logger, check_set_memoize +from .ai_utils import infer_graph # add this inside classes and have a method that can set log level logger = setup_logger(name=__name__, verbose=config.VERBOSE) diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 6af12f3655..c96b568ce2 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -4,8 +4,11 @@ import pandas as pd from .feature_utils import FeatureMixin -from .ai_utils import search_to_df, setup_logger -from .constants import WEIGHT, N_TREES, DISTANCE, VERBOSE, TRACE +from .ai_utils import search_to_df, build_annoy_index, query_by_vector +from .constants import WEIGHT, DISTANCE +from logging import getLogger + +logger = getLogger(__name__) from typing import ( Hashable, @@ -20,13 +23,12 @@ ) # noqa -logger = setup_logger(__name__, verbose=VERBOSE, fullpath=TRACE) - if TYPE_CHECKING: MIXIN_BASE = FeatureMixin else: MIXIN_BASE = object + class SearchToGraphMixin(MIXIN_BASE): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -35,7 +37,7 @@ def assert_fitted(self): # assert self._umap is not None, 'Umap needs to be fit first, run g.umap(..) to fit a model' assert ( self._get_feature('nodes') is not None - ), "Graphistry Instance is not fit, run g.featurize(kind='nodes', ..) to fit a model' \ + ), "Graphistry Instance is not fit, run g.featurize(kind='nodes', ..) to fit a model ' \ 'if you have nodes & edges dataframe or g.umap(kind='nodes', ..) if you only have nodes dataframe" def assert_features_line_up_with_nodes(self): @@ -44,49 +46,27 @@ def assert_features_line_up_with_nodes(self): a, b = ndf.shape[0], X.shape[0] assert a == b, 'Nodes dataframe and feature vectors are not same size, '\ f'found nodes: {a}, feats: {b}. Did you mutate nodes between fit?' + + def _build_search_index(self, X, angular=False, n_trees=None): + # builds local index from X + return build_annoy_index(X, angular, n_trees) def build_index(self, angular=False, n_trees=None): - from annoy import AnnoyIndex # type: ignore # builds local index self.assert_fitted() self.assert_features_line_up_with_nodes() X = self._get_feature('nodes') - logger.info(f"Building Index of size {X.shape}") - - if angular: - logger.info('-using angular metric') - metric = 'angular' - else: - logger.info('-using euclidean metric') - metric = 'euclidean' - - search_index = AnnoyIndex(X.shape[1], metric) - # Add all the feature vectors to the search index - for i in range(len(X)): - search_index.add_item(i, X.values[i]) - if n_trees is None: - n_trees = N_TREES - - logger.info(f'-building index with {n_trees} trees') - search_index.build(n_trees) + self.search_index = self._build_search_index(X, angular, n_trees) - self.search_index = search_index def _query_from_dataframe(self, qdf: pd.DataFrame, top_n: int, thresh: float): # Use the loaded featurizers to transform the dataframe vect, _ = self.transform(qdf, None, kind="nodes") - - indices, distances = self.search_index.get_nns_by_vector( - vect.values[0], top_n, include_distances=True - ) - - results = self._nodes.iloc[indices] - results[DISTANCE] = distances - results = results.query(f"{DISTANCE} < {thresh}") - results = results.sort_values(by=[DISTANCE]) + results = query_by_vector(vect, self._nodes, self.search_index, top_n) + results = results.query(f"{DISTANCE} < {thresh}") return results, vect From 3908e52f2c9fe2cb6e099468d1ef515314b1ff82 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 Jan 2023 14:14:22 -0800 Subject: [PATCH 036/432] feat(adds better graph inference and options for featurize/umap/dbscan, adds eps=`auto` which finds good threshold for graph clustering --- graphistry/ai_utils.py | 86 ++++++++++++++++++++++++----------- graphistry/compute/cluster.py | 82 ++++++++++++++++++++++++--------- graphistry/feature_utils.py | 20 ++++++-- graphistry/umap_utils.py | 12 ++--- 4 files changed, 142 insertions(+), 58 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 944fa535ca..89533572bd 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -190,7 +190,7 @@ def query_by_vector(vect, df, search_index, top_n): ########################################################################################################################## -def infer_graph(res, emb, X, y, df, use_umap, eps=1, sample=None): +def infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps='auto', sample=None): """ Infer a graph from a graphistry object @@ -200,21 +200,28 @@ def infer_graph(res, emb, X, y, df, use_umap, eps=1, sample=None): X: minibatch transformed dataframe emb: minibatch UMAP embedding kind: 'nodes' or 'edges' - eps: distance threshold for a minibatchh point to cluster to existing graph + eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph n_nearest: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. """ # if we have node features in this mini_batch, we can - if use_umap and emb is not None: #would conflict if node_features is of shape (n, 2) + if use_umap_embedding and emb is not None: #would conflict if node_features is of shape (n, 2) X_ = res._node_embedding W = emb - else: + F = res._node_features + EMB = X_ + else: # can still be umap, but want to do the inference on the higher dimensional features X_ = res._node_features W = X + F = X_ + EMB = res._node_embedding + Y = res._node_target assert df.shape[0] == X.shape[0], 'minibatches df and X must have same number of rows since f(df) = X' + if emb is not None: + assert emb.shape[0] == df.shape[0], 'minibatches emb and X must have same number of rows since h(df) = emb' + df = df.assign(x=emb.x, y=emb.y) # add x and y to df for graphistry instance - new_edges = [] # if umap, need to add '_n' as node id to df, adding new indices to existing graph df['_n'] = range(X.shape[0], X.shape[0]+df.shape[0]) df['_batch'] = 1 # 1 for minibatch, 0 for existing graph @@ -226,6 +233,7 @@ def infer_graph(res, emb, X, y, df, use_umap, eps=1, sample=None): src = res._source dst = res._destination + new_edges = [] old_edges = [] old_nodes = [] mdists=[] @@ -233,44 +241,68 @@ def infer_graph(res, emb, X, y, df, use_umap, eps=1, sample=None): #vsearch = build_search_index(X_, angular=False) for i in range(W.shape[0]): - record_df = df.iloc[i, :] + #record_df = df.iloc[i, :] diff = X_ - W.iloc[i, :] dist = np.linalg.norm(diff, axis=1) # Euclidean distance - - mdist = dist.mean() - mdists.append(mdist) - for nn in np.where(dist < eps)[0]: - this_ndf = NDF.iloc[nn, :] + mdists.append(dist) + + m, std = np.mean(mdists), np.std(mdists) + #logger.info(f'--Mean distance to existing nodes {m} +/- {std}') + print(f'--Mean distance to existing nodes {m:.2f} +/- {std:.2f}') + if eps == 'auto': + eps = np.min([np.abs(m - 2*std), m]) + print(f'{eps:.2f} epsilon for min distance threshold') + + for i, dist in enumerate(mdists): + record_df = df.iloc[i, :] + for j in np.where(dist < eps)[0]: + this_ndf = NDF.iloc[j, :] if sample: local_edges = EDF[(EDF[src] == this_ndf[node]) | (EDF[dst] == this_ndf[node])] if not local_edges.empty: old_edges.append(local_edges.sample(sample, replace=True)) new_edges.append([this_ndf[node], record_df[node], 1, 1]) old_nodes.append(this_ndf) - - print('mean dist', np.mean(mdist)) + new_edges = pd.DataFrame(new_edges, columns=[src, dst, '_weight', '_batch']) + print(len(new_edges), 'new edges') + + all_nodes = [] + if len(old_edges): + old_edges = pd.concat(old_edges, axis=0).assign(_batch=0) + all_nodes = old_edges[src].append(old_edges[dst]).append(new_edges[src]).append(new_edges[dst]) + if sample: - new_edges = pd.concat([new_edges, pd.concat(old_edges, axis=0).assign(_batch=0)], axis=0) + new_edges = pd.concat([new_edges, old_edges], axis=0) + print('sampled', len(new_edges), 'new edges') new_edges = new_edges.drop_duplicates() - - old_nodes = pd.DataFrame(old_nodes).drop_duplicates(subset=[node]) - old_emb = X_.loc[old_nodes.index] - new_emb = pd.concat([W, old_emb], axis=0) + print(len(new_edges), 'new edges after dropping duplicates') + + if len(old_nodes): + old_nodes = pd.DataFrame(old_nodes)#.drop_duplicates(subset=[node]) + old_nodes = pd.concat([old_nodes, NDF[NDF[node].isin(all_nodes)]], axis=0).drop_duplicates(subset=[node]) + print(f'Old nodes {len(old_nodes)}') + + old_emb = None + if EMB is not None: + old_emb = EMB.loc[old_nodes.index] + new_emb = None + if emb is not None: + new_emb = pd.concat([emb, old_emb], axis=0) + print(f'Old emb {old_emb.shape if old_emb is not None else None}') + + new_features = pd.concat([X, F.loc[old_nodes.index]], axis=0) - new_nodes = pd.concat([df, old_nodes], axis=0)#.reset_index(drop=True) # append minibatch at top + new_nodes = pd.concat([df, old_nodes], axis=0) # append minibatch at top + + new_targets = pd.concat([y, Y.loc[old_nodes.index]]) if y is not None else Y # ######################################################### g = res.nodes(new_nodes, node).edges(new_edges, src, dst) - if use_umap: - g._node_embedding = new_emb - g._node_features = X_ - else: - g._node_features = new_emb - g._node_embedding = X_ - - g._node_targets = pd.concat([y, Y.loc[old_nodes.index]]) if y is not None else Y + g._node_embedding = new_emb + g._node_features = new_features + g._node_targets = new_targets return g \ No newline at end of file diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 6f6cb99f7c..913ccc4ffc 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -79,7 +79,7 @@ def get_model_matrix(g, kind, cols, umap): return df -def dbscan_fit(g, dbscan, kind='nodes', cols=None, umap=True): +def dbscan_fit(g, dbscan, kind='nodes', cols=None, use_umap_embedding=True): """ Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe @@ -89,7 +89,7 @@ def dbscan_fit(g, dbscan, kind='nodes', cols=None, umap=True): cols: list of columns to use for clustering given `g.featurize` has been run umap: whether to use UMAP embeddings or features dataframe """ - df = get_model_matrix(g, kind, cols, umap) + df = get_model_matrix(g, kind, cols, use_umap_embedding) dbscan.fit(df) labels = dbscan.labels_ @@ -97,7 +97,7 @@ def dbscan_fit(g, dbscan, kind='nodes', cols=None, umap=True): if kind == 'nodes': g._nodes = g._nodes.assign(_dbscan = labels) elif kind == 'edges': - g._nodes = g._edges.assign(_dbscan = labels) + g._edges = g._edges.assign(_dbscan = labels) else: raise ValueError('kind must be one of `nodes` or `edges`') @@ -141,23 +141,23 @@ class ClusterMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): pass - def _cluster_dbscan(self, res, kind, cols, umap, eps, min_samples, **kwargs): + def _cluster_dbscan(self, res, kind, cols, use_umap_embedding, eps, min_samples, **kwargs): """ DBSCAN clustering on cpu or gpu infered by .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() res.engine = resolve_cpu_gpu_engine("auto") - res._kwargs_dbscan = ModelDict('latest dbscan kwargs', kind=kind, cols=cols, umap=umap, eps=eps, min_samples=min_samples, **kwargs) + res._kwargs_dbscan = ModelDict('latest dbscan kwargs', kind=kind, cols=cols, umap=use_umap_embedding, eps=eps, min_samples=min_samples, **kwargs) dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if res.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) - res = dbscan_fit(res, dbscan, kind=kind, cols=cols, umap=umap) + res = dbscan_fit(res, dbscan, kind=kind, cols=cols, use_umap_embedding=use_umap_embedding) return res - def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_samples: int = 1, **kwargs): + def dbscan(self, kind = 'nodes', cols = None, use_umap_embedding = True, eps: float = 1., min_samples: int = 1, **kwargs): """DBSCAN clustering on cpu or gpu infered automatically Examples: @@ -194,14 +194,14 @@ def dbscan(self, kind = 'nodes', cols = None, umap = True, eps: float = 1., min_ kind: 'nodes' or 'edges' cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] - umap: whether to use UMAP embeddings or features dataframe + use_umap_embedding: whether to use UMAP embeddings or features dataframe to cluster DBSCAN eps: The maximum distance between two samples for them to be considered as in the same neighborhood. min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. """ res = self.bind() - res = res._cluster_dbscan(res, kind=kind, cols=cols, umap=umap, eps=eps, min_samples=min_samples, **kwargs) + res = res._cluster_dbscan(res, kind=kind, cols=cols, use_umap_embedding=use_umap_embedding, eps=eps, min_samples=min_samples, **kwargs) return res @@ -214,7 +214,26 @@ def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd g2 = g.featurize().dbscan() predict: - labels = g2.transform_dbscan(ndf) + emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) + # or + g3 = g2.transform_dbscan(ndf, ndf, return_graph=True) + g3.plot() + + likewise for umap: + fit: + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') + g2 = g.umap().dbscan() + + predict: + emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) + # or + g3 = g2.transform_dbscan(ndf, ndf, return_graph=True) + g3.plot() + + args: + df: dataframe to transform + ydf: optional labels dataframe + kind: 'nodes' or 'edges' """ @@ -227,12 +246,12 @@ def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd dbscan = res._node_dbscan if kind == 'nodes' else res._edge_dbscan emb = None - if umap: + if umap and cols is None: emb, X, y = res.transform_umap(df, ydf, kind=kind, return_graph=False) else: - X, _ = res.transform(df, ydf, kind=kind) - if cols is not None: - X = get_matrix_by_column_parts(X, cols) + X, y = res.transform(df, ydf, kind=kind, return_graph=False) + if cols is not None: + X = get_matrix_by_column_parts(X, cols) if umap: X_ = emb @@ -240,22 +259,43 @@ def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd X_ = X labels = dbscan_predict(X_, dbscan) - df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) + if umap: + df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) + else: + df = df.assign(_dbscan=labels) + return emb, X, y, df else: raise Exception('No dbscan model found. Please run `g.dbscan()` first') - def transform_dbscan(self, df, y=None, eps=30, use_umap_embedding=True, n_nearest=None, kind='nodes', return_graph=True): - """Transforms a dataframe to one with a new column '_cluster' containing the DBSCAN cluster labels on the minibatch - if return_graph is True, then a graph is returned with the minibatch added to the existing graph - if return_graph is False, then the enriched minibatch dataframe, features, and UMAP embedding are returned + def transform_dbscan(self, df: pd.DataFrame, y: pd.DataFrame = None, + eps: Union[float, str]='auto', + use_umap_embedding:bool=True, + sample:int=None, + kind:str='nodes', + return_graph=True): + """ + Transforms a minibatch dataframe to one with a new column '_cluster' containing the DBSCAN cluster labels on the minibatch + and generates a graph with the minibatch and the original graph, with edges between the minibatch and the original graph inferred + works for + + args: + df: dataframe to transform + y: optional labels dataframe + eps: The maximum distance between two samples for them to be considered as in the same neighborhood. + smaller values will result in less edges between the minibatch and the original graph. + Default 'auto', infers eps from the mean distance and std of new points to the original graph + use_umap_embedding: whether to use UMAP embeddings or features dataframe when running DBSCAN + n_nearest: number of nearest neighbors to use for DBSCAN + kind: 'nodes' or 'edges' + return_graph: whether to return a graph or the minibatch enriched with cluster labels, default True """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) if return_graph: res = self.bind() - g = infer_graph(res, emb, X, y, df, use_umap=use_umap_embedding, eps=eps, sample=n_nearest) + g = infer_graph(res, emb, X, y, df, use_umap_embedding=use_umap_embedding, eps=eps, sample=sample) return g - return emb, X, df + return emb, X, y, df diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 02e3e658b2..20218f09a0 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2123,8 +2123,20 @@ def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): "before being able to transform data" ) - def transform(self, df, ydf=None, kind='nodes', return_graph=False, eps=1, n_nearest=None): - """Transform new data""" + def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', sample=None): + """Transform new data and append to existing graph. + + args: + df: pd.DataFrame, raw data to transform + ydf: pd.DataFrame, optional + kind: str # one of `nodes`, `edges` + return_graph: bool, if True, will return a graph with inferred edges + eps: float, if return_graph is True, will use this value for eps in NN search, or 'auto' to infer a good value + sample: int, if return_graph is True, will use sample value for NN search over existing edges + returns: + X: pd.DataFrame, transformed data if return_graph is False + or a graph with inferred edges if return_graph is True + """ if kind == "nodes": X, y = self._transform("_node_encoder", df, ydf) elif kind == "edges": @@ -2134,8 +2146,8 @@ def transform(self, df, ydf=None, kind='nodes', return_graph=False, eps=1, n_nea f"`edges`, found {kind}") if return_graph: res = self.bind() - emb = None - g = infer_graph(res, emb, X, y, df, use_umap=False, eps=eps, n_nearest=n_nearest) + emb = None # will not be able to decide umap coordinates, but will be able to infer graph from existing edges + g = infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps=eps, sample=sample) return g return X, y diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 6394ac69ff..fe84379e92 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -278,20 +278,20 @@ def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = No return emb def transform_umap( # noqa: E303 - self, df: pd.DataFrame, ydf=None, kind: str = "nodes", use_umap=True, eps=1, n_nearest=None, return_graph=True + self, df: pd.DataFrame, ydf=None, kind: str = "nodes", eps='auto', sample=None, return_graph=True, use_umap_embedding=False ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: try: logger.debug(f"Going into Transform umap {df.shape}") except: pass - x, y = self.transform(df, ydf, kind=kind) - emb = self._umap.transform(x) # type: ignore + X, y = self.transform(df, ydf, kind=kind, return_graph=False) + emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) if return_graph: res = self.bind() - g = infer_graph(res, df, x, emb, y, use_umap=use_umap, eps=eps, sample=n_nearest) + g = infer_graph(res, emb, X, y, df, use_umap_embedding=use_umap_embedding, eps=eps, sample=sample) return g - return emb, x, y + return emb, X, y def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 @@ -580,7 +580,7 @@ def umap( res = res.prune_self_edges() if dbscan: - res = res.dbscan(kind=kind, umap=True) # type: ignore + res = res.dbscan(kind=kind, use_umap_embedding=True) # type: ignore if not inplace: return res From 0032eb1a0ceaa9bb67bc0a4da84706d759834f41 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Jan 2023 12:31:20 -0800 Subject: [PATCH 037/432] fix(missing index during scaling, fixed) --- graphistry/feature_utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 20218f09a0..f59823e2c2 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1555,10 +1555,9 @@ def transform( text_cols, ) = res - # feature_columns = X_enc.columns - # feature_columns_target = y_enc.columns logger.info("-" * 90) - + + index = df.index y = pd.DataFrame([]) T = pd.DataFrame([]) # encode nodes @@ -1616,11 +1615,11 @@ def transform( if scaling_pipeline and not X.empty: logger.info("--Scaling Features") - X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns) + X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns, index=index) if scaling_pipeline_target and not y.empty: logger.info(f"--Scaling Target {scaling_pipeline_target}") y = pd.DataFrame( - scaling_pipeline_target.transform(y), columns=y.columns + scaling_pipeline_target.transform(y), columns=y.columns, index=index ) return X, y @@ -2147,7 +2146,7 @@ def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', s if return_graph: res = self.bind() emb = None # will not be able to decide umap coordinates, but will be able to infer graph from existing edges - g = infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps=eps, sample=sample) + g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=False, eps=eps, sample=sample) return g return X, y From f92f3e31d4c59ad7684c5d7fb2dd6255b724e213 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Jan 2023 14:11:33 -0800 Subject: [PATCH 038/432] fix(if node is not bound, adds index back after reset_index), small changes --- graphistry/umap_utils.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index fe84379e92..87d6653022 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -278,7 +278,7 @@ def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = No return emb def transform_umap( # noqa: E303 - self, df: pd.DataFrame, ydf=None, kind: str = "nodes", eps='auto', sample=None, return_graph=True, use_umap_embedding=False + self, df: pd.DataFrame, ydf=None, kind: str = "nodes", eps='auto', sample=None, return_graph=True, fit_umap_embedding=False ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: try: logger.debug(f"Going into Transform umap {df.shape}") @@ -289,19 +289,18 @@ def transform_umap( # noqa: E303 emb = self._bundle_embedding(emb, index=df.index) if return_graph: res = self.bind() - g = infer_graph(res, emb, X, y, df, use_umap_embedding=use_umap_embedding, eps=eps, sample=sample) + g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample) return g return emb, X, y def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 - if emb.shape[1] == 2: - emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) - else: + columns = [config.X, config.Y] + if emb.shape[1] > 2: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1] - 2) ] - emb = pd.DataFrame(emb, columns=columns, index=index) + emb = pd.DataFrame(emb, columns=columns, index=index) return emb def _process_umap( @@ -480,6 +479,7 @@ def umap( if kind == "nodes": + index = res._nodes.index if res._node is None: logger.debug("-Writing new node name") @@ -489,6 +489,8 @@ def umap( .rename(columns={"index": config.IMPLICIT_NODE_ID}), config.IMPLICIT_NODE_ID, ) + res._nodes.index = index + #print(res.) nodes = res._nodes[res._node].values index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) @@ -580,7 +582,7 @@ def umap( res = res.prune_self_edges() if dbscan: - res = res.dbscan(kind=kind, use_umap_embedding=True) # type: ignore + res = res.dbscan(kind=kind, fit_umap_embedding=True) # type: ignore if not inplace: return res From 09575ea501a084999f5a2d5ef120fc36227f1dab Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 00:32:04 -0800 Subject: [PATCH 039/432] fix(bug where lazy init umap wasnt taking umap specific args) --- graphistry/umap_utils.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 87d6653022..42dd0e11c0 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -203,6 +203,8 @@ def umap_lazy_init( "negative_sample_rate": negative_sample_rate, } ) + + print('umap_kwargs', umap_kwargs) self.n_components = n_components self.metric = metric @@ -278,7 +280,11 @@ def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = No return emb def transform_umap( # noqa: E303 - self, df: pd.DataFrame, ydf=None, kind: str = "nodes", eps='auto', sample=None, return_graph=True, fit_umap_embedding=False + self, df: pd.DataFrame, ydf: pd.DataFrame = None, kind: str = "nodes", + eps='auto', + sample=None, + return_graph=True, + fit_umap_embedding=False ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: try: logger.debug(f"Going into Transform umap {df.shape}") @@ -298,8 +304,10 @@ def _bundle_embedding(self, emb, index): columns = [config.X, config.Y] if emb.shape[1] > 2: columns = [config.X, config.Y] + [ - f"umap_{k}" for k in range(2, emb.shape[1] - 2) + f"umap_{k}" for k in range(2, emb.shape[1]) ] + print('emb.shape', emb.shape) + print('columns', columns, len(columns)) emb = pd.DataFrame(emb, columns=columns, index=index) return emb @@ -460,6 +468,7 @@ def umap( repulsion_strength=repulsion_strength, negative_sample_rate=negative_sample_rate, engine=engine, + suffix=suffix, ) logger.debug("umap_kwargs: %s", umap_kwargs) @@ -468,7 +477,7 @@ def umap( else: res = self.bind() - res.umap_lazy_init(engine=engine, suffix=suffix) + res.umap_lazy_init(**umap_kwargs) logger.debug("umap input X :: %s", X) logger.debug("umap input y :: %s", y) From 673f9af0bafa94e6ed3e8078a75a63161a41181a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 00:34:49 -0800 Subject: [PATCH 040/432] optimize infer_graph variables --- graphistry/ai_utils.py | 59 ++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 89533572bd..f0b22cdab7 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -190,7 +190,7 @@ def query_by_vector(vect, df, search_index, top_n): ########################################################################################################################## -def infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps='auto', sample=None): +def infer_graph(res, emb, X, y, df, infer_on_umap_embedding=False, eps='auto', sample=None): """ Infer a graph from a graphistry object @@ -203,18 +203,17 @@ def infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps='auto', sample eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph n_nearest: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. """ - # if we have node features in this mini_batch, we can - if use_umap_embedding and emb is not None: #would conflict if node_features is of shape (n, 2) - X_ = res._node_embedding - W = emb - F = res._node_features - EMB = X_ - else: # can still be umap, but want to do the inference on the higher dimensional features - X_ = res._node_features - W = X - F = X_ - EMB = res._node_embedding + + new_index = df.index + if infer_on_umap_embedding and emb is not None: + X_previously_fit = res._node_embedding + X_new = emb + else: # can still be umap, but want to do the inference on the higher dimensional features + X_previously_fit = res._node_features + X_new = X + FEATS = res._node_features + EMB = res._node_embedding Y = res._node_target assert df.shape[0] == X.shape[0], 'minibatches df and X must have same number of rows since f(df) = X' @@ -223,8 +222,9 @@ def infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps='auto', sample df = df.assign(x=emb.x, y=emb.y) # add x and y to df for graphistry instance # if umap, need to add '_n' as node id to df, adding new indices to existing graph - df['_n'] = range(X.shape[0], X.shape[0]+df.shape[0]) - df['_batch'] = 1 # 1 for minibatch, 0 for existing graph + numeric_indices = range(X_previously_fit.shape[0], X_previously_fit.shape[0]+df.shape[0]) + df['_n'] = numeric_indices + df['_batch'] = 1 # 1 for minibatch, 0 for existing graph node = res._node NDF = res._nodes NDF['_batch'] = 0 @@ -238,20 +238,20 @@ def infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps='auto', sample old_nodes = [] mdists=[] - #vsearch = build_search_index(X_, angular=False) + # vsearch = build_search_index(X_previously_fit, angular=False) - for i in range(W.shape[0]): - #record_df = df.iloc[i, :] - diff = X_ - W.iloc[i, :] + for i in range(X_new.shape[0]): + # record_df = df.iloc[i, :] + diff = X_previously_fit - X_new.iloc[i, :] dist = np.linalg.norm(diff, axis=1) # Euclidean distance mdists.append(dist) m, std = np.mean(mdists), np.std(mdists) - #logger.info(f'--Mean distance to existing nodes {m} +/- {std}') - print(f'--Mean distance to existing nodes {m:.2f} +/- {std:.2f}') + logger.info(f'--Mean distance to existing nodes {m:.2f} +/- {std:.2f}') + # print(f'--Mean distance to existing nodes {m:.2f} +/- {std:.2f}') if eps == 'auto': eps = np.min([np.abs(m - 2*std), m]) - print(f'{eps:.2f} epsilon for min distance threshold') + logger.info(f'{eps:.2f} epsilon for max distance threshold to be considered a neighbor') for i, dist in enumerate(mdists): record_df = df.iloc[i, :] @@ -264,9 +264,7 @@ def infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps='auto', sample new_edges.append([this_ndf[node], record_df[node], 1, 1]) old_nodes.append(this_ndf) - new_edges = pd.DataFrame(new_edges, columns=[src, dst, '_weight', '_batch']) - print(len(new_edges), 'new edges') all_nodes = [] if len(old_edges): @@ -275,24 +273,23 @@ def infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps='auto', sample if sample: new_edges = pd.concat([new_edges, old_edges], axis=0) - print('sampled', len(new_edges), 'new edges') + # print('sampled', len(new_edges), 'new edges') new_edges = new_edges.drop_duplicates() - print(len(new_edges), 'new edges after dropping duplicates') + # print(len(new_edges), 'new edges after dropping duplicates') if len(old_nodes): - old_nodes = pd.DataFrame(old_nodes)#.drop_duplicates(subset=[node]) + old_nodes = pd.DataFrame(old_nodes) old_nodes = pd.concat([old_nodes, NDF[NDF[node].isin(all_nodes)]], axis=0).drop_duplicates(subset=[node]) - print(f'Old nodes {len(old_nodes)}') - + old_emb = None if EMB is not None: old_emb = EMB.loc[old_nodes.index] + new_emb = None if emb is not None: new_emb = pd.concat([emb, old_emb], axis=0) - print(f'Old emb {old_emb.shape if old_emb is not None else None}') - new_features = pd.concat([X, F.loc[old_nodes.index]], axis=0) + new_features = pd.concat([X, FEATS.loc[old_nodes.index]], axis=0) new_nodes = pd.concat([df, old_nodes], axis=0) # append minibatch at top @@ -305,4 +302,4 @@ def infer_graph(res, emb, X, y, df, use_umap_embedding=False, eps='auto', sample g._node_features = new_features g._node_targets = new_targets - return g \ No newline at end of file + return g From 1f2cd202b9afdf78b8512892e6df03eeb8b5035d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 00:35:45 -0800 Subject: [PATCH 041/432] removes deprecated attributes --- graphistry/PlotterBase.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 81591d8e43..4c3e56192f 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -169,20 +169,20 @@ def __init__(self, *args, **kwargs): self._node_embedding = None self._node_encoder = None self._node_features = None - self._node_ordinal_pipeline = None - self._node_ordinal_pipeline_target = None, + #self._node_scaling_pipeline = None + #self._node_ordinal_pipeline_target = None, self._node_target = None self._node_target_encoder = None - self._node_text_model = None + # self._node_text_model = None self._edge_embedding = None self._edge_encoder = None self._edge_features = None - self._edge_ordinal_pipeline = None - self._edge_ordinal_pipeline_target = None + #self._edge_ordinal_pipeline = None + #self._edge_ordinal_pipeline_target = None self._edge_target = None self._edge_target_encoder = None - self._edge_text_model = None + # self._edge_text_model = None self._weighted_adjacency_nodes = None self._weighted_adjacency_edges = None @@ -207,6 +207,9 @@ def __init__(self, *args, **kwargs): # Dbscan self._node_dbscan = None # the fit dbscan instance self._edge_dbscan = None + + # DGL + self.DGL_graph = None # the DGL graph def __repr__(self): From f763bc1d0ef429a1d564a4063987dd9ef73b8cfb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 00:38:03 -0800 Subject: [PATCH 042/432] feat(`g.dbscan(eps, min_sample)` ) --- graphistry/compute/cluster.py | 41 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 913ccc4ffc..8d223624f8 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -141,23 +141,23 @@ class ClusterMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): pass - def _cluster_dbscan(self, res, kind, cols, use_umap_embedding, eps, min_samples, **kwargs): + def _cluster_dbscan(self, res, kind, cols, fit_umap_embedding, eps, min_samples, **kwargs): """ DBSCAN clustering on cpu or gpu infered by .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() res.engine = resolve_cpu_gpu_engine("auto") - res._kwargs_dbscan = ModelDict('latest dbscan kwargs', kind=kind, cols=cols, umap=use_umap_embedding, eps=eps, min_samples=min_samples, **kwargs) + res._kwargs_dbscan = ModelDict('latest dbscan kwargs', kind=kind, cols=cols, umap=fit_umap_embedding, eps=eps, min_samples=min_samples, **kwargs) dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if res.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) - res = dbscan_fit(res, dbscan, kind=kind, cols=cols, use_umap_embedding=use_umap_embedding) + res = dbscan_fit(res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding) return res - def dbscan(self, kind = 'nodes', cols = None, use_umap_embedding = True, eps: float = 1., min_samples: int = 1, **kwargs): + def dbscan(self, eps: float = 0.2, min_samples: int = 1, cols = None, kind = 'nodes', fit_umap_embedding = True, **kwargs): """DBSCAN clustering on cpu or gpu infered automatically Examples: @@ -166,7 +166,7 @@ def dbscan(self, kind = 'nodes', cols = None, use_umap_embedding = True, eps: fl # cluster by UMAP embeddings kind = 'nodes' | 'edges' g2 = g.umap(kind=kind).dbscan(kind=kind) - print(g2._nodes['_cluster']) | print(g2._edges['_cluster']) + print(g2._nodes['_dbscan']) | print(g2._edges['_dbscan']) # dbscan with fixed parameters is default in umap g2 = g.umap(dbscan=True) @@ -183,7 +183,7 @@ def dbscan(self, kind = 'nodes', cols = None, use_umap_embedding = True, eps: fl # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) - g2.plot() # color by `_cluster` + g2.plot() # color by `_dbscan` Useful: Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, @@ -191,22 +191,23 @@ def dbscan(self, kind = 'nodes', cols = None, use_umap_embedding = True, eps: fl https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb Args: - kind: 'nodes' or 'edges' + eps float: The maximum distance between two samples for them to be considered as in the same neighborhood. + kind str: 'nodes' or 'edges' cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] - use_umap_embedding: whether to use UMAP embeddings or features dataframe to cluster DBSCAN - eps: The maximum distance between two samples for them to be considered as in the same neighborhood. + fit_umap_embedding bool: whether to use UMAP embeddings or features dataframe to cluster DBSCAN min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. """ res = self.bind() - res = res._cluster_dbscan(res, kind=kind, cols=cols, use_umap_embedding=use_umap_embedding, eps=eps, min_samples=min_samples, **kwargs) + res = res._cluster_dbscan(res, kind=kind, cols=cols, fit_umap_embedding=fit_umap_embedding, eps=eps, min_samples=min_samples, **kwargs) return res def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd.DataFrame: - """Transforms a dataframe to one with a new column '_cluster' containing the DBSCAN cluster labels + """ + Transforms a dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels and returns feature[cols] or UMAP embedding Examples: fit: @@ -216,7 +217,7 @@ def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd predict: emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) # or - g3 = g2.transform_dbscan(ndf, ndf, return_graph=True) + g3 = g2.transform_dbscan(ndf, return_graph=True) g3.plot() likewise for umap: @@ -227,7 +228,7 @@ def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd predict: emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) # or - g3 = g2.transform_dbscan(ndf, ndf, return_graph=True) + g3 = g2.transform_dbscan(ndf, return_graph=True) g3.plot() args: @@ -270,7 +271,7 @@ def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd def transform_dbscan(self, df: pd.DataFrame, y: pd.DataFrame = None, eps: Union[float, str]='auto', - use_umap_embedding:bool=True, + fit_umap_embedding:bool=False, sample:int=None, kind:str='nodes', return_graph=True): @@ -285,16 +286,20 @@ def transform_dbscan(self, df: pd.DataFrame, y: pd.DataFrame = None, eps: The maximum distance between two samples for them to be considered as in the same neighborhood. smaller values will result in less edges between the minibatch and the original graph. Default 'auto', infers eps from the mean distance and std of new points to the original graph - use_umap_embedding: whether to use UMAP embeddings or features dataframe when running DBSCAN - n_nearest: number of nearest neighbors to use for DBSCAN + fit_umap_embedding: whether to use UMAP embeddings or features dataframe when inferring edges between + the minibatch and the original graph. Default False, uses the features dataframe + sample: number of samples to use when inferring edges between the minibatch and the original graph, + if None, will only use closest point to the minibatch. If greater than 0, will sample the closest `sample` points + in existing graph to pull in more edges. Default None kind: 'nodes' or 'edges' - return_graph: whether to return a graph or the minibatch enriched with cluster labels, default True + return_graph: whether to return a graph or the (emb, X, y, minibatch enriched with DBSCAN labels), default True + """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) if return_graph: res = self.bind() - g = infer_graph(res, emb, X, y, df, use_umap_embedding=use_umap_embedding, eps=eps, sample=sample) + g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample) return g return emb, X, y, df From 5620f585b3ff7db702427c8bb0cad645d4e99aec Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 00:40:00 -0800 Subject: [PATCH 043/432] feat(default model args) --- graphistry/embed_utils.py | 3 +- graphistry/features.py | 97 +++++++++++++++++++++++---------------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index 42743d7537..ef09a02475 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -89,7 +89,8 @@ def __init__(self): self._device = "cpu" def _preprocess_embedding_data(self, res, train_split:Union[float, int] = 0.8) -> Plottable: - _, torch, _, _, _, _, F, _ = lazy_embed_import_dep() + _, torch, _, _, _, _, _, _ = lazy_embed_import_dep() + import torch log('Preprocessing embedding data') src, dst = res._source, res._destination relation = res._relation diff --git a/graphistry/features.py b/graphistry/features.py index 7567b159db..35cfb4e5a6 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -21,8 +21,8 @@ # ################# graphistry featurization config constants ################# N_TOPICS = 42 N_TOPICS_TARGET = 10 -HIGH_CARD = 4e7 # forces one hot encoding -MID_CARD = 2e3 # todo: forces hashing +HIGH_CARD = 1e9 # forces one hot encoding +MID_CARD = 1e3 # todo: force hashing LOW_CARD = 2 CARD_THRESH = 40 @@ -88,7 +88,7 @@ # ############################################################################# class ModelDict(UserDict): - """Helper class to print out model names + """Helper class to print out model names and keep track of updates Args: message: description of model @@ -160,55 +160,74 @@ def update(self, *args, **kwargs): # customize the default parameters for each model you want to test # Ngrams Model over features -ngrams_model = ModelDict("Ngrams Model", verbose=True, **default_featurize_parameters) -ngrams_model.update(dict(use_ngrams=True, min_words=HIGH_CARD)) +ngrams_model = ModelDict("Ngrams Model", + use_ngrams=True, + min_words=HIGH_CARD, + verbose=True) +#ngrams_model.update(dict(use_ngrams=True, min_words=HIGH_CARD)) # Topic Model over features -topic_model = ModelDict("Topic Model", verbose=True, **default_featurize_parameters) -topic_model.update( - dict( - cardinality_threshold=LOW_CARD, # force topic model - cardinality_threshold_target=LOW_CARD, # force topic model - n_topics=N_TOPICS, - n_topics_target=N_TOPICS_TARGET, - min_words=HIGH_CARD, # make sure it doesn't turn into sentence model, but rather topic models - ) -) +topic_model = ModelDict("Reliable Topic Models on Features and Target", + cardinality_threshold=LOW_CARD, # force topic model + cardinality_threshold_target=LOW_CARD, # force topic model + n_topics=N_TOPICS, + n_topics_target=N_TOPICS_TARGET, + min_words=HIGH_CARD, # make sure it doesn't turn into sentence model, but rather topic models + verbose=True + ) +#**default_featurize_parameters) +# topic_model.update( +# dict( +# cardinality_threshold=LOW_CARD, # force topic model +# cardinality_threshold_target=LOW_CARD, # force topic model +# n_topics=N_TOPICS, +# n_topics_target=N_TOPICS_TARGET, +# min_words=HIGH_CARD, # make sure it doesn't turn into sentence model, but rather topic models +# ) +# ) # useful for text data that you want to paraphrase -embedding_model = ModelDict( - f"{PARAPHRASE_SMALL_MODEL} Embedding Model", - verbose=True, - **default_featurize_parameters, -) -embedding_model.update( - dict( - min_words=FORCE_EMBEDDING_ALL_COLUMNS, - model_name=PARAPHRASE_SMALL_MODEL, # if we need multilingual support, use PARAPHRASE_MULTILINGUAL_MODEL - ) +embedding_model = ModelDict(f"{PARAPHRASE_SMALL_MODEL} sbert Embedding Model", + min_words=FORCE_EMBEDDING_ALL_COLUMNS, + model_name=PARAPHRASE_SMALL_MODEL, # if we need multilingual support, use PARAPHRASE_MULTILINGUAL_MODEL + verbose=True, + #**default_featurize_parameters, ) +# embedding_model.update( +# dict( +# min_words=FORCE_EMBEDDING_ALL_COLUMNS, +# model_name=PARAPHRASE_SMALL_MODEL, # if we need multilingual support, use PARAPHRASE_MULTILINGUAL_MODEL +# ) +# ) # useful for when search input is much smaller than the encoded documents search_model = ModelDict( - f"{MSMARCO2} Search Model", verbose=True, **default_featurize_parameters -) -search_model.update( - dict( - min_words=FORCE_EMBEDDING_ALL_COLUMNS, - model_name=MSMARCO2, - ) + f"{MSMARCO2} Search Model", + verbose=True, + min_words=FORCE_EMBEDDING_ALL_COLUMNS, + model_name=MSMARCO2, + #**default_featurize_parameters ) +# search_model.update( +# dict( +# min_words=FORCE_EMBEDDING_ALL_COLUMNS, +# model_name=MSMARCO2, +# ) +# ) # Question Answering encodings for search qa_model = ModelDict( - f"{QA_SMALL_MODEL} QA Model", verbose=True, **default_featurize_parameters -) -qa_model.update( - dict( - min_words=FORCE_EMBEDDING_ALL_COLUMNS, - model_name=QA_SMALL_MODEL, - ) + f"{QA_SMALL_MODEL} QA Model", + min_words=FORCE_EMBEDDING_ALL_COLUMNS, + model_name=QA_SMALL_MODEL, + verbose=True, ) +# qa_model.update( +# dict( +# min_words=FORCE_EMBEDDING_ALL_COLUMNS, +# model_name=QA_SMALL_MODEL, +# ) +# ) BASE_MODELS = { From 9680e49e602347b410076d08adef1dfdb95b049c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 00:40:39 -0800 Subject: [PATCH 044/432] tests not passing, checkpoint --- graphistry/tests/test_umap_utils.py | 120 +++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index b0aef2432c..1da454a337 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -4,9 +4,11 @@ import warnings import graphistry + import logging import numpy as np import pandas as pd +from graphistry import Plottable from graphistry.feature_utils import remove_internal_namespace_if_present from graphistry.tests.test_feature_utils import ( ndf_reddit, @@ -67,6 +69,7 @@ class TestUMAPFitTransform(unittest.TestCase): def setUp(self): g = graphistry.nodes(ndf_reddit) + self.gn = g with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) @@ -78,14 +81,18 @@ def setUp(self): use_scaler='robust', cardinality_threshold=2) + self.g2 = g2 fenc = g2._node_encoder self.X, self.Y = fenc.X, fenc.y self.EMB = g2._node_embedding - self.emb, self.x, self.y = g2.transform_umap(ndf_reddit, ydf=double_target_reddit, kind='nodes') - + self.emb, self.x, self.y = g2.transform_umap(ndf_reddit, ydf=double_target_reddit, kind='nodes', return_graph=False) + self.g3 = g2.transform_umap(ndf_reddit, ydf=double_target_reddit, kind='nodes', return_graph=True) + + # do the same for edges edge_df22 = edge_df2.copy() edge_df22['rando'] = np.random.rand(edge_df2.shape[0]) g = graphistry.edges(edge_df22, 'src', 'dst') + self.ge = g with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -100,7 +107,9 @@ def setUp(self): fenc = g2._edge_encoder self.Xe, self.Ye = fenc.X, fenc.y self.EMBe = g2._edge_embedding - self.embe, self.xe, self.ye = g2.transform_umap(edge_df22, ydf=edge2_target_df, kind='edges') + self.embe, self.xe, self.ye = g2.transform_umap(edge_df22, ydf=edge2_target_df, kind='edges', return_graph=False) + self.g2e = g2 + self.g3e = g2.transform_umap(edge_df22, ydf=edge2_target_df, kind='edges', return_graph=True) @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_columns_match(self): @@ -109,6 +118,108 @@ def test_columns_match(self): assert all(self.Xe.columns == self.xe.columns), 'Edge Feature Columns do not match' assert all(self.Ye.columns == self.ye.columns), 'Edge Target Columns do not match' + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") + def test_index_match(self): + # nodes + assert all(self.gn._nodes.index == self.g2._nodes.index), 'Node Indexes do not match' + assert all(self.gn._nodes.index == self.EMB.index), 'Emb Indexes do not match' + assert all(self.gn._nodes.index == self.emb.index), 'Transformed Emb Indexes do not match' + assert all(self.gn._nodes.index == self.X.index), 'Transformed Node features Indexes do not match' + assert all(self.gn._nodes.index == self.y.index), 'Transformed Node target Indexes do not match' + + # edges + assert all(self.ge._edges.index == self.g2e._edges.index), 'Edge Indexes do not match' + assert all(self.ge._edges.index == self.EMBe.index), 'Edge Emb Indexes do not match' + assert all(self.ge._edges.index == self.embe.index), 'Edge Transformed Emb Indexes do not match' + assert all(self.ge._edges.index == self.Xe.index), 'Edge Transformed features Indexes do not match' + assert all(self.ge._edges.index == self.ye.index), 'Edge Transformed target Indexes do not match' + + # make sure the indexes match at transform time internally as well + assert all(self.X.index == self.x.index), 'Node Feature Indexes do not match' + assert all(self.Y.index == self.y.index), 'Node Target Indexes do not match' + assert all(self.Xe.index == self.xe.index), 'Edge Feature Indexes do not match' + assert all(self.Ye.index == self.ye.index), 'Edge Target Indexes do not match' + + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") + def test_index_match_in_infered_graph(self): + # nodes + assert all(self.g3._nodes.index == self.g2._nodes.index), 'Node Indexes do not match' + assert all(self.g3._nodes.index == self.EMB.index), 'Emb Indexes do not match' + assert all(self.g3._nodes.index == self.emb.index), 'Transformed Emb Indexes do not match' + assert all(self.g3._nodes.index == self.X.index), 'Transformed Node features Indexes do not match' + assert all(self.g3._nodes.index == self.y.index), 'Transformed Node target Indexes do not match' + + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") + def test_nodes_index_match_in_infered_graph(self): + # edges + ndf_infered = self.g3._nodes + assert all(ndf_infered.index == self.EMBe.index), 'Edge Emb Indexes do not match' + assert all(ndf_infered.index == self.embe.index), 'Edge Transformed Emb Indexes do not match' + assert all(ndf_infered.index == self.Xe.index), 'Edge Transformed features Indexes do not match' + assert all(ndf_infered.index == self.ye.index), 'Edge Transformed target Indexes do not match' + + # now test in set featurize method calls + assert all(self.g3._node_features.index == ndf_infered.index), 'Edge Feature Indexes do not match' + assert all(self.g3._node_embedding.index == ndf_infered.index), 'Edge Emb Indexes do not match' + assert all(self.g3._node_target.index == ndf_infered.index), 'Edge Transformed Emb Indexes do not match' + # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed features Indexes do not match' + # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed target Indexes do not match' + + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") + def test_edges_index_match_in_infered_graph(self): + # edges + edf_infered = self.g3e._edges + assert all(edf_infered.index == self.EMBe.index), 'Edge Emb Indexes do not match' + assert all(edf_infered.index == self.embe.index), 'Edge Transformed Emb Indexes do not match' + assert all(edf_infered.index == self.Xe.index), 'Edge Transformed features Indexes do not match' + assert all(edf_infered.index == self.ye.index), 'Edge Transformed target Indexes do not match' + + assert all(self.g3e._edge_features.index == edf_infered.index), 'Edge Feature Indexes do not match' + assert all(self.g3e._edge_embedding.index == edf_infered.index), 'Edge Emb Indexes do not match' + assert all(self.g3e._edge_target.index == edf_infered.index), 'Edge Transformed Emb Indexes do not match' + # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed features Indexes do not match' + # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed target Indexes do not match' + + + def test_transform_umap(self): + np.random.seed(41) + + train = ndf_reddit.sample(frac=0.8, random_state=42) + test = ndf_reddit.drop(train.index) + + # just process train + g = graphistry.nodes(train) + g2 = g.umap() + g3 = g2.transform_umap(train) + assert 2 * g2._node_embedding.shape[0] == g3._node_embedding.shape[0], 'Node Embedding Lengths do not match, found {} and {}'.format(g2._node_embedding.shape[0], g3._node_embedding.shape[0]) + # now feed it args + eps=['auto', 10] + sample=[None, 2] + return_graph=[True, False] + fit_umap_embedding = [True, False] + for ep in eps: + g4 = g2.transform_umap(test, eps=ep) + assert True + for return_g in return_graph: + g4 = g2.transform_umap(test, return_graph=return_g) + if return_g: + assert isinstance(g4, Plottable) + # == g4._node_embedding.shape + else: + assert len(g4) == 3 + assert isinstance(g4[0], pd.DataFrame) + assert isinstance(g4[1], pd.DataFrame) + assert isinstance(g4[2], pd.DataFrame) + assert g4[0].shape[1] == 2 + assert g4[1].shape[1] >= 2 + assert g4[2].shape[1] >= 1 + for sample_ in sample: + g4 = g2.transform_umap(test, sample=sample_) + assert True + for fit_umap_embedding_ in fit_umap_embedding: + g4 = g2.transform_umap(test, fit_umap_embedding=fit_umap_embedding_) + assert True + class TestUMAPMethods(unittest.TestCase): def _check_attributes(self, g, attributes): @@ -393,7 +504,8 @@ def test_filter_edges(self): self.assertGreaterEqual(shape[0], last_shape) last_shape = shape[0] - + + @pytest.mark.skipif( not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", From 554352c1d1d462021006caca2135ed48cf63fcb0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 01:39:41 -0800 Subject: [PATCH 045/432] adds g._infer_edges in feature_utils and propagates to other transformers in umap, dbscan/cluster --- graphistry/ai_utils.py | 2 +- graphistry/compute/cluster.py | 8 +++----- graphistry/feature_utils.py | 14 ++++++++++---- graphistry/umap_utils.py | 16 ++++++++-------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index f0b22cdab7..c718b0eefd 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -204,7 +204,7 @@ def infer_graph(res, emb, X, y, df, infer_on_umap_embedding=False, eps='auto', s n_nearest: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. """ - new_index = df.index + #new_index = df.index if infer_on_umap_embedding and emb is not None: X_previously_fit = res._node_embedding X_new = emb diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 8d223624f8..98b0fb0f04 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -2,7 +2,7 @@ import pandas as pd import numpy as np -from typing import Any, List, Union, TYPE_CHECKING +from typing import Any, List, Union, TYPE_CHECKING, Tuple, Optional, Dict, Callable from typing_extensions import Literal from collections import Counter @@ -11,7 +11,6 @@ from graphistry.constants import CUML, UMAP_LEARN # noqa type: ignore from graphistry.features import ModelDict from graphistry.feature_utils import get_matrix_by_column_parts -from graphistry.ai_utils import infer_graph logger = logging.getLogger("compute.cluster") @@ -274,7 +273,7 @@ def transform_dbscan(self, df: pd.DataFrame, y: pd.DataFrame = None, fit_umap_embedding:bool=False, sample:int=None, kind:str='nodes', - return_graph=True): + return_graph=True)-> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: """ Transforms a minibatch dataframe to one with a new column '_cluster' containing the DBSCAN cluster labels on the minibatch and generates a graph with the minibatch and the original graph, with edges between the minibatch and the original graph inferred @@ -298,8 +297,7 @@ def transform_dbscan(self, df: pd.DataFrame, y: pd.DataFrame = None, """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) if return_graph: - res = self.bind() - g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample) + g = self._infer_edges(emb, X, y, df, infer_on_umap_embedding=False, eps=eps, sample=sample) return g return emb, X, y, df diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index f59823e2c2..8f900ce7d1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1899,7 +1899,7 @@ def _featurize_nodes( feature_engine: FeatureEngineConcrete = "pandas", memoize: bool = True, ): - res = self.copy() + res = self.bind() # was self.copy() but changing to test ndf = res._nodes node = res._node @@ -1927,6 +1927,8 @@ def _featurize_nodes( y_resolved = resolve_y(ndf, y) feature_engine = resolve_feature_engine(feature_engine) + + from .features import ModelDict fkwargs = dict( X=X_resolved, @@ -1978,7 +1980,7 @@ def _featurize_nodes( X_resolved = remove_internal_namespace_if_present(X_resolved) keys_to_remove = ["X", "y", "remove_node_column"] - nfkwargs = {} + nfkwargs = dict() for key, value in fkwargs.items(): if key not in keys_to_remove: nfkwargs[key] = value @@ -2112,6 +2114,11 @@ def _featurize_edges( res._edge_encoder = encoder return res + + def _infer_edges(self, emb, X, y, df, eps='auto', sample=None, infer_on_umap_embedding=False, **kwargs): + res = self.bind() # will not be able to decide umap coordinates, but will be able to infer graph from existing edges + g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, eps=eps, sample=sample) + return g def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): if getattr(self, encoder) is not None: @@ -2144,9 +2151,8 @@ def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', s logger.debug("kind must be one of `nodes`," f"`edges`, found {kind}") if return_graph: - res = self.bind() emb = None # will not be able to decide umap coordinates, but will be able to infer graph from existing edges - g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=False, eps=eps, sample=sample) + g = self._infer_edges(emb, X, y, df, infer_on_umap_embedding=False, eps=eps, sample=sample) return g return X, y diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 42dd0e11c0..72bcb6f8b2 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -10,10 +10,11 @@ prune_weighted_edges_df_and_relabel_nodes, resolve_feature_engine) from .PlotterBase import Plottable, WeakValueDictionary -from .util import check_set_memoize, setup_logger -from .ai_utils import infer_graph +from .util import check_set_memoize -logger = setup_logger(name=__name__, verbose=config.VERBOSE) +import logging + +logger = logging.getLogger(__name__) if TYPE_CHECKING: MIXIN_BASE = FeatureMixin @@ -294,9 +295,8 @@ def transform_umap( # noqa: E303 emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) if return_graph: - res = self.bind() - g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample) - return g + g = self._infer_edges(emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample) + return g return emb, X, y def _bundle_embedding(self, emb, index): @@ -306,8 +306,8 @@ def _bundle_embedding(self, emb, index): columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1]) ] - print('emb.shape', emb.shape) - print('columns', columns, len(columns)) + # print('emb.shape', emb.shape) + # print('columns', columns, len(columns)) emb = pd.DataFrame(emb, columns=columns, index=index) return emb From b4ff370b4517385e63508c3e7142896844be5544 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 01:51:57 -0800 Subject: [PATCH 046/432] lint and bug fix in arg --- graphistry/ai_utils.py | 129 ++++++++++------- graphistry/compute/cluster.py | 261 ++++++++++++++++++++-------------- graphistry/text_utils.py | 172 ++++++++++++---------- 3 files changed, 322 insertions(+), 240 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index c718b0eefd..a6f4bd0845 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -36,7 +36,7 @@ def search_to_df(word, col, df, as_string=False): res = df[df[col].str.contains(word, case=False)] except TypeError as e: logger.error(e) - return pd.DataFrame([], columns = df.columns) + return pd.DataFrame([], columns=df.columns) return res @@ -131,14 +131,16 @@ def get_graphistry_from_milieu_search( g = graphistry.edges(tdf, src, dst).nodes(ntdf, node_col) return g + # ######################################################################################################################### # # Graphistry Vector Search Index # ########################################################################################################################## + def build_annoy_index(X, angular, n_trees=None): - """ Builds an Annoy Index for fast vector search + """Builds an Annoy Index for fast vector search Args: X (_type_): _description_ @@ -153,12 +155,12 @@ def build_annoy_index(X, angular, n_trees=None): logger.info(f"Building Index of size {X.shape}") if angular: - logger.info('-using angular metric') - metric = 'angular' + logger.info("-using angular metric") + metric = "angular" else: - logger.info('-using euclidean metric') - metric = 'euclidean' - + logger.info("-using euclidean metric") + metric = "euclidean" + search_index = AnnoyIndex(X.shape[1], metric) # Add all the feature vectors to the search index for i in range(len(X)): @@ -166,7 +168,7 @@ def build_annoy_index(X, angular, n_trees=None): if n_trees is None: n_trees = N_TREES - logger.info(f'-building index with {n_trees} trees') + logger.info(f"-building index with {n_trees} trees") search_index.build(n_trees) return search_index @@ -175,7 +177,7 @@ def query_by_vector(vect, df, search_index, top_n): indices, distances = search_index.get_nns_by_vector( vect.values[0], top_n, include_distances=True ) - + results = df.iloc[indices] results[DISTANCE] = distances results = results.sort_values(by=[DISTANCE]) @@ -190,87 +192,104 @@ def query_by_vector(vect, df, search_index, top_n): ########################################################################################################################## -def infer_graph(res, emb, X, y, df, infer_on_umap_embedding=False, eps='auto', sample=None): +def infer_graph( + res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None +): """ - Infer a graph from a graphistry object - - args: - res: graphistry object - df: outside minibatch dataframe to add to existing graph - X: minibatch transformed dataframe - emb: minibatch UMAP embedding - kind: 'nodes' or 'edges' - eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph - n_nearest: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. + Infer a graph from a graphistry object + + args: + res: graphistry object + df: outside minibatch dataframe to add to existing graph + X: minibatch transformed dataframe + emb: minibatch UMAP embedding + kind: 'nodes' or 'edges' + eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph + n_nearest: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. """ - #new_index = df.index + # new_index = df.index if infer_on_umap_embedding and emb is not None: X_previously_fit = res._node_embedding X_new = emb else: # can still be umap, but want to do the inference on the higher dimensional features X_previously_fit = res._node_features X_new = X - + FEATS = res._node_features EMB = res._node_embedding Y = res._node_target - - assert df.shape[0] == X.shape[0], 'minibatches df and X must have same number of rows since f(df) = X' + + assert ( + df.shape[0] == X.shape[0] + ), "minibatches df and X must have same number of rows since f(df) = X" if emb is not None: - assert emb.shape[0] == df.shape[0], 'minibatches emb and X must have same number of rows since h(df) = emb' + assert ( + emb.shape[0] == df.shape[0] + ), "minibatches emb and X must have same number of rows since h(df) = emb" df = df.assign(x=emb.x, y=emb.y) # add x and y to df for graphistry instance - + # if umap, need to add '_n' as node id to df, adding new indices to existing graph - numeric_indices = range(X_previously_fit.shape[0], X_previously_fit.shape[0]+df.shape[0]) - df['_n'] = numeric_indices - df['_batch'] = 1 # 1 for minibatch, 0 for existing graph + numeric_indices = range( + X_previously_fit.shape[0], X_previously_fit.shape[0] + df.shape[0] + ) + df["_n"] = numeric_indices + df["_batch"] = 1 # 1 for minibatch, 0 for existing graph node = res._node NDF = res._nodes - NDF['_batch'] = 0 + NDF["_batch"] = 0 EDF = res._edges - EDF['_batch'] = 0 + EDF["_batch"] = 0 src = res._source dst = res._destination - + new_edges = [] old_edges = [] old_nodes = [] - mdists=[] - + mdists = [] + # vsearch = build_search_index(X_previously_fit, angular=False) - + for i in range(X_new.shape[0]): # record_df = df.iloc[i, :] diff = X_previously_fit - X_new.iloc[i, :] dist = np.linalg.norm(diff, axis=1) # Euclidean distance mdists.append(dist) - + m, std = np.mean(mdists), np.std(mdists) - logger.info(f'--Mean distance to existing nodes {m:.2f} +/- {std:.2f}') + logger.info(f"--Mean distance to existing nodes {m:.2f} +/- {std:.2f}") # print(f'--Mean distance to existing nodes {m:.2f} +/- {std:.2f}') - if eps == 'auto': - eps = np.min([np.abs(m - 2*std), m]) - logger.info(f'{eps:.2f} epsilon for max distance threshold to be considered a neighbor') - + if eps == "auto": + eps = np.min([np.abs(m - 2 * std), m]) + logger.info( + f"{eps:.2f} epsilon for max distance threshold to be considered a neighbor" + ) + for i, dist in enumerate(mdists): record_df = df.iloc[i, :] for j in np.where(dist < eps)[0]: this_ndf = NDF.iloc[j, :] if sample: - local_edges = EDF[(EDF[src] == this_ndf[node]) | (EDF[dst] == this_ndf[node])] + local_edges = EDF[ + (EDF[src] == this_ndf[node]) | (EDF[dst] == this_ndf[node]) + ] if not local_edges.empty: old_edges.append(local_edges.sample(sample, replace=True)) new_edges.append([this_ndf[node], record_df[node], 1, 1]) old_nodes.append(this_ndf) - new_edges = pd.DataFrame(new_edges, columns=[src, dst, '_weight', '_batch']) - + new_edges = pd.DataFrame(new_edges, columns=[src, dst, "_weight", "_batch"]) + all_nodes = [] if len(old_edges): old_edges = pd.concat(old_edges, axis=0).assign(_batch=0) - all_nodes = old_edges[src].append(old_edges[dst]).append(new_edges[src]).append(new_edges[dst]) - + all_nodes = ( + old_edges[src] + .append(old_edges[dst]) + .append(new_edges[src]) + .append(new_edges[dst]) + ) + if sample: new_edges = pd.concat([new_edges, old_edges], axis=0) # print('sampled', len(new_edges), 'new edges') @@ -279,27 +298,29 @@ def infer_graph(res, emb, X, y, df, infer_on_umap_embedding=False, eps='auto', s if len(old_nodes): old_nodes = pd.DataFrame(old_nodes) - old_nodes = pd.concat([old_nodes, NDF[NDF[node].isin(all_nodes)]], axis=0).drop_duplicates(subset=[node]) - + old_nodes = pd.concat( + [old_nodes, NDF[NDF[node].isin(all_nodes)]], axis=0 + ).drop_duplicates(subset=[node]) + old_emb = None if EMB is not None: old_emb = EMB.loc[old_nodes.index] - + new_emb = None if emb is not None: new_emb = pd.concat([emb, old_emb], axis=0) - + new_features = pd.concat([X, FEATS.loc[old_nodes.index]], axis=0) new_nodes = pd.concat([df, old_nodes], axis=0) # append minibatch at top - + new_targets = pd.concat([y, Y.loc[old_nodes.index]]) if y is not None else Y - + # ######################################################### g = res.nodes(new_nodes, node).edges(new_edges, src, dst) - + g._node_embedding = new_emb g._node_features = new_features g._node_targets = new_targets - + return g diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 98b0fb0f04..272b9caa96 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -18,8 +18,8 @@ MIXIN_BASE = Plottable else: MIXIN_BASE = object - -DBSCANEngineConcrete = Literal['cuml', 'umap_learn'] + +DBSCANEngineConcrete = Literal["cuml", "umap_learn"] DBSCANEngine = Literal[DBSCANEngineConcrete, "auto"] @@ -30,7 +30,7 @@ def lazy_dbscan_import_has_dependency(): from sklearn.cluster import DBSCAN except ImportError: has_min_dependency = False - logger.info('Please install sklearn for CPU DBSCAN') + logger.info("Please install sklearn for CPU DBSCAN") has_cuml_dependency = True cuDBSCAN = None @@ -38,10 +38,9 @@ def lazy_dbscan_import_has_dependency(): from cuml import DBSCAN as cuDBSCAN except ImportError: has_cuml_dependency = False - logger.info('Please install cuml for GPU DBSCAN') - - return has_min_dependency, DBSCAN, has_cuml_dependency, cuDBSCAN + logger.info("Please install cuml for GPU DBSCAN") + return has_min_dependency, DBSCAN, has_cuml_dependency, cuDBSCAN def resolve_cpu_gpu_engine( @@ -50,11 +49,16 @@ def resolve_cpu_gpu_engine( if engine in [CUML, UMAP_LEARN]: return engine # type: ignore if engine in ["auto"]: - has_min_dependency, _, has_cuml_dependency, _ = lazy_dbscan_import_has_dependency() + ( + has_min_dependency, + _, + has_cuml_dependency, + _, + ) = lazy_dbscan_import_has_dependency() if has_cuml_dependency: - return 'cuml' + return "cuml" if has_min_dependency: - return 'umap_learn' + return "umap_learn" raise ValueError( # noqa f'engine expected to be "auto", ' @@ -62,60 +66,61 @@ def resolve_cpu_gpu_engine( f"but received: {engine} :: {type(engine)}" ) - + def get_model_matrix(g, kind, cols, umap): - assert kind in ['nodes', 'edges'] - assert hasattr(g, '_node_encoder') if kind == 'nodes' else hasattr(g, '_edge_encoder') - + assert kind in ["nodes", "edges"] + assert ( + hasattr(g, "_node_encoder") if kind == "nodes" else hasattr(g, "_edge_encoder") + ) + if cols is None: df = g._get_feature(kind) - else: + else: df = g.get_features_by_cols(cols, kind) - if umap and cols is None and g._umap is not None: + if umap and cols is None and g._umap is not None: df = g._get_embedding(kind) - + return df - -def dbscan_fit(g, dbscan, kind='nodes', cols=None, use_umap_embedding=True): + +def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True): """ - Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe - - args: - g: graphistry graph - kind: 'nodes' or 'edges' - cols: list of columns to use for clustering given `g.featurize` has been run - umap: whether to use UMAP embeddings or features dataframe + Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe + + args: + g: graphistry graph + kind: 'nodes' or 'edges' + cols: list of columns to use for clustering given `g.featurize` has been run + umap: whether to use UMAP embeddings or features dataframe """ df = get_model_matrix(g, kind, cols, use_umap_embedding) dbscan.fit(df) labels = dbscan.labels_ - - if kind == 'nodes': - g._nodes = g._nodes.assign(_dbscan = labels) - elif kind == 'edges': - g._edges = g._edges.assign(_dbscan = labels) + + if kind == "nodes": + g._nodes = g._nodes.assign(_dbscan=labels) + elif kind == "edges": + g._edges = g._edges.assign(_dbscan=labels) else: - raise ValueError('kind must be one of `nodes` or `edges`') - - kind = 'node' if kind == 'nodes' else 'edge' - setattr(g, f'_{kind}_dbscan', dbscan) - - return g + raise ValueError("kind must be one of `nodes` or `edges`") + + kind = "node" if kind == "nodes" else "edge" + setattr(g, f"_{kind}_dbscan", dbscan) + return g def dbscan_predict(X: pd.DataFrame, model): """ - DBSCAN has no predict per se, so we reverse engineer one here - from https://stackoverflow.com/questions/27822752/scikit-learn-predicting-new-points-with-dbscan + DBSCAN has no predict per se, so we reverse engineer one here + from https://stackoverflow.com/questions/27822752/scikit-learn-predicting-new-points-with-dbscan """ n_samples = X.shape[0] - + y_new = np.ones(shape=n_samples, dtype=int) * -1 - + for i in range(n_samples): diff = model.components_ - X.iloc[i, :].values # NumPy broadcasting @@ -128,40 +133,64 @@ def dbscan_predict(X: pd.DataFrame, model): return y_new -def dbscan_predict2(g, kind='nodes', cols=None, umap=True): + +def dbscan_predict2(g, kind="nodes", cols=None, umap=True): X = g._get_feature(kind) - dbscan = g._node_dbscan if kind == 'nodes' else g._edge_dbscan + dbscan = g._node_dbscan if kind == "nodes" else g._edge_dbscan preds = dbscan_predict(X, dbscan) return X, preds - + class ClusterMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): pass - - def _cluster_dbscan(self, res, kind, cols, fit_umap_embedding, eps, min_samples, **kwargs): + + def _cluster_dbscan( + self, res, kind, cols, fit_umap_embedding, eps, min_samples, **kwargs + ): """ - DBSCAN clustering on cpu or gpu infered by .engine flag + DBSCAN clustering on cpu or gpu infered by .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() - + res.engine = resolve_cpu_gpu_engine("auto") - res._kwargs_dbscan = ModelDict('latest dbscan kwargs', kind=kind, cols=cols, umap=fit_umap_embedding, eps=eps, min_samples=min_samples, **kwargs) - - dbscan = cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) if res.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) - - res = dbscan_fit(res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding) + res._kwargs_dbscan = ModelDict( + "latest dbscan kwargs", + kind=kind, + cols=cols, + umap=fit_umap_embedding, + eps=eps, + min_samples=min_samples, + **kwargs, + ) + + dbscan = ( + cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) + if res.engine == CUML + else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) + ) + + res = dbscan_fit( + res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding + ) return res - - - def dbscan(self, eps: float = 0.2, min_samples: int = 1, cols = None, kind = 'nodes', fit_umap_embedding = True, **kwargs): - """DBSCAN clustering on cpu or gpu infered automatically - + + def dbscan( + self, + eps: float = 0.2, + min_samples: int = 1, + cols=None, + kind="nodes", + fit_umap_embedding=True, + **kwargs, + ): + """DBSCAN clustering on cpu or gpu infered automatically + Examples: g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') - + # cluster by UMAP embeddings kind = 'nodes' | 'edges' g2 = g.umap(kind=kind).dbscan(kind=kind) @@ -169,82 +198,92 @@ def dbscan(self, eps: float = 0.2, min_samples: int = 1, cols = None, kind = 'no # dbscan with fixed parameters is default in umap g2 = g.umap(dbscan=True) - + # and with greater control over parameters via chaining, g2 = g.umap().dbscan(eps=1.2, min_samples=2, **kwargs) - + # cluster by feature embeddings g2 = g.featurize().dbscan(**kwargs) - - # cluster by a given set of feature column attributes + + # cluster by a given set of feature column attributes g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], **kwargs) - + # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) - + g2.plot() # color by `_dbscan` Useful: - Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, + Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, as well as assessing metrics per cluster, e.g. https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb - + Args: eps float: The maximum distance between two samples for them to be considered as in the same neighborhood. kind str: 'nodes' or 'edges' - cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by + cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] fit_umap_embedding bool: whether to use UMAP embeddings or features dataframe to cluster DBSCAN - min_samples: The number of samples in a neighborhood for a point to be considered as a core point. + min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. - + """ res = self.bind() - res = res._cluster_dbscan(res, kind=kind, cols=cols, fit_umap_embedding=fit_umap_embedding, eps=eps, min_samples=min_samples, **kwargs) - + res = res._cluster_dbscan( + res, + kind=kind, + cols=cols, + fit_umap_embedding=fit_umap_embedding, + eps=eps, + min_samples=min_samples, + **kwargs, + ) + return res - - def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd.DataFrame: + + def _transform_dbscan( + self, df: pd.DataFrame, ydf=None, kind: str = "nodes" + ) -> pd.DataFrame: """ Transforms a dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels and returns feature[cols] or UMAP embedding Examples: - fit: + fit: g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') g2 = g.featurize().dbscan() - + predict: emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) - # or + # or g3 = g2.transform_dbscan(ndf, return_graph=True) g3.plot() - + likewise for umap: fit: g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') g2 = g.umap().dbscan() - + predict: emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) # or g3 = g2.transform_dbscan(ndf, return_graph=True) g3.plot() - + args: df: dataframe to transform ydf: optional labels dataframe kind: 'nodes' or 'edges' - + """ - + res = self.bind() - if hasattr(res, '_kwargs_dbscan'): + if hasattr(res, "_kwargs_dbscan"): # Assume that we are transforming to last fit of dbscan - cols = res._kwargs_dbscan['cols'] - umap = res._kwargs_dbscan['umap'] - - dbscan = res._node_dbscan if kind == 'nodes' else res._edge_dbscan - + cols = res._kwargs_dbscan["cols"] + umap = res._kwargs_dbscan["umap"] + + dbscan = res._node_dbscan if kind == "nodes" else res._edge_dbscan + emb = None if umap and cols is None: emb, X, y = res.transform_umap(df, ydf, kind=kind, return_graph=False) @@ -252,12 +291,12 @@ def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd X, y = res.transform(df, ydf, kind=kind, return_graph=False) if cols is not None: X = get_matrix_by_column_parts(X, cols) - + if umap: X_ = emb else: X_ = X - + labels = dbscan_predict(X_, dbscan) if umap: df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) @@ -266,39 +305,45 @@ def _transform_dbscan(self, df: pd.DataFrame, ydf=None, kind: str='nodes') -> pd return emb, X, y, df else: - raise Exception('No dbscan model found. Please run `g.dbscan()` first') - - def transform_dbscan(self, df: pd.DataFrame, y: pd.DataFrame = None, - eps: Union[float, str]='auto', - fit_umap_embedding:bool=False, - sample:int=None, - kind:str='nodes', - return_graph=True)-> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: + raise Exception("No dbscan model found. Please run `g.dbscan()` first") + + def transform_dbscan( + self, + df: pd.DataFrame, + y: pd.DataFrame = None, + eps: Union[float, str] = "auto", + fit_umap_embedding: bool = False, + sample: int = None, + kind: str = "nodes", + return_graph=True, + ) -> Union[ + Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable + ]: """ Transforms a minibatch dataframe to one with a new column '_cluster' containing the DBSCAN cluster labels on the minibatch and generates a graph with the minibatch and the original graph, with edges between the minibatch and the original graph inferred - works for - + works for + args: df: dataframe to transform y: optional labels dataframe eps: The maximum distance between two samples for them to be considered as in the same neighborhood. - smaller values will result in less edges between the minibatch and the original graph. + smaller values will result in less edges between the minibatch and the original graph. Default 'auto', infers eps from the mean distance and std of new points to the original graph - fit_umap_embedding: whether to use UMAP embeddings or features dataframe when inferring edges between + fit_umap_embedding: whether to use UMAP embeddings or features dataframe when inferring edges between the minibatch and the original graph. Default False, uses the features dataframe - sample: number of samples to use when inferring edges between the minibatch and the original graph, - if None, will only use closest point to the minibatch. If greater than 0, will sample the closest `sample` points + sample: number of samples to use when inferring edges between the minibatch and the original graph, + if None, will only use closest point to the minibatch. If greater than 0, will sample the closest `sample` points in existing graph to pull in more edges. Default None kind: 'nodes' or 'edges' return_graph: whether to return a graph or the (emb, X, y, minibatch enriched with DBSCAN labels), default True - - + + """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) if return_graph: - g = self._infer_edges(emb, X, y, df, infer_on_umap_embedding=False, eps=eps, sample=sample) + g = self._infer_edges( + emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample + ) return g return emb, X, y, df - - diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index c96b568ce2..231980f601 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -1,6 +1,3 @@ -import os -from time import time -import numpy as np import pandas as pd from .feature_utils import FeatureMixin @@ -18,8 +15,8 @@ Any, Optional, Tuple, - TYPE_CHECKING, - Type + TYPE_CHECKING, + Type, ) # noqa @@ -36,17 +33,19 @@ def __init__(self, *args, **kwargs) -> None: def assert_fitted(self): # assert self._umap is not None, 'Umap needs to be fit first, run g.umap(..) to fit a model' assert ( - self._get_feature('nodes') is not None + self._get_feature("nodes") is not None ), "Graphistry Instance is not fit, run g.featurize(kind='nodes', ..) to fit a model ' \ 'if you have nodes & edges dataframe or g.umap(kind='nodes', ..) if you only have nodes dataframe" def assert_features_line_up_with_nodes(self): ndf = self._nodes - X = self._get_feature('nodes') + X = self._get_feature("nodes") a, b = ndf.shape[0], X.shape[0] - assert a == b, 'Nodes dataframe and feature vectors are not same size, '\ - f'found nodes: {a}, feats: {b}. Did you mutate nodes between fit?' - + assert a == b, ( + "Nodes dataframe and feature vectors are not same size, " + f"found nodes: {a}, feats: {b}. Did you mutate nodes between fit?" + ) + def _build_search_index(self, X, angular=False, n_trees=None): # builds local index from X return build_annoy_index(X, angular, n_trees) @@ -55,121 +54,136 @@ def build_index(self, angular=False, n_trees=None): # builds local index self.assert_fitted() self.assert_features_line_up_with_nodes() - - X = self._get_feature('nodes') - self.search_index = self._build_search_index(X, angular, n_trees) + X = self._get_feature("nodes") + self.search_index = self._build_search_index(X, angular, n_trees) def _query_from_dataframe(self, qdf: pd.DataFrame, top_n: int, thresh: float): # Use the loaded featurizers to transform the dataframe vect, _ = self.transform(qdf, None, kind="nodes") - results = query_by_vector(vect, self._nodes, self.search_index, top_n) + results = query_by_vector(vect, self._nodes, self.search_index, top_n) results = results.query(f"{DISTANCE} < {thresh}") return results, vect - + def _query(self, query: str, top_n: int, thresh: float): # build the query dataframe - if not hasattr(self, 'search_index'): + if not hasattr(self, "search_index"): self.build_index() qdf = pd.DataFrame([]) - + cols_text = self._node_encoder.text_cols # type: ignore if len(cols_text) == 0: - logger.warn('** Querying is only possible using Transformer/Ngrams embeddings') + logger.warn( + "** Querying is only possible using Transformer/Ngrams embeddings" + ) return pd.DataFrame([]), None - + qdf[cols_text[0]] = [query] if len(cols_text) > 1: for col in cols_text[1:]: - qdf[col] = [''] + qdf[col] = [""] # this is hookey and needs to be fixed on dirty_cat side (with errors='ignore') - # if however min_words = 0, all columns will be textual, + # if however min_words = 0, all columns will be textual, # and no other data_encoder will be generated - if hasattr(self._node_encoder.data_encoder, 'columns_'): # type: ignore + if hasattr(self._node_encoder.data_encoder, "columns_"): # type: ignore other_cols = self._node_encoder.data_encoder.columns_ # type: ignore if other_cols is not None and len(other_cols): - logger.warn('** There is no easy way to encode categorical or other features at query time. ' - f'Set `thresh` to a large value if no results show up.\ncolumns: {other_cols}') + logger.warn( + "** There is no easy way to encode categorical or other features at query time. " + f"Set `thresh` to a large value if no results show up.\ncolumns: {other_cols}" + ) df = self._nodes dt = df[other_cols].dtypes for col, v in zip(other_cols, dt.values): if str(v) in ["string", "object", "category"]: - qdf[col] = df.sample(1)[col].values # so hookey + qdf[col] = df.sample(1)[col].values # so hookey elif str(v) in [ - "int", - "float", - "float64", - "float32", - "float16", - "int64", - "int32", - "int16", - "uint64", - "uint32", - "uint16", - ]: + "int", + "float", + "float64", + "float32", + "float16", + "int64", + "int32", + "int16", + "uint64", + "uint32", + "uint16", + ]: qdf[col] = df[col].mean() return self._query_from_dataframe(qdf, thresh=thresh, top_n=top_n) def search( - self, query: str, cols = None, thresh: float = 5000, fuzzy: bool = True, top_n: int = 10 - ): + self, + query: str, + cols=None, + thresh: float = 5000, + fuzzy: bool = True, + top_n: int = 10, + ): """Natural language query over nodes that returns a dataframe of results sorted by relevance column "distance". - If node data is not yet feature-encoded (and explicit edges are given), - run automatic feature engineering: + If node data is not yet feature-encoded (and explicit edges are given), + run automatic feature engineering: ``` - g2 = g.featurize(kind='nodes', X=['text_col_1', ..], + g2 = g.featurize(kind='nodes', X=['text_col_1', ..], min_words=0 # forces all named columns are textually encoded - ) - ``` - - If edges do not yet exist, generate them via + ) + ``` + + If edges do not yet exist, generate them via ``` - g2 = g.umap(kind='nodes', X=['text_col_1', ..], + g2 = g.umap(kind='nodes', X=['text_col_1', ..], min_words=0 # forces all named columns are textually encoded - ) - ``` + ) + ``` If an index is not yet built, it is generated `g2.build_index()` on the fly at search time. - Otherwise, can set `g2.build_index()` and then subsequent `g2.search(...)` + Otherwise, can set `g2.build_index()` and then subsequent `g2.search(...)` calls will be not rebuilt index. Args: query (str): natural language query. - cols (list or str, optional): if fuzzy=False, select which column to query. + cols (list or str, optional): if fuzzy=False, select which column to query. Defaults to None since fuzzy=True by defaul. thresh (float, optional): distance threshold from query vector to returned results. - Defaults to 5000, set large just in case, + Defaults to 5000, set large just in case, but could be as low as 10. - fuzzy (bool, optional): if True, uses embedding + annoy index for recall, - otherwise does string matching over given `cols` + fuzzy (bool, optional): if True, uses embedding + annoy index for recall, + otherwise does string matching over given `cols` Defaults to True. top_n (int, optional): how many results to return. Defaults to 100. Returns: - pd.DataFrame, vector_encoding_of_query: + pd.DataFrame, vector_encoding_of_query: * rank ordered dataframe of results matching query * vector encoding of query via given transformer/ngrams model if fuzzy=True else None """ if not fuzzy: if cols is None: - logger.error(f'Columns to search for `{query}` \ - need to be given when fuzzy=False, found {cols}') - + logger.error( + f"Columns to search for `{query}` \ + need to be given when fuzzy=False, found {cols}" + ) + logger.info(f"-- Word Match: [[ {query} ]]") return ( - pd.concat([search_to_df(query, col, self._nodes, as_string=True) for col in cols]), - None + pd.concat( + [ + search_to_df(query, col, self._nodes, as_string=True) + for col in cols + ] + ), + None, ) else: logger.info(f"-- Search: [[ {query} ]]") @@ -184,7 +198,7 @@ def search_graph( broader: bool = False, inplace: bool = False, ): - """Input a natural language query and return a graph of results. + """Input a natural language query and return a graph of results. See help(g.search) for more information Args: @@ -192,7 +206,7 @@ def search_graph( scale (float, optional): edge weigh threshold, Defaults to 0.5. top_n (int, optional): how many results to return. Defaults to 100. thresh (float, optional): distance threshold from query vector to returned results. - Defaults to 5000, set large just in case, + Defaults to 5000, set large just in case, but could be as low as 10. broader (bool, optional): if True, will retrieve entities connected via an edge that were not necessarily bubbled up in the results_dataframe. Defaults to False. @@ -206,7 +220,7 @@ def search_graph( res = self else: res = self.bind() - + edf = edges = res._edges rdf = df = res._nodes node = res._node @@ -220,23 +234,21 @@ def search_graph( indices = rdf[node] # now get edges from indices if broader: # this will make a broader graph, finding NN in src OR dst - edges = edf[ - (edf[src].isin(indices)) | (edf[dst].isin(indices)) - ] - else: # finds only edges between results from query, if they exist, + edges = edf[(edf[src].isin(indices)) | (edf[dst].isin(indices))] + else: # finds only edges between results from query, if they exist, # default smaller graph - edges = edf[ - (edf[src].isin(indices)) & (edf[dst].isin(indices)) - ] + edges = edf[(edf[src].isin(indices)) & (edf[dst].isin(indices))] else: - logger.warn('**No results found due to empty DataFrame, returning original graph') + logger.warn( + "**No results found due to empty DataFrame, returning original graph" + ) return res - + try: # for umap'd edges edges = edges.query(f"{WEIGHT} > {scale}") except: # for explicit edges pass - + found_indices = pd.concat([edges[src], edges[dst], indices], axis=0).unique() try: tdf = rdf.iloc[found_indices] @@ -244,19 +256,22 @@ def search_graph( tdf = rdf[df[node].isin(found_indices)] logger.info(f" - Returning edge dataframe of size {edges.shape[0]}") # get all the unique nodes - logger.info(f" - Returning {tdf.shape[0]} unique nodes given scale {scale} and thresh {thresh}") - + logger.info( + f" - Returning {tdf.shape[0]} unique nodes given scale {scale} and thresh {thresh}" + ) + g = res.edges(edges, src, dst).nodes(tdf, node) - + if g._name is not None: - name = f'{g._name}-query:{query}' + name = f"{g._name}-query:{query}" else: - name = f'query:{query}' + name = f"query:{query}" g = g.name(name) # type: ignore return g def save_search_instance(self, savepath): from joblib import dump # type: ignore # need to make this onnx or similar + self.build_index() search = self.search_index del self.search_index # can't pickle Annoy @@ -267,6 +282,7 @@ def save_search_instance(self, savepath): @classmethod def load_search_instance(self, savepath): from joblib import load # type: ignore # need to make this onnx or similar + cls = load(savepath) cls.build_index() return cls From 20b5a3fa17aebe28b2b8743bd444bdf679a0a1f7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 18:28:13 -0800 Subject: [PATCH 047/432] fix(chaining was mute on changes in umap args; partial fix, works after 2 runs of umap --- graphistry/umap_utils.py | 83 ++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 72bcb6f8b2..434dd41f32 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -165,16 +165,17 @@ class UMAPMixin(MIXIN_BASE): _umap_memoize: WeakValueDictionary = WeakValueDictionary() def __init__(self, *args, **kwargs): - self.umap_initialized = False + self._umap_initialized = False def umap_lazy_init( self, + res, n_neighbors: int = 12, min_dist: float = 0.1, - spread=0.5, - local_connectivity=1, - repulsion_strength=1, - negative_sample_rate=5, + spread: float = 0.5, + local_connectivity: int = 1, + repulsion_strength: float = 1, + negative_sample_rate: int = 5, n_components: int = 2, metric: str = "euclidean", engine: UMAPEngine = "auto", @@ -191,7 +192,8 @@ def umap_lazy_init( "No umap engine, ensure 'auto', 'umap_learn', or 'cuml', and the library is installed" ) - if not self.umap_initialized: + if not self._umap_initialized: + from graphistry.features import ModelDict umap_kwargs = dict( { "n_components": n_components, @@ -204,21 +206,27 @@ def umap_lazy_init( "negative_sample_rate": negative_sample_rate, } ) + print('umap_kwargs init: ', umap_kwargs['n_components']) - print('umap_kwargs', umap_kwargs) - - self.n_components = n_components - self.metric = metric - self.n_neighbors = n_neighbors - self.min_dist = min_dist - self.spread = spread - self.local_connectivity = local_connectivity - self.repulsion_strength = repulsion_strength - self.negative_sample_rate = negative_sample_rate - self._umap = umap_engine.UMAP(**umap_kwargs) - self.umap_initialized = True - self.engine = engine_resolved - self.suffix = suffix + #print('umap_kwargs', umap_kwargs) + res._n_components = n_components + res._metric = metric + res._n_neighbors = n_neighbors + res._min_dist = min_dist + res._spread = spread + res._local_connectivity = local_connectivity + res._repulsion_strength = repulsion_strength + res._negative_sample_rate = negative_sample_rate + res._umap = umap_engine.UMAP(**umap_kwargs) + res.engine = engine_resolved + res._suffix = suffix + + res._umap_params = dict(# ModelDict(f'Umap Parameters', + **umap_kwargs) + # finally set the flag + res._umap_initialized = True + return res + def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): if y is None: @@ -240,7 +248,7 @@ def _get_embedding(self, kind='nodes'): elif kind == 'edges': return self._edge_embedding else: - raise ValueError('kind must be one of nodes or edges') + raise ValueError('kind must be one of `nodes` or `edges`') def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: @@ -253,7 +261,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self.engine == CUML and is_legacy_cuml(): from cuml.neighbors import NearestNeighbors - knn = NearestNeighbors(n_neighbors=self.n_neighbors) + knn = NearestNeighbors(n_neighbors=self._n_neighbors) cc = self._umap.fit(X, y, knn_graph=knn) knn.fit(cc.embedding_) self._umap.graph_ = knn.kneighbors_graph(cc.embedding_) @@ -285,13 +293,14 @@ def transform_umap( # noqa: E303 eps='auto', sample=None, return_graph=True, - fit_umap_embedding=False + fit_umap_embedding=False, + verbose=False ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: try: logger.debug(f"Going into Transform umap {df.shape}") except: pass - X, y = self.transform(df, ydf, kind=kind, return_graph=False) + X, y = self.transform(df, ydf, kind=kind, return_graph=False, verbose=verbose) emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) if return_graph: @@ -319,12 +328,14 @@ def _process_umap( kind, memoize: bool, featurize_kwargs, + verbose = False, **umap_kwargs, ): """ Returns res mutated with new _xy """ - res._umap = self._umap + umap_kwargs_pure = umap_kwargs.copy() + #res._umap = self._umap logger.debug("process_umap before kwargs: %s", umap_kwargs) umap_kwargs.update({"kind": kind, "X": X_, "y": y_}) @@ -335,14 +346,22 @@ def _process_umap( res, memoize, {**umap_kwargs, "featurize_kwargs": featurize_kwargs or {}} ) if old_res: + print(" --- [[ RE-USING UMAP ]]") if verbose else None logger.info(" --- [[ RE-USING UMAP ]]") + print('umap_kwargs', umap_kwargs['n_components']) if verbose else None fresh_res = copy.copy(res) for attr in ["_xy", "_weighted_edges_df", "_weighted_adjacency"]: setattr(fresh_res, attr, getattr(old_res, attr)) # have to set _raw_data attribute on umap? fresh_res._umap = old_res._umap # this saves the day! + fresh_res._umap_initialized = True + fresh_res._umap_params = umap_kwargs_pure return fresh_res - + + print('** Fitting UMAP') if verbose else None + res._umap_initialized=False + res = res.umap_lazy_init(res, **umap_kwargs_pure) + emb = res._umap_fit_transform(X_, y_) res._xy = emb return res @@ -407,6 +426,7 @@ def umap( inplace: bool = False, feature_engine: str = "auto", memoize: bool = True, + verbose: bool = False, **featurize_kwargs, ): """ @@ -477,7 +497,7 @@ def umap( else: res = self.bind() - res.umap_lazy_init(**umap_kwargs) + res = res.umap_lazy_init(res, **umap_kwargs) logger.debug("umap input X :: %s", X) logger.debug("umap input y :: %s", y) @@ -499,7 +519,6 @@ def umap( config.IMPLICIT_NODE_ID, ) res._nodes.index = index - #print(res.) nodes = res._nodes[res._node].values index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) @@ -517,7 +536,7 @@ def umap( logger.debug("umap y_: %s", y_) res = res._process_umap( - res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs + res, X_, y_, kind, memoize, featurize_kwargs, verbose, **umap_kwargs ) res._weighted_adjacency_nodes = res._weighted_adjacency @@ -607,8 +626,8 @@ def _bind_xy_from_umap( df = res._nodes if kind == "nodes" else res._edges df = df.copy(deep=False) - x_name = config.X + res.suffix - y_name = config.Y + res.suffix + x_name = config.X + res._suffix + y_name = config.Y + res._suffix if kind == "nodes": emb = res._node_embedding else: @@ -623,7 +642,7 @@ def _bind_xy_from_umap( if encode_weight and kind == "nodes": # adds the implicit edge dataframe and binds it to # graphistry instance - w_name = config.WEIGHT + res.suffix + w_name = config.WEIGHT + res._suffix umap_edges_df = res._weighted_edges_df_from_nodes.copy(deep=False) umap_edges_df = umap_edges_df.rename(columns={config.WEIGHT: w_name}) res = res.edges(umap_edges_df, config.SRC, config.DST) From 17ed17adbd5527fb2c055ab2ae41b8a9217fb978 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 18:36:16 -0800 Subject: [PATCH 048/432] lint --- graphistry/Plottable.py | 2 +- graphistry/ai_utils.py | 8 +- graphistry/constants.py | 2 +- graphistry/feature_utils.py | 109 ++++++++++++++++----------- graphistry/features.py | 142 +++++++++++++----------------------- graphistry/text_utils.py | 11 +-- graphistry/umap_utils.py | 49 +++++++------ graphistry/util.py | 55 +++++++++++++- 8 files changed, 209 insertions(+), 169 deletions(-) diff --git a/graphistry/Plottable.py b/graphistry/Plottable.py index ca0aaca31d..8a483ddade 100644 --- a/graphistry/Plottable.py +++ b/graphistry/Plottable.py @@ -84,7 +84,7 @@ class Plottable(object): # embed utils _relation : Optional[str] _use_feat: bool - triplets: Optional[List] # actually torch.Tensor too + _triplets: Optional[List] # actually torch.Tensor too _kg_embed_dim: int diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index a6f4bd0845..267aefa13a 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -193,7 +193,7 @@ def query_by_vector(vect, df, search_index, top_n): def infer_graph( - res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None + res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, verbose=False ): """ Infer a graph from a graphistry object @@ -212,9 +212,11 @@ def infer_graph( if infer_on_umap_embedding and emb is not None: X_previously_fit = res._node_embedding X_new = emb + print("Infering edges over UMAP embedding") if verbose else None else: # can still be umap, but want to do the inference on the higher dimensional features X_previously_fit = res._node_features X_new = X + print("Infering edges over features") if verbose else None FEATS = res._node_features EMB = res._node_embedding @@ -313,6 +315,10 @@ def infer_graph( new_features = pd.concat([X, FEATS.loc[old_nodes.index]], axis=0) new_nodes = pd.concat([df, old_nodes], axis=0) # append minibatch at top + print('-' * 80) if verbose else None + print("Final graph has", len(new_nodes), "nodes") if verbose else None + print("-Batch has", len(df), "nodes") if verbose else None + print("-Brought in ", len(old_nodes), "nodes") if verbose else None new_targets = pd.concat([y, Y.loc[old_nodes.index]]) if y is not None else Y diff --git a/graphistry/constants.py b/graphistry/constants.py index c7c2bf9bfa..1793447b8d 100644 --- a/graphistry/constants.py +++ b/graphistry/constants.py @@ -1,5 +1,5 @@ # ############################################################### -VERBOSE = None # set to true for info, false for debug, None for none +VERBOSE = True # set to true for info, false for debug, None for none TRACE = False # set to true for full trace of functions # ############################################################### # source and destination labels for consistent pipeline-ing across files diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 8f900ce7d1..a4777d8470 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1274,7 +1274,7 @@ def process_edge_dataframes( src: str, dst: str, cardinality_threshold: int = 40, - cardinality_threshold_target: int = 100, + cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, use_scaler: Optional[str] = None, @@ -1284,7 +1284,6 @@ def process_edge_dataframes( ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, model_name: str = "paraphrase-MiniLM-L6-v2", similarity: Optional[str] = None, @@ -1822,6 +1821,8 @@ def get_matrix_by_column_part(X: pd.DataFrame, column_part: str) -> pd.DataFrame def get_matrix_by_column_parts(X: pd.DataFrame, column_parts: Union[list, str]) -> pd.DataFrame: """Get the feature matrix by column parts list existing in column names.""" + if column_parts is None: + return X if isinstance(column_parts, str): column_parts = [column_parts] res = pd.concat([get_matrix_by_column_part(X, column_part) for column_part in column_parts], axis=1) # type: ignore @@ -1842,7 +1843,7 @@ class FeatureMixin(MIXIN_BASE): g = graphistry.edges(df, 'src', 'dst') g2 = g.featurize(kind='edges') - or chain them, + or chain them for both nodes and edges, g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node_column') g2 = g.featurize().featurize(kind='edges') @@ -1870,10 +1871,10 @@ def _featurize_nodes( self, X: XSymbolic = None, y: YSymbolic = None, - use_scaler: Optional[str] = "zscale", - use_scaler_target: Optional[str] = "kbins", + use_scaler: Optional[str] = None, + use_scaler_target: Optional[str] = None, cardinality_threshold: int = 40, - cardinality_threshold_target: int = 120, + cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, multilabel: bool = False, @@ -1882,7 +1883,6 @@ def _featurize_nodes( ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, model_name: str = "paraphrase-MiniLM-L6-v2", similarity: Optional[str] = None, @@ -1898,6 +1898,7 @@ def _featurize_nodes( remove_node_column: bool = True, feature_engine: FeatureEngineConcrete = "pandas", memoize: bool = True, + verbose: bool = False, ): res = self.bind() # was self.copy() but changing to test ndf = res._nodes @@ -1930,7 +1931,7 @@ def _featurize_nodes( from .features import ModelDict - fkwargs = dict( + fkwargs = ModelDict("Featurize Params", X=X_resolved, y=y_resolved, use_scaler=use_scaler, @@ -1945,7 +1946,6 @@ def _featurize_nodes( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, min_words=min_words, model_name=model_name, similarity=similarity, @@ -1969,6 +1969,7 @@ def _featurize_nodes( old_res = reuse_featurization(res, memoize, fkwargs) if old_res: + print("--- [[ RE-USING NODE FEATURIZATION ]]") if verbose else None logger.info("--- [[ RE-USING NODE FEATURIZATION ]]") fresh_res = copy.copy(res) for attr in ["_node_features", "_node_target", "_node_encoder"]: @@ -1985,6 +1986,8 @@ def _featurize_nodes( if key not in keys_to_remove: nfkwargs[key] = value + print('-'*80) if verbose else None + print("** Featuring nodes") if verbose else None ############################################################# encoder = FastEncoder(X_resolved, y_resolved, kind="nodes") encoder.fit(**nfkwargs) @@ -2003,17 +2006,16 @@ def _featurize_edges( self, X: XSymbolic = None, y: YSymbolic = None, - use_scaler: Optional[str] = "zscale", - use_scaler_target: Optional[str] = "kbins", + use_scaler: Optional[str] = None, + use_scaler_target: Optional[str] = None, cardinality_threshold: int = 40, - cardinality_threshold_target: int = 20, + cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, use_ngrams: bool = False, ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, multilabel: bool = False, model_name: str = "paraphrase-MiniLM-L6-v2", @@ -2029,6 +2031,7 @@ def _featurize_edges( keep_n_decimals: int = 5, feature_engine: FeatureEngineConcrete = "pandas", memoize: bool = True, + verbose: bool = False, ): res = self.copy() @@ -2061,7 +2064,6 @@ def _featurize_edges( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, min_words=min_words, model_name=model_name, similarity=similarity, @@ -2103,6 +2105,7 @@ def _featurize_edges( if key not in keys_to_remove: nfkwargs[key] = value + print("** Featuring edges") if verbose else None ############################################################### encoder = FastEncoder(X_resolved, y_resolved, kind="edges") encoder.fit(src=res._source, dst=res._destination, **nfkwargs) @@ -2115,9 +2118,9 @@ def _featurize_edges( return res - def _infer_edges(self, emb, X, y, df, eps='auto', sample=None, infer_on_umap_embedding=False, **kwargs): + def _infer_edges(self, emb, X, y, df, eps='auto', sample=None, infer_on_umap_embedding=False, verbose=False, **kwargs): res = self.bind() # will not be able to decide umap coordinates, but will be able to infer graph from existing edges - g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, eps=eps, sample=sample) + g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, eps=eps, sample=sample, verbose=verbose, **kwargs) return g def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): @@ -2129,7 +2132,7 @@ def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): "before being able to transform data" ) - def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', sample=None): + def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', sample=None, verbose=False): """Transform new data and append to existing graph. args: @@ -2138,6 +2141,7 @@ def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', s kind: str # one of `nodes`, `edges` return_graph: bool, if True, will return a graph with inferred edges eps: float, if return_graph is True, will use this value for eps in NN search, or 'auto' to infer a good value + eps represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. sample: int, if return_graph is True, will use sample value for NN search over existing edges returns: X: pd.DataFrame, transformed data if return_graph is False @@ -2152,7 +2156,7 @@ def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', s f"`edges`, found {kind}") if return_graph: emb = None # will not be able to decide umap coordinates, but will be able to infer graph from existing edges - g = self._infer_edges(emb, X, y, df, infer_on_umap_embedding=False, eps=eps, sample=sample) + g = self._infer_edges(emb, X, y, df, infer_on_umap_embedding=False, eps=eps, sample=sample, verbose=verbose) return g return X, y @@ -2173,6 +2177,17 @@ def scale( strategy: str = "uniform", keep_n_decimals: int = 5, ): + """Scale data using the same scalers as used in the featurization step. + + example usage: + g = graphistry.nodes(df) + g2 = g.umap().scale(df, ydf, kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) + + # scaled data + X = g2._node_features + y = g2._node_target + + """ if kind == "nodes" and hasattr(self, "_node_encoder"): # type: ignore if self._node_encoder is not None: # type: ignore @@ -2250,7 +2265,7 @@ def featurize( ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - min_words: float = 2.5, + min_words: float = 4.5, model_name: str = "paraphrase-MiniLM-L6-v2", impute: bool = True, n_quantiles: int = 100, @@ -2268,6 +2283,7 @@ def featurize( inplace: bool = False, feature_engine: FeatureEngine = "auto", memoize: bool = True, + verbose: bool = False, ): r""" Featurize Nodes or Edges of the underlying nodes/edges DataFrames. @@ -2328,20 +2344,21 @@ def featurize( but at cost of encoding time. If faster encoding is needed, `average_word_embeddings_komninos` is useful and produces less semantically relevant vectors. - Please see www.huggingface.co or sentence_transformer + Please see sentence_transformer (https://www.sbert.net/) library for all available models. :param multilabel: if True, will encode a *single* target column composed of lists of lists as multilabel outputs. This only works with y=['a_single_col'], default False :param embedding: If True, produces a random node embedding of size `n_topics` - default, False. + default, False. If no node features are provided, will produce random embeddings + (for GNN models, for example) :param use_ngrams: If True, will encode textual columns as TfIdf Vectors, default, False. :param ngram_range: if use_ngrams=True, can set ngram_range, eg: tuple = (1, 3) :param max_df: if use_ngrams=True, set max word frequency to consider in vocabulary eg: max_df = 0.2, :param min_df: if use_ngrams=True, set min word count to consider in vocabulary - eg: min_df = 3 + eg: min_df = 3 or 0.00001 :param categories: Optional[str] in ["auto", "k-means", "most_frequent"], decides which category to select in Similarity Encoding, default 'auto' :param impute: Whether to impute missing values, default True @@ -2366,7 +2383,7 @@ def featurize( not, default False. :param memoize: whether to store and reuse results across runs, default True. - :return: self, with new attributes set by the featurization process. + :return: graphistry instance with new attributes set by the featurization process. """ assert_imported() if inplace: @@ -2392,11 +2409,10 @@ def featurize( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, # deprecated min_words=min_words, model_name=model_name, - similarity=similarity, # deprecated - categories=categories, # deprecated + similarity=similarity, + categories=categories, impute=impute, n_quantiles=n_quantiles, quantile_range=quantile_range, @@ -2408,6 +2424,7 @@ def featurize( remove_node_column=remove_node_column, feature_engine=feature_engine, memoize=memoize, + verbose=verbose ) elif kind == "edges": res = res._featurize_edges( @@ -2424,11 +2441,10 @@ def featurize( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, # deprecated min_words=min_words, model_name=model_name, - similarity=similarity, # deprecated - categories=categories, # deprecated + similarity=similarity, + categories=categories, impute=impute, n_quantiles=n_quantiles, quantile_range=quantile_range, @@ -2439,6 +2455,7 @@ def featurize( keep_n_decimals=keep_n_decimals, feature_engine=feature_engine, memoize=memoize, + verbose=verbose ) else: logger.warning( @@ -2452,19 +2469,18 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( self, X: XSymbolic = None, y: YSymbolic = None, - use_scaler: Optional[str] = "zscale", - use_scaler_target: Optional[str] = "kbins", + use_scaler: Optional[str] = None, + use_scaler_target: Optional[str] = None, cardinality_threshold: int = 40, cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, multilabel: bool = False, - embedding=False, + embedding: bool = False, use_ngrams: bool = False, ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, model_name: str = "paraphrase-MiniLM-L6-v2", similarity: Optional[ @@ -2483,6 +2499,7 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( feature_engine: FeatureEngineConcrete = "pandas", reuse_if_existing=False, memoize: bool = True, + verbose: bool = False, ) -> Tuple[pd.DataFrame, pd.DataFrame, MIXIN_BASE]: """ helper method gets node feature and target matrix if X, y @@ -2533,6 +2550,7 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( remove_node_column=remove_node_column, feature_engine=feature_engine, memoize=memoize, + verbose=verbose, ) assert res._node_features is not None # ensure no infinite loop @@ -2548,10 +2566,10 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( self, X: XSymbolic = None, y: YSymbolic = None, - use_scaler: Optional[str] = "robust", - use_scaler_target: Optional[str] = "kbins", + use_scaler: Optional[str] = None, + use_scaler_target: Optional[str] = None, cardinality_threshold: int = 40, - cardinality_threshold_target: int = 20, + cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, multilabel: bool = False, @@ -2559,7 +2577,6 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, model_name: str = "paraphrase-MiniLM-L6-v2", similarity: Optional[ @@ -2577,6 +2594,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( feature_engine: FeatureEngineConcrete = "pandas", reuse_if_existing=False, memoize: bool = True, + verbose: bool = False, ) -> Tuple[pd.DataFrame, Optional[pd.DataFrame], MIXIN_BASE]: """ helper method gets edge feature and target matrix if X, y @@ -2626,6 +2644,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( keep_n_decimals=keep_n_decimals, feature_engine=feature_engine, memoize=memoize, + verbose=verbose, ) assert res._edge_features is not None # ensure no infinite loop @@ -2638,21 +2657,29 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ) - def get_features_by_cols(self, columns: Union[List, str], kind: str = 'nodes', target=False): + def get_features_by_cols(self, columns: Union[List, str] = None, kind: str = 'nodes', target: bool = False): """Returns feature matrix with only the columns that contain the string `column_part` in their name. `X = g.get_features_by_cols(['feature1', 'feature2'])` will retrieve a feature matrix with only the columns that contain the string `feature1` or `feature2` in their name. + Most useful for topic modeling, where the column names are of the form `topic_0`, `topic_1`, etc. + Can retrieve unique columns in original dataframe, or actual topic features like [ip_part, shoes, preference_x, etc]. + + Powerful way to retrieve features from a featurized graph by column or (top) features of interest. example: - res = g2.get_features_by_cols(['172', 'percent']) - res.columns + X = g2.get_features_by_cols(['172', 'percent']) + X.columns => ['ip_172.56.104.67', 'ip_172.58.129.252', 'item_percent'] + # or in targets + y = g2.get_features_by_cols(['total', 'percent'], target=True) + y.columns + => ['basket_price_total', 'conversion_percent', 'CTR_percent', 'CVR_percent'] Args: columns (Union[List, str]): list of column names or a single column name that may exist in columns - of the feature matrix. + of the feature matrix. If None, returns original feature matrix kind (str, optional): Node or Edge features. Defaults to 'nodes'. target (bool, optional): If True, returns the target matrix. Defaults to False. diff --git a/graphistry/features.py b/graphistry/features.py index 35cfb4e5a6..5fac7a031b 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -1,6 +1,6 @@ -from collections import UserDict from .util import setup_logger from .constants import VERBOSE, TRACE +from .util import ModelDict logger = setup_logger("graphistry.features", verbose=VERBOSE, fullpath=TRACE) @@ -53,6 +53,16 @@ NO_SCALER = None EXTRA_COLS_NEEDED = ["x", "y", "_n"] # ############################################################### +# ################# graphistry umap config constants ################# +UMAP_DIM = 2 +N_NEIGHBORS = 15 +MIN_DIST = 0.1 +SPREAD=0.5, +LOCAL_CONNECTIVITY=1, +REPULSION_STRENGTH=1, +NEGATIVE_SAMPLING_RATE=5, +METRIC = "euclidean", + # ############################################################### # ################# enrichments @@ -86,42 +96,10 @@ SPLIT_MEDIUM = 0.2 SPLIT_HIGH = 0.5 -# ############################################################################# -class ModelDict(UserDict): - """Helper class to print out model names and keep track of updates - - Args: - message: description of model - verbose: print out model names, logging happens regardless - """ - - def __init__(self, message, verbose=True, *args, **kwargs): - self._message = message - self._verbose = verbose - self._print_length = min(LENGTH_PRINT, len(message)) - self._updates = [] - super().__init__(*args, **kwargs) - - def __repr__(self): - logger.info(self._message) - if self._verbose: - print("_" * self._print_length) - print() - print(self._message) - print("_" * self._print_length) - print() - return super().__repr__() - def update(self, *args, **kwargs): - self._updates.append(args[0]) - if len(self._updates) > 1: # don't take first update since its the init/default - self._message += ( - "\n" + "_" * self._print_length + f"\n\nUpdated: {self._updates[-1]}" - ) - return super().update(*args, **kwargs) -default_featurize_parameters = dict( +default_featurize_parameters = ModelDict('Featurize Parameters', kind="nodes", use_scaler=NO_SCALER, use_scaler_target=NO_SCALER, @@ -154,80 +132,63 @@ def update(self, *args, **kwargs): ) +default_umap_parameters = ModelDict('Umap Parameters', + { + "n_components": UMAP_DIM, + **({"metric": METRIC} if True else {}), + "n_neighbors": N_NEIGHBORS, + "min_dist": MIN_DIST, + "spread": SPREAD, + "local_connectivity": LOCAL_CONNECTIVITY, + "repulsion_strength": REPULSION_STRENGTH, + "negative_sample_rate": NEGATIVE_SAMPLING_RATE, + } + ) + # ############################################################################# # Create useful presets for the user # makes naming and encoding models consistently and testing different models against eachother easy # customize the default parameters for each model you want to test # Ngrams Model over features -ngrams_model = ModelDict("Ngrams Model", - use_ngrams=True, - min_words=HIGH_CARD, - verbose=True) -#ngrams_model.update(dict(use_ngrams=True, min_words=HIGH_CARD)) +ngrams_model = ModelDict( + "Ngrams Model", use_ngrams=True, min_words=HIGH_CARD, verbose=True +) # Topic Model over features -topic_model = ModelDict("Reliable Topic Models on Features and Target", - cardinality_threshold=LOW_CARD, # force topic model - cardinality_threshold_target=LOW_CARD, # force topic model - n_topics=N_TOPICS, - n_topics_target=N_TOPICS_TARGET, - min_words=HIGH_CARD, # make sure it doesn't turn into sentence model, but rather topic models - verbose=True - ) -#**default_featurize_parameters) -# topic_model.update( -# dict( -# cardinality_threshold=LOW_CARD, # force topic model -# cardinality_threshold_target=LOW_CARD, # force topic model -# n_topics=N_TOPICS, -# n_topics_target=N_TOPICS_TARGET, -# min_words=HIGH_CARD, # make sure it doesn't turn into sentence model, but rather topic models -# ) -# ) +topic_model = ModelDict( + "Reliable Topic Models on Features and Target", + cardinality_threshold=LOW_CARD, # force topic model + cardinality_threshold_target=LOW_CARD, # force topic model + n_topics=N_TOPICS, + n_topics_target=N_TOPICS_TARGET, + min_words=HIGH_CARD, # make sure it doesn't turn into sentence model, but rather topic models + verbose=True, +) # useful for text data that you want to paraphrase -embedding_model = ModelDict(f"{PARAPHRASE_SMALL_MODEL} sbert Embedding Model", - min_words=FORCE_EMBEDDING_ALL_COLUMNS, - model_name=PARAPHRASE_SMALL_MODEL, # if we need multilingual support, use PARAPHRASE_MULTILINGUAL_MODEL - verbose=True, - #**default_featurize_parameters, +embedding_model = ModelDict( + f"{PARAPHRASE_SMALL_MODEL} sbert Embedding Model", + min_words=FORCE_EMBEDDING_ALL_COLUMNS, + model_name=PARAPHRASE_SMALL_MODEL, # if we need multilingual support, use PARAPHRASE_MULTILINGUAL_MODEL + verbose=True, ) -# embedding_model.update( -# dict( -# min_words=FORCE_EMBEDDING_ALL_COLUMNS, -# model_name=PARAPHRASE_SMALL_MODEL, # if we need multilingual support, use PARAPHRASE_MULTILINGUAL_MODEL -# ) -# ) # useful for when search input is much smaller than the encoded documents search_model = ModelDict( - f"{MSMARCO2} Search Model", - verbose=True, - min_words=FORCE_EMBEDDING_ALL_COLUMNS, - model_name=MSMARCO2, - #**default_featurize_parameters + f"{MSMARCO2} Search Model", + verbose=True, + min_words=FORCE_EMBEDDING_ALL_COLUMNS, + model_name=MSMARCO2, ) -# search_model.update( -# dict( -# min_words=FORCE_EMBEDDING_ALL_COLUMNS, -# model_name=MSMARCO2, -# ) -# ) # Question Answering encodings for search qa_model = ModelDict( - f"{QA_SMALL_MODEL} QA Model", - min_words=FORCE_EMBEDDING_ALL_COLUMNS, - model_name=QA_SMALL_MODEL, - verbose=True, + f"{QA_SMALL_MODEL} QA Model", + min_words=FORCE_EMBEDDING_ALL_COLUMNS, + model_name=QA_SMALL_MODEL, + verbose=True, ) -# qa_model.update( -# dict( -# min_words=FORCE_EMBEDDING_ALL_COLUMNS, -# model_name=QA_SMALL_MODEL, -# ) -# ) BASE_MODELS = { @@ -240,7 +201,8 @@ def update(self, *args, **kwargs): if __name__ == "__main__": - # python3 -m graphistry.features -m 'my awesome edge encoded model' -p '{"kind":"edges"}' + """ python3 -m graphistry.features -m 'my awesome edge encoded model' -p '{"kind":"edges"}' + """ import argparse import json diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 231980f601..803ce345dd 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -5,18 +5,8 @@ from .constants import WEIGHT, DISTANCE from logging import getLogger -logger = getLogger(__name__) - from typing import ( - Hashable, - List, - Union, - Dict, - Any, - Optional, - Tuple, TYPE_CHECKING, - Type, ) # noqa @@ -25,6 +15,7 @@ else: MIXIN_BASE = object +logger = getLogger(__name__) class SearchToGraphMixin(MIXIN_BASE): def __init__(self, *args, **kwargs) -> None: diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 434dd41f32..8a596be47c 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -221,8 +221,8 @@ def umap_lazy_init( res.engine = engine_resolved res._suffix = suffix - res._umap_params = dict(# ModelDict(f'Umap Parameters', - **umap_kwargs) + res._umap_params = dict(**umap_kwargs) # ModelDict(f'Umap Parameters', + # finally set the flag res._umap_initialized = True return res @@ -359,7 +359,7 @@ def _process_umap( return fresh_res print('** Fitting UMAP') if verbose else None - res._umap_initialized=False + res._umap_initialized = False res = res.umap_lazy_init(res, **umap_kwargs_pure) emb = res._umap_fit_transform(X_, y_) @@ -431,46 +431,47 @@ def umap( ): """ UMAP the featurized node or edges data, - or pass in your own X, y (optional). + or pass in your own X, y (optional) dataframes. - :param kind: `nodes` or `edges` or None. + kind: `nodes` or `edges` or None. If None, expects explicit X, y (optional) matrices, and will Not associate them to nodes or edges. If X, y (optional) is given, with kind = [nodes, edges], it will associate new matrices to nodes or edges attributes. - :param feature_engine: How to encode data + feature_engine: How to encode data ("none", "auto", "pandas", "dirty_cat", "torch") - :param encode_weight: if True, will set new edges_df from + encode_weight: if True, will set new edges_df from implicit UMAP, default True. - :param encode_position: whether to set default plotting bindings + encode_position: whether to set default plotting bindings -- positions x,y from umap for .plot() - :param X: either a dataframe ndarray of features, or column names to featurize - :param y: either an dataframe ndarray of targets, or column names to featurize + X: either a dataframe ndarray of features, or column names to featurize + y: either an dataframe ndarray of targets, or column names to featurize targets - :param scale: multiplicative scale for pruning weighted edge DataFrame + scale: multiplicative scale for pruning weighted edge DataFrame gotten from UMAP, between [0, ..) with high end meaning keep all edges - :param n_neighbors: UMAP number of nearest neighbors to include for + n_neighbors: UMAP number of nearest neighbors to include for UMAP connectivity, lower makes more compact layouts. Minimum 2 - :param min_dist: UMAP float between 0 and 1, lower makes more compact + min_dist: UMAP float between 0 and 1, lower makes more compact layouts. - :param spread: UMAP spread of values for relaxation - :param local_connectivity: UMAP connectivity parameter - :param repulsion_strength: UMAP repulsion strength - :param negative_sample_rate: UMAP negative sampling rate - :param n_components: number of components in the UMAP projection, + spread: UMAP spread of values for relaxation + local_connectivity: UMAP connectivity parameter + repulsion_strength: UMAP repulsion strength + negative_sample_rate: UMAP negative sampling rate + n_components: number of components in the UMAP projection, default 2 - :param metric: UMAP metric, default 'euclidean'. + metric: UMAP metric, default 'euclidean'. see (UMAP-LEARN)[https://umap-learn.readthedocs.io/ en/latest/parameters.html] documentation for more. - :param suffix: optional suffix to add to x, y attributes of umap. - :param play: Graphistry play parameter, default 0, how much to evolve + suffix: optional suffix to add to x, y attributes of umap. + play: Graphistry play parameter, default 0, how much to evolve the network during clustering. 0 preserves the original UMAP layout. - :param dbscan: whether to run DBSCAN on the UMAP embedding, default True. - :param engine: selects which engine to use to calculate UMAP: + dbscan: whether to run DBSCAN on the UMAP embedding, default True. + engine: selects which engine to use to calculate UMAP: default "auto" will use cuML if available, otherwise UMAP-LEARN. - :param memoize: whether to memoize the results of this method, + memoize: whether to memoize the results of this method, default True. + verbose: whether to print out extra information, default False. :return: self, with attributes set with new data """ if engine == UMAP_LEARN: diff --git a/graphistry/util.py b/graphistry/util.py index ce7249c934..4f230d68d7 100644 --- a/graphistry/util.py +++ b/graphistry/util.py @@ -10,6 +10,7 @@ import warnings from functools import lru_cache from typing import Any +from collections import UserDict from .constants import VERBOSE, CACHE_COERCION_SIZE, TRACE @@ -77,6 +78,11 @@ def hash_memoize_helper(v: Any) -> str: for k2, v2 in v.items(): rolling += f'{k2}:{hash_memoize_helper(v2)},' rolling += '}' + elif isinstance(v, ModelDict): + rolling = '{' + for k2, v2 in v.items(): + rolling += f'{k2}:{hash_memoize_helper(v2)},' + rolling += '}' elif isinstance(v, list): rolling = '[' for i in v: @@ -123,7 +129,7 @@ def check_set_memoize(g, metadata, attribute, name: str = '', memoize: bool = Tr hashed = None weakref = getattr(g, attribute) try: - hashed = hash_memoize(metadata) + hashed = hash_memoize(dict(data=metadata)) except TypeError: logger.warning( f'! Failed {name} speedup attempt. Continuing without memoization speedups.' @@ -281,6 +287,53 @@ def deprecated_func(*args, **kwargs): return deprecated_decorator +# ############################################################################# +# MODEL Parameter HELPERS +def get_timestamp(): + import datetime + return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + +class ModelDict(UserDict): + """Helper class to print out model names and keep track of updates + + Args: + message: description of model + verbose: print out model names, logging happens regardless + """ + + def __init__(self, message, verbose=True, timestamp=False, *args, **kwargs): + self._message = message + self._verbose = verbose + self._timestamp = timestamp + L = len(message) if timestamp is False else max(len(message), len(get_timestamp())+1) + self._print_length = min(80, L) + self._updates = [] + super().__init__(*args, **kwargs) + + def print(self, message): + if self._timestamp: + message = f"{message}\n{get_timestamp()}" + if self._verbose: + print("_" * self._print_length) + print() + print(message) + print("_" * self._print_length) + print() + + def __repr__(self): + #logger.info(self._message) + self.print(self._message) + return super().__repr__() + + def update(self, *args, **kwargs): + self._updates.append(args[0]) + if len(self._updates) > 1: # don't take first update since its the init/default + self._message += ( + "\n" + "_" * self._print_length + f"\n\nUpdated: {self._updates[-1]}" + ) + return super().update(*args, **kwargs) + + # # def inspect_decorator(func, args, kwargs): # import inspect From ab3d7e9ee3e9f7552499bfe1068687fff623446d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 18:45:12 -0800 Subject: [PATCH 049/432] adds tests and lint --- graphistry/feature_utils.py | 6 +- graphistry/features.py | 43 ++- graphistry/tests/test_umap_utils.py | 452 +++++++++++++++++++--------- graphistry/util.py | 105 ++++--- 4 files changed, 389 insertions(+), 217 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index a4777d8470..5683f96064 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1986,12 +1986,12 @@ def _featurize_nodes( if key not in keys_to_remove: nfkwargs[key] = value - print('-'*80) if verbose else None + print('-' * 80) if verbose else None print("** Featuring nodes") if verbose else None - ############################################################# + # ############################################################ encoder = FastEncoder(X_resolved, y_resolved, kind="nodes") encoder.fit(**nfkwargs) - ############################################################ + # ########################################################### # if changing, also update fresh_res res._node_features = encoder.X diff --git a/graphistry/features.py b/graphistry/features.py index 5fac7a031b..541eda9fbb 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -57,11 +57,11 @@ UMAP_DIM = 2 N_NEIGHBORS = 15 MIN_DIST = 0.1 -SPREAD=0.5, -LOCAL_CONNECTIVITY=1, -REPULSION_STRENGTH=1, -NEGATIVE_SAMPLING_RATE=5, -METRIC = "euclidean", +SPREAD = (0.5,) +LOCAL_CONNECTIVITY = (1,) +REPULSION_STRENGTH = (1,) +NEGATIVE_SAMPLING_RATE = (5,) +METRIC = ("euclidean",) # ############################################################### @@ -97,9 +97,8 @@ SPLIT_HIGH = 0.5 - - -default_featurize_parameters = ModelDict('Featurize Parameters', +default_featurize_parameters = ModelDict( + "Featurize Parameters", kind="nodes", use_scaler=NO_SCALER, use_scaler_target=NO_SCALER, @@ -132,18 +131,19 @@ ) -default_umap_parameters = ModelDict('Umap Parameters', - { - "n_components": UMAP_DIM, - **({"metric": METRIC} if True else {}), - "n_neighbors": N_NEIGHBORS, - "min_dist": MIN_DIST, - "spread": SPREAD, - "local_connectivity": LOCAL_CONNECTIVITY, - "repulsion_strength": REPULSION_STRENGTH, - "negative_sample_rate": NEGATIVE_SAMPLING_RATE, - } - ) +default_umap_parameters = ModelDict( + "Umap Parameters", + { + "n_components": UMAP_DIM, + **({"metric": METRIC} if True else {}), + "n_neighbors": N_NEIGHBORS, + "min_dist": MIN_DIST, + "spread": SPREAD, + "local_connectivity": LOCAL_CONNECTIVITY, + "repulsion_strength": REPULSION_STRENGTH, + "negative_sample_rate": NEGATIVE_SAMPLING_RATE, + }, +) # ############################################################################# # Create useful presets for the user @@ -201,8 +201,7 @@ if __name__ == "__main__": - """ python3 -m graphistry.features -m 'my awesome edge encoded model' -p '{"kind":"edges"}' - """ + """python3 -m graphistry.features -m 'my awesome edge encoded model' -p '{"kind":"edges"}'""" import argparse import json diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 1da454a337..c4f77dfaa7 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -22,9 +22,12 @@ edge2_target_df, model_avg_name, lazy_import_has_min_dependancy, - check_allclose_fit_transform_on_same_data + check_allclose_fit_transform_on_same_data, +) +from graphistry.umap_utils import ( + lazy_umap_import_has_dependancy, + lazy_cuml_import_has_dependancy, ) -from graphistry.umap_utils import lazy_umap_import_has_dependancy, lazy_cuml_import_has_dependancy has_dependancy, _ = lazy_import_has_min_dependancy() has_cuml, _, _ = lazy_cuml_import_has_dependancy() @@ -32,7 +35,7 @@ logger = logging.getLogger(__name__) -warnings.filterwarnings('ignore') +warnings.filterwarnings("ignore") triangleEdges = pd.DataFrame( @@ -70,132 +73,225 @@ def setUp(self): g = graphistry.nodes(ndf_reddit) self.gn = g - + with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) - g2 = g.umap(y=double_target_reddit, - use_ngrams=True, - ngram_range=(1, 2), - use_scaler='robust', - cardinality_threshold=2) - + g2 = g.umap( + y=double_target_reddit, + use_ngrams=True, + ngram_range=(1, 2), + use_scaler="robust", + cardinality_threshold=2, + ) + self.g2 = g2 fenc = g2._node_encoder self.X, self.Y = fenc.X, fenc.y self.EMB = g2._node_embedding - self.emb, self.x, self.y = g2.transform_umap(ndf_reddit, ydf=double_target_reddit, kind='nodes', return_graph=False) - self.g3 = g2.transform_umap(ndf_reddit, ydf=double_target_reddit, kind='nodes', return_graph=True) - + self.emb, self.x, self.y = g2.transform_umap( + ndf_reddit, ydf=double_target_reddit, kind="nodes", return_graph=False + ) + self.g3 = g2.transform_umap( + ndf_reddit, ydf=double_target_reddit, kind="nodes", return_graph=True + ) + # do the same for edges edge_df22 = edge_df2.copy() - edge_df22['rando'] = np.random.rand(edge_df2.shape[0]) - g = graphistry.edges(edge_df22, 'src', 'dst') + edge_df22["rando"] = np.random.rand(edge_df2.shape[0]) + g = graphistry.edges(edge_df22, "src", "dst") self.ge = g with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) - g2 = g.umap(y=edge2_target_df, kind='edges', - use_ngrams=True, - ngram_range=(1, 2), - use_scaler=None, - use_scaler_target=None, - cardinality_threshold=2, n_topics=4) - + g2 = g.umap( + y=edge2_target_df, + kind="edges", + use_ngrams=True, + ngram_range=(1, 2), + use_scaler=None, + use_scaler_target=None, + cardinality_threshold=2, + n_topics=4, + ) + fenc = g2._edge_encoder self.Xe, self.Ye = fenc.X, fenc.y self.EMBe = g2._edge_embedding - self.embe, self.xe, self.ye = g2.transform_umap(edge_df22, ydf=edge2_target_df, kind='edges', return_graph=False) + self.embe, self.xe, self.ye = g2.transform_umap( + edge_df22, ydf=edge2_target_df, kind="edges", return_graph=False + ) self.g2e = g2 - self.g3e = g2.transform_umap(edge_df22, ydf=edge2_target_df, kind='edges', return_graph=True) + self.g3e = g2.transform_umap( + edge_df22, ydf=edge2_target_df, kind="edges", return_graph=True + ) @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_columns_match(self): - assert all(self.X.columns == self.x.columns), 'Node Feature Columns do not match' - assert all(self.Y.columns == self.y.columns), 'Node Target Columns do not match' - assert all(self.Xe.columns == self.xe.columns), 'Edge Feature Columns do not match' - assert all(self.Ye.columns == self.ye.columns), 'Edge Target Columns do not match' + assert all( + self.X.columns == self.x.columns + ), "Node Feature Columns do not match" + assert all(self.Y.columns == self.y.columns), "Node Target Columns do not match" + assert all( + self.Xe.columns == self.xe.columns + ), "Edge Feature Columns do not match" + assert all( + self.Ye.columns == self.ye.columns + ), "Edge Target Columns do not match" @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_index_match(self): # nodes - assert all(self.gn._nodes.index == self.g2._nodes.index), 'Node Indexes do not match' - assert all(self.gn._nodes.index == self.EMB.index), 'Emb Indexes do not match' - assert all(self.gn._nodes.index == self.emb.index), 'Transformed Emb Indexes do not match' - assert all(self.gn._nodes.index == self.X.index), 'Transformed Node features Indexes do not match' - assert all(self.gn._nodes.index == self.y.index), 'Transformed Node target Indexes do not match' - - # edges - assert all(self.ge._edges.index == self.g2e._edges.index), 'Edge Indexes do not match' - assert all(self.ge._edges.index == self.EMBe.index), 'Edge Emb Indexes do not match' - assert all(self.ge._edges.index == self.embe.index), 'Edge Transformed Emb Indexes do not match' - assert all(self.ge._edges.index == self.Xe.index), 'Edge Transformed features Indexes do not match' - assert all(self.ge._edges.index == self.ye.index), 'Edge Transformed target Indexes do not match' - + assert all( + self.gn._nodes.index == self.g2._nodes.index + ), "Node Indexes do not match" + assert all(self.gn._nodes.index == self.EMB.index), "Emb Indexes do not match" + assert all( + self.gn._nodes.index == self.emb.index + ), "Transformed Emb Indexes do not match" + assert all( + self.gn._nodes.index == self.X.index + ), "Transformed Node features Indexes do not match" + assert all( + self.gn._nodes.index == self.y.index + ), "Transformed Node target Indexes do not match" + + # edges + assert all( + self.ge._edges.index == self.g2e._edges.index + ), "Edge Indexes do not match" + assert all( + self.ge._edges.index == self.EMBe.index + ), "Edge Emb Indexes do not match" + assert all( + self.ge._edges.index == self.embe.index + ), "Edge Transformed Emb Indexes do not match" + assert all( + self.ge._edges.index == self.Xe.index + ), "Edge Transformed features Indexes do not match" + assert all( + self.ge._edges.index == self.ye.index + ), "Edge Transformed target Indexes do not match" + # make sure the indexes match at transform time internally as well - assert all(self.X.index == self.x.index), 'Node Feature Indexes do not match' - assert all(self.Y.index == self.y.index), 'Node Target Indexes do not match' - assert all(self.Xe.index == self.xe.index), 'Edge Feature Indexes do not match' - assert all(self.Ye.index == self.ye.index), 'Edge Target Indexes do not match' + assert all(self.X.index == self.x.index), "Node Feature Indexes do not match" + assert all(self.Y.index == self.y.index), "Node Target Indexes do not match" + assert all(self.Xe.index == self.xe.index), "Edge Feature Indexes do not match" + assert all(self.Ye.index == self.ye.index), "Edge Target Indexes do not match" @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_index_match_in_infered_graph(self): # nodes - assert all(self.g3._nodes.index == self.g2._nodes.index), 'Node Indexes do not match' - assert all(self.g3._nodes.index == self.EMB.index), 'Emb Indexes do not match' - assert all(self.g3._nodes.index == self.emb.index), 'Transformed Emb Indexes do not match' - assert all(self.g3._nodes.index == self.X.index), 'Transformed Node features Indexes do not match' - assert all(self.g3._nodes.index == self.y.index), 'Transformed Node target Indexes do not match' + assert all( + self.g3._nodes.index == self.g2._nodes.index + ), "Node Indexes do not match" + assert all(self.g3._nodes.index == self.EMB.index), "Emb Indexes do not match" + assert all( + self.g3._nodes.index == self.emb.index + ), "Transformed Emb Indexes do not match" + assert all( + self.g3._nodes.index == self.X.index + ), "Transformed Node features Indexes do not match" + assert all( + self.g3._nodes.index == self.y.index + ), "Transformed Node target Indexes do not match" @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_nodes_index_match_in_infered_graph(self): - # edges + # edges ndf_infered = self.g3._nodes - assert all(ndf_infered.index == self.EMBe.index), 'Edge Emb Indexes do not match' - assert all(ndf_infered.index == self.embe.index), 'Edge Transformed Emb Indexes do not match' - assert all(ndf_infered.index == self.Xe.index), 'Edge Transformed features Indexes do not match' - assert all(ndf_infered.index == self.ye.index), 'Edge Transformed target Indexes do not match' - + assert all( + ndf_infered.index == self.EMBe.index + ), "Edge Emb Indexes do not match" + assert all( + ndf_infered.index == self.embe.index + ), "Edge Transformed Emb Indexes do not match" + assert all( + ndf_infered.index == self.Xe.index + ), "Edge Transformed features Indexes do not match" + assert all( + ndf_infered.index == self.ye.index + ), "Edge Transformed target Indexes do not match" + # now test in set featurize method calls - assert all(self.g3._node_features.index == ndf_infered.index), 'Edge Feature Indexes do not match' - assert all(self.g3._node_embedding.index == ndf_infered.index), 'Edge Emb Indexes do not match' - assert all(self.g3._node_target.index == ndf_infered.index), 'Edge Transformed Emb Indexes do not match' + assert all( + self.g3._node_features.index == ndf_infered.index + ), "Edge Feature Indexes do not match" + assert all( + self.g3._node_embedding.index == ndf_infered.index + ), "Edge Emb Indexes do not match" + assert all( + self.g3._node_target.index == ndf_infered.index + ), "Edge Transformed Emb Indexes do not match" # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed features Indexes do not match' # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed target Indexes do not match' - + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_edges_index_match_in_infered_graph(self): - # edges + # edges edf_infered = self.g3e._edges - assert all(edf_infered.index == self.EMBe.index), 'Edge Emb Indexes do not match' - assert all(edf_infered.index == self.embe.index), 'Edge Transformed Emb Indexes do not match' - assert all(edf_infered.index == self.Xe.index), 'Edge Transformed features Indexes do not match' - assert all(edf_infered.index == self.ye.index), 'Edge Transformed target Indexes do not match' - - assert all(self.g3e._edge_features.index == edf_infered.index), 'Edge Feature Indexes do not match' - assert all(self.g3e._edge_embedding.index == edf_infered.index), 'Edge Emb Indexes do not match' - assert all(self.g3e._edge_target.index == edf_infered.index), 'Edge Transformed Emb Indexes do not match' + assert all( + edf_infered.index == self.EMBe.index + ), "Edge Emb Indexes do not match" + assert all( + edf_infered.index == self.embe.index + ), "Edge Transformed Emb Indexes do not match" + assert all( + edf_infered.index == self.Xe.index + ), "Edge Transformed features Indexes do not match" + assert all( + edf_infered.index == self.ye.index + ), "Edge Transformed target Indexes do not match" + + assert all( + self.g3e._edge_features.index == edf_infered.index + ), "Edge Feature Indexes do not match" + assert all( + self.g3e._edge_embedding.index == edf_infered.index + ), "Edge Emb Indexes do not match" + assert all( + self.g3e._edge_target.index == edf_infered.index + ), "Edge Transformed Emb Indexes do not match" # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed features Indexes do not match' # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed target Indexes do not match' - - + + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") + def test_umap_kwargs(self): + umap_kwargs = dict( + { + "n_components": n_components, + **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), + "n_neighbors": n_neighbors, + "min_dist": min_dist, + "spread": spread, + "local_connectivity": local_connectivity, + "repulsion_strength": repulsion_strength, + "negative_sample_rate": negative_sample_rate, + } + ) + + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_transform_umap(self): np.random.seed(41) - + train = ndf_reddit.sample(frac=0.8, random_state=42) test = ndf_reddit.drop(train.index) - + # just process train g = graphistry.nodes(train) g2 = g.umap() g3 = g2.transform_umap(train) - assert 2 * g2._node_embedding.shape[0] == g3._node_embedding.shape[0], 'Node Embedding Lengths do not match, found {} and {}'.format(g2._node_embedding.shape[0], g3._node_embedding.shape[0]) + assert ( + 2 * g2._node_embedding.shape[0] == g3._node_embedding.shape[0] + ), "Node Embedding Lengths do not match, found {} and {}".format( + g2._node_embedding.shape[0], g3._node_embedding.shape[0] + ) # now feed it args - eps=['auto', 10] - sample=[None, 2] - return_graph=[True, False] + eps = ["auto", 10] + sample = [None, 2] + return_graph = [True, False] fit_umap_embedding = [True, False] for ep in eps: g4 = g2.transform_umap(test, eps=ep) @@ -229,13 +325,18 @@ def _check_attributes(self, g, attributes): for attribute in attributes: self.assertTrue(hasattr(g, attribute), msg.format(attribute)) self.assertTrue(getattr(g, attribute) is not None, msg2.format(attribute)) - if 'df' in attribute: - self.assertIsInstance(getattr(g, attribute), pd.DataFrame, msg.format(attribute)) - if 'node_' in attribute: - self.assertIsInstance(getattr(g, attribute), pd.DataFrame, msg.format(attribute)) - if 'edge_' in attribute: - self.assertIsInstance(getattr(g, attribute), pd.DataFrame, msg.format(attribute)) - + if "df" in attribute: + self.assertIsInstance( + getattr(g, attribute), pd.DataFrame, msg.format(attribute) + ) + if "node_" in attribute: + self.assertIsInstance( + getattr(g, attribute), pd.DataFrame, msg.format(attribute) + ) + if "edge_" in attribute: + self.assertIsInstance( + getattr(g, attribute), pd.DataFrame, msg.format(attribute) + ) def cases_check_node_attributes(self, g): attributes = [ @@ -298,7 +399,7 @@ def _test_umap(self, g, use_cols, targets, name, kind, df): model_name=model_avg_name, feature_engine=feature_engine, n_neighbors=2, - dbscan=False + dbscan=False, ) self.cases_test_graph(g2, kind=kind, df=df) @@ -331,7 +432,9 @@ def test_edge_umap(self): df=triangleEdges, ) - @pytest.mark.skipif(not has_dependancy or not has_umap, reason="requires umap feature dependencies") + @pytest.mark.skipif( + not has_dependancy or not has_umap, reason="requires umap feature dependencies" + ) def test_filter_edges(self): for kind, g in [("nodes", graphistry.nodes(triangleNodes))]: g2 = g.umap(kind=kind, feature_engine="none") @@ -344,7 +447,9 @@ def test_filter_edges(self): f"{kind} -- scale: {scale}: resulting edges dataframe shape: {shape}" ) logger.debug("-" * 80) - self.assertGreaterEqual(shape[0], last_shape) # should return more and more edges + self.assertGreaterEqual( + shape[0], last_shape + ) # should return more and more edges last_shape = shape[0] @@ -356,28 +461,36 @@ class TestUMAPAIMethods(TestUMAPMethods): def _test_umap(self, g, use_cols, targets, name, kind, df): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) - for scaler in ['kbins', 'robust']: + for scaler in ["kbins", "robust"]: for cardinality in [2, 200]: for use_ngram in [True, False]: for use_col in use_cols: for target in targets: logger.debug("*" * 90) - value = [scaler, cardinality, use_ngram, target, use_col] + value = [ + scaler, + cardinality, + use_ngram, + target, + use_col, + ] logger.debug(f"{value}") logger.debug("-" * 80) - - g2 = g.umap(kind=kind, + + g2 = g.umap( + kind=kind, X=use_col, y=target, model_name=model_avg_name, use_scaler=scaler, use_scaler_target=scaler, use_ngrams=use_ngram, - engine='umap_learn', + engine="umap_learn", cardinality_threshold=cardinality, cardinality_threshold_target=cardinality, - n_neighbors=3, - dbscan=False) + n_neighbors=3, + dbscan=False, + ) self.cases_test_graph(g2, kind=kind, df=df) @@ -410,8 +523,8 @@ def test_node_umap(self): ) def test_edge_umap(self): g = graphistry.edges(edge_df2, "src", "dst") - targets = [None, 'label'] - use_cols = [None, 'title'] + targets = [None, "label"] + use_cols = [None, "title"] with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -434,19 +547,22 @@ def test_chaining_nodes(self): g = graphistry.nodes(ndf_reddit) g2 = g.umap(dbscan=False) - logger.debug('======= g.umap() done ======') + logger.debug("======= g.umap() done ======") g3a = g2.featurize() - logger.debug('======= g3a.featurize() done ======') + logger.debug("======= g3a.featurize() done ======") g3 = g3a.umap(dbscan=False) - logger.debug('======= g3.umap() done ======') + logger.debug("======= g3.umap() done ======") assert g2._node_features.shape == g3._node_features.shape # since g3 has feature params with x and y. - g3._feature_params['nodes']['X'].pop('x') - g3._feature_params['nodes']['X'].pop('y') - assert all(g2._feature_params['nodes']['X'] == g3._feature_params['nodes']['X']) - assert g2._feature_params['nodes']['y'].shape == g3._feature_params['nodes']['y'].shape # None + g3._feature_params["nodes"]["X"].pop("x") + g3._feature_params["nodes"]["X"].pop("y") + assert all(g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"]) + assert ( + g2._feature_params["nodes"]["y"].shape + == g3._feature_params["nodes"]["y"].shape + ) # None assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce - + @pytest.mark.skipif( not has_dependancy or not has_umap, reason="requires ai+umap feature dependencies", @@ -457,11 +573,13 @@ def test_chaining_edges(self): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) - g2 = g.umap(kind='edges', dbscan=False) - g3 = g.featurize(kind='edges').umap(kind='edges', dbscan=False) - - assert all(g2._feature_params['edges']['X'] == g3._feature_params['edges']['X']) - assert all(g2._feature_params['edges']['y'] == g3._feature_params['edges']['y']) # None + g2 = g.umap(kind="edges", dbscan=False) + g3 = g.featurize(kind="edges").umap(kind="edges", dbscan=False) + + assert all(g2._feature_params["edges"]["X"] == g3._feature_params["edges"]["X"]) + assert all( + g2._feature_params["edges"]["y"] == g3._feature_params["edges"]["y"] + ) # None assert all(g2._edge_features == g3._edge_features) @pytest.mark.skipif( @@ -471,19 +589,32 @@ def test_chaining_edges(self): def test_feature_kwargs_yield_different_values_using_umap_api(self): g = graphistry.nodes(ndf_reddit) n_topics_target = 6 - + with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) - g2 = g.umap(X="type", y="label", cardinality_threshold_target=3, n_topics_target=n_topics_target) # makes a GapEncoded Target - g3 = g.umap(X="type", y="label", cardinality_threshold_target=30000) # makes a one-hot-encoded target - - assert all(g2._feature_params['nodes']['X'] == g3._feature_params['nodes']['X']), "features should be the same" - assert all(g2._feature_params['nodes']['y'] != g3._feature_params['nodes']['y']), "targets in memoize should be different" # None - assert g2._node_target.shape[1] != g3._node_target.shape[1], 'Targets should be different' - assert g2._node_target.shape[1] == n_topics_target, 'Targets ' + g2 = g.umap( + X="type", + y="label", + cardinality_threshold_target=3, + n_topics_target=n_topics_target, + ) # makes a GapEncoded Target + g3 = g.umap( + X="type", y="label", cardinality_threshold_target=30000 + ) # makes a one-hot-encoded target + + assert all( + g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"] + ), "features should be the same" + assert all( + g2._feature_params["nodes"]["y"] != g3._feature_params["nodes"]["y"] + ), "targets in memoize should be different" # None + assert ( + g2._node_target.shape[1] != g3._node_target.shape[1] + ), "Targets should be different" + assert g2._node_target.shape[1] == n_topics_target, "Targets " @pytest.mark.skipif( not has_dependancy or not has_umap, @@ -504,8 +635,7 @@ def test_filter_edges(self): self.assertGreaterEqual(shape[0], last_shape) last_shape = shape[0] - - + @pytest.mark.skipif( not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", @@ -518,27 +648,35 @@ class TestCUMLMethods(TestUMAPMethods): def _test_umap(self, g, use_cols, targets, name, kind, df): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) - for scaler in ['kbins', 'robust']: + for scaler in ["kbins", "robust"]: for cardinality in [2, 200]: for use_ngram in [True, False]: for use_col in use_cols: for target in targets: logger.debug("*" * 90) - value = [scaler, cardinality, use_ngram, target, use_col] + value = [ + scaler, + cardinality, + use_ngram, + target, + use_col, + ] logger.debug(f"{value}") logger.debug("-" * 80) - - g2 = g.umap(kind=kind, + + g2 = g.umap( + kind=kind, X=use_col, y=target, model_name=model_avg_name, use_scaler=scaler, use_scaler_target=scaler, use_ngrams=use_ngram, - engine='cuml', + engine="cuml", cardinality_threshold=cardinality, cardinality_threshold_target=cardinality, - n_neighbors=3) + n_neighbors=3, + ) self.cases_test_graph(g2, kind=kind, df=df) @@ -571,8 +709,8 @@ def test_node_umap(self): ) def test_edge_umap(self): g = graphistry.edges(edge_df2, "src", "dst") - targets = [None, 'label'] - use_cols = [None, 'title'] + targets = [None, "label"] + use_cols = [None, "title"] with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -595,19 +733,22 @@ def test_chaining_nodes(self): g = graphistry.nodes(ndf_reddit) g2 = g.umap() - logger.debug('======= g.umap() done ======') + logger.debug("======= g.umap() done ======") g3a = g2.featurize() - logger.debug('======= g3a.featurize() done ======') + logger.debug("======= g3a.featurize() done ======") g3 = g3a.umap() - logger.debug('======= g3.umap() done ======') + logger.debug("======= g3.umap() done ======") assert g2._node_features.shape == g3._node_features.shape # since g3 has feature params with x and y. - g3._feature_params['nodes']['X'].pop('x') - g3._feature_params['nodes']['X'].pop('y') - assert all(g2._feature_params['nodes']['X'] == g3._feature_params['nodes']['X']) - assert g2._feature_params['nodes']['y'].shape == g3._feature_params['nodes']['y'].shape # None + g3._feature_params["nodes"]["X"].pop("x") + g3._feature_params["nodes"]["X"].pop("y") + assert all(g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"]) + assert ( + g2._feature_params["nodes"]["y"].shape + == g3._feature_params["nodes"]["y"].shape + ) # None assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce - + @pytest.mark.skipif( not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", @@ -618,11 +759,13 @@ def test_chaining_edges(self): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) - g2 = g.umap(kind='edges') - g3 = g.featurize(kind='edges').umap(kind='edges') - - assert all(g2._feature_params['edges']['X'] == g3._feature_params['edges']['X']) - assert all(g2._feature_params['edges']['y'] == g3._feature_params['edges']['y']) # None + g2 = g.umap(kind="edges") + g3 = g.featurize(kind="edges").umap(kind="edges") + + assert all(g2._feature_params["edges"]["X"] == g3._feature_params["edges"]["X"]) + assert all( + g2._feature_params["edges"]["y"] == g3._feature_params["edges"]["y"] + ) # None assert all(g2._edge_features == g3._edge_features) @pytest.mark.skipif( @@ -632,19 +775,32 @@ def test_chaining_edges(self): def test_feature_kwargs_yield_different_values_using_umap_api(self): g = graphistry.nodes(ndf_reddit) n_topics_target = 6 - + with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) - g2 = g.umap(X="type", y="label", cardinality_threshold_target=3, n_topics_target=n_topics_target) # makes a GapEncoded Target - g3 = g.umap(X="type", y="label", cardinality_threshold_target=30000) # makes a one-hot-encoded target - - assert all(g2._feature_params['nodes']['X'] == g3._feature_params['nodes']['X']), "features should be the same" - assert all(g2._feature_params['nodes']['y'] != g3._feature_params['nodes']['y']), "targets in memoize should be different" # None - assert g2._node_target.shape[1] != g3._node_target.shape[1], 'Targets should be different' - assert g2._node_target.shape[1] == n_topics_target, 'Targets ' + g2 = g.umap( + X="type", + y="label", + cardinality_threshold_target=3, + n_topics_target=n_topics_target, + ) # makes a GapEncoded Target + g3 = g.umap( + X="type", y="label", cardinality_threshold_target=30000 + ) # makes a one-hot-encoded target + + assert all( + g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"] + ), "features should be the same" + assert all( + g2._feature_params["nodes"]["y"] != g3._feature_params["nodes"]["y"] + ), "targets in memoize should be different" # None + assert ( + g2._node_target.shape[1] != g3._node_target.shape[1] + ), "Targets should be different" + assert g2._node_target.shape[1] == n_topics_target, "Targets " @pytest.mark.skipif( not has_dependancy or not has_umap, diff --git a/graphistry/util.py b/graphistry/util.py index 4f230d68d7..51115246bb 100644 --- a/graphistry/util.py +++ b/graphistry/util.py @@ -17,22 +17,24 @@ # ##################################### + def global_logger(): logger = logging.getLogger() return logger + def setup_logger(name, verbose=VERBOSE, fullpath=TRACE): - #if fullpath: + # if fullpath: # FORMAT = "[%(filename)s:%(lineno)s - %(funcName)20s() ]\n %(message)s\n" - #else: + # else: # FORMAT = " %(message)s\n" - #logging.basicConfig(format=FORMAT) - #logger = logging.getLogger()#f'graphistry.{name}') - #if verbose is None: + # logging.basicConfig(format=FORMAT) + # logger = logging.getLogger()#f'graphistry.{name}') + # if verbose is None: # logger.setLevel(logging.ERROR) - #else: + # else: # logger.setLevel(logging.INFO if verbose else logging.DEBUG) - #return logger + # return logger return global_logger() @@ -40,6 +42,8 @@ def setup_logger(name, verbose=VERBOSE, fullpath=TRACE): # Caching utils _cache_coercion_val = None + + @lru_cache(maxsize=CACHE_COERCION_SIZE) def cache_coercion_helper(k): return _cache_coercion_val @@ -47,8 +51,8 @@ def cache_coercion_helper(k): def cache_coercion(k, v): """ - Holds references to last 100 used coercions - Use with weak key/value dictionaries for actual lookups + Holds references to last 100 used coercions + Use with weak key/value dictionaries for actual lookups """ global _cache_coercion_val _cache_coercion_val = v @@ -66,35 +70,37 @@ def __init__(self, v): def hash_pdf(df: pd.DataFrame) -> str: # can be 20% faster via to_parquet (see lmeyerov issue in pandas gh), but unclear if always available return ( - hashlib.sha256(putil.hash_pandas_object(df, index=True).to_numpy().tobytes()).hexdigest() - + hashlib.sha256(str(df.columns).encode('utf-8')).hexdigest() # noqa: W503 + hashlib.sha256( + putil.hash_pandas_object(df, index=True).to_numpy().tobytes() + ).hexdigest() + + hashlib.sha256(str(df.columns).encode("utf-8")).hexdigest() # noqa: W503 ) def hash_memoize_helper(v: Any) -> str: if isinstance(v, dict): - rolling = '{' + rolling = "{" for k2, v2 in v.items(): - rolling += f'{k2}:{hash_memoize_helper(v2)},' - rolling += '}' + rolling += f"{k2}:{hash_memoize_helper(v2)}," + rolling += "}" elif isinstance(v, ModelDict): - rolling = '{' + rolling = "{" for k2, v2 in v.items(): - rolling += f'{k2}:{hash_memoize_helper(v2)},' - rolling += '}' + rolling += f"{k2}:{hash_memoize_helper(v2)}," + rolling += "}" elif isinstance(v, list): - rolling = '[' + rolling = "[" for i in v: - rolling += f'{hash_memoize_helper(i)},' - rolling += ']' + rolling += f"{hash_memoize_helper(i)}," + rolling += "]" elif isinstance(v, tuple): - rolling = '(' + rolling = "(" for i in v: - rolling += f'{hash_memoize_helper(i)},' - rolling += ')' + rolling += f"{hash_memoize_helper(i)}," + rolling += ")" elif isinstance(v, bool): - rolling = 'T' if v else 'F' + rolling = "T" if v else "F" elif isinstance(v, int): rolling = str(v) elif isinstance(v, float): @@ -102,49 +108,54 @@ def hash_memoize_helper(v: Any) -> str: elif isinstance(v, str): rolling = v elif v is None: - rolling = 'N' + rolling = "N" elif isinstance(v, pd.DataFrame): rolling = hash_pdf(v) else: - raise TypeError(f'Unsupported memoization type: {type(v)}') + raise TypeError(f"Unsupported memoization type: {type(v)}") return rolling + def hash_memoize(v: Any) -> str: - return hashlib.sha256(hash_memoize_helper(v).encode('utf-8')).hexdigest() + return hashlib.sha256(hash_memoize_helper(v).encode("utf-8")).hexdigest() + -def check_set_memoize(g, metadata, attribute, name: str = '', memoize: bool = True): # noqa: C901 +def check_set_memoize( + g, metadata, attribute, name: str = "", memoize: bool = True +): # noqa: C901 """ - Helper Memoize function that checks if metadata args have changed for object g -- which is unconstrained save - for the fact that it must have `attribute`. If they have not changed, will return memoized version, - if False, will continue with whatever pipeline it is in front. + Helper Memoize function that checks if metadata args have changed for object g -- which is unconstrained save + for the fact that it must have `attribute`. If they have not changed, will return memoized version, + if False, will continue with whatever pipeline it is in front. """ - - logger = setup_logger(f'{__name__}.memoization') + + logger = setup_logger(f"{__name__}.memoization") if not memoize: - logger.debug('Memoization disabled') + logger.debug("Memoization disabled") return False - + hashed = None weakref = getattr(g, attribute) try: hashed = hash_memoize(dict(data=metadata)) except TypeError: logger.warning( - f'! Failed {name} speedup attempt. Continuing without memoization speedups.' + f"! Failed {name} speedup attempt. Continuing without memoization speedups." ) try: if hashed in weakref: - logger.debug(f'{name} memoization hit: %s', hashed) + logger.debug(f"{name} memoization hit: %s", hashed) return weakref[hashed].v else: - logger.debug(f'{name} memoization miss for id (of %s): %s', - len(weakref), hashed) + logger.debug( + f"{name} memoization miss for id (of %s): %s", len(weakref), hashed + ) except: - logger.debug(f'Failed to hash {name} kwargs', exc_info=True) + logger.debug(f"Failed to hash {name} kwargs", exc_info=True) pass - + if memoize and (hashed is not None): w = WeakValueWrapper(g) cache_coercion(hashed, w) @@ -291,8 +302,10 @@ def deprecated_func(*args, **kwargs): # MODEL Parameter HELPERS def get_timestamp(): import datetime + return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + class ModelDict(UserDict): """Helper class to print out model names and keep track of updates @@ -305,11 +318,15 @@ def __init__(self, message, verbose=True, timestamp=False, *args, **kwargs): self._message = message self._verbose = verbose self._timestamp = timestamp - L = len(message) if timestamp is False else max(len(message), len(get_timestamp())+1) + L = ( + len(message) + if timestamp is False + else max(len(message), len(get_timestamp()) + 1) + ) self._print_length = min(80, L) self._updates = [] super().__init__(*args, **kwargs) - + def print(self, message): if self._timestamp: message = f"{message}\n{get_timestamp()}" @@ -321,7 +338,7 @@ def print(self, message): print() def __repr__(self): - #logger.info(self._message) + # logger.info(self._message) self.print(self._message) return super().__repr__() From e77601dc35edcbfea2ac9f0ca1c5798481787b9b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 18:52:53 -0800 Subject: [PATCH 050/432] adds test for umap args --- graphistry/tests/test_umap_utils.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index c4f77dfaa7..2a328430bc 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -261,16 +261,27 @@ def test_edges_index_match_in_infered_graph(self): def test_umap_kwargs(self): umap_kwargs = dict( { - "n_components": n_components, - **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), - "n_neighbors": n_neighbors, - "min_dist": min_dist, - "spread": spread, - "local_connectivity": local_connectivity, - "repulsion_strength": repulsion_strength, - "negative_sample_rate": negative_sample_rate, + "n_components": 2, + "metric": 'euclidean', + "n_neighbors": 3, + "min_dist": 1, + "spread": 1, + "local_connectivity": 1, + "repulsion_strength": 1, + "negative_sample_rate": 5, } ) + umap_kwargs2 = {k:v+1 for k, v in umap_kwargs.items()} + g = graphistry.nodes(ndf_reddit) + g2 = g.umap(**umap_kwargs) + g3 = g.umap(**umap_kwargs2) + assert g2._umap_params == umap_kwargs, f"Umap params do not match, found {g2._umap_params} vs {umap_kwargs} " + assert g3._umap_params == umap_kwargs2, f"Umap params do not match, found {g3._umap_params} vs {umap_kwargs2} " + g4 = g2.transform_umap(ndf_reddit) + assert g4._umap_params == umap_kwargs, f"Umap params do not match, found {g4._umap_params} vs {umap_kwargs} " + g5 = g3.transform_umap(ndf_reddit) + assert g5._umap_params == umap_kwargs2, f"Umap params do not match, found {g5._umap_params} vs {umap_kwargs2} " + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_transform_umap(self): From 92e9edcf54e2d067701d2632204118a25a85327c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 18:58:12 -0800 Subject: [PATCH 051/432] lint --- graphistry/tests/test_umap_utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 2a328430bc..ad382aef0c 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -271,6 +271,7 @@ def test_umap_kwargs(self): "negative_sample_rate": 5, } ) + umap_kwargs2 = {k:v+1 for k, v in umap_kwargs.items()} g = graphistry.nodes(ndf_reddit) g2 = g.umap(**umap_kwargs) @@ -569,8 +570,7 @@ def test_chaining_nodes(self): g3._feature_params["nodes"]["X"].pop("y") assert all(g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"]) assert ( - g2._feature_params["nodes"]["y"].shape - == g3._feature_params["nodes"]["y"].shape + g2._feature_params["nodes"]["y"].shape == g3._feature_params["nodes"]["y"].shape ) # None assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce @@ -755,8 +755,7 @@ def test_chaining_nodes(self): g3._feature_params["nodes"]["X"].pop("y") assert all(g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"]) assert ( - g2._feature_params["nodes"]["y"].shape - == g3._feature_params["nodes"]["y"].shape + g2._feature_params["nodes"]["y"].shape == g3._feature_params["nodes"]["y"].shape ) # None assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce From 01dcb363ec9463c7076a8b6aa8f752c84c1ef929 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 19:03:51 -0800 Subject: [PATCH 052/432] lint --- graphistry/tests/test_umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index ad382aef0c..197a484071 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -272,7 +272,7 @@ def test_umap_kwargs(self): } ) - umap_kwargs2 = {k:v+1 for k, v in umap_kwargs.items()} + umap_kwargs2 = {k: v+1 for k, v in umap_kwargs.items()} g = graphistry.nodes(ndf_reddit) g2 = g.umap(**umap_kwargs) g3 = g.umap(**umap_kwargs2) From 4f478030c73ec6ffb97d3223e4aa41c40bbbed17 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 19:08:19 -0800 Subject: [PATCH 053/432] lint --- graphistry/tests/test_umap_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 197a484071..c1120e5a30 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -272,7 +272,8 @@ def test_umap_kwargs(self): } ) - umap_kwargs2 = {k: v+1 for k, v in umap_kwargs.items()} + + umap_kwargs2 = { k : v+1 for k, v in umap_kwargs.items()} g = graphistry.nodes(ndf_reddit) g2 = g.umap(**umap_kwargs) g3 = g.umap(**umap_kwargs2) From 83cb8fefc0004872ac1fc9c2399bf796f81e5492 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 21:42:25 -0800 Subject: [PATCH 054/432] lint --- graphistry/tests/test_umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index c1120e5a30..b99bac6a72 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -273,7 +273,7 @@ def test_umap_kwargs(self): ) - umap_kwargs2 = { k : v+1 for k, v in umap_kwargs.items()} + umap_kwargs2 = { k : v+1 for k , v in umap_kwargs.items() } g = graphistry.nodes(ndf_reddit) g2 = g.umap(**umap_kwargs) g3 = g.umap(**umap_kwargs2) From 1efa4604febada443089ddf6e86f5ac6ea28480e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 21:44:00 -0800 Subject: [PATCH 055/432] lint --- graphistry/tests/test_umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index b99bac6a72..485fce8536 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -273,7 +273,7 @@ def test_umap_kwargs(self): ) - umap_kwargs2 = { k : v+1 for k , v in umap_kwargs.items() } + umap_kwargs2 = {k: v+1 for k, v in umap_kwargs.items()} # type: ignore g = graphistry.nodes(ndf_reddit) g2 = g.umap(**umap_kwargs) g3 = g.umap(**umap_kwargs2) From 7d90f74268e49627f09c3a2bd5a67deb20970799 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 21:46:17 -0800 Subject: [PATCH 056/432] lint --- graphistry/tests/test_umap_utils.py | 50 ++++++++++++++++------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 485fce8536..9ed5c9651b 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -259,31 +259,35 @@ def test_edges_index_match_in_infered_graph(self): @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_umap_kwargs(self): - umap_kwargs = dict( - { - "n_components": 2, - "metric": 'euclidean', - "n_neighbors": 3, - "min_dist": 1, - "spread": 1, - "local_connectivity": 1, - "repulsion_strength": 1, - "negative_sample_rate": 5, - } - ) - - - umap_kwargs2 = {k: v+1 for k, v in umap_kwargs.items()} # type: ignore + umap_kwargs = { + "n_components": 2, + "metric": "euclidean", + "n_neighbors": 3, + "min_dist": 1, + "spread": 1, + "local_connectivity": 1, + "repulsion_strength": 1, + "negative_sample_rate": 5, + } + + umap_kwargs2 = {k: v + 1 for k, v in umap_kwargs.items()} # type: ignore g = graphistry.nodes(ndf_reddit) g2 = g.umap(**umap_kwargs) g3 = g.umap(**umap_kwargs2) - assert g2._umap_params == umap_kwargs, f"Umap params do not match, found {g2._umap_params} vs {umap_kwargs} " - assert g3._umap_params == umap_kwargs2, f"Umap params do not match, found {g3._umap_params} vs {umap_kwargs2} " + assert ( + g2._umap_params == umap_kwargs + ), f"Umap params do not match, found {g2._umap_params} vs {umap_kwargs} " + assert ( + g3._umap_params == umap_kwargs2 + ), f"Umap params do not match, found {g3._umap_params} vs {umap_kwargs2} " g4 = g2.transform_umap(ndf_reddit) - assert g4._umap_params == umap_kwargs, f"Umap params do not match, found {g4._umap_params} vs {umap_kwargs} " + assert ( + g4._umap_params == umap_kwargs + ), f"Umap params do not match, found {g4._umap_params} vs {umap_kwargs} " g5 = g3.transform_umap(ndf_reddit) - assert g5._umap_params == umap_kwargs2, f"Umap params do not match, found {g5._umap_params} vs {umap_kwargs2} " - + assert ( + g5._umap_params == umap_kwargs2 + ), f"Umap params do not match, found {g5._umap_params} vs {umap_kwargs2} " @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_transform_umap(self): @@ -571,7 +575,8 @@ def test_chaining_nodes(self): g3._feature_params["nodes"]["X"].pop("y") assert all(g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"]) assert ( - g2._feature_params["nodes"]["y"].shape == g3._feature_params["nodes"]["y"].shape + g2._feature_params["nodes"]["y"].shape + == g3._feature_params["nodes"]["y"].shape ) # None assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce @@ -756,7 +761,8 @@ def test_chaining_nodes(self): g3._feature_params["nodes"]["X"].pop("y") assert all(g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"]) assert ( - g2._feature_params["nodes"]["y"].shape == g3._feature_params["nodes"]["y"].shape + g2._feature_params["nodes"]["y"].shape + == g3._feature_params["nodes"]["y"].shape ) # None assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce From 4e2570041a0b4081575beadac1fb23c328ca900f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 21:48:14 -0800 Subject: [PATCH 057/432] lint --- graphistry/tests/test_umap_utils.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 9ed5c9651b..5f218d4889 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -276,18 +276,18 @@ def test_umap_kwargs(self): g3 = g.umap(**umap_kwargs2) assert ( g2._umap_params == umap_kwargs - ), f"Umap params do not match, found {g2._umap_params} vs {umap_kwargs} " + ), f"Umap params do not match, found {g2._umap_params} vs {umap_kwargs}" assert ( g3._umap_params == umap_kwargs2 - ), f"Umap params do not match, found {g3._umap_params} vs {umap_kwargs2} " + ), f"Umap params do not match, found {g3._umap_params} vs {umap_kwargs2}" g4 = g2.transform_umap(ndf_reddit) assert ( g4._umap_params == umap_kwargs - ), f"Umap params do not match, found {g4._umap_params} vs {umap_kwargs} " + ), f"Umap params do not match, found {g4._umap_params} vs {umap_kwargs}" g5 = g3.transform_umap(ndf_reddit) assert ( g5._umap_params == umap_kwargs2 - ), f"Umap params do not match, found {g5._umap_params} vs {umap_kwargs2} " + ), f"Umap params do not match, found {g5._umap_params} vs {umap_kwargs2}" @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_transform_umap(self): @@ -575,8 +575,7 @@ def test_chaining_nodes(self): g3._feature_params["nodes"]["X"].pop("y") assert all(g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"]) assert ( - g2._feature_params["nodes"]["y"].shape - == g3._feature_params["nodes"]["y"].shape + g2._feature_params["nodes"]["y"].shape == g3._feature_params["nodes"]["y"].shape ) # None assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce @@ -761,8 +760,7 @@ def test_chaining_nodes(self): g3._feature_params["nodes"]["X"].pop("y") assert all(g2._feature_params["nodes"]["X"] == g3._feature_params["nodes"]["X"]) assert ( - g2._feature_params["nodes"]["y"].shape - == g3._feature_params["nodes"]["y"].shape + g2._feature_params["nodes"]["y"].shape == g3._feature_params["nodes"]["y"].shape ) # None assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce From c756803f73f2a0fe7cee18851f558b347c55c0a1 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 21:57:11 -0800 Subject: [PATCH 058/432] lint --- graphistry/embed_utils.py | 2 +- graphistry/feature_utils.py | 2 +- graphistry/umap_utils.py | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index ef09a02475..f15d4be1ce 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -89,7 +89,7 @@ def __init__(self): self._device = "cpu" def _preprocess_embedding_data(self, res, train_split:Union[float, int] = 0.8) -> Plottable: - _, torch, _, _, _, _, _, _ = lazy_embed_import_dep() + #_, torch, _, _, _, _, _, _ = lazy_embed_import_dep() import torch log('Preprocessing embedding data') src, dst = res._source, res._destination diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 5683f96064..411c369f24 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2657,7 +2657,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ) - def get_features_by_cols(self, columns: Union[List, str] = None, kind: str = 'nodes', target: bool = False): + def get_features_by_cols(self, columns: Union[List, str, None] = None, kind: str = 'nodes', target: bool = False): """Returns feature matrix with only the columns that contain the string `column_part` in their name. `X = g.get_features_by_cols(['feature1', 'feature2'])` diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 8a596be47c..46cfacfb17 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -197,7 +197,7 @@ def umap_lazy_init( umap_kwargs = dict( { "n_components": n_components, - **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), + **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), # noqa "n_neighbors": n_neighbors, "min_dist": min_dist, "spread": spread, @@ -258,10 +258,10 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info("-" * 90) logger.info(f"Starting UMAP-ing data of shape {X.shape}") - if self.engine == CUML and is_legacy_cuml(): + if self.engine == CUML and is_legacy_cuml(): # noqa from cuml.neighbors import NearestNeighbors - knn = NearestNeighbors(n_neighbors=self._n_neighbors) + knn = NearestNeighbors(n_neighbors=self._n_neighbors) # noqa cc = self._umap.fit(X, y, knn_graph=knn) knn.fit(cc.embedding_) self._umap.graph_ = knn.kneighbors_graph(cc.embedding_) @@ -272,7 +272,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): self._weighted_adjacency = self._umap.graph_ # if changing, also update fresh_res self._weighted_edges_df = umap_graph_to_weighted_edges( - self._umap.graph_, self.engine, is_legacy_cuml() + self._umap.graph_, self.engine, is_legacy_cuml() # noqa ) mins = (time() - t) / 60 @@ -289,7 +289,7 @@ def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = No return emb def transform_umap( # noqa: E303 - self, df: pd.DataFrame, ydf: pd.DataFrame = None, kind: str = "nodes", + self, df: pd.DataFrame, ydf: Union[pd.DataFrame, None] = None, kind: str = "nodes", eps='auto', sample=None, return_graph=True, @@ -498,7 +498,7 @@ def umap( else: res = self.bind() - res = res.umap_lazy_init(res, **umap_kwargs) + res = res.umap_lazy_init(res, **umap_kwargs) # type: ignore logger.debug("umap input X :: %s", X) logger.debug("umap input y :: %s", y) @@ -607,7 +607,7 @@ def umap( res, kind, encode_position, encode_weight, play ) # noqa: E501 - if res.engine == CUML and is_legacy_cuml(): + if res.engine == CUML and is_legacy_cuml(): # type: ignore res = res.prune_self_edges() if dbscan: From 768ed4e7ff325fbfc79af61eea5f513719c23dcb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Jan 2023 22:02:55 -0800 Subject: [PATCH 059/432] qa --- graphistry/compute/cluster.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 272b9caa96..b974f427cf 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -243,7 +243,8 @@ def dbscan( def _transform_dbscan( self, df: pd.DataFrame, ydf=None, kind: str = "nodes" - ) -> pd.DataFrame: + ) -> Union[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: + """ Transforms a dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels and returns feature[cols] or UMAP embedding @@ -342,7 +343,7 @@ def transform_dbscan( """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) if return_graph: - g = self._infer_edges( + g = self._infer_edges( # type: ignore emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample ) return g From 749c6db8972b86350b0e6f366321e88b6e1873c4 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 00:14:54 -0800 Subject: [PATCH 060/432] deprecates(zscale -> standard, ydf -> y), simplifies scaler, expeeeriments --- graphistry/feature_utils.py | 132 ++++++++++++++++++++++++------------ 1 file changed, 88 insertions(+), 44 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 411c369f24..e149e3533a 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -544,7 +544,7 @@ def get_preprocessing_pipeline( :param X: np.ndarray :param impute: whether to run imputing or not :param use_scaler: string in None or - ["minmax", "quantile", "zscale", "robust", "kbins"], + ["minmax", "quantile", "standard", "robust", "kbins"], selects scaling transformer, default None :param n_quantiles: if use_scaler = 'quantile', sets the quantile bin size. @@ -573,7 +573,7 @@ def get_preprocessing_pipeline( available_preprocessors = [ "minmax", "quantile", - "zscale", + "standard", "robust", "kbins", ] @@ -600,7 +600,7 @@ def get_preprocessing_pipeline( scaler = QuantileTransformer( n_quantiles=n_quantiles, output_distribution=output_distribution ) - elif use_scaler == "zscale": + elif use_scaler == "standard": scaler = StandardScaler() elif use_scaler == "robust": scaler = RobustScaler(quantile_range=quantile_range) @@ -884,7 +884,7 @@ def process_dirty_dataframes( threshold, encoder is OneHot, above, it is GapEncoder :param n_topics: number of topics for GapEncoder, default 42 :param use_scaler: None or string in - ['minmax', 'zscale', 'robust', 'quantile'] + ['minmax', 'standard', 'robust', 'quantile'] :param similarity: one of 'ngram', 'levenshtein-ratio', 'jaro', or'jaro-winkler'}) – The type of pairwise string similarity to use. If None or False, uses a SuperVectorizer @@ -1042,7 +1042,7 @@ def process_nodes_dataframes( :param df: pandas DataFrame of data :param y: pandas DataFrame of targets :param use_scaler: None or string in - ['minmax', 'zscale', 'robust', 'quantile'] + ['minmax', 'standard', 'robust', 'quantile'] :param n_topics: number of topics in Gap Encoder :param use_scaler: :param confidence: Number between 0 and 1, will pass @@ -1319,7 +1319,7 @@ def process_edge_dataframes( :param src: source column to select in edf :param dst: destination column to select in edf :param use_scaler: None or string in - ['minmax', 'zscale', 'robust', 'quantile'] + ['minmax', 'standard', 'robust', 'quantile'] :return: Encoded data matrix and target (if not None), the data encoders, and the label encoder. """ @@ -1713,33 +1713,48 @@ def fit_transform(self, src=None, dst=None, *args, **kwargs): self.fit(src=src, dst=dst, *args, **kwargs) return self.X, self.y - def scale(self, df, ydf=None, set_scaler=False, *args, **kwargs): - # pretty hacky but gets job done -- - """Fits new scaling functions on df, ydf via args-kwargs - (ie use downstream as X_train, X_test ,... or batch - when different scaling on the outputs is required) + def scale(self, X=None, y=None, set_scaler=False, *args, **kwargs): + """Fits new scaling functions on df, y via args-kwargs + + example: + g = graphistry.nodes(df) + g2 = g.umap() + + X, y = g2.scale(X, y, use_scaler='minmax', use_scaler_target='kbins', n_bins=5) + + args: + X: pd.DataFrame of features + y: pd.DataFrame of target features + kind: str, one of 'nodes' or 'edges' + set_scaler: bool, if True, will set the new scaler as the default for the encoder + *args, **kwargs: passed to smart_scaler + returns: + scaled X, y """ # pop off the previous scaler so that .transform won't use it self.res[4] = None self.res[5] = None - - X, y = self.transform(df, ydf) # these are the raw transforms, + logger.info("-Fitting new scaler on raw features") X, y, scaling_pipeline, scaling_pipeline_target = smart_scaler( X_enc=X, y_enc=y, *args, **kwargs ) + + def _set(res, scaling_pipeline, scaling_pipeline_target): + logger.info("--Setting fit scaler to self") + res.res[4] = scaling_pipeline + res.res[5] = scaling_pipeline_target + res.scaling_pipeline = scaling_pipeline + res.scaling_pipeline_target = scaling_pipeline_target + return res if set_scaler: - logger.info("--Setting fit scaler to self") - self.res[4] = scaling_pipeline - self.res[5] = scaling_pipeline_target - self.scaling_pipeline = scaling_pipeline - self.scaling_pipeline_target = scaling_pipeline_target + self = _set(self, scaling_pipeline, scaling_pipeline_target) else: # add the original back self.res[4] = self.scaling_pipeline self.res[5] = self.scaling_pipeline_target - - return X, y, scaling_pipeline, scaling_pipeline_target + + return X, y # ###################################################################################################################### @@ -1995,7 +2010,9 @@ def _featurize_nodes( # if changing, also update fresh_res res._node_features = encoder.X + res._node_features_raw = encoder.X#.copy() res._node_target = encoder.y + res._node_target_raw = encoder.y#.copy() res._node_encoder = encoder # now this does # all the work `._node_encoder.transform(df, y)` etc @@ -2113,7 +2130,9 @@ def _featurize_edges( # if editing, should also update fresh_res res._edge_features = encoder.X + res._edge_features_raw = encoder.X#.copy() res._edge_target = encoder.y + res._edge_target_raw = encoder.y#.copy() res._edge_encoder = encoder return res @@ -2132,7 +2151,12 @@ def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): "before being able to transform data" ) - def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', sample=None, verbose=False): + def transform(self, df: pd.DataFrame, + y: Union[pd.DataFrame, None] = None, + kind: str ='nodes', + return_graph: bool = True, + eps: Union[str, float, int] = 'auto', sample = None, + verbose = False): """Transform new data and append to existing graph. args: @@ -2148,9 +2172,9 @@ def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', s or a graph with inferred edges if return_graph is True """ if kind == "nodes": - X, y = self._transform("_node_encoder", df, ydf) + X, y = self._transform("_node_encoder", df, y) elif kind == "edges": - X, y = self._transform("_edge_encoder", df, ydf) + X, y = self._transform("_edge_encoder", df, y) else: logger.debug("kind must be one of `nodes`," f"`edges`, found {kind}") @@ -2162,12 +2186,11 @@ def transform(self, df, ydf=None, kind='nodes', return_graph=True, eps='auto', s def scale( self, - df, - ydf, - kind, - use_scaler, - use_scaler_target, - set_scaler=False, + df: pd.DataFrame, + y: pd.DataFrame = None, + kind: str = "nodes", + use_scaler: Union[str, None] = None, + use_scaler_target: Union[str, None] = None, impute: bool = True, n_quantiles: int = 10, output_distribution: str = "normal", @@ -2175,30 +2198,54 @@ def scale( n_bins: int = 2, encode: str = "ordinal", strategy: str = "uniform", + set_scaler: bool = False, keep_n_decimals: int = 5, ): """Scale data using the same scalers as used in the featurization step. example usage: g = graphistry.nodes(df) - g2 = g.umap().scale(df, ydf, kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) + g2 = g.umap().scale(eps=0.2, sample=None, kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) # scaled data + g3 = g.scale( kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) X = g2._node_features y = g2._node_target + args: + df: pd.DataFrame, raw data to transform + y: pd.DataFrame, optional + kind: str # one of `nodes`, `edges` + use_scaler: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` + use_scaler_target: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` + impute: bool, if True, will impute missing values + n_quantiles: int, number of quantiles to use for quantile scaler + output_distribution: str, one of `normal`, `uniform`, `lognormal` + quantile_range: tuple, range of quantiles to use for quantile scaler + n_bins: int, number of bins to use for KBinsDiscretizer + encode: str, one of `ordinal`, `onehot`, `onehot-dense`, `binary` + strategy: str, one of `uniform`, `quantile`, `kmeans` + set_scaler: bool, if True, will set the scaler to the new scaler + keep_n_decimals: int, number of decimals to keep after scaling + returns: + (X, y) transformed data if return_graph is False + or a graph with inferred edges if return_graph is True, """ + + if df is None: # use the original data + # df = self._nodes if kind == "nodes" else self._edges + X, y = (self._node_features_raw, self._node_target_raw) if kind == "nodes" else (self._edge_features_raw, self._edge_target_raw) + else: + X, y = self.transform(df, y, kind=kind, return_graph=False) if kind == "nodes" and hasattr(self, "_node_encoder"): # type: ignore if self._node_encoder is not None: # type: ignore ( X, - y, - scaling_pipeline, - scaling_pipeline_target, + y ) = self._node_encoder.scale( - df, - ydf, + X, + y, set_scaler=set_scaler, use_scaler=use_scaler, use_scaler_target=use_scaler_target, @@ -2222,12 +2269,10 @@ def scale( if self._edge_encoder is not None: # type: ignore ( X, - y, - scaling_pipeline, - scaling_pipeline_target, + y ) = self._edge_encoder.scale( - df, - ydf, + X, + y, set_scaler=set_scaler, use_scaler=use_scaler, use_scaler_target=use_scaler_target, @@ -2245,8 +2290,7 @@ def scale( 'Please run g.featurize(kind="edges", *args, **kwargs) ' 'first before scaling matrices and targets is possible.' ) - - return X, y, scaling_pipeline, scaling_pipeline_target + return X, y def featurize( self, @@ -2301,11 +2345,11 @@ def featurize( :param use_scaler: selects which scaler (and automatically imputes missing values using mean strategy) to scale the data. Options are; - "minmax", "quantile", "zscale", "robust", + "minmax", "quantile", "standard", "robust", "kbins", default None. Please see scikits-learn documentation https://scikit-learn.org/stable/modules/preprocessing.html - Here 'zscale' corresponds to 'StandardScaler' in scikits. + Here 'standard' corresponds to 'StandardScaler' in scikits. :param cardinality_threshold: dirty_cat threshold on cardinality of categorical labels across columns. If value is greater than threshold, will run GapEncoder From aa5ae1d9c6d18f0e4299e1e57aa3c3310eb3b457 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 00:15:32 -0800 Subject: [PATCH 061/432] typing and doc --- graphistry/compute/cluster.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index b974f427cf..6fa94999c6 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -243,7 +243,7 @@ def dbscan( def _transform_dbscan( self, df: pd.DataFrame, ydf=None, kind: str = "nodes" - ) -> Union[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: + ) -> Tuple[Union[pd.DataFrame, None], pd.DataFrame, pd.DataFrame, pd.DataFrame]: """ Transforms a dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels @@ -298,20 +298,20 @@ def _transform_dbscan( else: X_ = X - labels = dbscan_predict(X_, dbscan) + labels = dbscan_predict(X_, dbscan) # type: ignore if umap: - df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) + df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) # type: ignore else: df = df.assign(_dbscan=labels) - return emb, X, y, df + return emb, X, y, df # type: ignore else: raise Exception("No dbscan model found. Please run `g.dbscan()` first") def transform_dbscan( self, df: pd.DataFrame, - y: pd.DataFrame = None, + y: Union[pd.DataFrame, None] = None, eps: Union[float, str] = "auto", fit_umap_embedding: bool = False, sample: int = None, @@ -321,9 +321,9 @@ def transform_dbscan( Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable ]: """ - Transforms a minibatch dataframe to one with a new column '_cluster' containing the DBSCAN cluster labels on the minibatch + Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels on the minibatch and generates a graph with the minibatch and the original graph, with edges between the minibatch and the original graph inferred - works for + from the umap embedding or features dataframe. args: df: dataframe to transform @@ -337,8 +337,7 @@ def transform_dbscan( if None, will only use closest point to the minibatch. If greater than 0, will sample the closest `sample` points in existing graph to pull in more edges. Default None kind: 'nodes' or 'edges' - return_graph: whether to return a graph or the (emb, X, y, minibatch enriched with DBSCAN labels), default True - + return_graph: whether to return a graph or the (emb, X, y, minibatch df enriched with DBSCAN labels), default True """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) From ebd5c3ab6ca635f7f0b2f8fd3e3c1527e143d054 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 00:17:41 -0800 Subject: [PATCH 062/432] lint --- graphistry/feature_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index e149e3533a..4f68aef002 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2010,9 +2010,9 @@ def _featurize_nodes( # if changing, also update fresh_res res._node_features = encoder.X - res._node_features_raw = encoder.X#.copy() + res._node_features_raw = encoder.X #.copy() res._node_target = encoder.y - res._node_target_raw = encoder.y#.copy() + res._node_target_raw = encoder.y #.copy() res._node_encoder = encoder # now this does # all the work `._node_encoder.transform(df, y)` etc @@ -2130,9 +2130,9 @@ def _featurize_edges( # if editing, should also update fresh_res res._edge_features = encoder.X - res._edge_features_raw = encoder.X#.copy() + res._edge_features_raw = encoder.X #.copy() res._edge_target = encoder.y - res._edge_target_raw = encoder.y#.copy() + res._edge_target_raw = encoder.y #.copy() res._edge_encoder = encoder return res From be15c06ef9ad154760c5cc3fc5792a9e48274a97 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 00:19:51 -0800 Subject: [PATCH 063/432] lint --- graphistry/feature_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 4f68aef002..a527cf8d9e 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2010,9 +2010,9 @@ def _featurize_nodes( # if changing, also update fresh_res res._node_features = encoder.X - res._node_features_raw = encoder.X #.copy() + res._node_features_raw = encoder.X # .copy() res._node_target = encoder.y - res._node_target_raw = encoder.y #.copy() + res._node_target_raw = encoder.y # .copy() res._node_encoder = encoder # now this does # all the work `._node_encoder.transform(df, y)` etc @@ -2130,9 +2130,9 @@ def _featurize_edges( # if editing, should also update fresh_res res._edge_features = encoder.X - res._edge_features_raw = encoder.X #.copy() + res._edge_features_raw = encoder.X # .copy() res._edge_target = encoder.y - res._edge_target_raw = encoder.y #.copy() + res._edge_target_raw = encoder.y # .copy() res._edge_encoder = encoder return res From 3089ae1588c57978c1ba6bf60d6046b285c807fb Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 00:23:05 -0800 Subject: [PATCH 064/432] lint --- graphistry/feature_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index a527cf8d9e..1bd3815919 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2153,7 +2153,7 @@ def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): def transform(self, df: pd.DataFrame, y: Union[pd.DataFrame, None] = None, - kind: str ='nodes', + kind: str = 'nodes', return_graph: bool = True, eps: Union[str, float, int] = 'auto', sample = None, verbose = False): @@ -2232,7 +2232,7 @@ def scale( or a graph with inferred edges if return_graph is True, """ - if df is None: # use the original data + if df is None: # use the original data # df = self._nodes if kind == "nodes" else self._edges X, y = (self._node_features_raw, self._node_target_raw) if kind == "nodes" else (self._edge_features_raw, self._edge_target_raw) else: From 504484219990bb5abfd332c56dde54d0d60d6334 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 14:39:44 -0800 Subject: [PATCH 065/432] lint --- graphistry/feature_utils.py | 4 ++-- graphistry/umap_utils.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 1bd3815919..b5672c4c2c 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2142,7 +2142,7 @@ def _infer_edges(self, emb, X, y, df, eps='auto', sample=None, infer_on_umap_emb g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, eps=eps, sample=sample, verbose=verbose, **kwargs) return g - def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): + def _transform(self, encoder: str, df: pd.DataFrame, ydf: Optional[pd.DataFrame]): if getattr(self, encoder) is not None: return getattr(self, encoder).transform(df, ydf) else: @@ -2187,7 +2187,7 @@ def transform(self, df: pd.DataFrame, def scale( self, df: pd.DataFrame, - y: pd.DataFrame = None, + y: Optional[pd.DataFrame] = None, kind: str = "nodes", use_scaler: Union[str, None] = None, use_scaler_target: Union[str, None] = None, diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 46cfacfb17..661ac8e5a0 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -166,6 +166,7 @@ class UMAPMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): self._umap_initialized = False + #self.engine = self.engine if hasattr(self, "engine") else None def umap_lazy_init( self, @@ -197,7 +198,7 @@ def umap_lazy_init( umap_kwargs = dict( { "n_components": n_components, - **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), # noqa + **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), # type: ignore "n_neighbors": n_neighbors, "min_dist": min_dist, "spread": spread, @@ -258,21 +259,20 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info("-" * 90) logger.info(f"Starting UMAP-ing data of shape {X.shape}") - if self.engine == CUML and is_legacy_cuml(): # noqa + if self.engine == CUML and is_legacy_cuml(): # type: ignore from cuml.neighbors import NearestNeighbors - knn = NearestNeighbors(n_neighbors=self._n_neighbors) # noqa + knn = NearestNeighbors(n_neighbors=self._n_neighbors) # type: ignore cc = self._umap.fit(X, y, knn_graph=knn) knn.fit(cc.embedding_) self._umap.graph_ = knn.kneighbors_graph(cc.embedding_) - self._weighted_adjacency = self._umap.graph_ - else: self._umap.fit(X, y) - self._weighted_adjacency = self._umap.graph_ + + self._weighted_adjacency = self._umap.graph_ # if changing, also update fresh_res self._weighted_edges_df = umap_graph_to_weighted_edges( - self._umap.graph_, self.engine, is_legacy_cuml() # noqa + self._umap.graph_, self.engine, is_legacy_cuml() # type: ignore ) mins = (time() - t) / 60 From dd0947110c21330055e32e24d437e4f94a2bb500 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 14:46:21 -0800 Subject: [PATCH 066/432] typing --- graphistry/compute/cluster.py | 8 ++++---- graphistry/feature_utils.py | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 6fa94999c6..7f9c94ebf6 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -314,12 +314,12 @@ def transform_dbscan( y: Union[pd.DataFrame, None] = None, eps: Union[float, str] = "auto", fit_umap_embedding: bool = False, - sample: int = None, + sample: Optional[int] = None, kind: str = "nodes", return_graph=True, - ) -> Union[ - Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable - ]: + ): #-> Union[ + # Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable + # ]: """ Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels on the minibatch and generates a graph with the minibatch and the original graph, with edges between the minibatch and the original graph inferred diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index b5672c4c2c..f3ff1e634d 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2560,7 +2560,7 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( res._node_target = None if reuse_if_existing and res._node_features is not None: - # logger.info('-Reusing Existing Featurization') + logger.info('-Reusing Existing Node Featurization') return res._node_features, res._node_target, res res = res._featurize_nodes( @@ -2578,7 +2578,6 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, min_words=min_words, model_name=model_name, similarity=similarity, @@ -2656,7 +2655,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( res._edge_target = None if reuse_if_existing and res._edge_features is not None: - # logger.info('-Reusing Existing Featurization') + logger.info('-Reusing Existing Edge Featurization') return res._edge_features, res._edge_target, res res = res._featurize_edges( @@ -2673,7 +2672,6 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, min_words=min_words, model_name=model_name, similarity=similarity, @@ -2701,7 +2699,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ) - def get_features_by_cols(self, columns: Union[List, str, None] = None, kind: str = 'nodes', target: bool = False): + def get_features_by_cols(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False): """Returns feature matrix with only the columns that contain the string `column_part` in their name. `X = g.get_features_by_cols(['feature1', 'feature2'])` From 0561a7f6c6ee0eb907de4d907c2d697334a038b2 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 14:48:16 -0800 Subject: [PATCH 067/432] typing --- graphistry/compute/cluster.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 7f9c94ebf6..69a11a7572 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -317,9 +317,7 @@ def transform_dbscan( sample: Optional[int] = None, kind: str = "nodes", return_graph=True, - ): #-> Union[ - # Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable - # ]: + ): # type: ignore """ Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels on the minibatch and generates a graph with the minibatch and the original graph, with edges between the minibatch and the original graph inferred From edfd8442ff618490efbaf2e365082958e889011e Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 14:54:07 -0800 Subject: [PATCH 068/432] typing --- graphistry/compute/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 69a11a7572..ec506dd195 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -311,7 +311,7 @@ def _transform_dbscan( def transform_dbscan( self, df: pd.DataFrame, - y: Union[pd.DataFrame, None] = None, + y: Optional[pd.DataFrame] = None, eps: Union[float, str] = "auto", fit_umap_embedding: bool = False, sample: Optional[int] = None, From a137c356d5085bbead31a8aa7ec1668ec5976996 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 14:57:54 -0800 Subject: [PATCH 069/432] typing --- graphistry/feature_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index f3ff1e634d..589df0f432 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1834,7 +1834,7 @@ def get_matrix_by_column_part(X: pd.DataFrame, column_part: str) -> pd.DataFrame transformed_columns = X.columns[X.columns.map(lambda x: True if column_part in x else False)] # type: ignore return X[transformed_columns] -def get_matrix_by_column_parts(X: pd.DataFrame, column_parts: Union[list, str]) -> pd.DataFrame: +def get_matrix_by_column_parts(X: pd.DataFrame, column_parts: Optional[Union[list, str]]) -> pd.DataFrame: """Get the feature matrix by column parts list existing in column names.""" if column_parts is None: return X @@ -2699,7 +2699,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ) - def get_features_by_cols(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False): + def get_features_by_cols(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False) -> pd.DataFrame: """Returns feature matrix with only the columns that contain the string `column_part` in their name. `X = g.get_features_by_cols(['feature1', 'feature2'])` From dd4be1c77aa24410f2be81d3f6850252290dd8a6 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 18:09:09 -0800 Subject: [PATCH 070/432] adds test for g.get_features_by_cols, passing --- graphistry/tests/test_feature_utils.py | 50 +++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index f3738b3707..e0264c2e6b 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -19,6 +19,9 @@ FastEncoder ) +from graphistry.features import topic_model, ngrams_model + +np.random.seed(137) has_min_dependancy, _ = lazy_import_has_min_dependancy() has_min_dependancy_text, _, _ = lazy_import_has_dependancy_text() @@ -127,7 +130,7 @@ target_names_node = [['label'], ['label', 'type']] # test also sending in a dataframe for target double_target_reddit = pd.DataFrame( - {"label": ndf_reddit.label.values, "type": ndf_reddit["type"].values} + {"label": ndf_reddit.label.values, "type": ndf_reddit["type"].values}, index=ndf_reddit.index ) single_target_reddit = pd.DataFrame({"label": ndf_reddit.label.values}) @@ -136,6 +139,11 @@ edge_df2['dst'] = np.random.random_integers(0, 120, size=len(edge_df2)) edge2_target_df = pd.DataFrame({'label': edge_df2.label}) +## ################################################ +what = ['whatever', 'on what', 'what do', 'what do you', 'what do you think', 'to what', 'but what', 'what is', 'what it', 'what kind', 'what kind of', 'of what', 'know what', 'what are', 'what are the', 'what to', 'what to do', 'from what', 'with what', 'and what', 'what you', 'whats', 'know what to', 'don know what', 'what the'] +freedom = ['title: dyslexics, experience, language', + 'label: languagelearning, agile, leaves', + 'title: freedom, finally, moved'] # ################################################ # data to test textual and numeric DataFrame # ndf_stocks, price_df_stocks = get_stocks_dataframe() @@ -162,6 +170,46 @@ def check_allclose_fit_transform_on_same_data(X, x, Y=None, y=None): allclose_stats(Y, y, value, name) +class TestFeaturizeGetMethods(unittest.TestCase): + + @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") + def setUp(self) -> None: + g = graphistry.nodes(ndf_reddit) + g2 = g.featurize( # ngrams + y=double_target_reddit, + use_ngrams=True, + ngram_range=(1, 4) + ) + + g3 = g.featurize( # topic model + **topic_model + ) + self.g = g + self.g2 = g2 + self.g3 = g3 + + @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") + def test_get_col_matrix(self): + # no edges so this should be None + assert self.g2.get_features_by_cols(kind='edges') == None + + # test target methods + assert all(self.g2.get_features_by_cols(target=True).columns == self.g2._node_target.columns) + assert self.g2.get_features_by_cols('Anxiety', target=True).shape[0] == len(self.g2._node_target) + # test str vs list + assert (self.g2.get_features_by_cols('Anxiety', target=True) == self.g2.get_features_by_cols(['Anxiety'], target=True)).all().values[0] + + assert list(self.g2.get_features_by_cols(['Anxiety', 'education', 'computer'], target=True).columns) == ['label_Anxiety', 'label_education', 'label_computervision'] + + # test feature methods + # ngrams + assert (self.g2.get_features_by_cols().columns == self.g2._node_features.columns).all() + assert list(self.g2.get_features_by_cols('what').columns) == what, list(self.g2.get_features_by_cols('what').columns) + + # topic + assert all(self.g3.get_features_by_cols().columns == self.g3._node_features.columns) + assert list(self.g3.get_features_by_cols(['language', 'freedom']).columns) == freedom, self.g3.get_features_by_cols(['language', 'freedom']).columns + class TestFastEncoder(unittest.TestCase): # we test how far off the fit returned values different from the transformed From 0b63fa833b5e86a8f3760e4c8cf68fc979bae25a Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 22:20:22 -0800 Subject: [PATCH 071/432] feat(refactors featurization logic - moves scaling outside of previous pipeline and exposes `g._{}_encoder.transform_scaled` method. Adds working transform tests for featurize and umap --- graphistry/ai_utils.py | 19 ++- graphistry/compute/cluster.py | 35 ++++- graphistry/feature_utils.py | 135 ++++++++++------- graphistry/tests/test_umap_utils.py | 226 ++++++++++------------------ graphistry/umap_utils.py | 28 ++-- 5 files changed, 210 insertions(+), 233 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 267aefa13a..a3c628dc6a 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -207,7 +207,7 @@ def infer_graph( eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph n_nearest: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. """ - + print('*Infering graph from existing graphistry object') if verbose else None # new_index = df.index if infer_on_umap_embedding and emb is not None: X_previously_fit = res._node_embedding @@ -219,8 +219,10 @@ def infer_graph( print("Infering edges over features") if verbose else None FEATS = res._node_features - EMB = res._node_embedding - Y = res._node_target + if FEATS is None: + raise ValueError("Must have node features to infer edges") + EMB = res._node_embedding if res._node_embedding is not None else FEATS.index + Y = res._node_target if res._node_target is not None else FEATS.index assert ( df.shape[0] == X.shape[0] @@ -290,19 +292,22 @@ def infer_graph( .append(old_edges[dst]) .append(new_edges[src]) .append(new_edges[dst]) - ) + ).drop_duplicates() + print(len(all_nodes), "nodes in new graph") if verbose else None if sample: - new_edges = pd.concat([new_edges, old_edges], axis=0) - # print('sampled', len(new_edges), 'new edges') + new_edges = pd.concat([new_edges, old_edges], axis=0).drop_duplicates() + print('sampled', len(old_edges.drop_duplicates()), 'previous old edges') if verbose else None new_edges = new_edges.drop_duplicates() - # print(len(new_edges), 'new edges after dropping duplicates') + print(len(new_edges), 'total edges pairs after dropping duplicates') if verbose else None if len(old_nodes): old_nodes = pd.DataFrame(old_nodes) old_nodes = pd.concat( [old_nodes, NDF[NDF[node].isin(all_nodes)]], axis=0 ).drop_duplicates(subset=[node]) + else: + old_nodes = NDF[NDF[node].isin(all_nodes)] old_emb = None if EMB is not None: diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index ec506dd195..86851f1ad9 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -67,16 +67,27 @@ def resolve_cpu_gpu_engine( ) -def get_model_matrix(g, kind, cols, umap): +def get_model_matrix(g, kind, cols, umap, target): + """ + Allows for a single function to get the model matrix for both nodes and edges as well as targets, embeddings, and features + + Args: + g (_type_): _description_ + kind (_type_): _description_ + cols (_type_): _description_ + umap (_type_): _description_ + target (_type_): _description_ + + Returns: + _type_: dataframe of model matrix given the inputs + """ assert kind in ["nodes", "edges"] assert ( hasattr(g, "_node_encoder") if kind == "nodes" else hasattr(g, "_edge_encoder") ) - if cols is None: - df = g._get_feature(kind) - else: - df = g.get_features_by_cols(cols, kind) + + df = g.get_features_by_cols(cols, kind=kind, target=target) if umap and cols is None and g._umap is not None: df = g._get_embedding(kind) @@ -84,9 +95,10 @@ def get_model_matrix(g, kind, cols, umap): return df -def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True): +def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, target=False): """ Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe + or target dataframe if target is True. args: g: graphistry graph @@ -94,7 +106,10 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True): cols: list of columns to use for clustering given `g.featurize` has been run umap: whether to use UMAP embeddings or features dataframe """ - df = get_model_matrix(g, kind, cols, use_umap_embedding) + df = get_model_matrix(g, kind, cols, use_umap_embedding, target) + + if df.empty: + raise ValueError("No features found for clustering") dbscan.fit(df) labels = dbscan.labels_ @@ -147,7 +162,7 @@ def __init__(self, *args, **kwargs): pass def _cluster_dbscan( - self, res, kind, cols, fit_umap_embedding, eps, min_samples, **kwargs + self, res, kind, cols, fit_umap_embedding, target, eps, min_samples, *args, **kwargs ): """ DBSCAN clustering on cpu or gpu infered by .engine flag @@ -159,9 +174,11 @@ def _cluster_dbscan( "latest dbscan kwargs", kind=kind, cols=cols, + target=target, umap=fit_umap_embedding, eps=eps, min_samples=min_samples, + *args, **kwargs, ) @@ -184,6 +201,7 @@ def dbscan( cols=None, kind="nodes", fit_umap_embedding=True, + target=False, **kwargs, ): """DBSCAN clustering on cpu or gpu infered automatically @@ -234,6 +252,7 @@ def dbscan( kind=kind, cols=cols, fit_umap_embedding=fit_umap_embedding, + target=target, eps=eps, min_samples=min_samples, **kwargs, diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 589df0f432..1d786b3c57 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1025,6 +1025,8 @@ def process_nodes_dataframes( feature_engine: FeatureEngineConcrete = "pandas" # test_size: Optional[bool] = None, ) -> Tuple[ + pd.DataFrame, + Any, pd.DataFrame, Any, SuperVectorizer, @@ -1155,7 +1157,7 @@ def process_nodes_dataframes( f"--The entire Encoding process took {(time()-t)/60:.2f} minutes" ) - X_enc, y_enc, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa + X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa X_enc, y_enc, use_scaler, @@ -1173,6 +1175,8 @@ def process_nodes_dataframes( return ( X_enc, y_enc, + X_encs, + y_encs, data_encoder, label_encoder, scaling_pipeline, @@ -1298,6 +1302,8 @@ def process_edge_dataframes( keep_n_decimals: int = 5, feature_engine: FeatureEngineConcrete = "pandas", ) -> Tuple[ + pd.DataFrame, + pd.DataFrame, pd.DataFrame, pd.DataFrame, List[Any], @@ -1354,7 +1360,7 @@ def process_edge_dataframes( # add the two datasets together X_enc = pd.concat([T, X_enc], axis=1) # then scale them - X_enc, y_enc, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa + X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa X_enc, y_enc, use_scaler, @@ -1374,6 +1380,8 @@ def process_edge_dataframes( return ( X_enc, y_enc, + X_encs, + y_encs, [mlb_pairwise_edge_encoder, data_encoder], label_encoder, scaling_pipeline, @@ -1385,6 +1393,8 @@ def process_edge_dataframes( ( X_enc, y_enc, + _, + _, data_encoder, label_encoder, _, @@ -1426,7 +1436,7 @@ def process_edge_dataframes( f" {(time()-t)/60:.2f} minutes" ) - X_enc, y_enc, scaling_pipeline, scaling_pipeline_target = smart_scaler( + X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( X_enc, y_enc, use_scaler, @@ -1444,6 +1454,8 @@ def process_edge_dataframes( res = ( X_enc, y_enc, + X_encs, + y_encs, [mlb_pairwise_edge_encoder, data_encoder], label_encoder, scaling_pipeline, @@ -1501,7 +1513,7 @@ def transform_dirty( data_encoder: Union[SuperVectorizer, FunctionTransformer], # type: ignore name: str = "", ) -> pd.DataFrame: - from sklearn.preprocessing import MultiLabelBinarizer + # from sklearn.preprocessing import MultiLabelBinarizer logger.debug(f"-{name} Encoder:") logger.debug(f"\t{data_encoder}\n") # print(f"-{name} Encoder:") @@ -1516,7 +1528,8 @@ def transform_dirty( # ##################################### for dirty_cat 0.3.0 use_columns = getattr(data_encoder, 'columns_', []) if len(use_columns): - X = data_encoder.transform(df[use_columns]) + #print(f"Using columns: {use_columns}") + X = data_encoder.transform(df[df.columns.intersection(use_columns)]) # ##################################### with dirty_cat 0.2.0 else: X = data_encoder.transform(df) @@ -1544,12 +1557,14 @@ def transform( # this function aligns with what is computed during # processing nodes or edges. ( - X_enc, - y_enc, + _, + _, + _, + _, data_encoder, label_encoder, - scaling_pipeline, - scaling_pipeline_target, + _, + _, text_model, text_cols, ) = res @@ -1612,14 +1627,14 @@ def transform( logger.info(f"--Features matrix shape: {X.shape}") logger.info(f"--Target matrix shape: {y.shape}") - if scaling_pipeline and not X.empty: - logger.info("--Scaling Features") - X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns, index=index) - if scaling_pipeline_target and not y.empty: - logger.info(f"--Scaling Target {scaling_pipeline_target}") - y = pd.DataFrame( - scaling_pipeline_target.transform(y), columns=y.columns, index=index - ) + # if scaling_pipeline and not X.empty: + # logger.info("--Scaling Features") + # X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns, index=index) + # if scaling_pipeline_target and not y.empty: + # logger.info(f"--Scaling Target {scaling_pipeline_target}") + # y = pd.DataFrame( + # scaling_pipeline_target.transform(y), columns=y.columns, index=index + # ) return X, y @@ -1675,6 +1690,8 @@ def _set_result(self, res): [ X_enc, y_enc, + X_encs, + y_encs, data_encoder, label_encoder, scaling_pipeline, @@ -1688,8 +1705,10 @@ def _set_result(self, res): # label_encoder.target_names_in = self.target_names_in self.feature_columns = X_enc.columns self.feature_columns_target = y_enc.columns - self.X = X_enc - self.y = y_enc + self.X = X_encs + self.y = y_encs + self.X_orignal = X_enc + self.y_orignal = y_enc self.data_encoder = data_encoder # is list for edges self.label_encoder = label_encoder self.scaling_pipeline = scaling_pipeline @@ -1706,14 +1725,33 @@ def fit(self, src=None, dst=None, *args, **kwargs): self._set_result(res) def transform(self, df, ydf=None): + "Raw transform, no scaling." X, y = transform(df, ydf, self.res, self.kind, self.src, self.dst) return X, y + + def _transform_scaled(self, df, ydf, scaling_pipeline, scaling_pipeline_target): + """Transform with scaling fit durning fit.""" + X, y = transform(df, ydf, self.res, self.kind, self.src, self.dst) + if scaling_pipeline is not None: + print("scaling") + X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns, index=X.index) + if scaling_pipeline_target is not None: + print("scaling target") + y = pd.DataFrame(scaling_pipeline_target.transform(y), columns=y.columns, index=y.index) + return X, y + + def transform_scaled(self, df, ydf=None, scaling_pipeline=None, scaling_pipeline_target=None): + if scaling_pipeline is None: + scaling_pipeline = self.scaling_pipeline + if scaling_pipeline_target is None: + scaling_pipeline_target = self.scaling_pipeline_target + return self._transform_scaled(df, ydf, scaling_pipeline, scaling_pipeline_target) def fit_transform(self, src=None, dst=None, *args, **kwargs): self.fit(src=src, dst=dst, *args, **kwargs) return self.X, self.y - def scale(self, X=None, y=None, set_scaler=False, *args, **kwargs): + def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): """Fits new scaling functions on df, y via args-kwargs example: @@ -1726,34 +1764,16 @@ def scale(self, X=None, y=None, set_scaler=False, *args, **kwargs): X: pd.DataFrame of features y: pd.DataFrame of target features kind: str, one of 'nodes' or 'edges' - set_scaler: bool, if True, will set the new scaler as the default for the encoder *args, **kwargs: passed to smart_scaler returns: scaled X, y """ - # pop off the previous scaler so that .transform won't use it - self.res[4] = None - self.res[5] = None - logger.info("-Fitting new scaler on raw features") X, y, scaling_pipeline, scaling_pipeline_target = smart_scaler( X_enc=X, y_enc=y, *args, **kwargs ) - - def _set(res, scaling_pipeline, scaling_pipeline_target): - logger.info("--Setting fit scaler to self") - res.res[4] = scaling_pipeline - res.res[5] = scaling_pipeline_target - res.scaling_pipeline = scaling_pipeline - res.scaling_pipeline_target = scaling_pipeline_target - return res - - if set_scaler: - self = _set(self, scaling_pipeline, scaling_pipeline_target) - else: # add the original back - self.res[4] = self.scaling_pipeline - self.res[5] = self.scaling_pipeline_target - + if return_pipeline: + return X, y, scaling_pipeline, scaling_pipeline_target return X, y @@ -2010,9 +2030,9 @@ def _featurize_nodes( # if changing, also update fresh_res res._node_features = encoder.X - res._node_features_raw = encoder.X # .copy() + res._node_features_raw = encoder.X_orignal # .copy() res._node_target = encoder.y - res._node_target_raw = encoder.y # .copy() + res._node_target_raw = encoder.y_orignal # .copy() res._node_encoder = encoder # now this does # all the work `._node_encoder.transform(df, y)` etc @@ -2142,8 +2162,10 @@ def _infer_edges(self, emb, X, y, df, eps='auto', sample=None, infer_on_umap_emb g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, eps=eps, sample=sample, verbose=verbose, **kwargs) return g - def _transform(self, encoder: str, df: pd.DataFrame, ydf: Optional[pd.DataFrame]): + def _transform(self, encoder: str, df: pd.DataFrame, ydf: Optional[pd.DataFrame], scaled): if getattr(self, encoder) is not None: + if scaled: + return getattr(self, encoder).transform_scaled(df, ydf) return getattr(self, encoder).transform(df, ydf) else: logger.debug( @@ -2152,12 +2174,15 @@ def _transform(self, encoder: str, df: pd.DataFrame, ydf: Optional[pd.DataFrame] ) def transform(self, df: pd.DataFrame, - y: Union[pd.DataFrame, None] = None, + y: Optional[pd.DataFrame] = None, kind: str = 'nodes', - return_graph: bool = True, - eps: Union[str, float, int] = 'auto', sample = None, - verbose = False): - """Transform new data and append to existing graph. + eps: Union[str, float, int] = 'auto', + sample: Optional[int] = None, + return_graph: bool = True, + scaled: bool = True, + verbose: bool = False): + """ + Transform new data and append to existing graph, or return dataframes args: df: pd.DataFrame, raw data to transform @@ -2172,17 +2197,17 @@ def transform(self, df: pd.DataFrame, or a graph with inferred edges if return_graph is True """ if kind == "nodes": - X, y = self._transform("_node_encoder", df, y) + X, y_ = self._transform("_node_encoder", df, y, scaled=scaled) elif kind == "edges": - X, y = self._transform("_edge_encoder", df, y) + X, y_ = self._transform("_edge_encoder", df, y, scaled=scaled) else: logger.debug("kind must be one of `nodes`," f"`edges`, found {kind}") if return_graph: emb = None # will not be able to decide umap coordinates, but will be able to infer graph from existing edges - g = self._infer_edges(emb, X, y, df, infer_on_umap_embedding=False, eps=eps, sample=sample, verbose=verbose) + g = self._infer_edges(emb, X, y_, df, infer_on_umap_embedding=False, eps=eps, sample=sample, verbose=verbose) return g - return X, y + return X, y_ def scale( self, @@ -2198,7 +2223,6 @@ def scale( n_bins: int = 2, encode: str = "ordinal", strategy: str = "uniform", - set_scaler: bool = False, keep_n_decimals: int = 5, ): """Scale data using the same scalers as used in the featurization step. @@ -2225,7 +2249,6 @@ def scale( n_bins: int, number of bins to use for KBinsDiscretizer encode: str, one of `ordinal`, `onehot`, `onehot-dense`, `binary` strategy: str, one of `uniform`, `quantile`, `kmeans` - set_scaler: bool, if True, will set the scaler to the new scaler keep_n_decimals: int, number of decimals to keep after scaling returns: (X, y) transformed data if return_graph is False @@ -2236,7 +2259,7 @@ def scale( # df = self._nodes if kind == "nodes" else self._edges X, y = (self._node_features_raw, self._node_target_raw) if kind == "nodes" else (self._edge_features_raw, self._edge_target_raw) else: - X, y = self.transform(df, y, kind=kind, return_graph=False) + X, y = self.transform(df, y, kind=kind, return_graph=False, scaled=False) if kind == "nodes" and hasattr(self, "_node_encoder"): # type: ignore if self._node_encoder is not None: # type: ignore @@ -2246,7 +2269,6 @@ def scale( ) = self._node_encoder.scale( X, y, - set_scaler=set_scaler, use_scaler=use_scaler, use_scaler_target=use_scaler_target, impute=impute, @@ -2273,7 +2295,6 @@ def scale( ) = self._edge_encoder.scale( X, y, - set_scaler=set_scaler, use_scaler=use_scaler, use_scaler_target=use_scaler_target, impute=impute, diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 5f218d4889..54fdab6e4c 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -70,20 +70,24 @@ class TestUMAPFitTransform(unittest.TestCase): # check to see that .fit and transform gives similar embeddings on same data @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def setUp(self): - + verbose = True g = graphistry.nodes(ndf_reddit) self.gn = g + + self.test = ndf_reddit.sample(5) + with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) g2 = g.umap( - y=double_target_reddit, + y=['label', 'type'], use_ngrams=True, ngram_range=(1, 2), use_scaler="robust", cardinality_threshold=2, + verbose=verbose, ) self.g2 = g2 @@ -91,10 +95,10 @@ def setUp(self): self.X, self.Y = fenc.X, fenc.y self.EMB = g2._node_embedding self.emb, self.x, self.y = g2.transform_umap( - ndf_reddit, ydf=double_target_reddit, kind="nodes", return_graph=False + ndf_reddit, ndf_reddit, kind="nodes", return_graph=False, verbose=verbose ) self.g3 = g2.transform_umap( - ndf_reddit, ydf=double_target_reddit, kind="nodes", return_graph=True + ndf_reddit, ndf_reddit, kind="nodes", return_graph=True, verbose=verbose ) # do the same for edges @@ -107,7 +111,7 @@ def setUp(self): warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) g2 = g.umap( - y=edge2_target_df, + y=['label'], kind="edges", use_ngrams=True, ngram_range=(1, 2), @@ -115,148 +119,71 @@ def setUp(self): use_scaler_target=None, cardinality_threshold=2, n_topics=4, + verbose=verbose, ) fenc = g2._edge_encoder self.Xe, self.Ye = fenc.X, fenc.y self.EMBe = g2._edge_embedding self.embe, self.xe, self.ye = g2.transform_umap( - edge_df22, ydf=edge2_target_df, kind="edges", return_graph=False - ) + edge_df22, y=edge2_target_df, kind="edges", return_graph=False, verbose=verbose + ) self.g2e = g2 - self.g3e = g2.transform_umap( - edge_df22, ydf=edge2_target_df, kind="edges", return_graph=True - ) + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_columns_match(self): - assert all( - self.X.columns == self.x.columns - ), "Node Feature Columns do not match" - assert all(self.Y.columns == self.y.columns), "Node Target Columns do not match" - assert all( - self.Xe.columns == self.xe.columns - ), "Edge Feature Columns do not match" - assert all( - self.Ye.columns == self.ye.columns - ), "Edge Target Columns do not match" + d = self.g2._node_features.shape[1] + dt = self.g2._node_target.shape[1] + de = self.g2e._edge_features.shape[1] + det = self.g2e._edge_target.shape[1] + assert (self.X.columns == self.x.columns).sum() == d, "Node Feature Columns do not match" + assert (self.Y.columns == self.y.columns).sum() == dt, "Node Target Columns do not match" + assert (self.Xe.columns == self.xe.columns).sum() == de, "Edge Feature Columns do not match" + assert (self.Ye.columns == self.ye.columns).sum() == det, "Edge Target Columns do not match" @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_index_match(self): # nodes - assert all( - self.gn._nodes.index == self.g2._nodes.index - ), "Node Indexes do not match" - assert all(self.gn._nodes.index == self.EMB.index), "Emb Indexes do not match" - assert all( - self.gn._nodes.index == self.emb.index - ), "Transformed Emb Indexes do not match" - assert all( - self.gn._nodes.index == self.X.index - ), "Transformed Node features Indexes do not match" - assert all( - self.gn._nodes.index == self.y.index - ), "Transformed Node target Indexes do not match" + d = self.g2._nodes.shape[0] + de = self.g2e._edges.shape[0] + assert (self.gn._nodes.index == self.g2._nodes.index).sum() == d, "Node Indexes do not match" + assert (self.gn._nodes.index == self.EMB.index).sum() == d, "Emb Indexes do not match" + assert (self.gn._nodes.index == self.emb.index).sum() == d, "Transformed Emb Indexes do not match" + assert (self.gn._nodes.index == self.X.index).sum() == d, "Transformed Node features Indexes do not match" + assert (self.gn._nodes.index == self.y.index).sum() == d, "Transformed Node target Indexes do not match" # edges - assert all( - self.ge._edges.index == self.g2e._edges.index - ), "Edge Indexes do not match" - assert all( - self.ge._edges.index == self.EMBe.index - ), "Edge Emb Indexes do not match" - assert all( - self.ge._edges.index == self.embe.index - ), "Edge Transformed Emb Indexes do not match" - assert all( - self.ge._edges.index == self.Xe.index - ), "Edge Transformed features Indexes do not match" - assert all( - self.ge._edges.index == self.ye.index - ), "Edge Transformed target Indexes do not match" + assert (self.ge._edges.index == self.g2e._edges.index).sum() == de, "Edge Indexes do not match" + assert (self.ge._edges.index == self.EMBe.index).sum() == de, "Edge Emb Indexes do not match" + assert (self.ge._edges.index == self.embe.index).sum() == de, "Edge Transformed Emb Indexes do not match" + assert (self.ge._edges.index == self.Xe.index).sum() == de, "Edge Transformed features Indexes do not match" + assert (self.ge._edges.index == self.ye.index).sum() == de, "Edge Transformed target Indexes do not match" # make sure the indexes match at transform time internally as well - assert all(self.X.index == self.x.index), "Node Feature Indexes do not match" - assert all(self.Y.index == self.y.index), "Node Target Indexes do not match" - assert all(self.Xe.index == self.xe.index), "Edge Feature Indexes do not match" - assert all(self.Ye.index == self.ye.index), "Edge Target Indexes do not match" + assert (self.X.index == self.x.index).sum() == d, "Node Feature Indexes do not match" + assert (self.Y.index == self.y.index).sum() == d, "Node Target Indexes do not match" + assert (self.Xe.index == self.xe.index).sum() == de, "Edge Feature Indexes do not match" + assert (self.Ye.index == self.ye.index).sum() == de, "Edge Target Indexes do not match" @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") - def test_index_match_in_infered_graph(self): + def test_node_index_match_in_infered_graph(self): # nodes - assert all( - self.g3._nodes.index == self.g2._nodes.index - ), "Node Indexes do not match" - assert all(self.g3._nodes.index == self.EMB.index), "Emb Indexes do not match" - assert all( - self.g3._nodes.index == self.emb.index - ), "Transformed Emb Indexes do not match" - assert all( - self.g3._nodes.index == self.X.index - ), "Transformed Node features Indexes do not match" - assert all( - self.g3._nodes.index == self.y.index - ), "Transformed Node target Indexes do not match" + g3 = self.g2._nodes + assert (g3.index == self.EMB.index).sum() == len(g3), "Node Emb Indexes do not match" + assert (g3.index == self.emb.index).sum() == len(g3), "Node Transformed Emb Indexes do not match" + assert (g3.index == self.X.index).sum() == len(g3), "Node Transformed features Indexes do not match" + assert (g3.index == self.y.index).sum() == len(g3), "Node Transformed target Indexes do not match" @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") - def test_nodes_index_match_in_infered_graph(self): - # edges - ndf_infered = self.g3._nodes - assert all( - ndf_infered.index == self.EMBe.index - ), "Edge Emb Indexes do not match" - assert all( - ndf_infered.index == self.embe.index - ), "Edge Transformed Emb Indexes do not match" - assert all( - ndf_infered.index == self.Xe.index - ), "Edge Transformed features Indexes do not match" - assert all( - ndf_infered.index == self.ye.index - ), "Edge Transformed target Indexes do not match" - - # now test in set featurize method calls - assert all( - self.g3._node_features.index == ndf_infered.index - ), "Edge Feature Indexes do not match" - assert all( - self.g3._node_embedding.index == ndf_infered.index - ), "Edge Emb Indexes do not match" - assert all( - self.g3._node_target.index == ndf_infered.index - ), "Edge Transformed Emb Indexes do not match" - # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed features Indexes do not match' - # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed target Indexes do not match' - - @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") - def test_edges_index_match_in_infered_graph(self): - # edges - edf_infered = self.g3e._edges - assert all( - edf_infered.index == self.EMBe.index - ), "Edge Emb Indexes do not match" - assert all( - edf_infered.index == self.embe.index - ), "Edge Transformed Emb Indexes do not match" - assert all( - edf_infered.index == self.Xe.index - ), "Edge Transformed features Indexes do not match" - assert all( - edf_infered.index == self.ye.index - ), "Edge Transformed target Indexes do not match" - - assert all( - self.g3e._edge_features.index == edf_infered.index - ), "Edge Feature Indexes do not match" - assert all( - self.g3e._edge_embedding.index == edf_infered.index - ), "Edge Emb Indexes do not match" - assert all( - self.g3e._edge_target.index == edf_infered.index - ), "Edge Transformed Emb Indexes do not match" - # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed features Indexes do not match' - # assert all(self.g3e._edges.index == edf_infered.index), 'Edge Transformed target Indexes do not match' - + def test_edge_index_match_in_infered_graph(self): + g3 = self.g2e._edges + assert (g3.index == self.EMBe.index).sum() == len(g3), "Edge Emb Indexes do not match" + assert (g3.index == self.embe.index).sum() == len(g3), "Edge Transformed Emb Indexes do not match" + assert (g3.index == self.Xe.index).sum() == len(g3), "Edge Transformed Node features Indexes do not match" + assert (g3.index == self.ye.index).sum() == len(g3), "Edge Transformed Node target Indexes do not match" + + @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_umap_kwargs(self): umap_kwargs = { @@ -270,21 +197,32 @@ def test_umap_kwargs(self): "negative_sample_rate": 5, } - umap_kwargs2 = {k: v + 1 for k, v in umap_kwargs.items()} # type: ignore - g = graphistry.nodes(ndf_reddit) - g2 = g.umap(**umap_kwargs) - g3 = g.umap(**umap_kwargs2) + umap_kwargs2 = {k: v + 1 for k, v in umap_kwargs.items() if k not in ['metric']} # type: ignore + umap_kwargs2['metric'] = 'euclidean' + g = graphistry.nodes(self.test) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + g2 = g.umap(**umap_kwargs) + g3 = g.umap(**umap_kwargs2) assert ( g2._umap_params == umap_kwargs ), f"Umap params do not match, found {g2._umap_params} vs {umap_kwargs}" + assert len(g2._node_embedding.columns) == 2, f"Umap params do not match, found {len(g2._node_embedding.columns)} vs 2" + assert ( g3._umap_params == umap_kwargs2 ), f"Umap params do not match, found {g3._umap_params} vs {umap_kwargs2}" - g4 = g2.transform_umap(ndf_reddit) + assert len(g3._node_embedding.columns) == 3, f"Umap params do not match, found {len(g3._node_embedding.columns)} vs 3" + + g4 = g2.transform_umap(self.test) assert ( g4._umap_params == umap_kwargs ), f"Umap params do not match, found {g4._umap_params} vs {umap_kwargs}" - g5 = g3.transform_umap(ndf_reddit) + assert g4._n_components == 2, f"Umap params do not match, found {g2._n_components} vs 2" + + g5 = g3.transform_umap(self.test) assert ( g5._umap_params == umap_kwargs2 ), f"Umap params do not match, found {g5._umap_params} vs {umap_kwargs2}" @@ -292,18 +230,11 @@ def test_umap_kwargs(self): @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_transform_umap(self): np.random.seed(41) - - train = ndf_reddit.sample(frac=0.8, random_state=42) - test = ndf_reddit.drop(train.index) - - # just process train - g = graphistry.nodes(train) - g2 = g.umap() - g3 = g2.transform_umap(train) - assert ( - 2 * g2._node_embedding.shape[0] == g3._node_embedding.shape[0] + test = self.test + assert ( + 2*self.g2._node_embedding.shape[0] == self.g3._node_embedding.shape[0] ), "Node Embedding Lengths do not match, found {} and {}".format( - g2._node_embedding.shape[0], g3._node_embedding.shape[0] + self.g2._node_embedding.shape[0], self.g3._node_embedding.shape[0] ) # now feed it args eps = ["auto", 10] @@ -311,13 +242,12 @@ def test_transform_umap(self): return_graph = [True, False] fit_umap_embedding = [True, False] for ep in eps: - g4 = g2.transform_umap(test, eps=ep) + g4 = self.g2.transform_umap(test, test, eps=ep) assert True for return_g in return_graph: - g4 = g2.transform_umap(test, return_graph=return_g) + g4 = self.g2.transform_umap(test, test, return_graph=return_g) if return_g: - assert isinstance(g4, Plottable) - # == g4._node_embedding.shape + assert True else: assert len(g4) == 3 assert isinstance(g4[0], pd.DataFrame) @@ -325,12 +255,12 @@ def test_transform_umap(self): assert isinstance(g4[2], pd.DataFrame) assert g4[0].shape[1] == 2 assert g4[1].shape[1] >= 2 - assert g4[2].shape[1] >= 1 + assert g4[2].shape[0] == test.shape[0] for sample_ in sample: - g4 = g2.transform_umap(test, sample=sample_) + g4 = self.g2.transform_umap(test, sample=sample_) assert True for fit_umap_embedding_ in fit_umap_embedding: - g4 = g2.transform_umap(test, fit_umap_embedding=fit_umap_embedding_) + g4 = self.g2.transform_umap(test, fit_umap_embedding=fit_umap_embedding_) assert True diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 661ac8e5a0..c9a52ceb4f 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -181,6 +181,7 @@ def umap_lazy_init( metric: str = "euclidean", engine: UMAPEngine = "auto", suffix: str = "", + verbose: bool = False, ): engine_resolved = resolve_umap_engine(engine) # FIXME remove as set_new_kwargs will always replace? @@ -207,7 +208,7 @@ def umap_lazy_init( "negative_sample_rate": negative_sample_rate, } ) - print('umap_kwargs init: ', umap_kwargs['n_components']) + #print('umap_kwargs init n_components: ', umap_kwargs['n_components']) if verbose else None #print('umap_kwargs', umap_kwargs) res._n_components = n_components @@ -288,25 +289,26 @@ def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = No emb = self._bundle_embedding(emb, index=X.index) return emb - def transform_umap( # noqa: E303 - self, df: pd.DataFrame, ydf: Union[pd.DataFrame, None] = None, kind: str = "nodes", - eps='auto', - sample=None, - return_graph=True, - fit_umap_embedding=False, - verbose=False + def transform_umap(self, df: pd.DataFrame, + y: Optional[pd.DataFrame] = None, + kind: str = 'nodes', + eps: Union[str, float, int] = 'auto', + sample: Optional[int] = None, + return_graph: bool = True, + fit_umap_embedding: bool = False, + verbose=False ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: try: logger.debug(f"Going into Transform umap {df.shape}") except: pass - X, y = self.transform(df, ydf, kind=kind, return_graph=False, verbose=verbose) + X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) if return_graph: - g = self._infer_edges(emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample) + g = self._infer_edges(emb, X, y_, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample) return g - return emb, X, y + return emb, X, y_ def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 @@ -360,7 +362,7 @@ def _process_umap( print('** Fitting UMAP') if verbose else None res._umap_initialized = False - res = res.umap_lazy_init(res, **umap_kwargs_pure) + res = res.umap_lazy_init(res, verbose=verbose, **umap_kwargs_pure) emb = res._umap_fit_transform(X_, y_) res._xy = emb @@ -498,7 +500,7 @@ def umap( else: res = self.bind() - res = res.umap_lazy_init(res, **umap_kwargs) # type: ignore + res = res.umap_lazy_init(res, verbose=verbose, **umap_kwargs) # type: ignore logger.debug("umap input X :: %s", X) logger.debug("umap input y :: %s", y) From e53ee8fd0ef75b76a8c3654074e0feaf2d573000 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 22:28:07 -0800 Subject: [PATCH 072/432] lint --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 1d786b3c57..97b78b13c7 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1571,7 +1571,7 @@ def transform( logger.info("-" * 90) - index = df.index + # index = df.index y = pd.DataFrame([]) T = pd.DataFrame([]) # encode nodes From 8548f3f2315416f52ed57ad6d45caacc307da594 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 22:34:46 -0800 Subject: [PATCH 073/432] lint --- graphistry/tests/test_feature_utils.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index e0264c2e6b..eb481ceefe 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -139,8 +139,11 @@ edge_df2['dst'] = np.random.random_integers(0, 120, size=len(edge_df2)) edge2_target_df = pd.DataFrame({'label': edge_df2.label}) -## ################################################ -what = ['whatever', 'on what', 'what do', 'what do you', 'what do you think', 'to what', 'but what', 'what is', 'what it', 'what kind', 'what kind of', 'of what', 'know what', 'what are', 'what are the', 'what to', 'what to do', 'from what', 'with what', 'and what', 'what you', 'whats', 'know what to', 'don know what', 'what the'] +# ############################################################################################################# +what = ['whatever', 'on what', 'what do', 'what do you', 'what do you think', \ + 'to what', 'but what', 'what is', 'what it', 'what kind', 'what kind of', \ + 'of what', 'know what', 'what are', 'what are the', 'what to', 'what to do', \ + 'from what', 'with what', 'and what', 'what you', 'whats', 'know what to', 'don know what', 'what the'] freedom = ['title: dyslexics, experience, language', 'label: languagelearning, agile, leaves', 'title: freedom, finally, moved'] @@ -175,14 +178,12 @@ class TestFeaturizeGetMethods(unittest.TestCase): @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") def setUp(self) -> None: g = graphistry.nodes(ndf_reddit) - g2 = g.featurize( # ngrams - y=double_target_reddit, + g2 = g.featurize(y=double_target_reddit, # ngrams use_ngrams=True, ngram_range=(1, 4) ) - g3 = g.featurize( # topic model - **topic_model + g3 = g.featurize(**topic_model # topic model ) self.g = g self.g2 = g2 @@ -191,7 +192,7 @@ def setUp(self) -> None: @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") def test_get_col_matrix(self): # no edges so this should be None - assert self.g2.get_features_by_cols(kind='edges') == None + assert self.g2.get_features_by_cols(kind='edges') is None # test target methods assert all(self.g2.get_features_by_cols(target=True).columns == self.g2._node_target.columns) From 0e3799692a1878fc280dcb4c19b22dc9351aacdd Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 23:09:26 -0800 Subject: [PATCH 074/432] lint --- graphistry/tests/test_feature_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index eb481ceefe..d8c1db38aa 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -140,9 +140,9 @@ edge2_target_df = pd.DataFrame({'label': edge_df2.label}) # ############################################################################################################# -what = ['whatever', 'on what', 'what do', 'what do you', 'what do you think', \ - 'to what', 'but what', 'what is', 'what it', 'what kind', 'what kind of', \ - 'of what', 'know what', 'what are', 'what are the', 'what to', 'what to do', \ +what = ['whatever', 'on what', 'what do', 'what do you', 'what do you think', + 'to what', 'but what', 'what is', 'what it', 'what kind', 'what kind of', + 'of what', 'know what', 'what are', 'what are the', 'what to', 'what to do', 'from what', 'with what', 'and what', 'what you', 'whats', 'know what to', 'don know what', 'what the'] freedom = ['title: dyslexics, experience, language', 'label: languagelearning, agile, leaves', From 2242e01ac2e7ac41682ee26bb0c401620fdfc3a4 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 23:11:26 -0800 Subject: [PATCH 075/432] lint --- graphistry/tests/test_umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 54fdab6e4c..26ad84596f 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -232,7 +232,7 @@ def test_transform_umap(self): np.random.seed(41) test = self.test assert ( - 2*self.g2._node_embedding.shape[0] == self.g3._node_embedding.shape[0] + 2 * self.g2._node_embedding.shape[0] == self.g3._node_embedding.shape[0] ), "Node Embedding Lengths do not match, found {} and {}".format( self.g2._node_embedding.shape[0], self.g3._node_embedding.shape[0] ) From ee9509fd6256fe4dcc135e1bfdfcf99e47870633 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Jan 2023 23:17:22 -0800 Subject: [PATCH 076/432] adds return_graph=False flag to search api --- graphistry/feature_utils.py | 4 +++- graphistry/text_utils.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 97b78b13c7..7c1037ad21 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1070,7 +1070,7 @@ def process_nodes_dataframes( X_enc, y_enc, data_encoder, label_encoder = get_numeric_transformers( df, y ) - X_enc, y_enc, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa + X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa X_enc, y_enc, use_scaler, @@ -1092,6 +1092,8 @@ def process_nodes_dataframes( return ( X_enc, y_enc, + X_encs, + y_encs, data_encoder, label_encoder, scaling_pipeline, diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 803ce345dd..40c72f939f 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -52,7 +52,7 @@ def build_index(self, angular=False, n_trees=None): def _query_from_dataframe(self, qdf: pd.DataFrame, top_n: int, thresh: float): # Use the loaded featurizers to transform the dataframe - vect, _ = self.transform(qdf, None, kind="nodes") + vect, _ = self.transform(qdf, None, kind="nodes", return_graph=False) results = query_by_vector(vect, self._nodes, self.search_index, top_n) results = results.query(f"{DISTANCE} < {thresh}") @@ -213,7 +213,9 @@ def search_graph( res = self.bind() edf = edges = res._edges + #print('shape of edges', edf.shape) rdf = df = res._nodes + #print('shape of nodes', rdf.shape) node = res._node indices = rdf[node] src = res._source @@ -262,7 +264,6 @@ def search_graph( def save_search_instance(self, savepath): from joblib import dump # type: ignore # need to make this onnx or similar - self.build_index() search = self.search_index del self.search_index # can't pickle Annoy From cb2ed06dd5b8027de46c79e4bfed1e0815f166f4 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 8 Jan 2023 00:12:16 -0800 Subject: [PATCH 077/432] test(bug in cuml deps, should be and, not or) --- graphistry/tests/test_umap_utils.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 26ad84596f..2baac9a9d4 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -583,12 +583,12 @@ def test_filter_edges(self): @pytest.mark.skipif( - not has_dependancy or not has_cuml, + not has_dependancy and not has_cuml, reason="requires cuml feature dependencies", ) class TestCUMLMethods(TestUMAPMethods): @pytest.mark.skipif( - not has_dependancy or not has_cuml, + not has_dependancy and not has_cuml, reason="requires cuml feature dependencies", ) def _test_umap(self, g, use_cols, targets, name, kind, df): @@ -627,7 +627,7 @@ def _test_umap(self, g, use_cols, targets, name, kind, df): self.cases_test_graph(g2, kind=kind, df=df) @pytest.mark.skipif( - not has_dependancy or not has_cuml, + not has_dependancy and not has_cuml, reason="requires cuml feature dependencies", ) def test_node_umap(self): @@ -650,7 +650,7 @@ def test_node_umap(self): ) @pytest.mark.skipif( - not has_dependancy or not has_cuml, + not has_dependancy and not has_cuml, reason="requires cuml feature dependencies", ) def test_edge_umap(self): @@ -672,7 +672,7 @@ def test_edge_umap(self): ) @pytest.mark.skipif( - not has_dependancy or not has_cuml, + not has_dependancy and not has_cuml, reason="requires cuml feature dependencies", ) def test_chaining_nodes(self): @@ -695,7 +695,7 @@ def test_chaining_nodes(self): assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce @pytest.mark.skipif( - not has_dependancy or not has_cuml, + not has_dependancy and not has_cuml, reason="requires cuml feature dependencies", ) def test_chaining_edges(self): @@ -714,7 +714,7 @@ def test_chaining_edges(self): assert all(g2._edge_features == g3._edge_features) @pytest.mark.skipif( - not has_dependancy or not has_cuml, + not has_dependancy and not has_cuml, reason="requires cuml feature dependencies", ) def test_feature_kwargs_yield_different_values_using_umap_api(self): @@ -748,8 +748,8 @@ def test_feature_kwargs_yield_different_values_using_umap_api(self): assert g2._node_target.shape[1] == n_topics_target, "Targets " @pytest.mark.skipif( - not has_dependancy or not has_umap, - reason="requires ai+umap feature dependencies", + not has_dependancy and not has_umap, + reason="requires cuml feature dependencies", ) def test_filter_edges(self): for kind, g in [("nodes", graphistry.nodes(ndf_reddit))]: From ee8dcc6947cbf6d838a970e3ede222f2fd256a5e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 9 Jan 2023 18:09:38 -0800 Subject: [PATCH 078/432] adds handling of kind=edges as infered graph only works for nodes. adds n_neighbors param to search for n_neightbors within an eps ball of point, previously was iterating over 3k+ NN. Tested on Red team data and new demo notebooks --- graphistry/ai_utils.py | 54 ++++++++++++++++++++++------------- graphistry/compute/cluster.py | 10 +++++-- graphistry/feature_utils.py | 11 +++++-- graphistry/umap_utils.py | 13 +++++---- 4 files changed, 56 insertions(+), 32 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index a3c628dc6a..8b71dd885c 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -193,7 +193,7 @@ def query_by_vector(vect, df, search_index, top_n): def infer_graph( - res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, verbose=False + res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, n_neighbors=None, verbose=False, ): """ Infer a graph from a graphistry object @@ -205,10 +205,16 @@ def infer_graph( emb: minibatch UMAP embedding kind: 'nodes' or 'edges' eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph - n_nearest: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. + sample: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. + n_neighbors: number of nearest neighbors to include per batch point """ - print('*Infering graph from existing graphistry object') if verbose else None - # new_index = df.index + print("-"*50) if verbose else None + + if n_neighbors is None and emb is not None: + n_neighbors = res._umap_params['n_neighbors'] + elif n_neighbors is None and emb is None: + n_neighbors = 4 + if infer_on_umap_embedding and emb is not None: X_previously_fit = res._node_embedding X_new = emb @@ -216,7 +222,9 @@ def infer_graph( else: # can still be umap, but want to do the inference on the higher dimensional features X_previously_fit = res._node_features X_new = X - print("Infering edges over features") if verbose else None + print("Infering edges over features embedding") if verbose else None + + print("-"*45) if verbose else None FEATS = res._node_features if FEATS is None: @@ -255,23 +263,27 @@ def infer_graph( # vsearch = build_search_index(X_previously_fit, angular=False) for i in range(X_new.shape[0]): - # record_df = df.iloc[i, :] diff = X_previously_fit - X_new.iloc[i, :] dist = np.linalg.norm(diff, axis=1) # Euclidean distance mdists.append(dist) m, std = np.mean(mdists), np.std(mdists) logger.info(f"--Mean distance to existing nodes {m:.2f} +/- {std:.2f}") - # print(f'--Mean distance to existing nodes {m:.2f} +/- {std:.2f}') + print(f' Mean distance to existing nodes {m:.2f} +/- {std:.2f}') if verbose else None if eps == "auto": - eps = np.min([np.abs(m - 2 * std), m]) + eps = np.min([np.abs(m - std), m]) logger.info( - f"{eps:.2f} epsilon for max distance threshold to be considered a neighbor" + f"-epsilon = {eps:.2f} max distance threshold to be considered a neighbor" ) - + print(f' epsilon = {eps:.2f}; max distance threshold to be considered a neighbor') if verbose else None + + print(f'Finding {n_neighbors} nearest neighbors') if verbose else None + nn = [] for i, dist in enumerate(mdists): record_df = df.iloc[i, :] - for j in np.where(dist < eps)[0]: + nearest = np.where(dist < eps)[0] + nn.append(len(nearest)) + for j in nearest[:n_neighbors]: # add n_neighbors nearest neighbors, if any, super speedup hack this_ndf = NDF.iloc[j, :] if sample: local_edges = EDF[ @@ -281,7 +293,9 @@ def infer_graph( old_edges.append(local_edges.sample(sample, replace=True)) new_edges.append([this_ndf[node], record_df[node], 1, 1]) old_nodes.append(this_ndf) - + + print(' ', np.mean(nn), f'neighbors per node within epsilon {eps}') if verbose else None + new_edges = pd.DataFrame(new_edges, columns=[src, dst, "_weight", "_batch"]) all_nodes = [] @@ -293,13 +307,13 @@ def infer_graph( .append(new_edges[src]) .append(new_edges[dst]) ).drop_duplicates() - print(len(all_nodes), "nodes in new graph") if verbose else None + print(' ', len(all_nodes), "nodes in new graph") if verbose else None if sample: new_edges = pd.concat([new_edges, old_edges], axis=0).drop_duplicates() - print('sampled', len(old_edges.drop_duplicates()), 'previous old edges') if verbose else None + print(' Sampled', len(old_edges.drop_duplicates()), 'previous old edges') if verbose else None new_edges = new_edges.drop_duplicates() - print(len(new_edges), 'total edges pairs after dropping duplicates') if verbose else None + print('', len(new_edges), 'total edges pairs after dropping duplicates') if verbose else None if len(old_nodes): old_nodes = pd.DataFrame(old_nodes) @@ -320,10 +334,9 @@ def infer_graph( new_features = pd.concat([X, FEATS.loc[old_nodes.index]], axis=0) new_nodes = pd.concat([df, old_nodes], axis=0) # append minibatch at top - print('-' * 80) if verbose else None - print("Final graph has", len(new_nodes), "nodes") if verbose else None - print("-Batch has", len(df), "nodes") if verbose else None - print("-Brought in ", len(old_nodes), "nodes") if verbose else None + print("** Final graph has", len(new_nodes), "nodes") if verbose else None + print(" - Batch has", len(df), "nodes") if verbose else None + print(" - Brought in", len(old_nodes), "nodes") if verbose else None new_targets = pd.concat([y, Y.loc[old_nodes.index]]) if y is not None else Y @@ -333,5 +346,6 @@ def infer_graph( g._node_embedding = new_emb g._node_features = new_features g._node_targets = new_targets - + + print("-"*50) if verbose else None return g diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 86851f1ad9..92af990851 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -318,7 +318,7 @@ def _transform_dbscan( X_ = X labels = dbscan_predict(X_, dbscan) # type: ignore - if umap: + if umap and cols is None: df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) # type: ignore else: df = df.assign(_dbscan=labels) @@ -334,8 +334,10 @@ def transform_dbscan( eps: Union[float, str] = "auto", fit_umap_embedding: bool = False, sample: Optional[int] = None, + n_neighbors: Optional[int] = None, kind: str = "nodes", return_graph=True, + verbose=False, ): # type: ignore """ Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels on the minibatch @@ -358,9 +360,11 @@ def transform_dbscan( """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) - if return_graph: + if return_graph and kind not in ["edges"]: g = self._infer_edges( # type: ignore - emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample + emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, + eps=eps, sample=sample, n_neighbors=n_neighbors, + verbose=verbose ) return g return emb, X, y, df diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 7c1037ad21..cce0bb07e1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2180,6 +2180,7 @@ def transform(self, df: pd.DataFrame, kind: str = 'nodes', eps: Union[str, float, int] = 'auto', sample: Optional[int] = None, + n_neighbors: Optional[int] = None, return_graph: bool = True, scaled: bool = True, verbose: bool = False): @@ -2205,9 +2206,13 @@ def transform(self, df: pd.DataFrame, else: logger.debug("kind must be one of `nodes`," f"`edges`, found {kind}") - if return_graph: - emb = None # will not be able to decide umap coordinates, but will be able to infer graph from existing edges - g = self._infer_edges(emb, X, y_, df, infer_on_umap_embedding=False, eps=eps, sample=sample, verbose=verbose) + + if return_graph and kind not in ["edges"]: + emb = None # will not be able to infer graph from umap coordinates, but will be able to infer graph from existing edges + g = self._infer_edges(emb, X, y_, df, + infer_on_umap_embedding=False, + eps=eps, sample=sample, n_neighbors=n_neighbors, + verbose=verbose) return g return X, y_ diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index c9a52ceb4f..9b526a6e9e 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -294,6 +294,7 @@ def transform_umap(self, df: pd.DataFrame, kind: str = 'nodes', eps: Union[str, float, int] = 'auto', sample: Optional[int] = None, + n_neighbors: Optional[int] = None, return_graph: bool = True, fit_umap_embedding: bool = False, verbose=False @@ -305,8 +306,11 @@ def transform_umap(self, df: pd.DataFrame, X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) - if return_graph: - g = self._infer_edges(emb, X, y_, df, infer_on_umap_embedding=fit_umap_embedding, eps=eps, sample=sample) + if return_graph and kind not in ["edges"]: + g = self._infer_edges(emb, X, y_, df, + infer_on_umap_embedding=fit_umap_embedding, + eps=eps, sample=sample, n_neighbors=n_neighbors, + verbose=verbose) return g return emb, X, y_ @@ -317,8 +321,6 @@ def _bundle_embedding(self, emb, index): columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1]) ] - # print('emb.shape', emb.shape) - # print('columns', columns, len(columns)) emb = pd.DataFrame(emb, columns=columns, index=index) return emb @@ -337,7 +339,6 @@ def _process_umap( Returns res mutated with new _xy """ umap_kwargs_pure = umap_kwargs.copy() - #res._umap = self._umap logger.debug("process_umap before kwargs: %s", umap_kwargs) umap_kwargs.update({"kind": kind, "X": X_, "y": y_}) @@ -350,7 +351,7 @@ def _process_umap( if old_res: print(" --- [[ RE-USING UMAP ]]") if verbose else None logger.info(" --- [[ RE-USING UMAP ]]") - print('umap_kwargs', umap_kwargs['n_components']) if verbose else None + print('umap_kwargs n_components', umap_kwargs['n_components']) if verbose else None fresh_res = copy.copy(res) for attr in ["_xy", "_weighted_edges_df", "_weighted_adjacency"]: setattr(fresh_res, attr, getattr(old_res, attr)) From 938d21718228e122299622d2c1cee04b7780f951 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 9 Jan 2023 18:16:29 -0800 Subject: [PATCH 079/432] lint --- graphistry/ai_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 8b71dd885c..2378d841f9 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -208,7 +208,7 @@ def infer_graph( sample: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. n_neighbors: number of nearest neighbors to include per batch point """ - print("-"*50) if verbose else None + print("-" * 50) if verbose else None if n_neighbors is None and emb is not None: n_neighbors = res._umap_params['n_neighbors'] @@ -224,7 +224,7 @@ def infer_graph( X_new = X print("Infering edges over features embedding") if verbose else None - print("-"*45) if verbose else None + print("-" * 45) if verbose else None FEATS = res._node_features if FEATS is None: @@ -347,5 +347,5 @@ def infer_graph( g._node_features = new_features g._node_targets = new_targets - print("-"*50) if verbose else None + print("-" * 50) if verbose else None return g From a4dcb1cc8e1658661a7c36d4e1b2aa29d06bec7d Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Wed, 11 Jan 2023 01:27:18 +0530 Subject: [PATCH 080/432] Update mypy.ini --- mypy.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index fd6b4d9ece..01cff103e8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -python_version = 3.7 +python_version = 3.8 # TODO check tests exclude = graph_vector_pb2|versioneer|_version|graphistry/tests @@ -90,4 +90,4 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-cuml.*] -ignore_missing_imports = True \ No newline at end of file +ignore_missing_imports = True From 12049b47fc0eb2429bc6215f468cf1a8abfd73f6 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 10 Jan 2023 15:08:04 -0800 Subject: [PATCH 081/432] bug fixes, dbscan=False as default, better logging --- graphistry/ai_utils.py | 20 +++++++++++-- graphistry/compute/cluster.py | 25 ++++++++-------- graphistry/feature_utils.py | 44 +++++++++++++++-------------- graphistry/tests/test_umap_utils.py | 30 ++++++++++++-------- graphistry/umap_utils.py | 10 ++++--- graphistry/util.py | 37 ++++++++++++++++++++++-- 6 files changed, 112 insertions(+), 54 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 2378d841f9..ac73f61d73 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -4,6 +4,7 @@ import graphistry from .constants import N_TREES, DISTANCE +from .features import N_NEIGHBORS from logging import getLogger logger = getLogger(__name__) @@ -174,6 +175,17 @@ def build_annoy_index(X, angular, n_trees=None): def query_by_vector(vect, df, search_index, top_n): + """ Query by vector using annoy index and append distance to results + + it is assumed len(vect) == len(df) == len(search_index) + args: + vect: query vector + df: dataframe to query + search_index: annoy index + top_n: number of results to return + returns: + sorted dataframe with top_n results and distance + """ indices, distances = search_index.get_nns_by_vector( vect.values[0], top_n, include_distances=True ) @@ -208,12 +220,14 @@ def infer_graph( sample: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. n_neighbors: number of nearest neighbors to include per batch point """ + #enhanced = is_notebook() + print("-" * 50) if verbose else None if n_neighbors is None and emb is not None: n_neighbors = res._umap_params['n_neighbors'] elif n_neighbors is None and emb is None: - n_neighbors = 4 + n_neighbors = N_NEIGHBORS if infer_on_umap_embedding and emb is not None: X_previously_fit = res._node_embedding @@ -271,7 +285,7 @@ def infer_graph( logger.info(f"--Mean distance to existing nodes {m:.2f} +/- {std:.2f}") print(f' Mean distance to existing nodes {m:.2f} +/- {std:.2f}') if verbose else None if eps == "auto": - eps = np.min([np.abs(m - std), m]) + eps = np.min([np.abs(m - 2*std), np.abs(m - std), m]) logger.info( f"-epsilon = {eps:.2f} max distance threshold to be considered a neighbor" ) @@ -294,7 +308,7 @@ def infer_graph( new_edges.append([this_ndf[node], record_df[node], 1, 1]) old_nodes.append(this_ndf) - print(' ', np.mean(nn), f'neighbors per node within epsilon {eps}') if verbose else None + print(f'{np.mean(nn):.2f} neighbors per node within epsilon {eps:.2f}') if verbose else None new_edges = pd.DataFrame(new_edges, columns=[src, dst, "_weight", "_batch"]) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 92af990851..d0104b1f0c 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -106,12 +106,12 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ cols: list of columns to use for clustering given `g.featurize` has been run umap: whether to use UMAP embeddings or features dataframe """ - df = get_model_matrix(g, kind, cols, use_umap_embedding, target) + X = get_model_matrix(g, kind, cols, use_umap_embedding, target) - if df.empty: + if X.empty: raise ValueError("No features found for clustering") - dbscan.fit(df) + dbscan.fit(X) labels = dbscan.labels_ if kind == "nodes": @@ -131,6 +131,8 @@ def dbscan_predict(X: pd.DataFrame, model): """ DBSCAN has no predict per se, so we reverse engineer one here from https://stackoverflow.com/questions/27822752/scikit-learn-predicting-new-points-with-dbscan + + """ n_samples = X.shape[0] @@ -149,12 +151,12 @@ def dbscan_predict(X: pd.DataFrame, model): return y_new -def dbscan_predict2(g, kind="nodes", cols=None, umap=True): - X = g._get_feature(kind) - dbscan = g._node_dbscan if kind == "nodes" else g._edge_dbscan +# def dbscan_predict2(g, kind="nodes", cols=None, umap=True): +# X = g._get_feature(kind) +# dbscan = g._node_dbscan if kind == "nodes" else g._edge_dbscan - preds = dbscan_predict(X, dbscan) - return X, preds +# preds = dbscan_predict(X, dbscan) +# return X, preds class ClusterMixin(MIXIN_BASE): @@ -361,10 +363,9 @@ def transform_dbscan( """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) if return_graph and kind not in ["edges"]: - g = self._infer_edges( # type: ignore - emb, X, y, df, infer_on_umap_embedding=fit_umap_embedding, - eps=eps, sample=sample, n_neighbors=n_neighbors, - verbose=verbose + g = self._infer_edges(emb, X, y, df, eps=eps, sample=sample, n_neighbors=n_neighbors, + infer_on_umap_embedding=fit_umap_embedding, + verbose=verbose ) return g return emb, X, y, df diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index cce0bb07e1..4ee5941026 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1893,15 +1893,15 @@ def __init__(self, *args, **kwargs): pass def _get_feature(self, kind): - kind = kind.replace('s', '') - assert kind in ['node', 'edge'], f'kind needs to be in `nodes` or `edges`, found {kind}' - x = getattr(self, f'_{kind}_features') + kind2 = kind.replace('s', '') + assert kind2 in ['node', 'edge'], f'kind needs to be in `nodes` or `edges`, found {kind}' + x = getattr(self, f'_{kind2}_features') return x def _get_target(self, kind): - kind = kind.replace('s', '') - assert kind in ['node', 'edge'], f'kind needs to be in `nodes` or `edges`, found {kind}' - x = getattr(self, f'_{kind}_target') + kind2 = kind.replace('s', '') + assert kind2 in ['node', 'edge'], f'kind needs to be in `nodes` or `edges`, found {kind}' + x = getattr(self, f'_{kind2}_target') return x def _featurize_nodes( @@ -2152,9 +2152,9 @@ def _featurize_edges( # if editing, should also update fresh_res res._edge_features = encoder.X - res._edge_features_raw = encoder.X # .copy() + res._edge_features_raw = encoder.X_orignal # .copy() res._edge_target = encoder.y - res._edge_target_raw = encoder.y # .copy() + res._edge_target_raw = encoder.y_orignal # .copy() res._edge_encoder = encoder return res @@ -2194,7 +2194,10 @@ def transform(self, df: pd.DataFrame, return_graph: bool, if True, will return a graph with inferred edges eps: float, if return_graph is True, will use this value for eps in NN search, or 'auto' to infer a good value eps represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. - sample: int, if return_graph is True, will use sample value for NN search over existing edges + sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph + n_neighbors: int, optional (default = 15), if return_graph is True, will use this value for n_neighbors in NN search + scaled: bool, if True, will use scaled transformation of data set during featurization + verbose: bool, if True, will print metadata about the graph construction returns: X: pd.DataFrame, transformed data if return_graph is False or a graph with inferred edges if return_graph is True @@ -2208,10 +2211,10 @@ def transform(self, df: pd.DataFrame, f"`edges`, found {kind}") if return_graph and kind not in ["edges"]: - emb = None # will not be able to infer graph from umap coordinates, but will be able to infer graph from existing edges - g = self._infer_edges(emb, X, y_, df, + emb = None # will not be able to infer graph from umap coordinates, + # but will be able to infer graph from features of existing edges + g = self._infer_edges(emb, X, y_, df, eps=eps, sample=sample, n_neighbors=n_neighbors, infer_on_umap_embedding=False, - eps=eps, sample=sample, n_neighbors=n_neighbors, verbose=verbose) return g return X, y_ @@ -2236,17 +2239,12 @@ def scale( example usage: g = graphistry.nodes(df) - g2 = g.umap().scale(eps=0.2, sample=None, kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) + X, y = g.umap().scale(kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) - # scaled data - g3 = g.scale( kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) - X = g2._node_features - y = g2._node_target - args: df: pd.DataFrame, raw data to transform - y: pd.DataFrame, optional - kind: str # one of `nodes`, `edges` + y: pd.DataFrame, optional target data + kind: str, one of `nodes`, `edges` use_scaler: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` use_scaler_target: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` impute: bool, if True, will impute missing values @@ -2263,7 +2261,6 @@ def scale( """ if df is None: # use the original data - # df = self._nodes if kind == "nodes" else self._edges X, y = (self._node_features_raw, self._node_target_raw) if kind == "nodes" else (self._edge_features_raw, self._edge_target_raw) else: X, y = self.transform(df, y, kind=kind, return_graph=False, scaled=False) @@ -2354,6 +2351,7 @@ def featurize( remove_node_column: bool = True, inplace: bool = False, feature_engine: FeatureEngine = "auto", + dbscan: bool = False, memoize: bool = True, verbose: bool = False, ): @@ -2534,6 +2532,10 @@ def featurize( f"One may only featurize `nodes` or `edges`, got {kind}" ) return self + + if dbscan: + res = res.dbscan(kind=kind, fit_umap_embedding=False) # type: ignore + if not inplace: return res diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 2baac9a9d4..3f015fd667 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -33,6 +33,10 @@ has_cuml, _, _ = lazy_cuml_import_has_dependancy() has_umap, _, _ = lazy_umap_import_has_dependancy() +# print('has_dependancy', has_dependancy) +# print('has_cuml', has_cuml) +# print('has_umap', has_umap) + logger = logging.getLogger(__name__) warnings.filterwarnings("ignore") @@ -232,7 +236,7 @@ def test_transform_umap(self): np.random.seed(41) test = self.test assert ( - 2 * self.g2._node_embedding.shape[0] == self.g3._node_embedding.shape[0] + self.g2._node_embedding.shape[0] <= self.g3._node_embedding.shape[0] ), "Node Embedding Lengths do not match, found {} and {}".format( self.g2._node_embedding.shape[0], self.g3._node_embedding.shape[0] ) @@ -241,6 +245,7 @@ def test_transform_umap(self): sample = [None, 2] return_graph = [True, False] fit_umap_embedding = [True, False] + n_neighbors = [2, None] for ep in eps: g4 = self.g2.transform_umap(test, test, eps=ep) assert True @@ -256,6 +261,9 @@ def test_transform_umap(self): assert g4[0].shape[1] == 2 assert g4[1].shape[1] >= 2 assert g4[2].shape[0] == test.shape[0] + for n_neigh in n_neighbors: + g4 = self.g2.transform_umap(test, n_neighbors=n_neigh) + assert True for sample_ in sample: g4 = self.g2.transform_umap(test, sample=sample_) assert True @@ -583,12 +591,12 @@ def test_filter_edges(self): @pytest.mark.skipif( - not has_dependancy and not has_cuml, + not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", ) class TestCUMLMethods(TestUMAPMethods): @pytest.mark.skipif( - not has_dependancy and not has_cuml, + not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", ) def _test_umap(self, g, use_cols, targets, name, kind, df): @@ -607,7 +615,7 @@ def _test_umap(self, g, use_cols, targets, name, kind, df): target, use_col, ] - logger.debug(f"{value}") + logger.debug(f"{name}:\n{value}") logger.debug("-" * 80) g2 = g.umap( @@ -627,7 +635,7 @@ def _test_umap(self, g, use_cols, targets, name, kind, df): self.cases_test_graph(g2, kind=kind, df=df) @pytest.mark.skipif( - not has_dependancy and not has_cuml, + not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", ) def test_node_umap(self): @@ -650,7 +658,7 @@ def test_node_umap(self): ) @pytest.mark.skipif( - not has_dependancy and not has_cuml, + not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", ) def test_edge_umap(self): @@ -672,7 +680,7 @@ def test_edge_umap(self): ) @pytest.mark.skipif( - not has_dependancy and not has_cuml, + not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", ) def test_chaining_nodes(self): @@ -684,7 +692,7 @@ def test_chaining_nodes(self): logger.debug("======= g3a.featurize() done ======") g3 = g3a.umap() logger.debug("======= g3.umap() done ======") - assert g2._node_features.shape == g3._node_features.shape + assert g2._node_features.shape == g3._node_features.shape, f"featurize() should be idempotent, found {g2._node_features.shape} != {g3._node_features.shape}" # since g3 has feature params with x and y. g3._feature_params["nodes"]["X"].pop("x") g3._feature_params["nodes"]["X"].pop("y") @@ -695,7 +703,7 @@ def test_chaining_nodes(self): assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce @pytest.mark.skipif( - not has_dependancy and not has_cuml, + not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", ) def test_chaining_edges(self): @@ -714,7 +722,7 @@ def test_chaining_edges(self): assert all(g2._edge_features == g3._edge_features) @pytest.mark.skipif( - not has_dependancy and not has_cuml, + not has_dependancy or not has_cuml, reason="requires cuml feature dependencies", ) def test_feature_kwargs_yield_different_values_using_umap_api(self): @@ -748,7 +756,7 @@ def test_feature_kwargs_yield_different_values_using_umap_api(self): assert g2._node_target.shape[1] == n_topics_target, "Targets " @pytest.mark.skipif( - not has_dependancy and not has_umap, + not has_dependancy or not has_umap, reason="requires cuml feature dependencies", ) def test_filter_edges(self): diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 9b526a6e9e..142b67b1f6 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -42,7 +42,9 @@ def lazy_cuml_import_has_dependancy(): import warnings warnings.filterwarnings("ignore") - import cuml # type: ignore + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + import cuml # type: ignore return True, "ok", cuml except ModuleNotFoundError as e: @@ -50,14 +52,14 @@ def lazy_cuml_import_has_dependancy(): def assert_imported(): - has_dependancy_, import_exn, umap_learn = lazy_umap_import_has_dependancy() + has_dependancy_, import_exn, _ = lazy_umap_import_has_dependancy() if not has_dependancy_: logger.error("UMAP not found, trying running " "`pip install graphistry[ai]`") raise import_exn def assert_imported_cuml(): - has_cuml_dependancy_, import_cuml_exn, cuml = lazy_cuml_import_has_dependancy() + has_cuml_dependancy_, import_cuml_exn, _ = lazy_cuml_import_has_dependancy() if not has_cuml_dependancy_: logger.warning("cuML not found, trying running " "`pip install cuml`") raise import_cuml_exn @@ -424,7 +426,7 @@ def umap( play: Optional[int] = 0, encode_position: bool = True, encode_weight: bool = True, - dbscan: bool = True, + dbscan: bool = False, engine: UMAPEngine = "auto", inplace: bool = False, feature_engine: str = "auto", diff --git a/graphistry/util.py b/graphistry/util.py index 51115246bb..c3d85c5f35 100644 --- a/graphistry/util.py +++ b/graphistry/util.py @@ -314,13 +314,13 @@ class ModelDict(UserDict): verbose: print out model names, logging happens regardless """ - def __init__(self, message, verbose=True, timestamp=False, *args, **kwargs): + def __init__(self, message, verbose=True, _timestamp=False, *args, **kwargs): self._message = message self._verbose = verbose - self._timestamp = timestamp + self._timestamp = _timestamp # do no use this inside the class, as it will trigger memoization. Only use outside of class. L = ( len(message) - if timestamp is False + if _timestamp is False else max(len(message), len(get_timestamp()) + 1) ) self._print_length = min(80, L) @@ -342,6 +342,14 @@ def __repr__(self): self.print(self._message) return super().__repr__() + def __setitem__(self, key, value): + self._updates.append(key) + if len(self._updates) > 1: + self._message += ( + "\n" + "_" * self._print_length + f"\n\nUpdated: {self._updates[-1]}" + ) + return super().__setitem__(key, value) + def update(self, *args, **kwargs): self._updates.append(args[0]) if len(self._updates) > 1: # don't take first update since its the init/default @@ -351,6 +359,29 @@ def update(self, *args, **kwargs): return super().update(*args, **kwargs) +def is_notebook(): + """Check if running in a notebook""" + try: + from IPython import get_ipython + + if "IPKernelApp" not in get_ipython().config: # pragma: no cover + raise ImportError("console") + return False + if "VSCODE_PID" in os.environ: # pragma: no cover + raise ImportError("vscode") + return False + except: + return False + else: # pragma: no cover + return True + + +def printmd(string, color=None, size=20): + """Print markdown string in notebook""" + from IPython.display import Markdown, display + colorstr = "{}".format(color, size, string) + display(Markdown(colorstr)) + # # def inspect_decorator(func, args, kwargs): # import inspect From a47d923851a013bf1d490c8cc79587ebe980921b Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Wed, 11 Jan 2023 12:28:15 +0530 Subject: [PATCH 082/432] flake8 fix --- graphistry/ai_utils.py | 2 +- graphistry/compute/cluster.py | 4 ++-- graphistry/util.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index ac73f61d73..0053a91c0b 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -285,7 +285,7 @@ def infer_graph( logger.info(f"--Mean distance to existing nodes {m:.2f} +/- {std:.2f}") print(f' Mean distance to existing nodes {m:.2f} +/- {std:.2f}') if verbose else None if eps == "auto": - eps = np.min([np.abs(m - 2*std), np.abs(m - std), m]) + eps = np.min([np.abs(m - 2 * std), np.abs(m - std), m]) logger.info( f"-epsilon = {eps:.2f} max distance threshold to be considered a neighbor" ) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index d0104b1f0c..c8c244d808 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -364,8 +364,8 @@ def transform_dbscan( emb, X, y, df = self._transform_dbscan(df, y, kind=kind) if return_graph and kind not in ["edges"]: g = self._infer_edges(emb, X, y, df, eps=eps, sample=sample, n_neighbors=n_neighbors, - infer_on_umap_embedding=fit_umap_embedding, - verbose=verbose + infer_on_umap_embedding=fit_umap_embedding, + verbose=verbose ) return g return emb, X, y, df diff --git a/graphistry/util.py b/graphistry/util.py index c3d85c5f35..f8ffe483f3 100644 --- a/graphistry/util.py +++ b/graphistry/util.py @@ -345,7 +345,7 @@ def __repr__(self): def __setitem__(self, key, value): self._updates.append(key) if len(self._updates) > 1: - self._message += ( + self._message += ( "\n" + "_" * self._print_length + f"\n\nUpdated: {self._updates[-1]}" ) return super().__setitem__(key, value) From 6b245843e76ff9e55fe74ead49c3723725d4bd16 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Wed, 11 Jan 2023 12:34:57 +0530 Subject: [PATCH 083/432] mypy fixes --- graphistry/compute/cluster.py | 2 +- graphistry/embed_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index c8c244d808..dd08b2e023 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -363,7 +363,7 @@ def transform_dbscan( """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind) if return_graph and kind not in ["edges"]: - g = self._infer_edges(emb, X, y, df, eps=eps, sample=sample, n_neighbors=n_neighbors, + g = self._infer_edges(emb, X, y, df, eps=eps, sample=sample, n_neighbors=n_neighbors, # type: ignore infer_on_umap_embedding=fit_umap_embedding, verbose=verbose ) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index f15d4be1ce..713f7b67ea 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -126,7 +126,7 @@ def _preprocess_embedding_data(self, res, train_split:Union[float, int] = 0.8) - log(msg="--Splitting data") train_size = int(train_split * len(triplets)) test_size = len(triplets) - train_size - train_dataset, test_dataset = torch.utils.data.random_split(triplets, [train_size, test_size]) + train_dataset, test_dataset = torch.utils.data.random_split(triplets, [train_size, test_size]) # type: ignore res._train_idx = train_dataset.indices res._test_idx = test_dataset.indices From aa285c5e5a4129734e4d105922b427ecbdb202ce Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Wed, 11 Jan 2023 15:48:59 +0530 Subject: [PATCH 084/432] fix: test_cluster.py --- graphistry/compute/cluster.py | 5 +++-- graphistry/tests/test_cluster.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index dd08b2e023..6fab087be7 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -115,9 +115,9 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ labels = dbscan.labels_ if kind == "nodes": - g._nodes = g._nodes.assign(_dbscan=labels) + g._nodes = g._nodes.assign(_cluster=labels) elif kind == "edges": - g._edges = g._edges.assign(_dbscan=labels) + g._edges = g._edges.assign(_cluster=labels) else: raise ValueError("kind must be one of `nodes` or `edges`") @@ -248,6 +248,7 @@ def dbscan( This includes the point itself. """ + res = self.bind() res = res._cluster_dbscan( res, diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py index 4c39dced35..8f24821fce 100644 --- a/graphistry/tests/test_cluster.py +++ b/graphistry/tests/test_cluster.py @@ -29,7 +29,10 @@ def test_umap_cluster(self): self._condition(g2, kind) g3 = g.umap(kind=kind, n_topics=2, dbscan=True) self._condition(g3, kind) - self.assertEqual(g2._nodes['_cluster'].tolist(), g3._nodes['_cluster'].tolist()) + if kind == 'nodes': + self.assertEqual(g2._nodes['_cluster'].tolist(), g3._nodes['_cluster'].tolist()) + else: + self.assertEqual(g2._edges['_cluster'].tolist(), g3._edges['_cluster'].tolist()) @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") def test_featurize_cluster(self): From 91ae7f5dde9911e0ce6d2453e5f404a4a9f1206a Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Wed, 11 Jan 2023 18:10:05 +0530 Subject: [PATCH 085/432] some fixes (test_feature_utils.py) --- graphistry/tests/test_feature_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index d8c1db38aa..177f39b119 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -286,7 +286,8 @@ def test_process_node_dataframes_min_words(self): 2, 4000, ]: # last one should skip encoding, and throw all to dirty_cat - X_enc, y_enc, data_encoder, label_encoder, ordinal_pipeline, ordinal_pipeline_target, text_model, text_cols = process_nodes_dataframes( + + X_enc, y_enc, X_encs, y_encs, data_encoder, label_encoder, ordinal_pipeline, ordinal_pipeline_target, text_model, text_cols = process_nodes_dataframes( ndf_reddit, y=double_target_reddit, use_scaler=None, @@ -431,7 +432,7 @@ def test_edge_scaling(self): g2 = g.featurize(y='label', kind='edges', use_scaler=None, use_scaler_target=None) scalers = ['quantile', 'zscale', 'kbins', 'robust', 'minmax'] for scaler in scalers: - a, b, c, d = g2.scale(edge_df2, edge2_target_df, kind='edges', use_scaler=scaler, use_scaler_target=np.random.choice(scalers)) + X, y = g2.scale(edge_df2, edge2_target_df, kind='edges', use_scaler=scaler, use_scaler_target=np.random.choice(scalers)) From 7580bf52ff44260a8b0a9b60849514c529c8bf24 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 12:13:10 -0800 Subject: [PATCH 086/432] clean up and adds flags, changes tests --- graphistry/ai_utils.py | 9 +- graphistry/feature_utils.py | 10 +-- graphistry/features.py | 44 ++++++++-- graphistry/umap_utils.py | 165 +++++++++++++++++------------------- graphistry/util.py | 21 +++-- 5 files changed, 135 insertions(+), 114 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index ac73f61d73..2af7d68d53 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -285,7 +285,7 @@ def infer_graph( logger.info(f"--Mean distance to existing nodes {m:.2f} +/- {std:.2f}") print(f' Mean distance to existing nodes {m:.2f} +/- {std:.2f}') if verbose else None if eps == "auto": - eps = np.min([np.abs(m - 2*std), np.abs(m - std), m]) + eps = np.min([np.abs(m - 2 * std), np.abs(m - std), m]) logger.info( f"-epsilon = {eps:.2f} max distance threshold to be considered a neighbor" ) @@ -315,12 +315,7 @@ def infer_graph( all_nodes = [] if len(old_edges): old_edges = pd.concat(old_edges, axis=0).assign(_batch=0) - all_nodes = ( - old_edges[src] - .append(old_edges[dst]) - .append(new_edges[src]) - .append(new_edges[dst]) - ).drop_duplicates() + all_nodes = pd.concat([old_edges[src], old_edges[dst], new_edges[src], new_edges[dst]]).drop_duplicates() print(' ', len(all_nodes), "nodes in new graph") if verbose else None if sample: diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 4ee5941026..905c345841 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1520,11 +1520,11 @@ def transform_dirty( logger.debug(f"\t{data_encoder}\n") # print(f"-{name} Encoder:") # print(f"\t{data_encoder}\n") - try: - logger.debug(f"{data_encoder.get_feature_names_in}") - except Exception as e: - logger.warning(e) - pass + # try: + # logger.debug(f"{data_encoder.get_feature_names_in}") + # except Exception as e: + # logger.warning(e) + # pass logger.debug(f"TRANSFORM pre as df -- \t{df.shape}") # ##################################### for dirty_cat 0.3.0 diff --git a/graphistry/features.py b/graphistry/features.py index 541eda9fbb..9a756fb076 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -30,6 +30,7 @@ FORCE_EMBEDDING_ALL_COLUMNS = 0 # min_words HIGH_WORD_COUNT = 1024 +MID_WORD_COUNT = 128 LOW_WORD_COUNT = 3 NGRAMS_RANGE = (1, 3) @@ -42,7 +43,6 @@ N_QUANTILES = 100 OUTPUT_DISTRIBUTION = "normal" QUANTILES_RANGE = (25, 75) -N_BINS = 10 ENCODE = "ordinal" # kbins, onehot, ordinal, label STRATEGY = "uniform" # uniform, quantile, kmeans SIMILARITY = None # 'ngram' , default None uses Gap @@ -57,11 +57,11 @@ UMAP_DIM = 2 N_NEIGHBORS = 15 MIN_DIST = 0.1 -SPREAD = (0.5,) -LOCAL_CONNECTIVITY = (1,) -REPULSION_STRENGTH = (1,) -NEGATIVE_SAMPLING_RATE = (5,) -METRIC = ("euclidean",) +SPREAD = 0.5 +LOCAL_CONNECTIVITY = 1 +REPULSION_STRENGTH = 1 +NEGATIVE_SAMPLING_RATE = 5 +METRIC = "euclidean" # ############################################################### @@ -96,6 +96,8 @@ SPLIT_MEDIUM = 0.2 SPLIT_HIGH = 0.5 +# ############################################################################# +# Model Training {params} default_featurize_parameters = ModelDict( "Featurize Parameters", @@ -145,6 +147,36 @@ }, ) + +umap_hellinger = ModelDict( + "Umap Parameters Hellinger", + { + "n_components": UMAP_DIM, + "metric": "hellinger", # info metric, can't use on + # textual encodings since they contain negative values... + "n_neighbors": 15, + "min_dist": 0.3, + "spread": 0.5, + "local_connectivity": 1, + "repulsion_strength": 1, + "negative_sample_rate": 5, + } +) + +umap_euclidean = ModelDict( + "Umap Parameters Euclidean", + { + "n_components": UMAP_DIM, + "metric": "euclidean", + "n_neighbors": 12, + "min_dist": 0.1, + "spread": 0.5, + "local_connectivity": 1, + "repulsion_strength": 1, + "negative_sample_rate": 5, + } +) + # ############################################################################# # Create useful presets for the user # makes naming and encoding models consistently and testing different models against eachother easy diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 142b67b1f6..a520a615de 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -104,32 +104,6 @@ def resolve_umap_engine( ############################################################################### - -umap_kwargs_probs = { - "n_components": 2, - "metric": "hellinger", # info metric, can't use on - # textual encodings since they contain negative values... - "n_neighbors": 15, - "min_dist": 0.3, - "verbose": True, - "spread": 0.5, - "local_connectivity": 1, - "repulsion_strength": 1, - "negative_sample_rate": 5, -} - -umap_kwargs_euclidean = { - "n_components": 2, - "metric": "euclidean", - "n_neighbors": 12, - "min_dist": 0.1, - "verbose": True, - "spread": 0.5, - "local_connectivity": 1, - "repulsion_strength": 1, - "negative_sample_rate": 5, -} - # ############################################################################# # # Fast Memoize @@ -185,6 +159,8 @@ def umap_lazy_init( suffix: str = "", verbose: bool = False, ): + from graphistry.features import ModelDict + engine_resolved = resolve_umap_engine(engine) # FIXME remove as set_new_kwargs will always replace? if engine_resolved == UMAP_LEARN: @@ -195,11 +171,8 @@ def umap_lazy_init( raise ValueError( "No umap engine, ensure 'auto', 'umap_learn', or 'cuml', and the library is installed" ) - - if not self._umap_initialized: - from graphistry.features import ModelDict - umap_kwargs = dict( - { + umap_kwargs = ModelDict("UMAP Parameters", + **{ "n_components": n_components, **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), # type: ignore "n_neighbors": n_neighbors, @@ -210,9 +183,14 @@ def umap_lazy_init( "negative_sample_rate": negative_sample_rate, } ) + + print(umap_kwargs) if verbose else None + # set new umap kwargs + res._umap_params = umap_kwargs + + if not self._umap_initialized: #print('umap_kwargs init n_components: ', umap_kwargs['n_components']) if verbose else None - - #print('umap_kwargs', umap_kwargs) + print('Init Umap Params') if verbose else None res._n_components = n_components res._metric = metric res._n_neighbors = n_neighbors @@ -224,9 +202,7 @@ def umap_lazy_init( res._umap = umap_engine.UMAP(**umap_kwargs) res.engine = engine_resolved res._suffix = suffix - - res._umap_params = dict(**umap_kwargs) # ModelDict(f'Umap Parameters', - + # finally set the flag res._umap_initialized = True return res @@ -254,7 +230,7 @@ def _get_embedding(self, kind='nodes'): else: raise ValueError('kind must be one of `nodes` or `edges`') - def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): + def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None, verbose=False): if self._umap is None: raise ValueError("UMAP is not initialized") t = time() @@ -283,10 +259,10 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info(f" - or {X.shape[0]/mins:.2f} rows per minute") return self - def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): + def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None, verbose=False): if self._umap is None: raise ValueError("UMAP is not initialized") - self.umap_fit(X, y) + self.umap_fit(X, y, verbose=verbose) emb = self._umap.transform(X) emb = self._bundle_embedding(emb, index=X.index) return emb @@ -340,7 +316,8 @@ def _process_umap( """ Returns res mutated with new _xy """ - umap_kwargs_pure = umap_kwargs.copy() + from .features import ModelDict + umap_kwargs_pure = ModelDict("UMAP Parameters", umap_kwargs.copy()) logger.debug("process_umap before kwargs: %s", umap_kwargs) umap_kwargs.update({"kind": kind, "X": X_, "y": y_}) @@ -367,7 +344,7 @@ def _process_umap( res._umap_initialized = False res = res.umap_lazy_init(res, verbose=verbose, **umap_kwargs_pure) - emb = res._umap_fit_transform(X_, y_) + emb = res._umap_fit_transform(X_, y_, verbose=verbose) res._xy = emb return res @@ -410,9 +387,9 @@ def _set_features( # noqa: E303 def umap( self, - kind: str = "nodes", X: XSymbolic = None, y: YSymbolic = None, + kind: str = "nodes", scale: float = 1.0, n_neighbors: int = 12, min_dist: float = 0.1, @@ -428,55 +405,66 @@ def umap( encode_weight: bool = True, dbscan: bool = False, engine: UMAPEngine = "auto", - inplace: bool = False, feature_engine: str = "auto", + inplace: bool = False, memoize: bool = True, verbose: bool = False, **featurize_kwargs, ): """ - UMAP the featurized node or edges data, - or pass in your own X, y (optional) dataframes. - - kind: `nodes` or `edges` or None. - If None, expects explicit X, y (optional) matrices, - and will Not associate them to nodes or edges. - If X, y (optional) is given, with kind = [nodes, edges], - it will associate new matrices to nodes or edges attributes. - feature_engine: How to encode data - ("none", "auto", "pandas", "dirty_cat", "torch") - encode_weight: if True, will set new edges_df from - implicit UMAP, default True. - encode_position: whether to set default plotting bindings - -- positions x,y from umap for .plot() - X: either a dataframe ndarray of features, or column names to featurize - y: either an dataframe ndarray of targets, or column names to featurize - targets - scale: multiplicative scale for pruning weighted edge DataFrame - gotten from UMAP, between [0, ..) with high end meaning keep - all edges - n_neighbors: UMAP number of nearest neighbors to include for - UMAP connectivity, lower makes more compact layouts. Minimum 2 - min_dist: UMAP float between 0 and 1, lower makes more compact - layouts. - spread: UMAP spread of values for relaxation - local_connectivity: UMAP connectivity parameter - repulsion_strength: UMAP repulsion strength - negative_sample_rate: UMAP negative sampling rate - n_components: number of components in the UMAP projection, - default 2 - metric: UMAP metric, default 'euclidean'. - see (UMAP-LEARN)[https://umap-learn.readthedocs.io/ - en/latest/parameters.html] documentation for more. - suffix: optional suffix to add to x, y attributes of umap. - play: Graphistry play parameter, default 0, how much to evolve - the network during clustering. 0 preserves the original UMAP layout. - dbscan: whether to run DBSCAN on the UMAP embedding, default True. - engine: selects which engine to use to calculate UMAP: - default "auto" will use cuML if available, otherwise UMAP-LEARN. - memoize: whether to memoize the results of this method, - default True. - verbose: whether to print out extra information, default False. + UMAP the featurized nodes or edges data, + or pass in your own X, y (optional) dataframes of values + + Example + ------- + >>> import graphistry + >>> g = graphistry.nodes(pd.DataFrame({'node': [0,1,2], 'data': [1,2,3], 'meta': ['a', 'b', 'c']})) + >>> g2 = g.umap(n_components=3, spread=1.0, min_dist=0.1, n_neighbors=12, negative_sample_rate=5, local_connectivity=1, repulsion_strength=1.0, metric='euclidean', suffix='', play=0, encode_position=True, encode_weight=True, dbscan=False, engine='auto', feature_engine='auto', inplace=False, memoize=True, verbose=False) + >>> g2.plot() + + Parameters + ---------- + X: either a dataframe ndarray of features, or column names to featurize + y: either an dataframe ndarray of targets, or column names to featurize + targets + kind: `nodes` or `edges` or None. + If None, expects explicit X, y (optional) matrices, + and will Not associate them to nodes or edges. + If X, y (optional) is given, with kind = [nodes, edges], + it will associate new matrices to nodes or edges attributes. + scale: multiplicative scale for pruning weighted edge DataFrame + gotten from UMAP, between [0, ..) with high end meaning keep + all edges + n_neighbors: UMAP number of nearest neighbors to include for + UMAP connectivity, lower makes more compact layouts. Minimum 2 + min_dist: UMAP float between 0 and 1, lower makes more compact + layouts. + spread: UMAP spread of values for relaxation + local_connectivity: UMAP connectivity parameter + repulsion_strength: UMAP repulsion strength + negative_sample_rate: UMAP negative sampling rate + n_components: number of components in the UMAP projection, + default 2 + metric: UMAP metric, default 'euclidean'. + see (UMAP-LEARN)[https://umap-learn.readthedocs.io/ + en/latest/parameters.html] documentation for more. + suffix: optional suffix to add to x, y attributes of umap. + play: Graphistry play parameter, default 0, how much to evolve + the network during clustering. 0 preserves the original UMAP layout. + encode_weight: if True, will set new edges_df from + implicit UMAP, default True. + encode_position: whether to set default plotting bindings + -- positions x,y from umap for .plot(), default True + dbscan: whether to run DBSCAN on the UMAP embedding, default False. + engine: selects which engine to use to calculate UMAP: + default "auto" will use cuML if available, otherwise UMAP-LEARN. + feature_engine: How to encode data + ("none", "auto", "pandas", "dirty_cat", "torch") + inplace: bool = False, whether to modify the current object, default False. + when False, returns a new object, useful for chaining in a functional paradigm. + memoize: whether to memoize the results of this method, + default True. + verbose: whether to print out extra information, default False. :return: self, with attributes set with new data """ if engine == UMAP_LEARN: @@ -549,7 +537,6 @@ def umap( if res._xy is None: raise RuntimeError("This should not happen") res._node_embedding = res._xy - # TODO add edge filter so graph doesn't have double edges # TODO user-guidable edge merge policies like upsert? res._weighted_edges_df_from_nodes = ( prune_weighted_edges_df_and_relabel_nodes( @@ -590,7 +577,7 @@ def umap( ) if X is not None and isinstance(X, pd.DataFrame): logger.info("New Matrix `X` passed in for UMAP-ing") - xy = res._umap_fit_transform(X, y) + xy = res._umap_fit_transform(X, y, verbose=verbose) res._xy = xy res._weighted_edges_df = prune_weighted_edges_df_and_relabel_nodes( res._weighted_edges_df, scale=scale @@ -602,7 +589,7 @@ def umap( else: logger.error( "If `kind` is `None`, `X` and optionally `y`" - "must be given and be of type pd.DataFrame" + "must be given." ) else: raise ValueError( @@ -616,7 +603,7 @@ def umap( res = res.prune_self_edges() if dbscan: - res = res.dbscan(kind=kind, fit_umap_embedding=True) # type: ignore + res = res.dbscan(kind=kind, fit_umap_embedding=True, verbose=verbose) # type: ignore if not inplace: return res diff --git a/graphistry/util.py b/graphistry/util.py index c3d85c5f35..3fdd43222b 100644 --- a/graphistry/util.py +++ b/graphistry/util.py @@ -342,13 +342,20 @@ def __repr__(self): self.print(self._message) return super().__repr__() - def __setitem__(self, key, value): - self._updates.append(key) - if len(self._updates) > 1: - self._message += ( - "\n" + "_" * self._print_length + f"\n\nUpdated: {self._updates[-1]}" - ) - return super().__setitem__(key, value) + # def __setitem__(self, key, value): + # #targs = {key: value} + # self.update({key: value}) + + # def __setattr__(self, key, value): + # if hasattr(self, '_updates') is False: + # self._updates = [] + # self._updates.append({key: value}) + # if len(self._updates) > 1: # don't take first update since its the init/default + # self._message += ( + # "\n" + "_" * self._print_length + f"\n\nUpdated: {self._updates[-1]}" + # ) + # return super().__setattr__(key, value) + def update(self, *args, **kwargs): self._updates.append(args[0]) From ee1934cbf9d51a18f2145dd3f5b4d1587cb4933b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 12:14:32 -0800 Subject: [PATCH 087/432] tests and changes to cluster.py verbosiity --- graphistry/compute/cluster.py | 20 +++++++++++++++----- graphistry/tests/test_umap_utils.py | 1 + 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index d0104b1f0c..a2cf579e1a 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -95,7 +95,7 @@ def get_model_matrix(g, kind, cols, umap, target): return df -def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, target=False): +def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, target=False, verbose=False): """ Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe or target dataframe if target is True. @@ -113,7 +113,7 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ dbscan.fit(X) labels = dbscan.labels_ - + if kind == "nodes": g._nodes = g._nodes.assign(_dbscan=labels) elif kind == "edges": @@ -124,6 +124,13 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ kind = "node" if kind == "nodes" else "edge" setattr(g, f"_{kind}_dbscan", dbscan) + if verbose: + cnt = Counter(labels) + message = f"DBSCAN found {len(cnt)} clusters with {cnt[-1]} outliers" + print('-'*len(message)) + print(message) + print(f"--fit on size {X.shape} data") + return g @@ -164,7 +171,7 @@ def __init__(self, *args, **kwargs): pass def _cluster_dbscan( - self, res, kind, cols, fit_umap_embedding, target, eps, min_samples, *args, **kwargs + self, res, kind, cols, fit_umap_embedding, target, eps, min_samples, verbose, *args, **kwargs ): """ DBSCAN clustering on cpu or gpu infered by .engine flag @@ -189,10 +196,11 @@ def _cluster_dbscan( if res.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) ) + print(f"DBSCAN engine: {res.engine}") if verbose else None res = dbscan_fit( - res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding - ) + res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding, verbose=True + ) return res @@ -204,6 +212,7 @@ def dbscan( kind="nodes", fit_umap_embedding=True, target=False, + verbose=True, **kwargs, ): """DBSCAN clustering on cpu or gpu infered automatically @@ -257,6 +266,7 @@ def dbscan( target=target, eps=eps, min_samples=min_samples, + verbose=verbose, **kwargs, ) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 3f015fd667..1247389f00 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -265,6 +265,7 @@ def test_transform_umap(self): g4 = self.g2.transform_umap(test, n_neighbors=n_neigh) assert True for sample_ in sample: + print("sample", sample_) g4 = self.g2.transform_umap(test, sample=sample_) assert True for fit_umap_embedding_ in fit_umap_embedding: From 87a418babb3dcd33951fd500d31f35fce4315f9e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 12:20:11 -0800 Subject: [PATCH 088/432] lint --- graphistry/compute/cluster.py | 2 +- graphistry/features.py | 28 +++++++++++----------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 5268e8b775..d3b6f3e43e 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -127,7 +127,7 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ if verbose: cnt = Counter(labels) message = f"DBSCAN found {len(cnt)} clusters with {cnt[-1]} outliers" - print('-'*len(message)) + print('-' * len(message)) print(message) print(f"--fit on size {X.shape} data") diff --git a/graphistry/features.py b/graphistry/features.py index 9a756fb076..f7a2b8003a 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -133,10 +133,8 @@ ) -default_umap_parameters = ModelDict( - "Umap Parameters", - { - "n_components": UMAP_DIM, +default_umap_parameters = ModelDict("Umap Parameters", + {"n_components": UMAP_DIM, **({"metric": METRIC} if True else {}), "n_neighbors": N_NEIGHBORS, "min_dist": MIN_DIST, @@ -144,14 +142,12 @@ "local_connectivity": LOCAL_CONNECTIVITY, "repulsion_strength": REPULSION_STRENGTH, "negative_sample_rate": NEGATIVE_SAMPLING_RATE, - }, + } ) -umap_hellinger = ModelDict( - "Umap Parameters Hellinger", - { - "n_components": UMAP_DIM, +umap_hellinger = ModelDict("Umap Parameters Hellinger", + {"n_components": UMAP_DIM, "metric": "hellinger", # info metric, can't use on # textual encodings since they contain negative values... "n_neighbors": 15, @@ -159,22 +155,20 @@ "spread": 0.5, "local_connectivity": 1, "repulsion_strength": 1, - "negative_sample_rate": 5, - } + "negative_sample_rate": 5 + } ) -umap_euclidean = ModelDict( - "Umap Parameters Euclidean", - { - "n_components": UMAP_DIM, +umap_euclidean = ModelDict("Umap Parameters Euclidean", + {"n_components": UMAP_DIM, "metric": "euclidean", "n_neighbors": 12, "min_dist": 0.1, "spread": 0.5, "local_connectivity": 1, "repulsion_strength": 1, - "negative_sample_rate": 5, - } + "negative_sample_rate": 5 + } ) # ############################################################################# From 93644c78630deefd655922b7c6ed1631c74a5d34 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 14:00:41 -0800 Subject: [PATCH 089/432] adds if then on if umap_params exist --- graphistry/ai_utils.py | 3 ++- graphistry/util.py | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 2af7d68d53..ce1d18a56c 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -225,7 +225,8 @@ def infer_graph( print("-" * 50) if verbose else None if n_neighbors is None and emb is not None: - n_neighbors = res._umap_params['n_neighbors'] + if getattr(res, '_umap_params', None) is not None: + n_neighbors = res._umap_params['n_neighbors'] elif n_neighbors is None and emb is None: n_neighbors = N_NEIGHBORS diff --git a/graphistry/util.py b/graphistry/util.py index f8ffe483f3..025e8853b3 100644 --- a/graphistry/util.py +++ b/graphistry/util.py @@ -342,13 +342,13 @@ def __repr__(self): self.print(self._message) return super().__repr__() - def __setitem__(self, key, value): - self._updates.append(key) - if len(self._updates) > 1: - self._message += ( - "\n" + "_" * self._print_length + f"\n\nUpdated: {self._updates[-1]}" - ) - return super().__setitem__(key, value) + # def __setitem__(self, key, value): # can't get this to work properly as it doesn't get called on update + # self._updates.append({key: value}) + # if len(self._updates) > 1: + # self._message += ( + # "\n" + "_" * self._print_length + f"\n\nUpdated: {self._updates[-1]}" + # ) + # return super().__setitem__(key, value) def update(self, *args, **kwargs): self._updates.append(args[0]) From 1366765ddb3564599b24342b4179daeb4a674b02 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 14:20:05 -0800 Subject: [PATCH 090/432] bug(none was being passed as n_neighbors, fixes) --- graphistry/ai_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index ce1d18a56c..5c1bbca64c 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -226,7 +226,10 @@ def infer_graph( if n_neighbors is None and emb is not None: if getattr(res, '_umap_params', None) is not None: - n_neighbors = res._umap_params['n_neighbors'] + if getattr(res._umap_params, 'n_neighbors', None) is not None: + n_neighbors = res._umap_params['n_neighbors'] + else: + n_neighbors = N_NEIGHBORS elif n_neighbors is None and emb is None: n_neighbors = N_NEIGHBORS From 4e2a289191b9d0c615463774911ef506c785b038 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 14:47:05 -0800 Subject: [PATCH 091/432] n_neighbors in infere graph not to be confused with umap kwargs, fixed to default value of 7 --- graphistry/ai_utils.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 5c1bbca64c..e634ebc69c 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -205,7 +205,7 @@ def query_by_vector(vect, df, search_index, top_n): def infer_graph( - res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, n_neighbors=None, verbose=False, + res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, n_neighbors: int=7, verbose=False, ): """ Infer a graph from a graphistry object @@ -218,21 +218,12 @@ def infer_graph( kind: 'nodes' or 'edges' eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph sample: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. - n_neighbors: number of nearest neighbors to include per batch point + n_neighbors, int: number of nearest neighbors to include per batch point within epsilon """ #enhanced = is_notebook() print("-" * 50) if verbose else None - if n_neighbors is None and emb is not None: - if getattr(res, '_umap_params', None) is not None: - if getattr(res._umap_params, 'n_neighbors', None) is not None: - n_neighbors = res._umap_params['n_neighbors'] - else: - n_neighbors = N_NEIGHBORS - elif n_neighbors is None and emb is None: - n_neighbors = N_NEIGHBORS - if infer_on_umap_embedding and emb is not None: X_previously_fit = res._node_embedding X_new = emb From 18e0c46763940580797754cfd46214fb81fd2f55 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 15:00:05 -0800 Subject: [PATCH 092/432] fixes chaining umap_kwargs and setting proper kwargs to self under chaining --- graphistry/umap_utils.py | 50 +++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index a520a615de..208f8c093d 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -184,27 +184,29 @@ def umap_lazy_init( } ) + print('lazy init') print(umap_kwargs) if verbose else None # set new umap kwargs res._umap_params = umap_kwargs - if not self._umap_initialized: - #print('umap_kwargs init n_components: ', umap_kwargs['n_components']) if verbose else None - print('Init Umap Params') if verbose else None - res._n_components = n_components - res._metric = metric - res._n_neighbors = n_neighbors - res._min_dist = min_dist - res._spread = spread - res._local_connectivity = local_connectivity - res._repulsion_strength = repulsion_strength - res._negative_sample_rate = negative_sample_rate - res._umap = umap_engine.UMAP(**umap_kwargs) - res.engine = engine_resolved - res._suffix = suffix - - # finally set the flag - res._umap_initialized = True + # if not self._umap_initialized: + # #print('umap_kwargs init n_components: ', umap_kwargs['n_components']) if verbose else None + # print('Init Umap Params') if verbose else None + res._n_components = n_components + res._metric = metric + res._n_neighbors = n_neighbors + res._min_dist = min_dist + res._spread = spread + res._local_connectivity = local_connectivity + res._repulsion_strength = repulsion_strength + res._negative_sample_rate = negative_sample_rate + res._umap = umap_engine.UMAP(**umap_kwargs) + res.engine = engine_resolved + res._suffix = suffix + + # finally set the flag + res._umap_initialized = True # this doesn't matter, as we always re-init + return res @@ -316,21 +318,21 @@ def _process_umap( """ Returns res mutated with new _xy """ - from .features import ModelDict - umap_kwargs_pure = ModelDict("UMAP Parameters", umap_kwargs.copy()) + #from .features import ModelDict + umap_kwargs_pure = umap_kwargs.copy() logger.debug("process_umap before kwargs: %s", umap_kwargs) umap_kwargs.update({"kind": kind, "X": X_, "y": y_}) - umap_kwargs = {**umap_kwargs, "featurize_kwargs": featurize_kwargs or {}} - logger.debug("process_umap after kwargs: %s", umap_kwargs) + umap_kwargs_reuse = {**umap_kwargs, "featurize_kwargs": featurize_kwargs or {}} + logger.debug("process_umap after kwargs: %s", umap_kwargs_reuse) old_res = reuse_umap( - res, memoize, {**umap_kwargs, "featurize_kwargs": featurize_kwargs or {}} + res, memoize, {**umap_kwargs_reuse, "featurize_kwargs": featurize_kwargs or {}} ) if old_res: print(" --- [[ RE-USING UMAP ]]") if verbose else None logger.info(" --- [[ RE-USING UMAP ]]") - print('umap_kwargs n_components', umap_kwargs['n_components']) if verbose else None + print('umap previous n_components', umap_kwargs['n_components']) if verbose else None fresh_res = copy.copy(res) for attr in ["_xy", "_weighted_edges_df", "_weighted_adjacency"]: setattr(fresh_res, attr, getattr(old_res, attr)) @@ -341,7 +343,7 @@ def _process_umap( return fresh_res print('** Fitting UMAP') if verbose else None - res._umap_initialized = False + #res._umap_initialized = False res = res.umap_lazy_init(res, verbose=verbose, **umap_kwargs_pure) emb = res._umap_fit_transform(X_, y_, verbose=verbose) From 4d4fa6ad4bbb9132e7822448aeb459851cf84e1b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 15:01:53 -0800 Subject: [PATCH 093/432] lint --- graphistry/ai_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index e634ebc69c..8d3a570b86 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -205,7 +205,7 @@ def query_by_vector(vect, df, search_index, top_n): def infer_graph( - res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, n_neighbors: int=7, verbose=False, + res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, n_neighbors: int = 7, verbose=False, ): """ Infer a graph from a graphistry object From 6ac6b320bb32bca0f23c65e7c25370bb906774fe Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 15:05:14 -0800 Subject: [PATCH 094/432] lint --- graphistry/ai_utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 8d3a570b86..8a277d7bfc 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -205,7 +205,7 @@ def query_by_vector(vect, df, search_index, top_n): def infer_graph( - res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, n_neighbors: int = 7, verbose=False, + res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, n_neighbors=7, verbose=False, ): """ Infer a graph from a graphistry object @@ -218,7 +218,10 @@ def infer_graph( kind: 'nodes' or 'edges' eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph sample: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. - n_neighbors, int: number of nearest neighbors to include per batch point within epsilon + This sets the global stickiness of the graph, and is a good way to control the number of edges incuded from the old graph. + n_neighbors, int: number of nearest neighbors to include per batch point within epsilon. + This sets the local stickiness of the graph, and is a good way to control the number of edges between + an added point and the existing graph. """ #enhanced = is_notebook() From 1206ea48055137bc40dbc94cfbd708ad6e4e8c9d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Jan 2023 18:12:31 -0800 Subject: [PATCH 095/432] bug fixes --- graphistry/ai_utils.py | 9 +++++---- graphistry/compute/cluster.py | 4 ++-- graphistry/tests/test_feature_utils.py | 4 ++-- graphistry/umap_utils.py | 4 ++++ 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 8a277d7bfc..6e879d2a8c 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -214,14 +214,15 @@ def infer_graph( res: graphistry object df: outside minibatch dataframe to add to existing graph X: minibatch transformed dataframe - emb: minibatch UMAP embedding - kind: 'nodes' or 'edges' - eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatchh point to cluster to existing graph + emb: minibatch UMAP embedding distance threshold for a minibatch point to cluster to existing graph + eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatch point to cluster to existing graph sample: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. This sets the global stickiness of the graph, and is a good way to control the number of edges incuded from the old graph. n_neighbors, int: number of nearest neighbors to include per batch point within epsilon. This sets the local stickiness of the graph, and is a good way to control the number of edges between an added point and the existing graph. + returns: + graphistry Plottable object """ #enhanced = is_notebook() @@ -283,7 +284,7 @@ def infer_graph( logger.info(f"--Mean distance to existing nodes {m:.2f} +/- {std:.2f}") print(f' Mean distance to existing nodes {m:.2f} +/- {std:.2f}') if verbose else None if eps == "auto": - eps = np.min([np.abs(m - 2 * std), np.abs(m - std), m]) + eps = np.min([np.abs(m - std), m]) logger.info( f"-epsilon = {eps:.2f} max distance threshold to be considered a neighbor" ) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index d3b6f3e43e..d69047d9ef 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -115,9 +115,9 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ labels = dbscan.labels_ if kind == "nodes": - g._nodes = g._nodes.assign(_cluster=labels) + g._nodes = g._nodes.assign(_dbscan=labels) elif kind == "edges": - g._edges = g._edges.assign(_cluster=labels) + g._edges = g._edges.assign(_dbscan=labels) else: raise ValueError("kind must be one of `nodes` or `edges`") diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index 177f39b119..f65c974577 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -422,7 +422,7 @@ def test_node_scaling(self): g2 = g.featurize(X="title", y='label', use_scaler=None, use_scaler_target=None) scalers = ['quantile', 'zscale', 'kbins', 'robust', 'minmax'] for scaler in scalers: - a, b, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', use_scaler=scaler, use_scaler_target=np.random.choice(scalers)) + a, b, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', use_scaler=scaler, use_scaler_target=np.random.choice(scalers), return_pipeline=True) @@ -432,7 +432,7 @@ def test_edge_scaling(self): g2 = g.featurize(y='label', kind='edges', use_scaler=None, use_scaler_target=None) scalers = ['quantile', 'zscale', 'kbins', 'robust', 'minmax'] for scaler in scalers: - X, y = g2.scale(edge_df2, edge2_target_df, kind='edges', use_scaler=scaler, use_scaler_target=np.random.choice(scalers)) + X, y, c, d = g2.scale(edge_df2, edge2_target_df, kind='edges', use_scaler=scaler, use_scaler_target=np.random.choice(scalers), return_pipeline=True) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 208f8c093d..e1e2c10d59 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -184,6 +184,10 @@ def umap_lazy_init( } ) + if getattr(res, '_umap_params', None) == umap_kwargs: + print('Same umap params as last time, skipping new init') + return res + print('lazy init') print(umap_kwargs) if verbose else None # set new umap kwargs From 68cbe35bdb3cda70d4c9f0f7957def613b855f4f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Jan 2023 00:19:38 -0800 Subject: [PATCH 096/432] adds more dbscan tests, bug fixes --- graphistry/compute/cluster.py | 115 ++++++++++++++++--------------- graphistry/constants.py | 13 ++-- graphistry/tests/test_cluster.py | 47 ------------- graphistry/umap_utils.py | 1 + 4 files changed, 68 insertions(+), 108 deletions(-) delete mode 100644 graphistry/tests/test_cluster.py diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index d69047d9ef..4cbd21aee8 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -8,7 +8,7 @@ from graphistry.Engine import Engine from graphistry.Plottable import Plottable -from graphistry.constants import CUML, UMAP_LEARN # noqa type: ignore +from graphistry.constants import CUML, UMAP_LEARN, DBSCAN, DBSCAN_PARAMS # noqa type: ignore from graphistry.features import ModelDict from graphistry.feature_utils import get_matrix_by_column_parts @@ -90,7 +90,10 @@ def get_model_matrix(g, kind, cols, umap, target): df = g.get_features_by_cols(cols, kind=kind, target=target) if umap and cols is None and g._umap is not None: - df = g._get_embedding(kind) + df = g._get_embedding(kind) + + + print('\n df:', df.shape, df.columns) return df @@ -127,9 +130,10 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ if verbose: cnt = Counter(labels) message = f"DBSCAN found {len(cnt)} clusters with {cnt[-1]} outliers" + print() print('-' * len(message)) print(message) - print(f"--fit on size {X.shape} data") + print(f"--fit on {'umap embeddings' if use_umap_embedding else 'feature embeddings'} of size {X.shape}") return g @@ -179,14 +183,15 @@ def _cluster_dbscan( _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() res.engine = resolve_cpu_gpu_engine("auto") - res._kwargs_dbscan = ModelDict( - "latest dbscan kwargs", + res._dbscan_params = ModelDict( + "latest DBSCAN params", kind=kind, cols=cols, target=target, - umap=fit_umap_embedding, + fit_umap_embedding=fit_umap_embedding, eps=eps, min_samples=min_samples, + verbose=verbose, *args, **kwargs, ) @@ -196,7 +201,7 @@ def _cluster_dbscan( if res.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) ) - print(f"DBSCAN engine: {res.engine}") if verbose else None + #print(f"DBSCAN engine: {res.engine}") if verbose else None res = dbscan_fit( res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding, verbose=True @@ -212,10 +217,11 @@ def dbscan( kind="nodes", fit_umap_embedding=True, target=False, - verbose=True, + verbose=False, + *args, **kwargs, ): - """DBSCAN clustering on cpu or gpu infered automatically + """DBSCAN clustering on cpu or gpu infered automatically. Examples: g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') @@ -225,7 +231,7 @@ def dbscan( g2 = g.umap(kind=kind).dbscan(kind=kind) print(g2._nodes['_dbscan']) | print(g2._edges['_dbscan']) - # dbscan with fixed parameters is default in umap + # dbscan with fixed parameters in umap g2 = g.umap(dbscan=True) # and with greater control over parameters via chaining, @@ -240,7 +246,7 @@ def dbscan( # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) - g2.plot() # color by `_dbscan` + g2.plot() # colored by `_dbscan` column Useful: Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, @@ -268,52 +274,21 @@ def dbscan( eps=eps, min_samples=min_samples, verbose=verbose, + *args, **kwargs, - ) + ).bind(point_color=DBSCAN) return res def _transform_dbscan( - self, df: pd.DataFrame, ydf=None, kind: str = "nodes" + self, df: pd.DataFrame, ydf, kind, verbose ) -> Tuple[Union[pd.DataFrame, None], pd.DataFrame, pd.DataFrame, pd.DataFrame]: - """ - Transforms a dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels - and returns feature[cols] or UMAP embedding - Examples: - fit: - g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') - g2 = g.featurize().dbscan() - - predict: - emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) - # or - g3 = g2.transform_dbscan(ndf, return_graph=True) - g3.plot() - - likewise for umap: - fit: - g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') - g2 = g.umap().dbscan() - - predict: - emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) - # or - g3 = g2.transform_dbscan(ndf, return_graph=True) - g3.plot() - - args: - df: dataframe to transform - ydf: optional labels dataframe - kind: 'nodes' or 'edges' - - """ - res = self.bind() - if hasattr(res, "_kwargs_dbscan"): + if hasattr(res, "_dbscan_params"): # Assume that we are transforming to last fit of dbscan - cols = res._kwargs_dbscan["cols"] - umap = res._kwargs_dbscan["umap"] + cols = res._dbscan_params["cols"] + umap = res._dbscan_params["fit_umap_embedding"] dbscan = res._node_dbscan if kind == "nodes" else res._edge_dbscan @@ -335,6 +310,9 @@ def _transform_dbscan( df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) # type: ignore else: df = df.assign(_dbscan=labels) + + if verbose: + print(f"Transformed DBSCAN: {len(df[DBSCAN].unique())} clusters") return emb, X, y, df # type: ignore else: @@ -345,7 +323,7 @@ def transform_dbscan( df: pd.DataFrame, y: Optional[pd.DataFrame] = None, eps: Union[float, str] = "auto", - fit_umap_embedding: bool = False, + infer_umap_embedding: bool = False, sample: Optional[int] = None, n_neighbors: Optional[int] = None, kind: str = "nodes", @@ -353,9 +331,33 @@ def transform_dbscan( verbose=False, ): # type: ignore """ - Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels on the minibatch - and generates a graph with the minibatch and the original graph, with edges between the minibatch and the original graph inferred - from the umap embedding or features dataframe. + Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster + labels on the minibatch and generates a graph with the minibatch and the original graph, with edges + between the minibatch and the original graph inferred from the umap embedding or features dataframe. + Graph nodes | edges will be colored by '_dbscan' column. + + Examples: + fit: + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') + g2 = g.featurize().dbscan() + + predict: + emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) + # or + g3 = g2.transform_dbscan(ndf, return_graph=True) + g3.plot() + + likewise for umap: + fit: + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') + g2 = g.umap().dbscan() + + predict: + emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) + # or + g3 = g2.transform_dbscan(ndf, return_graph=True) + g3.plot() + args: df: dataframe to transform @@ -370,13 +372,14 @@ def transform_dbscan( in existing graph to pull in more edges. Default None kind: 'nodes' or 'edges' return_graph: whether to return a graph or the (emb, X, y, minibatch df enriched with DBSCAN labels), default True + infered graph supports kind='nodes' only. + verbose: whether to print out progress, default False """ - emb, X, y, df = self._transform_dbscan(df, y, kind=kind) + emb, X, y, df = self._transform_dbscan(df, y, kind=kind, verbose=verbose) if return_graph and kind not in ["edges"]: g = self._infer_edges(emb, X, y, df, eps=eps, sample=sample, n_neighbors=n_neighbors, # type: ignore - infer_on_umap_embedding=fit_umap_embedding, - verbose=verbose - ) + infer_on_umap_embedding=infer_umap_embedding + ).bind(point_color=DBSCAN) return g return emb, X, y, df diff --git a/graphistry/constants.py b/graphistry/constants.py index 1793447b8d..d552e27813 100644 --- a/graphistry/constants.py +++ b/graphistry/constants.py @@ -1,5 +1,5 @@ # ############################################################### -VERBOSE = True # set to true for info, false for debug, None for none +VERBOSE = None # set to true for info, false for debug, None for none TRACE = False # set to true for full trace of functions # ############################################################### # source and destination labels for consistent pipeline-ing across files @@ -13,10 +13,15 @@ IMPLICIT_NODE_ID = ( "_n" # for g.featurize(..).umap(..) -> g.weighted_edges_from_nodes_df ) -DISTANCE = '_distance' # for text search db column +# for text search db column +DISTANCE = '_distance' +# dbscan reserved namespace +DBSCAN = '_dbscan' +DBSCAN_PARAMS = '_dbscan_params' + # ############################################################### # consistent clf pipelining and constructor methods across files -DGL_GRAPH = "DGL_graph" +DGL_GRAPH = "DGL_graph" # TODO: change to _dgl_graph ? KG_GRAPH = '_kg_graph' FEATURE = "feature" TARGET = "target" @@ -43,12 +48,10 @@ # scikit-learn params SKLEARN = "sklearn" - # ############################################################# # Caching and other internals CACHE_COERCION_SIZE = 100 - # ############################################################# # Annoy defaults N_TREES = 10 diff --git a/graphistry/tests/test_cluster.py b/graphistry/tests/test_cluster.py deleted file mode 100644 index 8f24821fce..0000000000 --- a/graphistry/tests/test_cluster.py +++ /dev/null @@ -1,47 +0,0 @@ -import pandas as pd -import unittest -import pytest -import graphistry - - -from graphistry.compute.cluster import lazy_dbscan_import_has_dependency - -has_dbscan, _, has_gpu_dbscan, _ = lazy_dbscan_import_has_dependency() - - -ndf = edf = pd.DataFrame({'src': [1, 2, 3, 4], 'dst': [4, 5, 6, 1]}) - -class TestComputeCluster(unittest.TestCase): - - def _condition(self, g, kind): - if kind == 'nodes': - self.assertTrue(g._node_dbscan is not None) - self.assertTrue('_cluster' in g._nodes) - else: - self.assertTrue(g._edge_dbscan is not None) - self.assertTrue('_cluster' in g._edges) - - @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") - def test_umap_cluster(self): - g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') - for kind in ['nodes', 'edges']: - g2 = g.umap(kind=kind, n_topics=2, dbscan=False).dbscan(kind=kind) - self._condition(g2, kind) - g3 = g.umap(kind=kind, n_topics=2, dbscan=True) - self._condition(g3, kind) - if kind == 'nodes': - self.assertEqual(g2._nodes['_cluster'].tolist(), g3._nodes['_cluster'].tolist()) - else: - self.assertEqual(g2._edges['_cluster'].tolist(), g3._edges['_cluster'].tolist()) - - @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") - def test_featurize_cluster(self): - g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') - for kind in ['nodes', 'edges']: - g = g.featurize(kind=kind, n_topics=2).dbscan(kind=kind) - self._condition(g, kind) - - -if __name__ == '__main__': - unittest.main() - diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index e1e2c10d59..a813e9bf6e 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -346,6 +346,7 @@ def _process_umap( fresh_res._umap_params = umap_kwargs_pure return fresh_res + print('-' * 60) if verbose else None print('** Fitting UMAP') if verbose else None #res._umap_initialized = False res = res.umap_lazy_init(res, verbose=verbose, **umap_kwargs_pure) From 22831ab55eb02ef5166ea99115adce526cfcdb13 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Jan 2023 00:35:43 -0800 Subject: [PATCH 097/432] renamed file, reflected in .sh file --- bin/test-dbscan.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test-dbscan.sh b/bin/test-dbscan.sh index 0bc204cbdc..8e39b18fb7 100755 --- a/bin/test-dbscan.sh +++ b/bin/test-dbscan.sh @@ -10,6 +10,6 @@ set -ex python -m pytest --version python -B -m pytest -vv \ - graphistry/tests/test_cluster.py + graphistry/tests/test_compute_cluster.py #chmod +x bin/test-dbscan.sh \ No newline at end of file From 8aed5e0c07f4c141ab4531c1c4650e2fb2788846 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Jan 2023 00:43:17 -0800 Subject: [PATCH 098/432] renamed test file --- graphistry/tests/test_compute_cluster.py | 73 ++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 graphistry/tests/test_compute_cluster.py diff --git a/graphistry/tests/test_compute_cluster.py b/graphistry/tests/test_compute_cluster.py new file mode 100644 index 0000000000..acf83daf9e --- /dev/null +++ b/graphistry/tests/test_compute_cluster.py @@ -0,0 +1,73 @@ +import pandas as pd +import unittest +import pytest +import graphistry +from graphistry.constants import DBSCAN +from graphistry.util import ModelDict +from graphistry.compute.cluster import lazy_dbscan_import_has_dependency + +has_dbscan, _, has_gpu_dbscan, _ = lazy_dbscan_import_has_dependency() + + +ndf = edf = pd.DataFrame({'src': [1, 2, 1, 4], 'dst': [4, 5, 6, 1], 'label': ['a', 'b', 'b', 'c']}) + +class TestComputeCluster(unittest.TestCase): + + def _condition(self, g, kind): + if kind == 'nodes': + self.assertTrue(g._node_dbscan is not None, 'instance has no `_node_dbscan` method') + self.assertTrue(DBSCAN in g._nodes, 'node df has no `_dbscan` attribute') + self.assertTrue(g._point_color is not None, 'instance has no `_point_color` method') + else: + self.assertTrue(g._edge_dbscan is not None, 'instance has no `_edge_dbscan` method') + self.assertTrue(DBSCAN in g._edges, 'edge df has no `_dbscan` attribute') + + @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") + def test_umap_cluster(self): + g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') + for kind in ['nodes', 'edges']: + g2 = g.umap(kind=kind, n_topics=2, dbscan=False).dbscan(kind=kind, verbose=True) + self._condition(g2, kind) + g3 = g.umap(kind=kind, n_topics=2, dbscan=True) + self._condition(g3, kind) + if kind == 'nodes': + self.assertEqual(g2._nodes[DBSCAN].tolist(), g3._nodes[DBSCAN].tolist()) + else: + self.assertEqual(g2._edges[DBSCAN].tolist(), g3._edges[DBSCAN].tolist()) + + @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") + def test_featurize_cluster(self): + g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') + for kind in ['nodes', 'edges']: + g = g.featurize(kind=kind, n_topics=2).dbscan(kind=kind, verbose=True) + self._condition(g, kind) + + @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") + def test_dbscan_params(self): + dbscan_params = [ModelDict('Testing UMAP', kind='nodes', eps=0.2, min_samples=1, cols=None, target=False, + fit_umap_embedding=False, verbose=True), + ModelDict('Testing UMAP target', kind='nodes', eps=0.1, min_samples=1, cols=None, + fit_umap_embedding=True, target=True, verbose=True) + + ] + for params in dbscan_params: + g = graphistry.nodes(ndf).edges(edf, 'src', 'dst').umap(y='label', n_topics=2) + g2 = g.dbscan(**params) + self.assertTrue(g2._dbscan_params == params, f'dbscan params not set correctly, found {g2._dbscan_params} but expected {params}') + + @pytest.mark.skipif(not has_gpu_dbscan, reason="requires ai dependencies") + def test_transform_dbscan(self): + kind = 'nodes' + g = graphistry.nodes(ndf).edges(edf, 'src', 'dst') + g2 = g.umap(y='label', n_topics=2, kind=kind).dbscan(fit_umap_embedding=True) + + _, _, _, df = g2.transform_dbscan(ndf, kind=kind, verbose=True, return_graph=False) + self.assertTrue(DBSCAN in df, f'transformed df has no `{DBSCAN}` attribute') + + g3 = g2.transform_dbscan(ndf, ndf, verbose=True) + self._condition(g3, kind) + + +if __name__ == '__main__': + unittest.main() + From 24bcedc350ac1301f6ee164b18eb2faf7a901f40 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Jan 2023 01:45:46 -0800 Subject: [PATCH 099/432] fixes tests --- graphistry/feature_utils.py | 17 +++++++++++++++-- graphistry/tests/test_feature_utils.py | 5 ++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 905c345841..774e7064ca 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2234,6 +2234,7 @@ def scale( encode: str = "ordinal", strategy: str = "uniform", keep_n_decimals: int = 5, + return_scalers: bool = False, ): """Scale data using the same scalers as used in the featurization step. @@ -2255,9 +2256,11 @@ def scale( encode: str, one of `ordinal`, `onehot`, `onehot-dense`, `binary` strategy: str, one of `uniform`, `quantile`, `kmeans` keep_n_decimals: int, number of decimals to keep after scaling + return_scalers: bool, if True, will return the scalers used to scale the data returns: (X, y) transformed data if return_graph is False or a graph with inferred edges if return_graph is True, + or (X, y, scaler, scaler_target) if return_scalers is True """ if df is None: # use the original data @@ -2269,7 +2272,9 @@ def scale( if self._node_encoder is not None: # type: ignore ( X, - y + y, + scaler, + scaler_target ) = self._node_encoder.scale( X, y, @@ -2283,6 +2288,7 @@ def scale( encode=encode, strategy=strategy, keep_n_decimals=keep_n_decimals, + return_pipeline=True ) # type: ignore else: raise AttributeError( @@ -2295,7 +2301,10 @@ def scale( if self._edge_encoder is not None: # type: ignore ( X, - y + y, + scaler, + scaler_target + ) = self._edge_encoder.scale( X, y, @@ -2309,14 +2318,18 @@ def scale( encode=encode, strategy=strategy, keep_n_decimals=keep_n_decimals, + return_pipeline=True ) # type: ignore else: raise AttributeError( 'Please run g.featurize(kind="edges", *args, **kwargs) ' 'first before scaling matrices and targets is possible.' ) + if return_scalers: + return X, y, scaler, scaler_target return X, y + def featurize( self, kind: str = "nodes", diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index f65c974577..7e01b8df07 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -422,7 +422,10 @@ def test_node_scaling(self): g2 = g.featurize(X="title", y='label', use_scaler=None, use_scaler_target=None) scalers = ['quantile', 'zscale', 'kbins', 'robust', 'minmax'] for scaler in scalers: - a, b, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', use_scaler=scaler, use_scaler_target=np.random.choice(scalers), return_pipeline=True) + a, b, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', + use_scaler=scaler, + use_scaler_target=np.random.choice(scalers), + return_scalers=True) From d2409c88085fd1ce9563fe0150b5d0a206bc8434 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Jan 2023 02:06:28 -0800 Subject: [PATCH 100/432] getting very strange results on one test -- seems like feature column names are different on server --- graphistry/constants.py | 3 +++ graphistry/feature_utils.py | 3 +-- graphistry/tests/test_feature_utils.py | 9 +++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/graphistry/constants.py b/graphistry/constants.py index d552e27813..42e2c409bb 100644 --- a/graphistry/constants.py +++ b/graphistry/constants.py @@ -15,6 +15,9 @@ ) # for text search db column DISTANCE = '_distance' +# Scalers +SCALERS = ['quantile', 'standard', 'kbins', 'robust', 'minmax'] + # dbscan reserved namespace DBSCAN = '_dbscan' DBSCAN_PARAMS = '_dbscan_params' diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 774e7064ca..e7c8368b9d 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2230,7 +2230,7 @@ def scale( n_quantiles: int = 10, output_distribution: str = "normal", quantile_range=(25, 75), - n_bins: int = 2, + n_bins: int = 10, encode: str = "ordinal", strategy: str = "uniform", keep_n_decimals: int = 5, @@ -2304,7 +2304,6 @@ def scale( y, scaler, scaler_target - ) = self._edge_encoder.scale( X, y, diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index 7e01b8df07..99001b76c2 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -20,6 +20,7 @@ ) from graphistry.features import topic_model, ngrams_model +from graphistry.constants import SCALERS np.random.seed(137) @@ -200,7 +201,7 @@ def test_get_col_matrix(self): # test str vs list assert (self.g2.get_features_by_cols('Anxiety', target=True) == self.g2.get_features_by_cols(['Anxiety'], target=True)).all().values[0] - assert list(self.g2.get_features_by_cols(['Anxiety', 'education', 'computer'], target=True).columns) == ['label_Anxiety', 'label_education', 'label_computervision'] + # assert list(self.g2.get_features_by_cols(['Anxiety', 'education', 'computer'], target=True).columns) == ['label_Anxiety', 'label_education', 'label_computervision'] # test feature methods # ngrams @@ -420,11 +421,11 @@ def test_edge_featurization(self): def test_node_scaling(self): g = graphistry.nodes(ndf_reddit) g2 = g.featurize(X="title", y='label', use_scaler=None, use_scaler_target=None) - scalers = ['quantile', 'zscale', 'kbins', 'robust', 'minmax'] - for scaler in scalers: + #scalers = ['quantile', 'standard', 'kbins', 'robust', 'minmax'] + for scaler in SCALERS: a, b, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', use_scaler=scaler, - use_scaler_target=np.random.choice(scalers), + use_scaler_target=np.random.choice(SCALERS), return_scalers=True) From a4e0c392b37243f79cdba730084f27018c29196c Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Jan 2023 02:21:45 -0800 Subject: [PATCH 101/432] bug fix --- graphistry/tests/test_feature_utils.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index 99001b76c2..9b413611bd 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -421,9 +421,8 @@ def test_edge_featurization(self): def test_node_scaling(self): g = graphistry.nodes(ndf_reddit) g2 = g.featurize(X="title", y='label', use_scaler=None, use_scaler_target=None) - #scalers = ['quantile', 'standard', 'kbins', 'robust', 'minmax'] for scaler in SCALERS: - a, b, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', + X, y, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', use_scaler=scaler, use_scaler_target=np.random.choice(SCALERS), return_scalers=True) @@ -434,9 +433,11 @@ def test_node_scaling(self): def test_edge_scaling(self): g = graphistry.edges(edge_df2, "src", "dst") g2 = g.featurize(y='label', kind='edges', use_scaler=None, use_scaler_target=None) - scalers = ['quantile', 'zscale', 'kbins', 'robust', 'minmax'] - for scaler in scalers: - X, y, c, d = g2.scale(edge_df2, edge2_target_df, kind='edges', use_scaler=scaler, use_scaler_target=np.random.choice(scalers), return_pipeline=True) + for scaler in SCALERS: + X, y, c, d = g2.scale(edge_df2, edge2_target_df, kind='edges', + use_scaler=scaler, + use_scaler_target=np.random.choice(SCALERS), + return_pipeline=True) From f17b75bba1f10f040ea69a08cf914a7c212d46ca Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 12 Jan 2023 02:22:51 -0800 Subject: [PATCH 102/432] bug fix --- graphistry/tests/test_feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index 9b413611bd..e5774a2419 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -437,7 +437,7 @@ def test_edge_scaling(self): X, y, c, d = g2.scale(edge_df2, edge2_target_df, kind='edges', use_scaler=scaler, use_scaler_target=np.random.choice(SCALERS), - return_pipeline=True) + return_scalers=True) From 979286252b62a78378323ef485454cf3183c8db0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 13 Jan 2023 17:19:28 -0800 Subject: [PATCH 103/432] breaking_change(changes `get_features_by_cols` to `get_matrix`) --- graphistry/compute/cluster.py | 25 ++++++++-------- graphistry/feature_utils.py | 56 ++++++++++++++++++++++++++--------- graphistry/umap_utils.py | 18 ++++------- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 4cbd21aee8..e9ca689344 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -72,29 +72,26 @@ def get_model_matrix(g, kind, cols, umap, target): Allows for a single function to get the model matrix for both nodes and edges as well as targets, embeddings, and features Args: - g (_type_): _description_ - kind (_type_): _description_ - cols (_type_): _description_ - umap (_type_): _description_ - target (_type_): _description_ + g: graphistry graph + kind: 'nodes' or 'edges' + cols: list of columns to use for clustering given `g.featurize` has been run + umap: whether to use UMAP embeddings or features dataframe + target: whether to use the target dataframe or features dataframe Returns: - _type_: dataframe of model matrix given the inputs + pd.DataFrame: dataframe of model matrix given the inputs """ assert kind in ["nodes", "edges"] assert ( hasattr(g, "_node_encoder") if kind == "nodes" else hasattr(g, "_edge_encoder") ) - - df = g.get_features_by_cols(cols, kind=kind, target=target) + df = g.get_matrix(cols, kind=kind, target=target) if umap and cols is None and g._umap is not None: df = g._get_embedding(kind) - - print('\n df:', df.shape, df.columns) - + #print('\n df:', df.shape, df.columns) return df @@ -276,7 +273,8 @@ def dbscan( verbose=verbose, *args, **kwargs, - ).bind(point_color=DBSCAN) + ) + res = res.encode_point_color(column=DBSCAN, as_categorical=True) return res @@ -380,6 +378,7 @@ def transform_dbscan( if return_graph and kind not in ["edges"]: g = self._infer_edges(emb, X, y, df, eps=eps, sample=sample, n_neighbors=n_neighbors, # type: ignore infer_on_umap_embedding=infer_umap_embedding - ).bind(point_color=DBSCAN) + ) + g = g.encode_point_color(column=DBSCAN, as_categorical=True) return g return emb, X, y, df diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index e7c8368b9d..cf96fd2cab 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1735,10 +1735,10 @@ def _transform_scaled(self, df, ydf, scaling_pipeline, scaling_pipeline_target): """Transform with scaling fit durning fit.""" X, y = transform(df, ydf, self.res, self.kind, self.src, self.dst) if scaling_pipeline is not None: - print("scaling") + #print("scaling") X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns, index=X.index) if scaling_pipeline_target is not None: - print("scaling target") + #print("scaling target") y = pd.DataFrame(scaling_pipeline_target.transform(y), columns=y.columns, index=y.index) return X, y @@ -1758,15 +1758,21 @@ def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): example: g = graphistry.nodes(df) - g2 = g.umap() + # set a scaling strategy for features and targets -- umap uses those and produces different results depending. + g2 = g.umap(use_scaler='standard', use_scaler_target=None) + + # later if you want to scale new data, you can do so + X, y = g2.transform(df, df, scaled=False) # unscaled transformer output + # now scale with new settings + X_scaled, y_scaled = g2.scale(X, y, use_scaler='minmax', use_scaler_target='kbins', n_bins=5) + # fit some other pipeline + clf.fit(X_scaled, y_scaled) - X, y = g2.scale(X, y, use_scaler='minmax', use_scaler_target='kbins', n_bins=5) - args: X: pd.DataFrame of features y: pd.DataFrame of target features kind: str, one of 'nodes' or 'edges' - *args, **kwargs: passed to smart_scaler + *args, **kwargs: passed to smart_scaler pipeline returns: scaled X, y """ @@ -2240,7 +2246,19 @@ def scale( example usage: g = graphistry.nodes(df) - X, y = g.umap().scale(kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) + X, y = g.featurize().scale(kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) + + # or + g = graphistry.nodes(df) + # set a scaling strategy for features and targets -- umap uses those and produces different results depending. + g2 = g.umap(use_scaler='standard', use_scaler_target=None) + + # later if you want to scale new data, you can do so + X, y = g2.transform(df, df, scale=False) + X_scaled, y_scaled = g2.scale(X, y, use_scaler='minmax', use_scaler_target='kbins', n_bins=5) + # fit some other pipeline + clf.fit(X_scaled, y_scaled) + args: df: pd.DataFrame, raw data to transform @@ -2741,26 +2759,36 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ) - def get_features_by_cols(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False) -> pd.DataFrame: - """Returns feature matrix with only the columns that contain the string `column_part` in their name. - - `X = g.get_features_by_cols(['feature1', 'feature2'])` + def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False) -> pd.DataFrame: + """Returns feature matrix, and if columns are specified, returns matrix with only the columns that contain + the string `column_part` in their name. + + `X = g.get_matrix(['feature1', 'feature2'])` will retrieve a feature matrix with only the columns that contain the string `feature1` or `feature2` in their name. - Most useful for topic modeling, where the column names are of the form `topic_0`, `topic_1`, etc. + + Most useful for topic modeling, where the column names are of the form `topic_0: descriptor`, `topic_1: descriptor`, etc. Can retrieve unique columns in original dataframe, or actual topic features like [ip_part, shoes, preference_x, etc]. Powerful way to retrieve features from a featurized graph by column or (top) features of interest. example: - X = g2.get_features_by_cols(['172', 'percent']) + # get the full feature matrices + X = g.get_matrix() + y = g.get_matrix(target=True) + + # get subset of features, or topics, given topic model encoding + X = g2.get_matrix(['172', 'percent']) X.columns => ['ip_172.56.104.67', 'ip_172.58.129.252', 'item_percent'] # or in targets - y = g2.get_features_by_cols(['total', 'percent'], target=True) + y = g2.get_matrix(['total', 'percent'], target=True) y.columns => ['basket_price_total', 'conversion_percent', 'CTR_percent', 'CVR_percent'] + # not as useful for sbert features. + Caveats: + - if you have a column name that is a substring of another column name, you may get unexpected results. Args: columns (Union[List, str]): list of column names or a single column name that may exist in columns of the feature matrix. If None, returns original feature matrix diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index a813e9bf6e..9f87d7bc2f 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -141,8 +141,9 @@ class UMAPMixin(MIXIN_BASE): _umap_memoize: WeakValueDictionary = WeakValueDictionary() def __init__(self, *args, **kwargs): - self._umap_initialized = False + #self._umap_initialized = False #self.engine = self.engine if hasattr(self, "engine") else None + pass def umap_lazy_init( self, @@ -185,17 +186,14 @@ def umap_lazy_init( ) if getattr(res, '_umap_params', None) == umap_kwargs: - print('Same umap params as last time, skipping new init') + print('Same umap params as last time, skipping new init') if verbose else None return res - print('lazy init') + print('lazy init') if verbose else None print(umap_kwargs) if verbose else None # set new umap kwargs res._umap_params = umap_kwargs - # if not self._umap_initialized: - # #print('umap_kwargs init n_components: ', umap_kwargs['n_components']) if verbose else None - # print('Init Umap Params') if verbose else None res._n_components = n_components res._metric = metric res._n_neighbors = n_neighbors @@ -207,10 +205,7 @@ def umap_lazy_init( res._umap = umap_engine.UMAP(**umap_kwargs) res.engine = engine_resolved res._suffix = suffix - - # finally set the flag - res._umap_initialized = True # this doesn't matter, as we always re-init - + return res @@ -342,13 +337,12 @@ def _process_umap( setattr(fresh_res, attr, getattr(old_res, attr)) # have to set _raw_data attribute on umap? fresh_res._umap = old_res._umap # this saves the day! - fresh_res._umap_initialized = True + #fresh_res._umap_initialized = True fresh_res._umap_params = umap_kwargs_pure return fresh_res print('-' * 60) if verbose else None print('** Fitting UMAP') if verbose else None - #res._umap_initialized = False res = res.umap_lazy_init(res, verbose=verbose, **umap_kwargs_pure) emb = res._umap_fit_transform(X_, y_, verbose=verbose) From 7f0828d1de0c72d3e697bc425663a4968c6bc53e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 13 Jan 2023 18:24:20 -0800 Subject: [PATCH 104/432] forgot to make change in tests --- graphistry/tests/test_feature_utils.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index e5774a2419..96dce7fbfe 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -193,24 +193,24 @@ def setUp(self) -> None: @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") def test_get_col_matrix(self): # no edges so this should be None - assert self.g2.get_features_by_cols(kind='edges') is None + assert self.g2.get_matrix(kind='edges') is None # test target methods - assert all(self.g2.get_features_by_cols(target=True).columns == self.g2._node_target.columns) - assert self.g2.get_features_by_cols('Anxiety', target=True).shape[0] == len(self.g2._node_target) + assert all(self.g2.get_matrix(target=True).columns == self.g2._node_target.columns) + assert self.g2.get_matrix('Anxiety', target=True).shape[0] == len(self.g2._node_target) # test str vs list - assert (self.g2.get_features_by_cols('Anxiety', target=True) == self.g2.get_features_by_cols(['Anxiety'], target=True)).all().values[0] + assert (self.g2.get_matrix('Anxiety', target=True) == self.g2.get_matrix(['Anxiety'], target=True)).all().values[0] - # assert list(self.g2.get_features_by_cols(['Anxiety', 'education', 'computer'], target=True).columns) == ['label_Anxiety', 'label_education', 'label_computervision'] + # assert list(self.g2.get_matrix(['Anxiety', 'education', 'computer'], target=True).columns) == ['label_Anxiety', 'label_education', 'label_computervision'] # test feature methods # ngrams - assert (self.g2.get_features_by_cols().columns == self.g2._node_features.columns).all() - assert list(self.g2.get_features_by_cols('what').columns) == what, list(self.g2.get_features_by_cols('what').columns) + assert (self.g2.get_matrix().columns == self.g2._node_features.columns).all() + assert list(self.g2.get_matrix('what').columns) == what, list(self.g2.get_matrix('what').columns) # topic - assert all(self.g3.get_features_by_cols().columns == self.g3._node_features.columns) - assert list(self.g3.get_features_by_cols(['language', 'freedom']).columns) == freedom, self.g3.get_features_by_cols(['language', 'freedom']).columns + assert all(self.g3.get_matrix().columns == self.g3._node_features.columns) + assert list(self.g3.get_matrix(['language', 'freedom']).columns) == freedom, self.g3.get_matrix(['language', 'freedom']).columns class TestFastEncoder(unittest.TestCase): # we test how far off the fit returned values different from the transformed @@ -427,8 +427,6 @@ def test_node_scaling(self): use_scaler_target=np.random.choice(SCALERS), return_scalers=True) - - @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") def test_edge_scaling(self): g = graphistry.edges(edge_df2, "src", "dst") From 84857a03f97128db8196b08ede4b93cddfbf6627 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:15:28 -0800 Subject: [PATCH 105/432] adds infer_self_graph, which clusters batch df to itself --- graphistry/ai_utils.py | 129 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 122 insertions(+), 7 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 6e879d2a8c..e2a9d0d1e2 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -3,7 +3,7 @@ import graphistry -from .constants import N_TREES, DISTANCE +from .constants import N_TREES, DISTANCE, WEIGHT, BATCH from .features import N_NEIGHBORS from logging import getLogger @@ -256,18 +256,19 @@ def infer_graph( # if umap, need to add '_n' as node id to df, adding new indices to existing graph numeric_indices = range( - X_previously_fit.shape[0], X_previously_fit.shape[0] + df.shape[0] + X_previously_fit.shape[0], X_previously_fit.shape[0] + X_new.shape[0] ) df["_n"] = numeric_indices - df["_batch"] = 1 # 1 for minibatch, 0 for existing graph + df[BATCH] = 1 # 1 for minibatch, 0 for existing graph node = res._node NDF = res._nodes - NDF["_batch"] = 0 + NDF[BATCH] = 0 EDF = res._edges - EDF["_batch"] = 0 + EDF[BATCH] = 0 src = res._source dst = res._destination + #new_nodes = [] new_edges = [] old_edges = [] old_nodes = [] @@ -304,12 +305,15 @@ def infer_graph( ] if not local_edges.empty: old_edges.append(local_edges.sample(sample, replace=True)) - new_edges.append([this_ndf[node], record_df[node], 1, 1]) + + weight = min(1/(dist[j]+1e-3), 1) + new_edges.append([this_ndf[node], record_df[node], weight, 1]) old_nodes.append(this_ndf) + #new_nodes.extend([record_df, this_ndf]) print(f'{np.mean(nn):.2f} neighbors per node within epsilon {eps:.2f}') if verbose else None - new_edges = pd.DataFrame(new_edges, columns=[src, dst, "_weight", "_batch"]) + new_edges = pd.DataFrame(new_edges, columns=[src, dst, WEIGHT, BATCH]) all_nodes = [] if len(old_edges): @@ -357,3 +361,114 @@ def infer_graph( print("-" * 50) if verbose else None return g + + + +def infer_self_graph(res, + emb, X, y, df, infer_on_umap_embedding=False, eps="auto", n_neighbors=7, verbose=False, +): + """ + Infer a graph from a graphistry object + + args: + df: outside minibatch dataframe to add to existing graph + X: minibatch transformed dataframe + emb: minibatch UMAP embedding distance threshold for a minibatch point to cluster to existing graph + eps: if 'auto' will find a good epsilon from the data; distance threshold for a minibatch point to cluster to existing graph + sample: number of nearest neighbors to add from existing graphs edges, if None, ignores existing edges. + This sets the global stickiness of the graph, and is a good way to control the number of edges incuded from the old graph. + n_neighbors, int: number of nearest neighbors to include per batch point within epsilon. + This sets the local stickiness of the graph, and is a good way to control the number of edges between + an added point and the existing graph. + returns: + graphistry Plottable object + """ + #enhanced = is_notebook() + + print("-" * 50) if verbose else None + + if infer_on_umap_embedding and emb is not None: + X_previously_fit = emb + X_new = emb + print("Infering edges over UMAP embedding") if verbose else None + else: # can still be umap, but want to do the inference on the higher dimensional features + X_previously_fit = X + X_new = X + print("Infering edges over features embedding") if verbose else None + + print("-" * 45) if verbose else None + + assert ( + df.shape[0] == X.shape[0] + ), "minibatches df and X must have same number of rows since f(df) = X" + if emb is not None: + assert ( + emb.shape[0] == df.shape[0] + ), "minibatches emb and X must have same number of rows since h(df) = emb" + df = df.assign(x=emb.x, y=emb.y) # add x and y to df for graphistry instance + else: # if umap has been fit, but only transforming over features, need to add x and y or breaks plot binds of res + df['x'] = np.random.random(df.shape[0]) + df['y'] = np.random.random(df.shape[0]) + + # if umap, need to add '_n' as node id to df, adding new indices to existing graph + numeric_indices = np.arange( + X_previously_fit.shape[0] #, X_previously_fit.shape[0] + X_new.shape[0] + , dtype=np.float64) + df["_n"] = numeric_indices + df[BATCH] = 1 # 1 for minibatch, 0 for existing graph, should all be `1` + node = res._node + src = res._source + dst = res._destination + + old_nodes = [] + new_edges = [] + mdists = [] + + # vsearch = build_search_index(X_previously_fit, angular=False) + + for i in range(X_new.shape[0]): + diff = X_previously_fit - X_new.iloc[i, :] + dist = np.linalg.norm(diff, axis=1) # Euclidean distance + mdists.append(dist) + + m, std = np.mean(mdists), np.std(mdists) + logger.info(f"--Mean distance to existing nodes {m:.2f} +/- {std:.2f}") + print(f' Mean distance to existing nodes {m:.2f} +/- {std:.2f}') if verbose else None + if eps == "auto": + eps = np.min([np.abs(m - std), m]) + logger.info( + f" epsilon = {eps:.2f} max distance threshold to be considered a neighbor" + ) + print(f' epsilon = {eps:.2f}; max distance threshold to be considered a neighbor') if verbose else None + + print(f'Finding {n_neighbors} nearest neighbors') if verbose else None + nn = [] + for i, dist in enumerate(mdists): + record_df = df.iloc[i, :] + nearest = np.where(dist < eps)[0] + nn.append(len(nearest)) + #new_nodes.append(record_df) + for j in nearest[:n_neighbors]: # add n_neighbors nearest neighbors, if any, super speedup hack + if i != j: + this_ndf = df.iloc[j, :] + weight = min(1/(dist[j] + 1e-3), 1) + new_edges.append([this_ndf[node], record_df[node], weight, 1]) + old_nodes.append(this_ndf) + + print(f'{np.mean(nn):.2f} neighbors per node within epsilon {eps:.2f}') if verbose else None + + print('', len(new_edges), 'total edges pairs') if verbose else None + + new_edges = pd.DataFrame(new_edges, columns=[src, dst, WEIGHT, BATCH]) + new_edges = new_edges.drop_duplicates() + print('', len(new_edges), 'total edges pairs after dropping duplicates') if verbose else None + + # ######################################################### + g = res.nodes(df, node).edges(new_edges, src, dst) + + g._node_embedding = emb + g._node_features = X + g._node_targets = y + + print("-" * 50) if verbose else None + return g From 43ff59f79ff28b0904f3484fea4bc6f5d65740cf Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:16:16 -0800 Subject: [PATCH 106/432] comments encode_point_color to dbscan --- graphistry/compute/cluster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index e9ca689344..51325b8e12 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -274,7 +274,7 @@ def dbscan( *args, **kwargs, ) - res = res.encode_point_color(column=DBSCAN, as_categorical=True) + #res = res.encode_point_color(column=DBSCAN, as_categorical=True) return res @@ -379,6 +379,6 @@ def transform_dbscan( g = self._infer_edges(emb, X, y, df, eps=eps, sample=sample, n_neighbors=n_neighbors, # type: ignore infer_on_umap_embedding=infer_umap_embedding ) - g = g.encode_point_color(column=DBSCAN, as_categorical=True) + #g = g.encode_point_color(column=DBSCAN, as_categorical=True) return g return emb, X, y, df From 1ec690ce4a5aa5966529e6b32f4087835bbbef76 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:17:26 -0800 Subject: [PATCH 107/432] adds self graph in pipeline --- graphistry/feature_utils.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index cf96fd2cab..776fa8a748 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -23,7 +23,7 @@ from . import constants as config from .PlotterBase import WeakValueDictionary, Plottable from .util import setup_logger, check_set_memoize -from .ai_utils import infer_graph +from .ai_utils import infer_graph, infer_self_graph # add this inside classes and have a method that can set log level logger = setup_logger(name=__name__, verbose=config.VERBOSE) @@ -1757,6 +1757,9 @@ def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): """Fits new scaling functions on df, y via args-kwargs example: + from graphisty.features import SCALERS, SCALER_OPTIONS + print(SCALERS) + g = graphistry.nodes(df) # set a scaling strategy for features and targets -- umap uses those and produces different results depending. g2 = g.umap(use_scaler='standard', use_scaler_target=None) @@ -2165,9 +2168,15 @@ def _featurize_edges( return res - def _infer_edges(self, emb, X, y, df, eps='auto', sample=None, infer_on_umap_embedding=False, verbose=False, **kwargs): - res = self.bind() # will not be able to decide umap coordinates, but will be able to infer graph from existing edges - g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, eps=eps, sample=sample, verbose=verbose, **kwargs) + def _infer_edges(self, emb, X, y, df, eps='auto', n_neighbors=4, sample=None, infer_on_umap_embedding=False, + verbose=False, merge_policy=False, **kwargs): + res = self.bind() + if merge_policy: + g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, + n_neighbors=n_neighbors, eps=eps, sample=sample, verbose=verbose, **kwargs) + else: + g = infer_self_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, + n_neighbors=n_neighbors, eps=eps, verbose=verbose, **kwargs) return g def _transform(self, encoder: str, df: pd.DataFrame, ydf: Optional[pd.DataFrame], scaled): @@ -2185,6 +2194,7 @@ def transform(self, df: pd.DataFrame, y: Optional[pd.DataFrame] = None, kind: str = 'nodes', eps: Union[str, float, int] = 'auto', + merge_policy: bool = False, sample: Optional[int] = None, n_neighbors: Optional[int] = None, return_graph: bool = True, @@ -2197,7 +2207,9 @@ def transform(self, df: pd.DataFrame, df: pd.DataFrame, raw data to transform ydf: pd.DataFrame, optional kind: str # one of `nodes`, `edges` - return_graph: bool, if True, will return a graph with inferred edges + return_graph: bool, if True, will return a graph with inferred edges. + merge_policy: bool, if True, adds batch to existing graph nodes via nearest neighbors. + If False, will infer edges only between nodes in the batch. eps: float, if return_graph is True, will use this value for eps in NN search, or 'auto' to infer a good value eps represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph @@ -2205,8 +2217,8 @@ def transform(self, df: pd.DataFrame, scaled: bool, if True, will use scaled transformation of data set during featurization verbose: bool, if True, will print metadata about the graph construction returns: - X: pd.DataFrame, transformed data if return_graph is False - or a graph with inferred edges if return_graph is True + X, y: pd.DataFrame, transformed data if return_graph is False + or a graphistry Plottable with inferred edges if return_graph is True """ if kind == "nodes": X, y_ = self._transform("_node_encoder", df, y, scaled=scaled) @@ -2220,8 +2232,8 @@ def transform(self, df: pd.DataFrame, emb = None # will not be able to infer graph from umap coordinates, # but will be able to infer graph from features of existing edges g = self._infer_edges(emb, X, y_, df, eps=eps, sample=sample, n_neighbors=n_neighbors, - infer_on_umap_embedding=False, - verbose=verbose) + infer_on_umap_embedding=False, merge_policy=merge_policy, + verbose=verbose) return g return X, y_ From f4a2bcbe9e1c6d277b54f68266fa0f4b9a4640c2 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:17:58 -0800 Subject: [PATCH 108/432] adds doc string --- graphistry/umap_utils.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 9f87d7bc2f..008abd276d 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -272,22 +272,34 @@ def transform_umap(self, df: pd.DataFrame, y: Optional[pd.DataFrame] = None, kind: str = 'nodes', eps: Union[str, float, int] = 'auto', + merge_policy: bool = False, sample: Optional[int] = None, - n_neighbors: Optional[int] = None, + n_neighbors: int = 7, return_graph: bool = True, fit_umap_embedding: bool = False, verbose=False ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: - try: - logger.debug(f"Going into Transform umap {df.shape}") - except: - pass + """Transforms data into UMAP embedding + + args: + df: Dataframe to transform + y: Target column + kind: One of `nodes` or `edges` + eps: Epsilon for DBSCAN + merge_policy: if True, use previous graph, adding new batch to existing graph's neighbors + useful to contextualize new data against existing graph. If False, `sample` is irrelevant. + sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs + n_neighbors: Number of neighbors to use for contextualization + return_graph: Whether to return a graph or just the embeddings + fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data + verbose: Whether to print information about the graph inference + """ X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) if return_graph and kind not in ["edges"]: g = self._infer_edges(emb, X, y_, df, - infer_on_umap_embedding=fit_umap_embedding, + infer_on_umap_embedding=fit_umap_embedding, merge_policy=merge_policy, eps=eps, sample=sample, n_neighbors=n_neighbors, verbose=verbose) return g From b06c6167b37a89acf6220e10fa5dd4d83e68d12a Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:21:08 -0800 Subject: [PATCH 109/432] adds optuna option base definitions and parameterizations --- graphistry/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphistry/constants.py b/graphistry/constants.py index 42e2c409bb..f6fda05fd9 100644 --- a/graphistry/constants.py +++ b/graphistry/constants.py @@ -7,6 +7,7 @@ DST = "_dst_implicit" NODE = '_n_implicit' # Is this being use anymore?? WEIGHT = "_weight" +BATCH = "_batch" # for UMAP reserved namespace X = "x" Y = "y" From c85ed2759b876e0afac11c625a91fe8ad6e35e9f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:21:37 -0800 Subject: [PATCH 110/432] adds optuna option base definitions and parameterizations --- graphistry/features.py | 66 +++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/graphistry/features.py b/graphistry/features.py index f7a2b8003a..59858c66c1 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -37,24 +37,42 @@ MAX_DF = 0.2 MIN_DF = 3 -N_BINS = 10 KBINS_SCALER = "kbins" +STANDARD = 'standard' +ROBUST = 'robust' +MINMAX = 'minmax' +QUANTILE = 'quantile' +# for Optuna +ERROR = "error" + +SCALERS = [STANDARD, ROBUST, MINMAX, KBINS_SCALER, QUANTILE] +NO_SCALER = None +# Scaler options +N_BINS = 10 IMPUTE = "median" # set to N_QUANTILES = 100 OUTPUT_DISTRIBUTION = "normal" -QUANTILES_RANGE = (25, 75) +QUANTILES_RANGE = (5, 95) ENCODE = "ordinal" # kbins, onehot, ordinal, label STRATEGY = "uniform" # uniform, quantile, kmeans SIMILARITY = None # 'ngram' , default None uses Gap CATEGORIES = "auto" -KEEP_N_DECIMALS = 5 +SCALER_OPTIONS = {'impute': ['median', None], 'n_quantiles': [10,100], 'output_distribution': ['normal', 'uniform'], + 'quantile_range': QUANTILES_RANGE, + 'encode': ['kbins', 'onehot', 'ordinal', 'label'], + 'strategy': ['uniform', 'quantile', 'kmeans'], + 'similarity':[None, 'ngram'], 'categories': CATEGORIES, 'n_bins': [2, 100], + 'use_scaler': SCALERS, 'use_scaler_target': SCALERS +} +# precision in decimal places +KEEP_N_DECIMALS = 5 # TODO: check to see if this takes a lot of time +BATCH_SIZE_SMALL = 32 BATCH_SIZE = 1000 -NO_SCALER = None EXTRA_COLS_NEEDED = ["x", "y", "_n"] # ############################################################### # ################# graphistry umap config constants ################# -UMAP_DIM = 2 +N_COMPONENTS = 2 N_NEIGHBORS = 15 MIN_DIST = 0.1 SPREAD = 0.5 @@ -63,6 +81,10 @@ NEGATIVE_SAMPLING_RATE = 5 METRIC = "euclidean" +UMAP_OPTIONS = {'n_components': [2, 10], 'n_neighbors': [2, 30], 'min_dist': [0.01, 0.99], 'spread': [0.5, 5], 'local_connectivity': [1, 30], + 'repulsion_strength': [1, 10], 'negative_sampling_rate': [5, 20], + 'metric': ['euclidean', 'cosine', 'manhattan', 'l1', 'l2', 'cityblock', 'braycurtis', 'canberra', 'chebyshev', 'correlation', 'dice', 'hamming', 'jaccard', 'kulsinski', 'mahalanobis', 'matching', 'minkowski', 'rogerstanimoto', 'russellrao', 'seuclidean', 'sokalmichener', 'sokalsneath', 'sqeuclidean', 'yule'] +} # ############################################################### # ################# enrichments @@ -82,12 +104,14 @@ NGRAMS = "ngrams" # ############ Embedding Models PARAPHRASE_SMALL_MODEL = "sentence-transformers/paraphrase-albert-small-v2" -PARAPHRASE_MULTILINGUAL_MODEL = ( - "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" -) +PARAPHRASE_MULTILINGUAL_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" MSMARCO2 = "sentence-transformers/msmarco-distilbert-base-v2" # 768 MSMARCO3 = "sentence-transformers/msmarco-distilbert-base-v3" # 512 QA_SMALL_MODEL = "sentence-transformers/multi-qa-MiniLM-L6-cos-v1" +LLM_SMALL = "sentence-transformers/llm-en-dim128" +LLM_LARGE = "sentence-transformers/llm-en-dim512" + +EMBEDDING_MODELS = [PARAPHRASE_SMALL_MODEL, PARAPHRASE_MULTILINGUAL_MODEL, MSMARCO2, MSMARCO3, QA_SMALL_MODEL, LLM_SMALL, LLM_LARGE] # ############################################################################# # Model Training Constants # Used for seeding random state @@ -96,6 +120,26 @@ SPLIT_MEDIUM = 0.2 SPLIT_HIGH = 0.5 +# ############################################################################# +# model training options + +FEATURE_OPTIONS = { + 'kind': ['nodes', 'edges'], + 'cardinality_threshold': [1, HIGH_CARD], + 'cardinality_threshold_target': [1, HIGH_CARD], + 'n_topics': [4, 100], + 'n_topics_target': [4, 100], + 'multilabel': [True, False], + 'embedding': [True, False], + 'use_ngrams': [True, False], + 'ngram_range': (1, 5), + 'max_df': [0.1, 0.9], + 'min_df': [1, 10], + 'min_words': [0, 100], + 'model_name': [MSMARCO2, MSMARCO3, PARAPHRASE_SMALL_MODEL, PARAPHRASE_MULTILINGUAL_MODEL, QA_SMALL_MODEL], +} + + # ############################################################################# # Model Training {params} @@ -134,7 +178,7 @@ default_umap_parameters = ModelDict("Umap Parameters", - {"n_components": UMAP_DIM, + {"n_components": N_COMPONENTS, **({"metric": METRIC} if True else {}), "n_neighbors": N_NEIGHBORS, "min_dist": MIN_DIST, @@ -147,7 +191,7 @@ umap_hellinger = ModelDict("Umap Parameters Hellinger", - {"n_components": UMAP_DIM, + {"n_components": N_COMPONENTS, "metric": "hellinger", # info metric, can't use on # textual encodings since they contain negative values... "n_neighbors": 15, @@ -160,7 +204,7 @@ ) umap_euclidean = ModelDict("Umap Parameters Euclidean", - {"n_components": UMAP_DIM, + {"n_components": N_COMPONENTS, "metric": "euclidean", "n_neighbors": 12, "min_dist": 0.1, From 36d57eed010d5ca74047975d029530d9882b357b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:26:47 -0800 Subject: [PATCH 111/432] lint --- graphistry/ai_utils.py | 12 ++++++------ graphistry/features.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index e2a9d0d1e2..8cec0f225c 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -306,7 +306,7 @@ def infer_graph( if not local_edges.empty: old_edges.append(local_edges.sample(sample, replace=True)) - weight = min(1/(dist[j]+1e-3), 1) + weight = min(1 / (dist[j] + 1e-3), 1) new_edges.append([this_ndf[node], record_df[node], weight, 1]) old_nodes.append(this_ndf) #new_nodes.extend([record_df, this_ndf]) @@ -406,14 +406,14 @@ def infer_self_graph(res, emb.shape[0] == df.shape[0] ), "minibatches emb and X must have same number of rows since h(df) = emb" df = df.assign(x=emb.x, y=emb.y) # add x and y to df for graphistry instance - else: # if umap has been fit, but only transforming over features, need to add x and y or breaks plot binds of res + else: # if umap has been fit, but only transforming over features, need to add x and y or breaks plot binds of res df['x'] = np.random.random(df.shape[0]) df['y'] = np.random.random(df.shape[0]) - # if umap, need to add '_n' as node id to df, adding new indices to existing graph + # if umap, need to add '_n' as node id to df, adding new indices to existing graph numeric_indices = np.arange( - X_previously_fit.shape[0] #, X_previously_fit.shape[0] + X_new.shape[0] - , dtype=np.float64) + X_previously_fit.shape[0], #, X_previously_fit.shape[0] + X_new.shape[0] + dtype=np.float64) df["_n"] = numeric_indices df[BATCH] = 1 # 1 for minibatch, 0 for existing graph, should all be `1` node = res._node @@ -451,7 +451,7 @@ def infer_self_graph(res, for j in nearest[:n_neighbors]: # add n_neighbors nearest neighbors, if any, super speedup hack if i != j: this_ndf = df.iloc[j, :] - weight = min(1/(dist[j] + 1e-3), 1) + weight = min(1 / (dist[j] + 1e-3), 1) new_edges.append([this_ndf[node], record_df[node], weight, 1]) old_nodes.append(this_ndf) diff --git a/graphistry/features.py b/graphistry/features.py index 59858c66c1..3adbc829db 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -66,7 +66,7 @@ } # precision in decimal places -KEEP_N_DECIMALS = 5 # TODO: check to see if this takes a lot of time +KEEP_N_DECIMALS = 5 # TODO: check to see if this takes a lot of time BATCH_SIZE_SMALL = 32 BATCH_SIZE = 1000 EXTRA_COLS_NEEDED = ["x", "y", "_n"] From b3ef07a2a369ea43248146e800107c1d18de7c64 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:30:19 -0800 Subject: [PATCH 112/432] lint --- graphistry/ai_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 8cec0f225c..4dfb61b148 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -412,8 +412,9 @@ def infer_self_graph(res, # if umap, need to add '_n' as node id to df, adding new indices to existing graph numeric_indices = np.arange( - X_previously_fit.shape[0], #, X_previously_fit.shape[0] + X_new.shape[0] - dtype=np.float64) + X_previously_fit.shape[0], # X_previously_fit.shape[0] + X_new.shape[0] + dtype=np.float64 # this seems off but works + ) df["_n"] = numeric_indices df[BATCH] = 1 # 1 for minibatch, 0 for existing graph, should all be `1` node = res._node @@ -447,7 +448,6 @@ def infer_self_graph(res, record_df = df.iloc[i, :] nearest = np.where(dist < eps)[0] nn.append(len(nearest)) - #new_nodes.append(record_df) for j in nearest[:n_neighbors]: # add n_neighbors nearest neighbors, if any, super speedup hack if i != j: this_ndf = df.iloc[j, :] From 31f168f361c07038971b6a3f50a9c718434ff7b2 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 16 Jan 2023 22:45:38 -0800 Subject: [PATCH 113/432] fixes test --- graphistry/tests/test_compute_cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_compute_cluster.py b/graphistry/tests/test_compute_cluster.py index acf83daf9e..05e8dba9df 100644 --- a/graphistry/tests/test_compute_cluster.py +++ b/graphistry/tests/test_compute_cluster.py @@ -17,7 +17,7 @@ def _condition(self, g, kind): if kind == 'nodes': self.assertTrue(g._node_dbscan is not None, 'instance has no `_node_dbscan` method') self.assertTrue(DBSCAN in g._nodes, 'node df has no `_dbscan` attribute') - self.assertTrue(g._point_color is not None, 'instance has no `_point_color` method') + #self.assertTrue(g._point_color is not None, 'instance has no `_point_color` method') else: self.assertTrue(g._edge_dbscan is not None, 'instance has no `_edge_dbscan` method') self.assertTrue(DBSCAN in g._edges, 'edge df has no `_dbscan` attribute') From d0d0cea823cd1d1988fef5c99e83eb760c440782 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Jan 2023 16:08:17 -0800 Subject: [PATCH 114/432] adds edgelist to adjacency function and adds more model hydration between transform methods --- graphistry/ai_utils.py | 58 ++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 4dfb61b148..67ab5327f5 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -203,6 +203,27 @@ def query_by_vector(vect, df, search_index, top_n): # ########################################################################################################################## +def edgelist_to_weighted_adjacency(g, weights=None): + """ Convert edgelist to weighted adjacency matrix in sparse coo_matrix""" + import scipy.sparse as ss + import numpy as np + res = g._edges[[g._source, g._destination]].values.astype(np.int64) + rows, cols = res.T[0], res.T[1] + if weights is None: + weights = np.ones(len(rows)) + M = ss.coo_matrix((weights, (rows, cols))) + return M.tocsr() + +def hydrate_graph(res, new_nodes, new_edges, node, src, dst, new_emb, new_features, new_targets): + # ######################################################### + g = res.nodes(new_nodes, node).edges(new_edges, src, dst) + + g._weighted_adjacency = edgelist_to_weighted_adjacency(g) + g._node_embedding = new_emb + g._node_features = new_features + g._node_targets = new_targets + return g + def infer_graph( res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", sample=None, n_neighbors=7, verbose=False, @@ -289,9 +310,9 @@ def infer_graph( logger.info( f"-epsilon = {eps:.2f} max distance threshold to be considered a neighbor" ) - print(f' epsilon = {eps:.2f}; max distance threshold to be considered a neighbor') if verbose else None + print(f' Max distance threshold; epsilon = {eps:.2f}') if verbose else None - print(f'Finding {n_neighbors} nearest neighbors') if verbose else None + print(f' Finding {n_neighbors} nearest neighbors') if verbose else None nn = [] for i, dist in enumerate(mdists): record_df = df.iloc[i, :] @@ -311,7 +332,7 @@ def infer_graph( old_nodes.append(this_ndf) #new_nodes.extend([record_df, this_ndf]) - print(f'{np.mean(nn):.2f} neighbors per node within epsilon {eps:.2f}') if verbose else None + print(f' {np.mean(nn):.2f} neighbors per node within epsilon {eps:.2f}') if verbose else None new_edges = pd.DataFrame(new_edges, columns=[src, dst, WEIGHT, BATCH]) @@ -319,7 +340,7 @@ def infer_graph( if len(old_edges): old_edges = pd.concat(old_edges, axis=0).assign(_batch=0) all_nodes = pd.concat([old_edges[src], old_edges[dst], new_edges[src], new_edges[dst]]).drop_duplicates() - print(' ', len(all_nodes), "nodes in new graph") if verbose else None + print('', len(all_nodes), "nodes in new graph") if verbose else None if sample: new_edges = pd.concat([new_edges, old_edges], axis=0).drop_duplicates() @@ -352,15 +373,9 @@ def infer_graph( new_targets = pd.concat([y, Y.loc[old_nodes.index]]) if y is not None else Y - # ######################################################### - g = res.nodes(new_nodes, node).edges(new_edges, src, dst) - - g._node_embedding = new_emb - g._node_features = new_features - g._node_targets = new_targets - print("-" * 50) if verbose else None - return g + return hydrate_graph(res, new_nodes, new_edges, node, src, dst, new_emb, new_features, new_targets) + @@ -440,9 +455,9 @@ def infer_self_graph(res, logger.info( f" epsilon = {eps:.2f} max distance threshold to be considered a neighbor" ) - print(f' epsilon = {eps:.2f}; max distance threshold to be considered a neighbor') if verbose else None + print(f' Max distance threshold; epsilon = {eps:.2f}') if verbose else None - print(f'Finding {n_neighbors} nearest neighbors') if verbose else None + print(f' Finding {n_neighbors} nearest neighbors') if verbose else None nn = [] for i, dist in enumerate(mdists): record_df = df.iloc[i, :] @@ -455,20 +470,13 @@ def infer_self_graph(res, new_edges.append([this_ndf[node], record_df[node], weight, 1]) old_nodes.append(this_ndf) - print(f'{np.mean(nn):.2f} neighbors per node within epsilon {eps:.2f}') if verbose else None + print(f' {np.mean(nn):.2f} neighbors per node within epsilon {eps:.2f}') if verbose else None - print('', len(new_edges), 'total edges pairs') if verbose else None - new_edges = pd.DataFrame(new_edges, columns=[src, dst, WEIGHT, BATCH]) new_edges = new_edges.drop_duplicates() print('', len(new_edges), 'total edges pairs after dropping duplicates') if verbose else None - + print("** Final graph has", len(df), "nodes") if verbose else None # ######################################################### - g = res.nodes(df, node).edges(new_edges, src, dst) - - g._node_embedding = emb - g._node_features = X - g._node_targets = y - print("-" * 50) if verbose else None - return g + return hydrate_graph(res, df, new_edges, node, src, dst, emb, X, y) + From 8bb6e278065c195026853d34304d73720fe325e2 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Jan 2023 16:09:23 -0800 Subject: [PATCH 115/432] adds full umap functionality from umap fit, docs --- graphistry/compute/cluster.py | 3 +-- graphistry/feature_utils.py | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 51325b8e12..adcf950116 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -198,10 +198,9 @@ def _cluster_dbscan( if res.engine == CUML else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) ) - #print(f"DBSCAN engine: {res.engine}") if verbose else None res = dbscan_fit( - res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding, verbose=True + res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding, verbose=verbose ) return res diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 776fa8a748..3242b141f6 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2172,9 +2172,11 @@ def _infer_edges(self, emb, X, y, df, eps='auto', n_neighbors=4, sample=None, in verbose=False, merge_policy=False, **kwargs): res = self.bind() if merge_policy: + # useful to cluster onto existing graph g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, n_neighbors=n_neighbors, eps=eps, sample=sample, verbose=verbose, **kwargs) else: + # useful to cluster onto self g = infer_self_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, n_neighbors=n_neighbors, eps=eps, verbose=verbose, **kwargs) return g @@ -2575,8 +2577,8 @@ def featurize( ) return self - if dbscan: - res = res.dbscan(kind=kind, fit_umap_embedding=False) # type: ignore + if dbscan: # this adds columns to the dataframe, will break tests of pure featurization & umap, so set to False in those + res = res.dbscan(eps=min_dist, n_neighbors=n_neighbors, kind=kind, fit_umap_embedding=False, verbose=verbose) # type: ignore if not inplace: return res From 82ccaf09f5610783632629717f2e96e9515c4194 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Jan 2023 16:09:44 -0800 Subject: [PATCH 116/432] adds full umap functionality from umap fit, docs --- graphistry/umap_utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 008abd276d..c5e69c4a0d 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -276,7 +276,7 @@ def transform_umap(self, df: pd.DataFrame, sample: Optional[int] = None, n_neighbors: int = 7, return_graph: bool = True, - fit_umap_embedding: bool = False, + fit_umap_embedding: bool = True, verbose=False ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: """Transforms data into UMAP embedding @@ -517,7 +517,6 @@ def umap( if kind == "nodes": index = res._nodes.index if res._node is None: - logger.debug("-Writing new node name") res = res.nodes( # type: ignore res._nodes.reset_index(drop=True) @@ -616,7 +615,7 @@ def umap( res = res.prune_self_edges() if dbscan: - res = res.dbscan(kind=kind, fit_umap_embedding=True, verbose=verbose) # type: ignore + res = res.dbscan(eps=min_dist, n_neighbors=n_neighbors, kind=kind, fit_umap_embedding=True, verbose=verbose) # type: ignore if not inplace: return res From 40c81ea7323866e7b0d24fb42c3f57c7f5a215bf Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Jan 2023 11:38:50 -0800 Subject: [PATCH 117/432] renames eps variabe to align with umaps min_dist throughout. --- graphistry/ai_utils.py | 8 ++++---- graphistry/compute/cluster.py | 30 +++++++++++++++++------------- graphistry/feature_utils.py | 9 +++++++-- graphistry/umap_utils.py | 4 ++-- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 67ab5327f5..7e3f213b06 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -346,7 +346,7 @@ def infer_graph( new_edges = pd.concat([new_edges, old_edges], axis=0).drop_duplicates() print(' Sampled', len(old_edges.drop_duplicates()), 'previous old edges') if verbose else None new_edges = new_edges.drop_duplicates() - print('', len(new_edges), 'total edges pairs after dropping duplicates') if verbose else None + print('', len(new_edges), 'total edges after dropping duplicates') if verbose else None if len(old_nodes): old_nodes = pd.DataFrame(old_nodes) @@ -367,7 +367,7 @@ def infer_graph( new_features = pd.concat([X, FEATS.loc[old_nodes.index]], axis=0) new_nodes = pd.concat([df, old_nodes], axis=0) # append minibatch at top - print("** Final graph has", len(new_nodes), "nodes") if verbose else None + print(" ** Final graph has", len(new_nodes), "nodes") if verbose else None print(" - Batch has", len(df), "nodes") if verbose else None print(" - Brought in", len(old_nodes), "nodes") if verbose else None @@ -474,8 +474,8 @@ def infer_self_graph(res, new_edges = pd.DataFrame(new_edges, columns=[src, dst, WEIGHT, BATCH]) new_edges = new_edges.drop_duplicates() - print('', len(new_edges), 'total edges pairs after dropping duplicates') if verbose else None - print("** Final graph has", len(df), "nodes") if verbose else None + print('', len(new_edges), 'total edges after dropping duplicates') if verbose else None + print(" ** Final graph has", len(df), "nodes") if verbose else None # ######################################################### print("-" * 50) if verbose else None return hydrate_graph(res, df, new_edges, node, src, dst, emb, X, y) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index adcf950116..ec4cf11cce 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -207,7 +207,7 @@ def _cluster_dbscan( def dbscan( self, - eps: float = 0.2, + min_dist: float = 0.2, min_samples: int = 1, cols=None, kind="nodes", @@ -267,7 +267,7 @@ def dbscan( cols=cols, fit_umap_embedding=fit_umap_embedding, target=target, - eps=eps, + eps=min_dist, min_samples=min_samples, verbose=verbose, *args, @@ -286,6 +286,7 @@ def _transform_dbscan( # Assume that we are transforming to last fit of dbscan cols = res._dbscan_params["cols"] umap = res._dbscan_params["fit_umap_embedding"] + target = res._dbscan_params["target"] dbscan = res._node_dbscan if kind == "nodes" else res._edge_dbscan @@ -294,13 +295,16 @@ def _transform_dbscan( emb, X, y = res.transform_umap(df, ydf, kind=kind, return_graph=False) else: X, y = res.transform(df, ydf, kind=kind, return_graph=False) - if cols is not None: - X = get_matrix_by_column_parts(X, cols) + XX = X + if target: + XX = y + if cols is not None: + XX = get_matrix_by_column_parts(XX, cols) if umap: X_ = emb else: - X_ = X + X_ = XX labels = dbscan_predict(X_, dbscan) # type: ignore if umap and cols is None: @@ -319,13 +323,13 @@ def transform_dbscan( self, df: pd.DataFrame, y: Optional[pd.DataFrame] = None, - eps: Union[float, str] = "auto", + min_dist: Union[float, str] = "auto", infer_umap_embedding: bool = False, sample: Optional[int] = None, n_neighbors: Optional[int] = None, kind: str = "nodes", - return_graph=True, - verbose=False, + return_graph: bool = True, + verbose: bool = False, ): # type: ignore """ Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster @@ -339,7 +343,7 @@ def transform_dbscan( g2 = g.featurize().dbscan() predict: - emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) + emb, X, _, ndf = g2.transform_dbscan(ndf, return_graph=False) # or g3 = g2.transform_dbscan(ndf, return_graph=True) g3.plot() @@ -347,12 +351,12 @@ def transform_dbscan( likewise for umap: fit: g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') - g2 = g.umap().dbscan() + g2 = g.umap(X=.., y=..).dbscan() predict: - emb, X, y, ndf = g2.transform_dbscan(ndf, return_graph=False) + emb, X, y, ndf = g2.transform_dbscan(ndf, ndf, return_graph=False) # or - g3 = g2.transform_dbscan(ndf, return_graph=True) + g3 = g2.transform_dbscan(ndf, ndf, return_graph=True) g3.plot() @@ -375,7 +379,7 @@ def transform_dbscan( """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind, verbose=verbose) if return_graph and kind not in ["edges"]: - g = self._infer_edges(emb, X, y, df, eps=eps, sample=sample, n_neighbors=n_neighbors, # type: ignore + g = self._infer_edges(emb, X, y, df, eps=min_dist, sample=sample, n_neighbors=n_neighbors, # type: ignore infer_on_umap_embedding=infer_umap_embedding ) #g = g.encode_point_color(column=DBSCAN, as_categorical=True) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 3242b141f6..67a8a32a6f 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2195,7 +2195,7 @@ def _transform(self, encoder: str, df: pd.DataFrame, ydf: Optional[pd.DataFrame] def transform(self, df: pd.DataFrame, y: Optional[pd.DataFrame] = None, kind: str = 'nodes', - eps: Union[str, float, int] = 'auto', + min_dist: Union[str, float, int] = 'auto', merge_policy: bool = False, sample: Optional[int] = None, n_neighbors: Optional[int] = None, @@ -2233,7 +2233,7 @@ def transform(self, df: pd.DataFrame, if return_graph and kind not in ["edges"]: emb = None # will not be able to infer graph from umap coordinates, # but will be able to infer graph from features of existing edges - g = self._infer_edges(emb, X, y_, df, eps=eps, sample=sample, n_neighbors=n_neighbors, + g = self._infer_edges(emb, X, y_, df, eps=min_dist, sample=sample, n_neighbors=n_neighbors, infer_on_umap_embedding=False, merge_policy=merge_policy, verbose=verbose) return g @@ -2396,6 +2396,8 @@ def featurize( inplace: bool = False, feature_engine: FeatureEngine = "auto", dbscan: bool = False, + min_dist: float = 0.5, # DBSCAN eps + n_neighbors: int = 5, # DBSCAN min_samples memoize: bool = True, verbose: bool = False, ): @@ -2493,6 +2495,9 @@ def featurize( :param keep_n_decimals: number of decimals to keep :param remove_node_column: whether to remove node column so it is not featurized, default True. + :param dbscan: whether to run DBSCAN, default False. + :param min_dist: DBSCAN eps parameter, default 0.5. + :param min_samples: DBSCAN min_samples parameter, default 5. :param inplace: whether to not return new graphistry instance or not, default False. :param memoize: whether to store and reuse results across runs, diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index c5e69c4a0d..0260dba99c 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -271,7 +271,7 @@ def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = No def transform_umap(self, df: pd.DataFrame, y: Optional[pd.DataFrame] = None, kind: str = 'nodes', - eps: Union[str, float, int] = 'auto', + min_dist: Union[str, float, int] = 'auto', merge_policy: bool = False, sample: Optional[int] = None, n_neighbors: int = 7, @@ -300,7 +300,7 @@ def transform_umap(self, df: pd.DataFrame, if return_graph and kind not in ["edges"]: g = self._infer_edges(emb, X, y_, df, infer_on_umap_embedding=fit_umap_embedding, merge_policy=merge_policy, - eps=eps, sample=sample, n_neighbors=n_neighbors, + eps=min_dist, sample=sample, n_neighbors=n_neighbors, verbose=verbose) return g return emb, X, y_ From 0d4ab78b684166cb09f369b572710d7fc0ee728f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Jan 2023 11:41:41 -0800 Subject: [PATCH 118/432] lint --- graphistry/ai_utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 7e3f213b06..e52637b018 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -377,8 +377,6 @@ def infer_graph( return hydrate_graph(res, new_nodes, new_edges, node, src, dst, new_emb, new_features, new_targets) - - def infer_self_graph(res, emb, X, y, df, infer_on_umap_embedding=False, eps="auto", n_neighbors=7, verbose=False, ): @@ -479,4 +477,3 @@ def infer_self_graph(res, # ######################################################### print("-" * 50) if verbose else None return hydrate_graph(res, df, new_edges, node, src, dst, emb, X, y) - From 07ed9e710bc3388c1173f29e22a57c693ed07184 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Jan 2023 11:48:22 -0800 Subject: [PATCH 119/432] fix args eps-> min_dist in tests --- graphistry/tests/test_compute_cluster.py | 4 ++-- graphistry/tests/test_umap_utils.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/graphistry/tests/test_compute_cluster.py b/graphistry/tests/test_compute_cluster.py index 05e8dba9df..3c1cfd8dca 100644 --- a/graphistry/tests/test_compute_cluster.py +++ b/graphistry/tests/test_compute_cluster.py @@ -44,9 +44,9 @@ def test_featurize_cluster(self): @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") def test_dbscan_params(self): - dbscan_params = [ModelDict('Testing UMAP', kind='nodes', eps=0.2, min_samples=1, cols=None, target=False, + dbscan_params = [ModelDict('Testing UMAP', kind='nodes', min_dist=0.2, min_samples=1, cols=None, target=False, fit_umap_embedding=False, verbose=True), - ModelDict('Testing UMAP target', kind='nodes', eps=0.1, min_samples=1, cols=None, + ModelDict('Testing UMAP target', kind='nodes', min_dist=0.1, min_samples=1, cols=None, fit_umap_embedding=True, target=True, verbose=True) ] diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 1247389f00..acfb39cfd7 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -241,13 +241,13 @@ def test_transform_umap(self): self.g2._node_embedding.shape[0], self.g3._node_embedding.shape[0] ) # now feed it args - eps = ["auto", 10] + min_dist = ["auto", 10] sample = [None, 2] return_graph = [True, False] fit_umap_embedding = [True, False] n_neighbors = [2, None] - for ep in eps: - g4 = self.g2.transform_umap(test, test, eps=ep) + for ep in min_dist: + g4 = self.g2.transform_umap(test, test, min_dist=ep) assert True for return_g in return_graph: g4 = self.g2.transform_umap(test, test, return_graph=return_g) From ed6c04522cac8bdf14d5d87fb34872c78c89f38b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Jan 2023 12:03:14 -0800 Subject: [PATCH 120/432] adds eps-> min_dist in params. Adds tests for text utils --- graphistry/compute/cluster.py | 8 ++++---- graphistry/tests/test_text_utils.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index ec4cf11cce..a5011b0f0f 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -172,7 +172,7 @@ def __init__(self, *args, **kwargs): pass def _cluster_dbscan( - self, res, kind, cols, fit_umap_embedding, target, eps, min_samples, verbose, *args, **kwargs + self, res, kind, cols, fit_umap_embedding, target, min_dist, min_samples, verbose, *args, **kwargs ): """ DBSCAN clustering on cpu or gpu infered by .engine flag @@ -186,7 +186,7 @@ def _cluster_dbscan( cols=cols, target=target, fit_umap_embedding=fit_umap_embedding, - eps=eps, + min_dist=min_dist, min_samples=min_samples, verbose=verbose, *args, @@ -194,9 +194,9 @@ def _cluster_dbscan( ) dbscan = ( - cuDBSCAN(eps=eps, min_samples=min_samples, **kwargs) + cuDBSCAN(eps=min_dist, min_samples=min_samples, **kwargs) if res.engine == CUML - else DBSCAN(eps=eps, min_samples=min_samples, **kwargs) + else DBSCAN(eps=min_dist, min_samples=min_samples, **kwargs) ) res = dbscan_fit( diff --git a/graphistry/tests/test_text_utils.py b/graphistry/tests/test_text_utils.py index b4ecc713af..710bdc5ece 100644 --- a/graphistry/tests/test_text_utils.py +++ b/graphistry/tests/test_text_utils.py @@ -55,18 +55,18 @@ def setUp(self): @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_query(self): for g in [self.g_ngrams, self.g_emb]: - res, _ = g.query('How to set up DNS', thresh=100) + res, _ = g.search('How to set up DNS', thresh=100) assert not res.empty, f'Results DataFrame should not be empty, found {res}' @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_query_graph(self): for name, g in zip(['ngrams', 'embedding'], [self.g_ngrams, self.g_emb]): - res = g.query_graph('How to set up DNS', thresh=100) + res = g.search_graph('How to set up DNS', thresh=100) assert not res._nodes.empty, f'{name}-Results DataFrame should not be empty, found {res._nodes}' #url = res.plot(render=False) #logger.info(f'{name}: {url}') - res = self.g_with_edges.query_graph('Wife', thresh=100) + res = self.g_with_edges.search_graph('Wife', thresh=100) assert not res._nodes.empty, f'Results DataFrame should not be empty, found {res._nodes}' #url = res.plot(render=False) #logger.info(f'With Explicit Edges: {url}') From 25387c485d54e78dab0de010d29244db3853d04a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Jan 2023 12:35:56 -0800 Subject: [PATCH 121/432] more fixes --- graphistry/ai_utils.py | 3 ++- graphistry/compute/cluster.py | 29 ++++++++++++++++------------- graphistry/feature_utils.py | 4 ++-- graphistry/umap_utils.py | 4 ++-- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index e52637b018..aad91bf48b 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -218,7 +218,8 @@ def hydrate_graph(res, new_nodes, new_edges, node, src, dst, new_emb, new_featur # ######################################################### g = res.nodes(new_nodes, node).edges(new_edges, src, dst) - g._weighted_adjacency = edgelist_to_weighted_adjacency(g) + # TODO this needs more work since edgelist_to_weighted_adjacency produces non square matrices (since infer_graph will add new nodes) + #g._weighted_adjacency = edgelist_to_weighted_adjacency(g) g._node_embedding = new_emb g._node_features = new_features g._node_targets = new_targets diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index a5011b0f0f..b0c84c28ba 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -217,7 +217,7 @@ def dbscan( *args, **kwargs, ): - """DBSCAN clustering on cpu or gpu infered automatically. + """DBSCAN clustering on cpu or gpu infered automatically. Adds a `_dbscan` column to nodes or edges. Examples: g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') @@ -227,22 +227,24 @@ def dbscan( g2 = g.umap(kind=kind).dbscan(kind=kind) print(g2._nodes['_dbscan']) | print(g2._edges['_dbscan']) - # dbscan with fixed parameters in umap - g2 = g.umap(dbscan=True) + # dbscan in umap or featurize API + g2 = g.umap(dbscan=True, min_dist=1.2, min_samples=2, **kwargs) + # or, here dbscan is infered from features, not umap embeddings + g2 = g.featurize(dbscan=True, min_dist=1.2, min_samples=2, **kwargs) - # and with greater control over parameters via chaining, - g2 = g.umap().dbscan(eps=1.2, min_samples=2, **kwargs) + # and via chaining, + g2 = g.umap().dbscan(min_dist=1.2, min_samples=2, **kwargs) # cluster by feature embeddings g2 = g.featurize().dbscan(**kwargs) - # cluster by a given set of feature column attributes - g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], **kwargs) + # cluster by a given set of feature column attributes, or with target=True + g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], target=False, **kwargs) # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) - g2.plot() # colored by `_dbscan` column + g2.plot() # color by `_dbscan` column Useful: Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, @@ -250,13 +252,14 @@ def dbscan( https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb Args: - eps float: The maximum distance between two samples for them to be considered as in the same neighborhood. + min_dist float: The maximum distance between two samples for them to be considered as in the same neighborhood. kind str: 'nodes' or 'edges' - cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features by + cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features or targets by fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] fit_umap_embedding bool: whether to use UMAP embeddings or features dataframe to cluster DBSCAN min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. + target: whether to use the target column as the clustering feature """ @@ -267,7 +270,7 @@ def dbscan( cols=cols, fit_umap_embedding=fit_umap_embedding, target=target, - eps=min_dist, + min_dist=min_dist, min_samples=min_samples, verbose=verbose, *args, @@ -363,9 +366,9 @@ def transform_dbscan( args: df: dataframe to transform y: optional labels dataframe - eps: The maximum distance between two samples for them to be considered as in the same neighborhood. + min_dist: The maximum distance between two samples for them to be considered as in the same neighborhood. smaller values will result in less edges between the minibatch and the original graph. - Default 'auto', infers eps from the mean distance and std of new points to the original graph + Default 'auto', infers min_dist from the mean distance and std of new points to the original graph fit_umap_embedding: whether to use UMAP embeddings or features dataframe when inferring edges between the minibatch and the original graph. Default False, uses the features dataframe sample: number of samples to use when inferring edges between the minibatch and the original graph, diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 67a8a32a6f..4e19206b3c 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2212,7 +2212,7 @@ def transform(self, df: pd.DataFrame, return_graph: bool, if True, will return a graph with inferred edges. merge_policy: bool, if True, adds batch to existing graph nodes via nearest neighbors. If False, will infer edges only between nodes in the batch. - eps: float, if return_graph is True, will use this value for eps in NN search, or 'auto' to infer a good value + min_dist: float, if return_graph is True, will use this value for eps in NN search, or 'auto' to infer a good value eps represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph n_neighbors: int, optional (default = 15), if return_graph is True, will use this value for n_neighbors in NN search @@ -2583,7 +2583,7 @@ def featurize( return self if dbscan: # this adds columns to the dataframe, will break tests of pure featurization & umap, so set to False in those - res = res.dbscan(eps=min_dist, n_neighbors=n_neighbors, kind=kind, fit_umap_embedding=False, verbose=verbose) # type: ignore + res = res.dbscan(min_dist=min_dist, n_neighbors=n_neighbors, kind=kind, fit_umap_embedding=False, verbose=verbose) # type: ignore if not inplace: return res diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 0260dba99c..8d324ab4d2 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -285,7 +285,7 @@ def transform_umap(self, df: pd.DataFrame, df: Dataframe to transform y: Target column kind: One of `nodes` or `edges` - eps: Epsilon for DBSCAN + min_dist: Epsilon for DBSCAN merge_policy: if True, use previous graph, adding new batch to existing graph's neighbors useful to contextualize new data against existing graph. If False, `sample` is irrelevant. sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs @@ -615,7 +615,7 @@ def umap( res = res.prune_self_edges() if dbscan: - res = res.dbscan(eps=min_dist, n_neighbors=n_neighbors, kind=kind, fit_umap_embedding=True, verbose=verbose) # type: ignore + res = res.dbscan(min_dist=min_dist, n_neighbors=n_neighbors, kind=kind, fit_umap_embedding=True, verbose=verbose) # type: ignore if not inplace: return res From 406342137a1f5668e1b83efc0f38019df08af66a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Jan 2023 12:49:13 -0800 Subject: [PATCH 122/432] min_samples != n_neighbors, removed in dbscan=True inside umap/featurize --- graphistry/compute/cluster.py | 2 -- graphistry/feature_utils.py | 2 +- graphistry/umap_utils.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index b0c84c28ba..03fb5ed740 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -189,8 +189,6 @@ def _cluster_dbscan( min_dist=min_dist, min_samples=min_samples, verbose=verbose, - *args, - **kwargs, ) dbscan = ( diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 4e19206b3c..6056976b08 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2583,7 +2583,7 @@ def featurize( return self if dbscan: # this adds columns to the dataframe, will break tests of pure featurization & umap, so set to False in those - res = res.dbscan(min_dist=min_dist, n_neighbors=n_neighbors, kind=kind, fit_umap_embedding=False, verbose=verbose) # type: ignore + res = res.dbscan(min_dist=min_dist, kind=kind, fit_umap_embedding=False, verbose=verbose) # type: ignore if not inplace: return res diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 8d324ab4d2..2eb1989daa 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -615,7 +615,7 @@ def umap( res = res.prune_self_edges() if dbscan: - res = res.dbscan(min_dist=min_dist, n_neighbors=n_neighbors, kind=kind, fit_umap_embedding=True, verbose=verbose) # type: ignore + res = res.dbscan(min_dist=min_dist, kind=kind, fit_umap_embedding=True, verbose=verbose) # type: ignore if not inplace: return res From 0a0ef90fa6088358ec513f88d59209650f941619 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 19 Jan 2023 20:54:17 -0800 Subject: [PATCH 123/432] docs and arguments --- graphistry/ai_utils.py | 1 + graphistry/compute/cluster.py | 1 + graphistry/feature_utils.py | 36 +++++++++++++++++------------------ graphistry/umap_utils.py | 8 ++++---- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index aad91bf48b..e29c334785 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -223,6 +223,7 @@ def hydrate_graph(res, new_nodes, new_edges, node, src, dst, new_emb, new_featur g._node_embedding = new_emb g._node_features = new_features g._node_targets = new_targets + g = g.settings(url_params={'play': 0}) return g diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 03fb5ed740..af1724d8d6 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -131,6 +131,7 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ print('-' * len(message)) print(message) print(f"--fit on {'umap embeddings' if use_umap_embedding else 'feature embeddings'} of size {X.shape}") + print('-' * len(message)) return g diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 6056976b08..0ab8ebe7c4 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2196,9 +2196,9 @@ def transform(self, df: pd.DataFrame, y: Optional[pd.DataFrame] = None, kind: str = 'nodes', min_dist: Union[str, float, int] = 'auto', + n_neighbors: int = 7, merge_policy: bool = False, sample: Optional[int] = None, - n_neighbors: Optional[int] = None, return_graph: bool = True, scaled: bool = True, verbose: bool = False): @@ -2211,13 +2211,13 @@ def transform(self, df: pd.DataFrame, kind: str # one of `nodes`, `edges` return_graph: bool, if True, will return a graph with inferred edges. merge_policy: bool, if True, adds batch to existing graph nodes via nearest neighbors. - If False, will infer edges only between nodes in the batch. - min_dist: float, if return_graph is True, will use this value for eps in NN search, or 'auto' to infer a good value - eps represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. + If False, will infer edges only between nodes in the batch, default False + min_dist: float, if return_graph is True, will use this value in NN search, or 'auto' to infer a good value + min_dist represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph - n_neighbors: int, optional (default = 15), if return_graph is True, will use this value for n_neighbors in NN search - scaled: bool, if True, will use scaled transformation of data set during featurization - verbose: bool, if True, will print metadata about the graph construction + n_neighbors: int, if return_graph is True, will use this value for n_neighbors in Nearest Neighbors search + scaled: bool, if True, will use scaled transformation of data set during featurization, default True + verbose: bool, if True, will print metadata about the graph construction, default False returns: X, y: pd.DataFrame, transformed data if return_graph is False or a graphistry Plottable with inferred edges if return_graph is True @@ -2241,7 +2241,7 @@ def transform(self, df: pd.DataFrame, def scale( self, - df: pd.DataFrame, + df: Optional[pd.DataFrame] = None, y: Optional[pd.DataFrame] = None, kind: str = "nodes", use_scaler: Union[str, None] = None, @@ -2275,7 +2275,7 @@ def scale( args: - df: pd.DataFrame, raw data to transform + df: pd.DataFrame, raw data to transform, if None, will use data from featurization fit y: pd.DataFrame, optional target data kind: str, one of `nodes`, `edges` use_scaler: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` @@ -2383,13 +2383,11 @@ def featurize( impute: bool = True, n_quantiles: int = 100, output_distribution: str = "normal", - quantile_range=(25, 75), + quantile_range = (25, 75), n_bins: int = 10, encode: str = "ordinal", strategy: str = "uniform", - similarity: Optional[ - str - ] = None, # turn this off in favor of Gap Encoder + similarity: Optional[str] = None, # turn this off in favor of Gap Encoder categories: Optional[str] = "auto", keep_n_decimals: int = 5, remove_node_column: bool = True, @@ -2397,7 +2395,7 @@ def featurize( feature_engine: FeatureEngine = "auto", dbscan: bool = False, min_dist: float = 0.5, # DBSCAN eps - n_neighbors: int = 5, # DBSCAN min_samples + min_samples: int = 1, # DBSCAN min_samples memoize: bool = True, verbose: bool = False, ): @@ -2484,7 +2482,7 @@ def featurize( can return distribution as ["normal", "uniform"] :param quantile_range: if use_scaler = 'robust'|'quantile', sets the quantile range. - :param n_bins: number of bins to use in kbins discretizer + :param n_bins: number of bins to use in kbins discretizer, default 10 :param encode: encoding for KBinsDiscretizer, can be one of `onehot`, `onehot-dense`, `ordinal`, default 'ordinal' :param strategy: strategy for KBinsDiscretizer, can be one of @@ -2492,12 +2490,12 @@ def featurize( :param n_quantiles: if use_scaler = "quantile", sets the number of quantiles, default=100 :param output_distribution: if use_scaler="quantile"|"robust", choose from ["normal", "uniform"] - :param keep_n_decimals: number of decimals to keep - :param remove_node_column: whether to remove node column so it is - not featurized, default True. :param dbscan: whether to run DBSCAN, default False. :param min_dist: DBSCAN eps parameter, default 0.5. :param min_samples: DBSCAN min_samples parameter, default 5. + :param keep_n_decimals: number of decimals to keep + :param remove_node_column: whether to remove node column so it is + not featurized, default True. :param inplace: whether to not return new graphistry instance or not, default False. :param memoize: whether to store and reuse results across runs, @@ -2583,7 +2581,7 @@ def featurize( return self if dbscan: # this adds columns to the dataframe, will break tests of pure featurization & umap, so set to False in those - res = res.dbscan(min_dist=min_dist, kind=kind, fit_umap_embedding=False, verbose=verbose) # type: ignore + res = res.dbscan(min_dist=min_dist, min_samples=min_samples, kind=kind, fit_umap_embedding=False, verbose=verbose) # type: ignore if not inplace: return res diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 2eb1989daa..2107710a3d 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -272,12 +272,12 @@ def transform_umap(self, df: pd.DataFrame, y: Optional[pd.DataFrame] = None, kind: str = 'nodes', min_dist: Union[str, float, int] = 'auto', + n_neighbors: int = 7, merge_policy: bool = False, sample: Optional[int] = None, - n_neighbors: int = 7, return_graph: bool = True, fit_umap_embedding: bool = True, - verbose=False + verbose: bool = False ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: """Transforms data into UMAP embedding @@ -285,11 +285,11 @@ def transform_umap(self, df: pd.DataFrame, df: Dataframe to transform y: Target column kind: One of `nodes` or `edges` - min_dist: Epsilon for DBSCAN + min_dist: Epsilon for including neighbors in infer_graph + n_neighbors: Number of neighbors to use for contextualization merge_policy: if True, use previous graph, adding new batch to existing graph's neighbors useful to contextualize new data against existing graph. If False, `sample` is irrelevant. sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs - n_neighbors: Number of neighbors to use for contextualization return_graph: Whether to return a graph or just the embeddings fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data verbose: Whether to print information about the graph inference From 166e4f4840e32642bd4cd315c489a3281b09c0e0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Jan 2023 10:23:52 -0800 Subject: [PATCH 124/432] user-attr setting in PlotterBase --- graphistry/PlotterBase.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 4c3e56192f..7b7d0604d0 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -169,20 +169,16 @@ def __init__(self, *args, **kwargs): self._node_embedding = None self._node_encoder = None self._node_features = None - #self._node_scaling_pipeline = None - #self._node_ordinal_pipeline_target = None, + self._node_features_raw = None self._node_target = None self._node_target_encoder = None - # self._node_text_model = None self._edge_embedding = None self._edge_encoder = None self._edge_features = None - #self._edge_ordinal_pipeline = None - #self._edge_ordinal_pipeline_target = None + self._edge_features_raw = None self._edge_target = None self._edge_target_encoder = None - # self._edge_text_model = None self._weighted_adjacency_nodes = None self._weighted_adjacency_edges = None From 601543ca1f7e1db438ca40db18a395d9ab44268e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Jan 2023 10:26:40 -0800 Subject: [PATCH 125/432] typecheck ignore --- graphistry/feature_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 0ab8ebe7c4..10be319062 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2045,7 +2045,6 @@ def _featurize_nodes( res._node_target = encoder.y res._node_target_raw = encoder.y_orignal # .copy() res._node_encoder = encoder # now this does - # all the work `._node_encoder.transform(df, y)` etc return res @@ -2296,7 +2295,7 @@ def scale( """ if df is None: # use the original data - X, y = (self._node_features_raw, self._node_target_raw) if kind == "nodes" else (self._edge_features_raw, self._edge_target_raw) + X, y = (self._node_features_raw, self._node_target_raw) if kind == "nodes" else (self._edge_features_raw, self._edge_target_raw) # type: ignore else: X, y = self.transform(df, y, kind=kind, return_graph=False, scaled=False) From f997752267fb6b574c8aa844ad9c47e4852d415d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Jan 2023 17:54:45 -0800 Subject: [PATCH 126/432] adds numeric only umap demo -- demonstrates umap, dbscan pivot, and graph transform methods --- .../Introduction/simple-power-of-umap.ipynb | 14642 ++++++++++++++++ 1 file changed, 14642 insertions(+) create mode 100644 demos/ai/Introduction/simple-power-of-umap.ipynb diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb new file mode 100644 index 0000000000..19538a5b60 --- /dev/null +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -0,0 +1,14642 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 44, + "id": "0270b0aa-7eea-4915-a3c5-601c0edd34e3", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "af4a54b9-1959-4fda-a00c-534be66e09a4", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from sklearn.datasets import load_breast_cancer, load_diabetes, load_digits\n", + "\n", + "from collections import Counter\n", + "\n", + "import graphistry\n", + "from graphistry.features import ModelDict" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b28209f4-6809-4cb1-b6a8-e0d65cbbe07d", + "metadata": {}, + "outputs": [], + "source": [ + "graphistry.register(api=3, protocol=\"https\", server=\"hub.graphistry.com\", username=os.environ['USERNAME'], password=os.environ['GRAPHISTRY_PASSWORD']) " + ] + }, + { + "cell_type": "markdown", + "id": "49752d24-00a6-480f-be5e-62134c9598d8", + "metadata": {}, + "source": [ + "# Explore Data in a Whole New Way\n", + "PyGraphistry is a GPU Graph AI visualization tool that unlocks the graph in your data. \n", + "\n", + "In the past loading, transforming and interacting with large multivariate datasets took time to set up pipelines and processes. Graphistry makes time-to-graph + AI + interactivity 100x faster. \n", + "\n", + "We will explore how to see data, explore relationships and create new graphs from batches using sci-kits like api, and even build a GNN model one could use in downstream DGL models. \n", + "\n", + "We will quickly analyze breast cancer, diabetes and digits datasets from sklearn.data\n", + "\n", + "Add your favorite dataset and explore it with graph AI and Visual exploration! " + ] + }, + { + "cell_type": "markdown", + "id": "d434a151", + "metadata": {}, + "source": [ + "## Tumor: Malignant or Benign " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ec079ad1-12d8-419c-9f35-d4ecad825635", + "metadata": {}, + "outputs": [], + "source": [ + "data = load_breast_cancer()\n", + "\n", + "good_features = list(data['feature_names'])\n", + "\n", + "df = pd.DataFrame(data['data'], columns=good_features)\n", + "df['target'] = data['target']" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "df071dc0-df4c-4701-8794-57864c37fc0d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean radiusmean texturemean perimetermean areamean smoothnessmean compactnessmean concavitymean concave pointsmean symmetrymean fractal dimension...worst textureworst perimeterworst areaworst smoothnessworst compactnessworst concavityworst concave pointsworst symmetryworst fractal dimensiontarget
017.9910.38122.801001.00.118400.277600.30010.147100.24190.07871...17.33184.602019.00.16220.66560.71190.26540.46010.118900
120.5717.77132.901326.00.084740.078640.08690.070170.18120.05667...23.41158.801956.00.12380.18660.24160.18600.27500.089020
219.6921.25130.001203.00.109600.159900.19740.127900.20690.05999...25.53152.501709.00.14440.42450.45040.24300.36130.087580
311.4220.3877.58386.10.142500.283900.24140.105200.25970.09744...26.5098.87567.70.20980.86630.68690.25750.66380.173000
420.2914.34135.101297.00.100300.132800.19800.104300.18090.05883...16.67152.201575.00.13740.20500.40000.16250.23640.076780
\n", + "

5 rows × 31 columns

\n", + "
" + ], + "text/plain": [ + " mean radius mean texture mean perimeter mean area mean smoothness \\\n", + "0 17.99 10.38 122.80 1001.0 0.11840 \n", + "1 20.57 17.77 132.90 1326.0 0.08474 \n", + "2 19.69 21.25 130.00 1203.0 0.10960 \n", + "3 11.42 20.38 77.58 386.1 0.14250 \n", + "4 20.29 14.34 135.10 1297.0 0.10030 \n", + "\n", + " mean compactness mean concavity mean concave points mean symmetry \\\n", + "0 0.27760 0.3001 0.14710 0.2419 \n", + "1 0.07864 0.0869 0.07017 0.1812 \n", + "2 0.15990 0.1974 0.12790 0.2069 \n", + "3 0.28390 0.2414 0.10520 0.2597 \n", + "4 0.13280 0.1980 0.10430 0.1809 \n", + "\n", + " mean fractal dimension ... worst texture worst perimeter worst area \\\n", + "0 0.07871 ... 17.33 184.60 2019.0 \n", + "1 0.05667 ... 23.41 158.80 1956.0 \n", + "2 0.05999 ... 25.53 152.50 1709.0 \n", + "3 0.09744 ... 26.50 98.87 567.7 \n", + "4 0.05883 ... 16.67 152.20 1575.0 \n", + "\n", + " worst smoothness worst compactness worst concavity worst concave points \\\n", + "0 0.1622 0.6656 0.7119 0.2654 \n", + "1 0.1238 0.1866 0.2416 0.1860 \n", + "2 0.1444 0.4245 0.4504 0.2430 \n", + "3 0.2098 0.8663 0.6869 0.2575 \n", + "4 0.1374 0.2050 0.4000 0.1625 \n", + "\n", + " worst symmetry worst fractal dimension target \n", + "0 0.4601 0.11890 0 \n", + "1 0.2750 0.08902 0 \n", + "2 0.3613 0.08758 0 \n", + "3 0.6638 0.17300 0 \n", + "4 0.2364 0.07678 0 \n", + "\n", + "[5 rows x 31 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "e6c6f61c-0923-4860-8045-18dfeffe69fa", + "metadata": {}, + "source": [ + "# UMAP\n", + "\n", + "Reduce the data into a 2 dimensional graph -- the edges come from similarity in features. \n", + "\n", + "UMAP is a powerful way to see the parts of the dataset -- one can not only visually confirm if a predictive model will 'separate' the data, one can explore relationships that can help feed insights and potential treatment strategies. \n", + "\n", + "What can you find in the data?" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "28ae2a26-c7e3-457a-8a50-78031797fad2", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "# let's split data and train on half the data\n", + "df_train, df_test, df_train_target, df_test_target = train_test_split(df, df[['target']], train_size=0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a3527e13-7e14-437f-b423-6643e15b9a08", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (284, 0) in UMAP fit, as it is not one dimensionalOMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 15.7 s, sys: 927 ms, total: 16.7 s\n", + "Wall time: 17.6 s\n" + ] + } + ], + "source": [ + "%%time\n", + "g = graphistry.nodes(df_train)\n", + "# fit on specific features by calling out via X=...\n", + "# plots are sensitive to scaling, we use_scaler='robust' for good umap separation \n", + "# (thought None does better in RF below)\n", + "\n", + "g2 = g.umap(X=good_features, use_scaler='robust') # y = 'target'" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "65758e38", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g2.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "b773839a", + "metadata": { + "tags": [] + }, + "source": [ + "## DBSCAN as new pivot \n", + "\n", + "Think of DBSCAN as a way to pivot by clustering in features of interest. \n", + "By setting `cols` you can pick out features from the matrix and have dbscan only focus on those.\n", + "Coloring by the `_dbscan` label in the UI finds clusters across those variables. \n", + "\n", + "Contrasting UMAP coordinates versus dbscan labels is a useful way to see total behavior against some part/pivot of interest. In the following we see k-clusters in the `worst` (case sensitive column selection) meta variable. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "97d11165", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
worst radiusworst textureworst perimeterworst areaworst smoothnessworst compactnessworst concavityworst concave pointsworst symmetryworst fractal dimension
527-0.22733-0.65650-0.24772-0.21837-0.07240-0.04575-0.207880.050380.46218-0.19976
822.415160.879082.631433.038610.944802.023001.537491.75843-0.590451.20651
5410.193470.676830.360120.179560.101361.054280.629970.178690.569541.07157
5090.336961.169070.457270.343461.520361.449731.327001.089280.326941.28361
4440.86417-0.362230.818090.96742-0.177380.260270.723110.492360.088530.10530
.................................
319-0.34180-0.55591-0.37400-0.29828-1.70534-0.87428-0.79177-0.69748-1.22342-0.99976
990.211210.583730.267610.208100.430770.446370.314800.52088-0.084350.64892
5331.323660.186731.250611.54312-0.778280.192040.276800.566500.61555-0.78096
4491.718660.713221.625302.157950.202710.485140.763711.20048-0.71175-0.28024
5671.728341.499732.004631.830151.223533.369602.621961.552171.824332.11735
\n", + "

284 rows × 10 columns

\n", + "
" + ], + "text/plain": [ + " worst radius worst texture worst perimeter worst area \\\n", + "527 -0.22733 -0.65650 -0.24772 -0.21837 \n", + "82 2.41516 0.87908 2.63143 3.03861 \n", + "541 0.19347 0.67683 0.36012 0.17956 \n", + "509 0.33696 1.16907 0.45727 0.34346 \n", + "444 0.86417 -0.36223 0.81809 0.96742 \n", + ".. ... ... ... ... \n", + "319 -0.34180 -0.55591 -0.37400 -0.29828 \n", + "99 0.21121 0.58373 0.26761 0.20810 \n", + "533 1.32366 0.18673 1.25061 1.54312 \n", + "449 1.71866 0.71322 1.62530 2.15795 \n", + "567 1.72834 1.49973 2.00463 1.83015 \n", + "\n", + " worst smoothness worst compactness worst concavity \\\n", + "527 -0.07240 -0.04575 -0.20788 \n", + "82 0.94480 2.02300 1.53749 \n", + "541 0.10136 1.05428 0.62997 \n", + "509 1.52036 1.44973 1.32700 \n", + "444 -0.17738 0.26027 0.72311 \n", + ".. ... ... ... \n", + "319 -1.70534 -0.87428 -0.79177 \n", + "99 0.43077 0.44637 0.31480 \n", + "533 -0.77828 0.19204 0.27680 \n", + "449 0.20271 0.48514 0.76371 \n", + "567 1.22353 3.36960 2.62196 \n", + "\n", + " worst concave points worst symmetry worst fractal dimension \n", + "527 0.05038 0.46218 -0.19976 \n", + "82 1.75843 -0.59045 1.20651 \n", + "541 0.17869 0.56954 1.07157 \n", + "509 1.08928 0.32694 1.28361 \n", + "444 0.49236 0.08853 0.10530 \n", + ".. ... ... ... \n", + "319 -0.69748 -1.22342 -0.99976 \n", + "99 0.52088 -0.08435 0.64892 \n", + "533 0.56650 0.61555 -0.78096 \n", + "449 1.20048 -0.71175 -0.28024 \n", + "567 1.55217 1.82433 2.11735 \n", + "\n", + "[284 rows x 10 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "X = g2.get_matrix('worst')\n", + "X" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "1829f1ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "-----------------------------------------\n", + "DBSCAN found 284 clusters with 0 outliers\n", + "--fit on feature embeddings of size (284, 10)\n", + "-----------------------------------------\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# suppose we want to cluster and color by these variables,\n", + "g2.dbscan(cols='worst', min_dist=0.3, verbose=True, fit_umap_embedding=True).plot()" + ] + }, + { + "cell_type": "markdown", + "id": "b4354d70", + "metadata": {}, + "source": [ + "Suppose you wanted to study part of the features matrix, like all entries in 'symmetry'" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d866936a-2ace-4f77-af35-a2cea7174427", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mean symmetrysymmetry errorworst symmetry
527-0.26657-0.839070.46218
820.15041-0.91072-0.59045
5410.278480.212150.56954
5090.084880.253570.32694
444-0.17424-0.653230.08853
............
319-0.912881.74027-1.22342
990.29933-0.46627-0.08435
5331.154131.049540.61555
449-0.66865-0.95102-0.71175
5671.842140.498741.82433
\n", + "

284 rows × 3 columns

\n", + "
" + ], + "text/plain": [ + " mean symmetry symmetry error worst symmetry\n", + "527 -0.26657 -0.83907 0.46218\n", + "82 0.15041 -0.91072 -0.59045\n", + "541 0.27848 0.21215 0.56954\n", + "509 0.08488 0.25357 0.32694\n", + "444 -0.17424 -0.65323 0.08853\n", + ".. ... ... ...\n", + "319 -0.91288 1.74027 -1.22342\n", + "99 0.29933 -0.46627 -0.08435\n", + "533 1.15413 1.04954 0.61555\n", + "449 -0.66865 -0.95102 -0.71175\n", + "567 1.84214 0.49874 1.82433\n", + "\n", + "[284 rows x 3 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# with new features you can add it too graphistry API to run further modeling\n", + "X = g2.get_matrix('symmetry')\n", + "small_study = X.columns # save for later so we can call out only these features during fit\n", + "X" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a938ae8d-e280-4299-af38-1b945d38a22b", + "metadata": {}, + "outputs": [], + "source": [ + "# add the target back so we can color by in UI\n", + "X['target'] = df_train.target" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f6eeb6de-b418-4b84-92c7-b28fa241ce97", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "! Failed umap speedup attempt. Continuing without memoization speedups.* Ignoring target column of shape (284, 0) in UMAP fit, as it is not one dimensional" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "lazy init\n", + "_______________\n", + "\n", + "UMAP Parameters\n", + "_______________\n", + "\n", + "{'n_components': 2, 'metric': 'euclidean', 'n_neighbors': 12, 'min_dist': 0.1, 'spread': 0.5, 'local_connectivity': 1, 'repulsion_strength': 1, 'negative_sample_rate': 5}\n", + "------------------------------------------------------------\n", + "** Fitting UMAP\n", + "Same umap params as last time, skipping new init\n" + ] + } + ], + "source": [ + "g = graphistry.nodes(X)\n", + "gq = g.umap(X=small_study, verbose=True).dbscan()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "baf3980c-df0a-479a-8ee0-4fc57109881e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gq.plot() # finds that `symmetry` features are also a good indicator of target on their own" + ] + }, + { + "cell_type": "markdown", + "id": "c8780fe0-ca07-4412-ba2a-1ddde26d46db", + "metadata": {}, + "source": [ + "# Transform Test Data into Graph\n", + "Can add batch onto closest neighbors of existing graph (from fit above) if merge_policy=True\n", + "otherwise, will create a new graph from the batch. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "8de9e1bf-290d-4901-96ef-4dd8ac9b5887", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------------------------------------\n", + "Infering edges over UMAP embedding\n", + "---------------------------------------------\n", + " Mean distance to existing nodes 1.88 +/- 1.07\n", + " Max distance threshold; epsilon = 1.00\n", + " Finding 7 nearest neighbors\n", + " 68.61 neighbors per node within epsilon 1.00\n", + " 1995 total edges after dropping duplicates\n", + " ** Final graph has 349 nodes\n", + " - Batch has 285 nodes\n", + " - Brought in 64 nodes\n", + "--------------------------------------------------\n", + "CPU times: user 6.78 s, sys: 34.4 ms, total: 6.82 s\n", + "Wall time: 6.86 s\n" + ] + } + ], + "source": [ + "%%time\n", + "# with merge_policy=True, will cluster minibatch to closests elements of existing graph -- useful if you want to find\n", + "# centroids in the old variables (imagine labeling goldenset with other targets, this would find which parts of minibatch are likely similar to known annotations)\n", + "g3 = g2.transform_umap(df_test, min_dist=1, merge_policy=True,\n", + " fit_umap_embedding=True, n_neighbors=7,\n", + " sample=None, \n", + " return_graph=True, \n", + " verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "6c032e7c-0fc9-4364-9feb-eddb0d8e3c6b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g3.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "084f07f9-db66-4018-8d01-74a189a3c7ad", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------------------------------------\n", + "Infering edges over features embedding\n", + "---------------------------------------------\n", + " Mean distance to existing nodes 5.96 +/- 3.08\n", + " Max distance threshold; epsilon = 6.00\n", + " Finding 7 nearest neighbors\n", + " 167.55 neighbors per node within epsilon 6.00\n", + " 1932 total edges after dropping duplicates\n", + " ** Final graph has 285 nodes\n", + "--------------------------------------------------\n", + "CPU times: user 2.76 s, sys: 21 ms, total: 2.78 s\n", + "Wall time: 2.82 s\n" + ] + } + ], + "source": [ + "%%time\n", + "# with merge_policy=False (default), it clusters just by the minibatch df_test here\n", + "g4 = g2.transform_umap(df_test, min_dist=6, merge_policy=False,\n", + " fit_umap_embedding=False, n_neighbors=7,\n", + " sample=None, \n", + " return_graph=True, \n", + " verbose=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "7f9d3612-b2e0-4500-b5ba-4c11c3577dc8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g4.dbscan(0.2).plot()" + ] + }, + { + "cell_type": "markdown", + "id": "9ab6b089-9c3c-4ad9-bd9f-c3e4b14c87e1", + "metadata": {}, + "source": [ + "# Regressive Targets\n", + "Diabetes dataset with risk scores" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "d175607a-e108-45e5-aa10-05e306532589", + "metadata": {}, + "outputs": [], + "source": [ + "data2 = load_diabetes()\n", + "diabetes_features = list(data2['feature_names'])\n", + "diabetes_df = pd.DataFrame(data2['data'], columns=diabetes_features)\n", + "# we add target to dataframe as we want all the data for visualization (think coloring by histogram in target)\n", + "diabetes_df['target'] = data2['target']" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "e1bbc28b-9d10-4ff3-8c28-3308b552bbd8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexbmibps1s2s3s4s5s6target
00.0380760.0506800.0616960.021872-0.044223-0.034821-0.043401-0.0025920.019908-0.017646151.0
1-0.001882-0.044642-0.051474-0.026328-0.008449-0.0191630.074412-0.039493-0.068330-0.09220475.0
20.0852990.0506800.044451-0.005671-0.045599-0.034194-0.032356-0.0025920.002864-0.025930141.0
3-0.089063-0.044642-0.011595-0.0366560.0121910.024991-0.0360380.0343090.022692-0.009362206.0
40.005383-0.044642-0.0363850.0218720.0039350.0155960.008142-0.002592-0.031991-0.046641135.0
....................................
4370.0417080.0506800.0196620.059744-0.005697-0.002566-0.028674-0.0025920.0311930.007207178.0
438-0.0055150.050680-0.015906-0.0676420.0493410.079165-0.0286740.034309-0.0181180.044485104.0
4390.0417080.050680-0.0159060.017282-0.037344-0.013840-0.024993-0.011080-0.0468790.015491132.0
440-0.045472-0.0446420.0390620.0012150.0163180.015283-0.0286740.0265600.044528-0.025930220.0
441-0.045472-0.044642-0.073030-0.0814140.0837400.0278090.173816-0.039493-0.0042200.00306457.0
\n", + "

442 rows × 11 columns

\n", + "
" + ], + "text/plain": [ + " age sex bmi bp s1 s2 s3 \\\n", + "0 0.038076 0.050680 0.061696 0.021872 -0.044223 -0.034821 -0.043401 \n", + "1 -0.001882 -0.044642 -0.051474 -0.026328 -0.008449 -0.019163 0.074412 \n", + "2 0.085299 0.050680 0.044451 -0.005671 -0.045599 -0.034194 -0.032356 \n", + "3 -0.089063 -0.044642 -0.011595 -0.036656 0.012191 0.024991 -0.036038 \n", + "4 0.005383 -0.044642 -0.036385 0.021872 0.003935 0.015596 0.008142 \n", + ".. ... ... ... ... ... ... ... \n", + "437 0.041708 0.050680 0.019662 0.059744 -0.005697 -0.002566 -0.028674 \n", + "438 -0.005515 0.050680 -0.015906 -0.067642 0.049341 0.079165 -0.028674 \n", + "439 0.041708 0.050680 -0.015906 0.017282 -0.037344 -0.013840 -0.024993 \n", + "440 -0.045472 -0.044642 0.039062 0.001215 0.016318 0.015283 -0.028674 \n", + "441 -0.045472 -0.044642 -0.073030 -0.081414 0.083740 0.027809 0.173816 \n", + "\n", + " s4 s5 s6 target \n", + "0 -0.002592 0.019908 -0.017646 151.0 \n", + "1 -0.039493 -0.068330 -0.092204 75.0 \n", + "2 -0.002592 0.002864 -0.025930 141.0 \n", + "3 0.034309 0.022692 -0.009362 206.0 \n", + "4 -0.002592 -0.031991 -0.046641 135.0 \n", + ".. ... ... ... ... \n", + "437 -0.002592 0.031193 0.007207 178.0 \n", + "438 0.034309 -0.018118 0.044485 104.0 \n", + "439 -0.011080 -0.046879 0.015491 132.0 \n", + "440 0.026560 0.044528 -0.025930 220.0 \n", + "441 -0.039493 -0.004220 0.003064 57.0 \n", + "\n", + "[442 rows x 11 columns]" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "diabetes_df" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "1f07666e-edf3-4585-b5a1-cc28dac9b04a", + "metadata": {}, + "outputs": [], + "source": [ + "from sklearn.model_selection import train_test_split\n", + "train_diabetes, test_diabetes, train_targets_diabetes, test_targets_diabetes = train_test_split(diabetes_df, diabetes_df.target, train_size=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "bb779dee-8df3-4096-adbe-49a76fcb7667", + "metadata": {}, + "source": [ + "This time let's add target during umap fit" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "994c5a46-6e9f-49b2-a5fa-49095d473cd4", + "metadata": {}, + "outputs": [], + "source": [ + "g = graphistry.nodes(train_diabetes)\n", + "g5 = g.umap(X=diabetes_features, y = 'target', \n", + " use_scaler=None, # 'robust',\n", + " use_scaler_target=None, #'standard'\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "15933c92-6f40-40be-b5ab-bf300e67e359", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g5.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "da4eef58-517c-4908-bff1-7bb5c4872617", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "----------------------------------------\n", + "DBSCAN found 15 clusters with 0 outliers\n", + "--fit on umap embeddings of size (221, 2)\n", + "----------------------------------------\n" + ] + } + ], + "source": [ + "#predict on unseen data\n", + "# notice you don't need to add y=test_diabetes.target, graphistry knows what column from fit\n", + "g_pred = g5.dbscan(verbose=True).transform_dbscan(test_diabetes, y=test_diabetes)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "7a14df9a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# will predict dbscan label from fit\n", + "g_pred.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "9a1d000d", + "metadata": {}, + "source": [ + "## Add your favorite model \n", + "\n", + "We will use Optuna to demonstrate a sample pipeline with HPO" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "6477f87a-82c7-471f-acfa-1bdec88eb655", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m[I 2023-01-20 17:48:23,033]\u001b[0m A new study created in memory with name: Diabetes\u001b[0m\u001b[32m[I 2023-01-20 17:48:23,150]\u001b[0m Trial 0 finished with value: 0.4491825511591293 and parameters: {'n_estimators': 67, 'max_depth': 199, 'min_samples_split': 36, 'scaler': 'robust'}. Best is trial 0 with value: 0.4491825511591293.\u001b[0m\u001b[32m[I 2023-01-20 17:48:23,344]\u001b[0m Trial 1 finished with value: 0.3611013176493034 and parameters: {'n_estimators': 186, 'max_depth': 36, 'min_samples_split': 88, 'scaler': 'standard'}. Best is trial 0 with value: 0.4491825511591293.\u001b[0m\u001b[32m[I 2023-01-20 17:48:23,636]\u001b[0m Trial 2 finished with value: 0.4599555207008448 and parameters: {'n_estimators': 221, 'max_depth': 186, 'min_samples_split': 31, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:23,953]\u001b[0m Trial 3 finished with value: 0.43972380164107383 and parameters: {'n_estimators': 237, 'max_depth': 187, 'min_samples_split': 41, 'scaler': 'standard'}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:24,217]\u001b[0m Trial 4 finished with value: 0.41916456260562696 and parameters: {'n_estimators': 232, 'max_depth': 161, 'min_samples_split': 58, 'scaler': 'quantile'}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:24,454]\u001b[0m Trial 5 finished with value: 0.4390071382812232 and parameters: {'n_estimators': 219, 'max_depth': 162, 'min_samples_split': 40, 'scaler': 'robust'}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:24,606]\u001b[0m Trial 6 finished with value: 0.4477315398732742 and parameters: {'n_estimators': 126, 'max_depth': 58, 'min_samples_split': 38, 'scaler': 'quantile'}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:24,857]\u001b[0m Trial 7 finished with value: 0.4238875440292236 and parameters: {'n_estimators': 181, 'max_depth': 175, 'min_samples_split': 56, 'scaler': 'quantile'}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:24,978]\u001b[0m Trial 8 finished with value: 0.34947836592039183 and parameters: {'n_estimators': 93, 'max_depth': 164, 'min_samples_split': 100, 'scaler': 'quantile'}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:25,141]\u001b[0m Trial 9 finished with value: 0.3410582039173441 and parameters: {'n_estimators': 167, 'max_depth': 144, 'min_samples_split': 96, 'scaler': 'robust'}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:25,331]\u001b[0m Trial 10 finished with value: 0.42789907031888397 and parameters: {'n_estimators': 137, 'max_depth': 104, 'min_samples_split': 3, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:25,406]\u001b[0m Trial 11 finished with value: 0.4407087833174965 and parameters: {'n_estimators': 54, 'max_depth': 198, 'min_samples_split': 18, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:25,483]\u001b[0m Trial 12 finished with value: 0.43123238250660256 and parameters: {'n_estimators': 56, 'max_depth': 118, 'min_samples_split': 21, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:25,592]\u001b[0m Trial 13 finished with value: 0.41759544231255075 and parameters: {'n_estimators': 93, 'max_depth': 129, 'min_samples_split': 65, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:25,722]\u001b[0m Trial 14 finished with value: 0.4460536456206613 and parameters: {'n_estimators': 103, 'max_depth': 3, 'min_samples_split': 21, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:25,946]\u001b[0m Trial 15 finished with value: 0.39157752162536286 and parameters: {'n_estimators': 209, 'max_depth': 88, 'min_samples_split': 71, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:26,138]\u001b[0m Trial 16 finished with value: 0.45319719982218454 and parameters: {'n_estimators': 157, 'max_depth': 199, 'min_samples_split': 31, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:26,350]\u001b[0m Trial 17 finished with value: 0.45767575785023773 and parameters: {'n_estimators': 158, 'max_depth': 138, 'min_samples_split': 7, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:26,616]\u001b[0m Trial 18 finished with value: 0.43892702761740565 and parameters: {'n_estimators': 192, 'max_depth': 135, 'min_samples_split': 3, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:26,865]\u001b[0m Trial 19 finished with value: 0.45450074484535274 and parameters: {'n_estimators': 206, 'max_depth': 85, 'min_samples_split': 12, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:27,032]\u001b[0m Trial 20 finished with value: 0.455948254738035 and parameters: {'n_estimators': 128, 'max_depth': 139, 'min_samples_split': 28, 'scaler': None}. Best is trial 2 with value: 0.4599555207008448.\u001b[0m\u001b[32m[I 2023-01-20 17:48:27,181]\u001b[0m Trial 21 finished with value: 0.4659875568563212 and parameters: {'n_estimators': 125, 'max_depth': 148, 'min_samples_split': 28, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:27,328]\u001b[0m Trial 22 finished with value: 0.45261144904606754 and parameters: {'n_estimators': 113, 'max_depth': 150, 'min_samples_split': 11, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:27,502]\u001b[0m Trial 23 finished with value: 0.4432510337508433 and parameters: {'n_estimators': 149, 'max_depth': 112, 'min_samples_split': 47, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:27,695]\u001b[0m Trial 24 finished with value: 0.454945193668833 and parameters: {'n_estimators': 169, 'max_depth': 176, 'min_samples_split': 28, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:28,000]\u001b[0m Trial 25 finished with value: 0.457519704282382 and parameters: {'n_estimators': 250, 'max_depth': 122, 'min_samples_split': 13, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:28,170]\u001b[0m Trial 26 finished with value: 0.454372244336522 and parameters: {'n_estimators': 145, 'max_depth': 153, 'min_samples_split': 26, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:28,262]\u001b[0m Trial 27 finished with value: 0.4426481977872042 and parameters: {'n_estimators': 74, 'max_depth': 177, 'min_samples_split': 50, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:28,517]\u001b[0m Trial 28 finished with value: 0.45478126295796806 and parameters: {'n_estimators': 118, 'max_depth': 90, 'min_samples_split': 8, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:28,620]\u001b[0m Trial 29 finished with value: 0.45498664133674827 and parameters: {'n_estimators': 73, 'max_depth': 187, 'min_samples_split': 17, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:28,804]\u001b[0m Trial 30 finished with value: 0.45590798145391953 and parameters: {'n_estimators': 166, 'max_depth': 130, 'min_samples_split': 36, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:29,089]\u001b[0m Trial 31 finished with value: 0.4571133333268268 and parameters: {'n_estimators': 238, 'max_depth': 120, 'min_samples_split': 15, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:29,416]\u001b[0m Trial 32 finished with value: 0.45617411970879174 and parameters: {'n_estimators': 249, 'max_depth': 77, 'min_samples_split': 7, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:29,664]\u001b[0m Trial 33 finished with value: 0.45364032098575513 and parameters: {'n_estimators': 223, 'max_depth': 104, 'min_samples_split': 33, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:29,996]\u001b[0m Trial 34 finished with value: 0.45900022338396884 and parameters: {'n_estimators': 247, 'max_depth': 151, 'min_samples_split': 24, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:30,244]\u001b[0m Trial 35 finished with value: 0.4588124181389295 and parameters: {'n_estimators': 222, 'max_depth': 159, 'min_samples_split': 23, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:30,537]\u001b[0m Trial 36 finished with value: 0.4491496739615546 and parameters: {'n_estimators': 203, 'max_depth': 167, 'min_samples_split': 45, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:30,860]\u001b[0m Trial 37 finished with value: 0.45754435816463646 and parameters: {'n_estimators': 223, 'max_depth': 185, 'min_samples_split': 23, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:31,158]\u001b[0m Trial 38 finished with value: 0.45246743668284095 and parameters: {'n_estimators': 235, 'max_depth': 160, 'min_samples_split': 38, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:31,385]\u001b[0m Trial 39 finished with value: 0.45125727640745616 and parameters: {'n_estimators': 183, 'max_depth': 155, 'min_samples_split': 42, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:31,617]\u001b[0m Trial 40 finished with value: 0.38310567273175145 and parameters: {'n_estimators': 217, 'max_depth': 170, 'min_samples_split': 80, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:31,841]\u001b[0m Trial 41 finished with value: 0.45135671120312215 and parameters: {'n_estimators': 194, 'max_depth': 146, 'min_samples_split': 34, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:32,173]\u001b[0m Trial 42 finished with value: 0.4623472408695728 and parameters: {'n_estimators': 243, 'max_depth': 186, 'min_samples_split': 25, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:32,464]\u001b[0m Trial 43 finished with value: 0.45880662185066146 and parameters: {'n_estimators': 231, 'max_depth': 193, 'min_samples_split': 25, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:32,725]\u001b[0m Trial 44 finished with value: 0.4254733474254583 and parameters: {'n_estimators': 245, 'max_depth': 187, 'min_samples_split': 56, 'scaler': None}. Best is trial 21 with value: 0.4659875568563212.\u001b[0m\u001b[32m[I 2023-01-20 17:48:32,998]\u001b[0m Trial 45 finished with value: 0.4660198609778968 and parameters: {'n_estimators': 229, 'max_depth': 179, 'min_samples_split': 20, 'scaler': None}. Best is trial 45 with value: 0.4660198609778968.\u001b[0m\u001b[32m[I 2023-01-20 17:48:33,258]\u001b[0m Trial 46 finished with value: 0.45625373004784886 and parameters: {'n_estimators': 242, 'max_depth': 179, 'min_samples_split': 41, 'scaler': None}. Best is trial 45 with value: 0.4660198609778968.\u001b[0m\u001b[32m[I 2023-01-20 17:48:33,534]\u001b[0m Trial 47 finished with value: 0.46619351207189874 and parameters: {'n_estimators': 230, 'max_depth': 171, 'min_samples_split': 30, 'scaler': None}. Best is trial 47 with value: 0.46619351207189874.\u001b[0m\u001b[32m[I 2023-01-20 17:48:33,771]\u001b[0m Trial 48 finished with value: 0.45494562762820034 and parameters: {'n_estimators': 213, 'max_depth': 192, 'min_samples_split': 31, 'scaler': None}. Best is trial 47 with value: 0.46619351207189874.\u001b[0m\u001b[32m[I 2023-01-20 17:48:34,041]\u001b[0m Trial 49 finished with value: 0.4557102826724324 and parameters: {'n_estimators': 231, 'max_depth': 169, 'min_samples_split': 19, 'scaler': None}. Best is trial 47 with value: 0.46619351207189874.\u001b[0m\u001b[32m[I 2023-01-20 17:48:34,319]\u001b[0m Trial 50 finished with value: 0.4516936561028091 and parameters: {'n_estimators': 202, 'max_depth': 50, 'min_samples_split': 30, 'scaler': None}. Best is trial 47 with value: 0.46619351207189874.\u001b[0m\u001b[32m[I 2023-01-20 17:48:34,594]\u001b[0m Trial 51 finished with value: 0.45929745923478704 and parameters: {'n_estimators': 229, 'max_depth': 180, 'min_samples_split': 21, 'scaler': None}. Best is trial 47 with value: 0.46619351207189874.\u001b[0m\u001b[32m[I 2023-01-20 17:48:34,919]\u001b[0m Trial 52 finished with value: 0.44727016157267296 and parameters: {'n_estimators': 228, 'max_depth': 183, 'min_samples_split': 19, 'scaler': None}. Best is trial 47 with value: 0.46619351207189874.\u001b[0m\u001b[32m[I 2023-01-20 17:48:35,188]\u001b[0m Trial 53 finished with value: 0.45218714108673586 and parameters: {'n_estimators': 240, 'max_depth': 197, 'min_samples_split': 36, 'scaler': None}. Best is trial 47 with value: 0.46619351207189874.\u001b[0m\u001b[32m[I 2023-01-20 17:48:35,432]\u001b[0m Trial 54 finished with value: 0.4701388301973731 and parameters: {'n_estimators': 211, 'max_depth': 174, 'min_samples_split': 28, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:35,647]\u001b[0m Trial 55 finished with value: 0.4593754940333289 and parameters: {'n_estimators': 194, 'max_depth': 169, 'min_samples_split': 29, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:35,878]\u001b[0m Trial 56 finished with value: 0.4419216452203949 and parameters: {'n_estimators': 213, 'max_depth': 199, 'min_samples_split': 44, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:36,055]\u001b[0m Trial 57 finished with value: 0.450938540662852 and parameters: {'n_estimators': 138, 'max_depth': 174, 'min_samples_split': 15, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:36,259]\u001b[0m Trial 58 finished with value: 0.43026746690972073 and parameters: {'n_estimators': 178, 'max_depth': 8, 'min_samples_split': 52, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:36,492]\u001b[0m Trial 59 finished with value: 0.45457491311201015 and parameters: {'n_estimators': 200, 'max_depth': 165, 'min_samples_split': 27, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:36,614]\u001b[0m Trial 60 finished with value: 0.40069290700280147 and parameters: {'n_estimators': 105, 'max_depth': 144, 'min_samples_split': 63, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:36,865]\u001b[0m Trial 61 finished with value: 0.45504892901006855 and parameters: {'n_estimators': 210, 'max_depth': 173, 'min_samples_split': 30, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:37,122]\u001b[0m Trial 62 finished with value: 0.45717348281658643 and parameters: {'n_estimators': 219, 'max_depth': 192, 'min_samples_split': 34, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:37,335]\u001b[0m Trial 63 finished with value: 0.4501861610344382 and parameters: {'n_estimators': 198, 'max_depth': 167, 'min_samples_split': 39, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:37,568]\u001b[0m Trial 64 finished with value: 0.44870852810746464 and parameters: {'n_estimators': 192, 'max_depth': 158, 'min_samples_split': 27, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:37,838]\u001b[0m Trial 65 finished with value: 0.4563869472586366 and parameters: {'n_estimators': 236, 'max_depth': 188, 'min_samples_split': 21, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:38,065]\u001b[0m Trial 66 finished with value: 0.4569723780426377 and parameters: {'n_estimators': 128, 'max_depth': 179, 'min_samples_split': 10, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:38,372]\u001b[0m Trial 67 finished with value: 0.456697483371164 and parameters: {'n_estimators': 225, 'max_depth': 173, 'min_samples_split': 15, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:38,635]\u001b[0m Trial 68 finished with value: 0.46013491230813797 and parameters: {'n_estimators': 242, 'max_depth': 165, 'min_samples_split': 32, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:38,926]\u001b[0m Trial 69 finished with value: 0.4397294382637812 and parameters: {'n_estimators': 242, 'max_depth': 163, 'min_samples_split': 49, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:39,190]\u001b[0m Trial 70 finished with value: 0.45716735862355085 and parameters: {'n_estimators': 235, 'max_depth': 150, 'min_samples_split': 37, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:39,437]\u001b[0m Trial 71 finished with value: 0.46796689949458525 and parameters: {'n_estimators': 218, 'max_depth': 183, 'min_samples_split': 32, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:39,671]\u001b[0m Trial 72 finished with value: 0.4456845438286513 and parameters: {'n_estimators': 213, 'max_depth': 185, 'min_samples_split': 33, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:39,995]\u001b[0m Trial 73 finished with value: 0.4514167986364557 and parameters: {'n_estimators': 220, 'max_depth': 192, 'min_samples_split': 25, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:40,273]\u001b[0m Trial 74 finished with value: 0.45177084288055314 and parameters: {'n_estimators': 250, 'max_depth': 179, 'min_samples_split': 31, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:40,438]\u001b[0m Trial 75 finished with value: 0.4594222462394846 and parameters: {'n_estimators': 119, 'max_depth': 164, 'min_samples_split': 23, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:40,565]\u001b[0m Trial 76 finished with value: 0.43638091942293633 and parameters: {'n_estimators': 91, 'max_depth': 182, 'min_samples_split': 18, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:40,810]\u001b[0m Trial 77 finished with value: 0.46255342600547644 and parameters: {'n_estimators': 207, 'max_depth': 200, 'min_samples_split': 27, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:41,077]\u001b[0m Trial 78 finished with value: 0.45610024289436835 and parameters: {'n_estimators': 228, 'max_depth': 200, 'min_samples_split': 27, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:41,346]\u001b[0m Trial 79 finished with value: 0.4582111765957191 and parameters: {'n_estimators': 242, 'max_depth': 194, 'min_samples_split': 36, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:41,597]\u001b[0m Trial 80 finished with value: 0.45155009333029805 and parameters: {'n_estimators': 217, 'max_depth': 157, 'min_samples_split': 21, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:41,823]\u001b[0m Trial 81 finished with value: 0.457165117559989 and parameters: {'n_estimators': 206, 'max_depth': 188, 'min_samples_split': 33, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:42,095]\u001b[0m Trial 82 finished with value: 0.45188605373933566 and parameters: {'n_estimators': 234, 'max_depth': 173, 'min_samples_split': 25, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:42,334]\u001b[0m Trial 83 finished with value: 0.44722523300012185 and parameters: {'n_estimators': 225, 'max_depth': 183, 'min_samples_split': 41, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:42,574]\u001b[0m Trial 84 finished with value: 0.4510143253057257 and parameters: {'n_estimators': 216, 'max_depth': 175, 'min_samples_split': 29, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:42,802]\u001b[0m Trial 85 finished with value: 0.46160001164446474 and parameters: {'n_estimators': 207, 'max_depth': 190, 'min_samples_split': 33, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:43,046]\u001b[0m Trial 86 finished with value: 0.4553338410446598 and parameters: {'n_estimators': 206, 'max_depth': 189, 'min_samples_split': 35, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:43,265]\u001b[0m Trial 87 finished with value: 0.4523616082050852 and parameters: {'n_estimators': 188, 'max_depth': 178, 'min_samples_split': 32, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:43,539]\u001b[0m Trial 88 finished with value: 0.4477729392160493 and parameters: {'n_estimators': 245, 'max_depth': 197, 'min_samples_split': 39, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:43,813]\u001b[0m Trial 89 finished with value: 0.45152815088723597 and parameters: {'n_estimators': 238, 'max_depth': 138, 'min_samples_split': 23, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:44,029]\u001b[0m Trial 90 finished with value: 0.46012304014983285 and parameters: {'n_estimators': 178, 'max_depth': 112, 'min_samples_split': 16, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:44,244]\u001b[0m Trial 91 finished with value: 0.4622758723528394 and parameters: {'n_estimators': 175, 'max_depth': 115, 'min_samples_split': 17, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:44,458]\u001b[0m Trial 92 finished with value: 0.4541918082851871 and parameters: {'n_estimators': 173, 'max_depth': 98, 'min_samples_split': 12, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:44,657]\u001b[0m Trial 93 finished with value: 0.43938742291576083 and parameters: {'n_estimators': 144, 'max_depth': 130, 'min_samples_split': 5, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:44,908]\u001b[0m Trial 94 finished with value: 0.45869732641582117 and parameters: {'n_estimators': 158, 'max_depth': 79, 'min_samples_split': 21, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:45,156]\u001b[0m Trial 95 finished with value: 0.45681249933713675 and parameters: {'n_estimators': 210, 'max_depth': 170, 'min_samples_split': 28, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:45,431]\u001b[0m Trial 96 finished with value: 0.452179948667231 and parameters: {'n_estimators': 231, 'max_depth': 195, 'min_samples_split': 19, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:45,681]\u001b[0m Trial 97 finished with value: 0.4573138975493334 and parameters: {'n_estimators': 198, 'max_depth': 69, 'min_samples_split': 25, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:45,872]\u001b[0m Trial 98 finished with value: 0.45309032342176114 and parameters: {'n_estimators': 163, 'max_depth': 148, 'min_samples_split': 31, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m\u001b[32m[I 2023-01-20 17:48:46,099]\u001b[0m Trial 99 finished with value: 0.3818803064112405 and parameters: {'n_estimators': 222, 'max_depth': 154, 'min_samples_split': 77, 'scaler': None}. Best is trial 54 with value: 0.4701388301973731.\u001b[0m" + ] + } + ], + "source": [ + "from sklearn.ensemble import RandomForestRegressor\n", + "from sklearn.model_selection import cross_val_score\n", + "import optuna\n", + "\n", + "#X_train, y_train = g5.get_matrix(), g5.get_matrix(target=True)\n", + "X_test, y_test = g5.transform(test_diabetes, test_diabetes, return_graph=False)\n", + "\n", + "def objective(trail):\n", + " n_estimators = trail.suggest_int('n_estimators', 50, 250)\n", + " max_depth = trail.suggest_int('max_depth', 2, 200)\n", + " min_samples_split = trail.suggest_int('min_samples_split', 2, 100)\n", + " \n", + " use_scaler = trail.suggest_categorical('scaler', [None, 'standard', 'robust', 'quantile'])\n", + " X_train, y_train = g5.scale(use_scaler=use_scaler)\n", + " X_test, y_test = g5.scale(test_diabetes, test_diabetes, use_scaler=use_scaler)\n", + " \n", + " rlf = RandomForestRegressor(n_estimators=n_estimators, max_depth=max_depth, min_samples_split=min_samples_split)\n", + " score = rlf.fit(X_train, y_train).score(X_test, y_test)\n", + " return score\n", + "\n", + " \n", + "study = optuna.create_study(study_name='Diabetes', direction='maximize')\n", + "\n", + "study.optimize(objective, n_trials=100)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "560a471b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'n_estimators': 211,\n", + " 'max_depth': 174,\n", + " 'min_samples_split': 28,\n", + " 'scaler': None}" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "study.best_params" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "0a859318", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "cliponaxis": false, + "hovertemplate": [ + "max_depth (IntUniformDistribution): 0.0021138252662894702", + "n_estimators (IntUniformDistribution): 0.0070885829626843345", + "scaler (CategoricalDistribution): 0.016431479175241275", + "min_samples_split (IntUniformDistribution): 0.974366112595785" + ], + "marker": { + "color": "rgb(66,146,198)" + }, + "orientation": "h", + "text": [ + "0.0021138252662894702", + "0.0070885829626843345", + "0.016431479175241275", + "0.974366112595785" + ], + "textposition": "outside", + "texttemplate": "%{text:.2f}", + "type": "bar", + "x": [ + 0.0021138252662894702, + 0.0070885829626843345, + 0.016431479175241275, + 0.974366112595785 + ], + "y": [ + "max_depth", + "n_estimators", + "scaler", + "min_samples_split" + ] + } + ], + "layout": { + "showlegend": false, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Hyperparameter Importances" + }, + "xaxis": { + "title": { + "text": "Importance for Objective Value" + } + }, + "yaxis": { + "title": { + "text": "Hyperparameter" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = optuna.visualization.plot_param_importances(study)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "4450a842", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "colorbar": { + "title": { + "text": "Objective Value" + } + }, + "colorscale": [ + [ + 0, + "rgb(5,10,172)" + ], + [ + 0.35, + "rgb(40,60,190)" + ], + [ + 0.5, + "rgb(70,100,245)" + ], + [ + 0.6, + "rgb(90,120,245)" + ], + [ + 0.7, + "rgb(106,137,247)" + ], + [ + 1, + "rgb(220,220,220)" + ] + ], + "connectgaps": true, + "contours": { + "coloring": "heatmap" + }, + "hoverinfo": "none", + "line": { + "smoothing": 1.3 + }, + "reversescale": false, + "type": "contour", + "x": [ + -6.850000000000001, + 3, + 8, + 36, + 50, + 58, + 69, + 77, + 79, + 85, + 88, + 90, + 98, + 104, + 112, + 115, + 118, + 120, + 122, + 129, + 130, + 135, + 138, + 139, + 144, + 146, + 148, + 150, + 151, + 153, + 154, + 155, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 167, + 169, + 170, + 171, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 182, + 183, + 185, + 186, + 187, + 188, + 189, + 190, + 192, + 193, + 194, + 195, + 197, + 198, + 199, + 200, + 209.85 + ], + "y": [ + -1.8500000000000005, + 3, + 5, + 7, + 8, + 10, + 11, + 12, + 13, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 44, + 45, + 47, + 49, + 50, + 52, + 56, + 58, + 63, + 65, + 71, + 77, + 80, + 88, + 96, + 100, + 104.85 + ], + "z": [ + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.42789907031888397, + null, + null, + null, + null, + null, + null, + null, + 0.43892702761740565, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.43938742291576083, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + 0.45617411970879174, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45767575785023773, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45478126295796806, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4569723780426377, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45261144904606754, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45450074484535274, + null, + null, + 0.4541918082851871, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.457519704282382, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4571133333268268, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.456697483371164, + 0.450938540662852, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.46012304014983285, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4622758723528394, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45498664133674827, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.43638091942293633, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4407087833174965, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4557102826724324, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.44727016157267296, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.452179948667231, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4660198609778968, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + 0.4460536456206613, + null, + null, + null, + null, + null, + null, + 0.45869732641582117, + null, + null, + null, + null, + null, + null, + null, + 0.43123238250660256, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45155009333029805, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45929745923478704, + null, + null, + null, + null, + null, + 0.4563869472586366, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45152815088723597, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4588124181389295, + null, + null, + null, + null, + 0.4594222462394846, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45754435816463646, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45900022338396884, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + 0.4573138975493334, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45188605373933566, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4623472408695728, + null, + null, + null, + null, + 0.4514167986364557, + 0.45880662185066146, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.454372244336522, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.44870852810746464, + null, + null, + null, + null, + null, + null, + 0.45457491311201015, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45610024289436835, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.455948254738035, + null, + null, + 0.4659875568563212, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45681249933713675, + null, + null, + 0.4701388301973731, + null, + 0.454945193668833, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4593754940333289, + null, + null, + null, + null, + 0.4510143253057257, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + 0.4516936561028091, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.46619351207189874, + 0.45504892901006855, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45309032342176114, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45177084288055314, + null, + null, + null, + null, + 0.4599555207008448, + null, + null, + null, + null, + 0.45494562762820034, + null, + null, + null, + null, + null, + 0.45319719982218454, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.46013491230813797, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4523616082050852, + null, + null, + null, + 0.46796689949458525, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45364032098575513, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4456845438286513, + null, + null, + 0.457165117559989, + null, + 0.46160001164446474, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45135671120312215, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45717348281658643, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4553338410446598, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45590798145391953, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4582111765957191, + null, + 0.45218714108673586, + null, + 0.4491825511591293, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45716735862355085, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + 0.4477315398732742, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45246743668284095, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4501861610344382, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4477729392160493, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4390071382812232, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45625373004784886, + null, + null, + 0.44722523300012185, + null, + null, + 0.43972380164107383, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45125727640745616, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4419216452203949, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4491496739615546, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4432510337508433, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4397294382637812, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4426481977872042, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + 0.43026746690972073, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4238875440292236, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4254733474254583, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.41916456260562696, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.40069290700280147, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.41759544231255075, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.39157752162536286, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.3818803064112405, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.38310567273175145, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + 0.3611013176493034, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.3410582039173441, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.34947836592039183, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + ] + }, + { + "marker": { + "color": "black", + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "type": "scatter", + "x": [ + 199, + 36, + 186, + 187, + 161, + 162, + 58, + 175, + 164, + 144, + 104, + 198, + 118, + 129, + 3, + 88, + 199, + 138, + 135, + 85, + 139, + 148, + 150, + 112, + 176, + 122, + 153, + 177, + 90, + 187, + 130, + 120, + 77, + 104, + 151, + 159, + 167, + 185, + 160, + 155, + 170, + 146, + 186, + 193, + 187, + 179, + 179, + 171, + 192, + 169, + 50, + 180, + 183, + 197, + 174, + 169, + 199, + 174, + 8, + 165, + 144, + 173, + 192, + 167, + 158, + 188, + 179, + 173, + 165, + 163, + 150, + 183, + 185, + 192, + 179, + 164, + 182, + 200, + 200, + 194, + 157, + 188, + 173, + 183, + 175, + 190, + 189, + 178, + 197, + 138, + 112, + 115, + 98, + 130, + 79, + 170, + 195, + 69, + 148, + 154 + ], + "y": [ + 36, + 88, + 31, + 41, + 58, + 40, + 38, + 56, + 100, + 96, + 3, + 18, + 21, + 65, + 21, + 71, + 31, + 7, + 3, + 12, + 28, + 28, + 11, + 47, + 28, + 13, + 26, + 50, + 8, + 17, + 36, + 15, + 7, + 33, + 24, + 23, + 45, + 23, + 38, + 42, + 80, + 34, + 25, + 25, + 56, + 20, + 41, + 30, + 31, + 19, + 30, + 21, + 19, + 36, + 28, + 29, + 44, + 15, + 52, + 27, + 63, + 30, + 34, + 39, + 27, + 21, + 10, + 15, + 32, + 49, + 37, + 32, + 33, + 25, + 31, + 23, + 18, + 27, + 27, + 36, + 21, + 33, + 25, + 41, + 29, + 33, + 35, + 32, + 39, + 23, + 16, + 17, + 12, + 5, + 21, + 28, + 19, + 25, + 31, + 77 + ] + } + ], + "layout": { + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Contour Plot" + }, + "xaxis": { + "range": [ + -6.850000000000001, + 209.85 + ], + "title": { + "text": "max_depth" + } + }, + "yaxis": { + "range": [ + -1.8500000000000005, + 104.85 + ], + "title": { + "text": "min_samples_split" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = optuna.visualization.plot_contour(study, params=[\"max_depth\", \"min_samples_split\"])\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "b9df8675", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "colorbar": { + "title": { + "text": "Objective Value" + } + }, + "colorscale": [ + [ + 0, + "rgb(5,10,172)" + ], + [ + 0.35, + "rgb(40,60,190)" + ], + [ + 0.5, + "rgb(70,100,245)" + ], + [ + 0.6, + "rgb(90,120,245)" + ], + [ + 0.7, + "rgb(106,137,247)" + ], + [ + 1, + "rgb(220,220,220)" + ] + ], + "connectgaps": true, + "contours": { + "coloring": "heatmap" + }, + "hoverinfo": "none", + "line": { + "smoothing": 1.3 + }, + "reversescale": false, + "type": "contour", + "x": [ + -6.850000000000001, + 3, + 8, + 36, + 50, + 58, + 69, + 77, + 79, + 85, + 88, + 90, + 98, + 104, + 112, + 115, + 118, + 120, + 122, + 129, + 130, + 135, + 138, + 139, + 144, + 146, + 148, + 150, + 151, + 153, + 154, + 155, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 167, + 169, + 170, + 171, + 173, + 174, + 175, + 176, + 177, + 178, + 179, + 180, + 182, + 183, + 185, + 186, + 187, + 188, + 189, + 190, + 192, + 193, + 194, + 195, + 197, + 198, + 199, + 200, + 209.85 + ], + "y": [ + 44.2, + 54, + 56, + 67, + 73, + 74, + 91, + 93, + 103, + 105, + 113, + 118, + 119, + 125, + 126, + 128, + 137, + 138, + 144, + 145, + 149, + 157, + 158, + 163, + 166, + 167, + 169, + 173, + 175, + 178, + 181, + 183, + 186, + 188, + 192, + 194, + 198, + 200, + 202, + 203, + 206, + 207, + 209, + 210, + 211, + 213, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 225, + 228, + 229, + 230, + 231, + 232, + 234, + 235, + 236, + 237, + 238, + 240, + 242, + 243, + 245, + 247, + 249, + 250, + 259.8 + ], + "z": [ + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4407087833174965, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.43123238250660256, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4491825511591293, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45498664133674827, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4426481977872042, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.43638091942293633, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.41759544231255075, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.34947836592039183, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + 0.4460536456206613, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.40069290700280147, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45261144904606754, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45478126295796806, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4594222462394846, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4659875568563212, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + 0.4477315398732742, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.455948254738035, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4569723780426377, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.42789907031888397, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.450938540662852, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.43938742291576083, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.454372244336522, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4432510337508433, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45319719982218454, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + 0.45869732641582117, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45767575785023773, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45309032342176114, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45590798145391953, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.3410582039173441, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.454945193668833, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4541918082851871, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4622758723528394, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + 0.43026746690972073, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.46012304014983285, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4238875440292236, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45125727640745616, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + 0.3611013176493034, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4523616082050852, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.43892702761740565, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.44870852810746464, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45135671120312215, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4593754940333289, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + 0.4573138975493334, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4501861610344382, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45457491311201015, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + 0.4516936561028091, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4491496739615546, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45450074484535274, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.457165117559989, + 0.4553338410446598, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.46160001164446474, + null, + null, + null, + null, + null, + null, + null, + 0.46255342600547644, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.39157752162536286, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45681249933713675, + null, + 0.45504892901006855, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4701388301973731, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4456845438286513, + null, + null, + null, + null, + null, + 0.45494562762820034, + null, + null, + null, + null, + null, + 0.4419216452203949, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4510143253057257, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45155009333029805, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.38310567273175145, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.46796689949458525, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4390071382812232, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45717348281658643, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4514167986364557, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4599555207008448, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.3818803064112405, + null, + null, + null, + 0.4588124181389295, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45364032098575513, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45754435816463646, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.456697483371164, + null, + null, + null, + null, + null, + null, + null, + null, + 0.44722523300012185, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.44727016157267296, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45610024289436835, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4660198609778968, + 0.45929745923478704, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.46619351207189874, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4557102826724324, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45880662185066146, + null, + 0.452179948667231, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.41916456260562696, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45188605373933566, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45716735862355085, + null, + null, + null, + null, + null, + null, + null, + 0.45246743668284095, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4563869472586366, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.43972380164107383, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4571133333268268, + null, + null, + null, + null, + 0.45152815088723597, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45218714108673586, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4397294382637812, + null, + 0.46013491230813797, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45625373004784886, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4582111765957191, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4623472408695728, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.4254733474254583, + null, + null, + null, + null, + null, + null, + null, + 0.4477729392160493, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45900022338396884, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + 0.45617411970879174, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.457519704282382, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 0.45177084288055314, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + ] + }, + { + "marker": { + "color": "black", + "line": { + "color": "Grey", + "width": 0.5 + } + }, + "mode": "markers", + "showlegend": false, + "type": "scatter", + "x": [ + 199, + 36, + 186, + 187, + 161, + 162, + 58, + 175, + 164, + 144, + 104, + 198, + 118, + 129, + 3, + 88, + 199, + 138, + 135, + 85, + 139, + 148, + 150, + 112, + 176, + 122, + 153, + 177, + 90, + 187, + 130, + 120, + 77, + 104, + 151, + 159, + 167, + 185, + 160, + 155, + 170, + 146, + 186, + 193, + 187, + 179, + 179, + 171, + 192, + 169, + 50, + 180, + 183, + 197, + 174, + 169, + 199, + 174, + 8, + 165, + 144, + 173, + 192, + 167, + 158, + 188, + 179, + 173, + 165, + 163, + 150, + 183, + 185, + 192, + 179, + 164, + 182, + 200, + 200, + 194, + 157, + 188, + 173, + 183, + 175, + 190, + 189, + 178, + 197, + 138, + 112, + 115, + 98, + 130, + 79, + 170, + 195, + 69, + 148, + 154 + ], + "y": [ + 67, + 186, + 221, + 237, + 232, + 219, + 126, + 181, + 93, + 167, + 137, + 54, + 56, + 93, + 103, + 209, + 157, + 158, + 192, + 206, + 128, + 125, + 113, + 149, + 169, + 250, + 145, + 74, + 118, + 73, + 166, + 238, + 249, + 223, + 247, + 222, + 203, + 223, + 235, + 183, + 217, + 194, + 243, + 231, + 245, + 229, + 242, + 230, + 213, + 231, + 202, + 229, + 228, + 240, + 211, + 194, + 213, + 138, + 178, + 200, + 105, + 210, + 219, + 198, + 192, + 236, + 128, + 225, + 242, + 242, + 235, + 218, + 213, + 220, + 250, + 119, + 91, + 207, + 228, + 242, + 217, + 206, + 234, + 225, + 216, + 207, + 206, + 188, + 245, + 238, + 178, + 175, + 173, + 144, + 158, + 210, + 231, + 198, + 163, + 222 + ] + } + ], + "layout": { + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Contour Plot" + }, + "xaxis": { + "range": [ + -6.850000000000001, + 209.85 + ], + "title": { + "text": "max_depth" + } + }, + "yaxis": { + "range": [ + 44.2, + 259.8 + ], + "title": { + "text": "n_estimators" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig = optuna.visualization.plot_contour(study, params=[\"max_depth\", \"n_estimators\"])\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "fa378193-667c-428c-8962-e0a0fb5fef06", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "g5.get_matrix(target=True).plot(kind='hist')" + ] + }, + { + "cell_type": "markdown", + "id": "b3589f00", + "metadata": {}, + "source": [ + "Fit a model without target. One dimensional targets are used by UMAP during fit. (n, k>1) targets are ignored during fit. " + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "f81a96d9-a45f-4bf4-a302-86af6d2aae15", + "metadata": {}, + "outputs": [], + "source": [ + "# let's removed `sex` as a feature, which splits the data strongly, and generate a new model,\n", + "feats = ['age', 'bmi', 'bp', 's1', 's2', 's3', 's4', 's5', 's6']" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "363c5a6a-5183-4aae-9439-7756ccbb8bb1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (221, 0) in UMAP fit, as it is not one dimensional" + ] + } + ], + "source": [ + "g = graphistry.nodes(train_diabetes) # \n", + "# this time we scale the data \n", + "g6 = g.umap(X = feats, # y='target', # don't include target for fun (which helps supervise umap fit when 1-dimensional)\n", + " use_scaler='standard', #None, #'robust', 'kbins', 'quantile', 'minmax'\n", + " use_scaler_target='standard', \n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "3509fac0-434f-4a08-bb54-256825b66af3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g6.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "f7379c1b-65d8-4350-b38b-b57f54cd97b3", + "metadata": {}, + "source": [ + "# Digits" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "0eb0f820-7118-43bb-8419-52acb4a4f925", + "metadata": {}, + "outputs": [], + "source": [ + "data3 = load_digits()\n", + "digit_features = list(data3['feature_names'])\n", + "digits_df = pd.DataFrame(data3['data'], columns=digit_features)\n", + "digits_df['target'] = data3['target'].astype(int)\n", + "digits_df['names'] = digits_df.target.astype(str)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "e3aa6bdd-b0b6-46a7-93ae-041321bef461", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (898, 0) in UMAP fit, as it is not one dimensional" + ] + } + ], + "source": [ + "a, b, c, d = train_test_split(digits_df, digits_df.target, train_size=0.5)\n", + "\n", + "g6=graphistry.nodes(a).umap(X=digit_features, \n", + " #y='target', this obviously works great to separate clusters during UMAP fit.\n", + " use_scaler=None)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "a1156045-87df-443d-b75f-63d418941924", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g6.bind(point_title='target').plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "afad90db-df22-4da6-abb9-cb161ef008c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------------------------------------\n", + "Infering edges over UMAP embedding\n", + "---------------------------------------------\n", + " Mean distance to existing nodes 5.85 +/- 3.68\n", + " Max distance threshold; epsilon = 2.17\n", + " Finding 7 nearest neighbors\n", + " 138.65 neighbors per node within epsilon 2.17\n", + " 6293 total edges after dropping duplicates\n", + " ** Final graph has 962 nodes\n", + " - Batch has 899 nodes\n", + " - Brought in 63 nodes\n", + "--------------------------------------------------\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g7 = g6.transform_umap(b, min_dist='auto', verbose=True, merge_policy=True)\n", + "g7.bind(point_title='target').plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "be9b4506-960a-4204-b6c6-194d7fa9135d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------------------------------------\n", + "Infering edges over UMAP embedding\n", + "---------------------------------------------\n", + " Mean distance to existing nodes 5.83 +/- 3.66\n", + " Max distance threshold; epsilon = 2.17\n", + " Finding 7 nearest neighbors\n", + " 137.59 neighbors per node within epsilon 2.17\n", + " 6246 total edges after dropping duplicates\n", + " ** Final graph has 899 nodes\n", + "--------------------------------------------------\n" + ] + }, + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g7 = g6.transform_umap(b, min_dist='auto', verbose=True, merge_policy=False)\n", + "g7.bind(point_title='target').plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "987a2bce-ffed-4724-a8a7-d3dc07f48262", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(array([[ 0., 0., 5., 13., 9., 1., 0., 0.],\n", + " [ 0., 0., 13., 15., 10., 15., 5., 0.],\n", + " [ 0., 3., 15., 2., 0., 11., 8., 0.],\n", + " [ 0., 4., 12., 0., 0., 8., 8., 0.],\n", + " [ 0., 5., 8., 0., 0., 9., 8., 0.],\n", + " [ 0., 4., 11., 0., 1., 12., 7., 0.],\n", + " [ 0., 2., 14., 5., 10., 12., 0., 0.],\n", + " [ 0., 0., 6., 13., 10., 0., 0., 0.]]),\n", + " 0.0)" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "digits_df[digit_features].iloc[0].values.reshape(8,8), df.iloc[0].target" + ] + }, + { + "cell_type": "markdown", + "id": "050acfb4-301e-42bc-bc7b-961bc4793608", + "metadata": {}, + "source": [ + "# Build a GNN model " + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "d0d2c3c0-cfa4-4335-b2d4-fa77ec0eae94", + "metadata": {}, + "outputs": [], + "source": [ + "g6 = g2.build_gnn(y_nodes='target')" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "7507c3d8-89d1-4d49-9534-9c2e50376934", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Graph(num_nodes=284, num_edges=4626,\n", + " ndata_schemes={'feature': Scheme(shape=(30,), dtype=torch.float64), 'train_mask': Scheme(shape=(), dtype=torch.bool), 'test_mask': Scheme(shape=(), dtype=torch.bool)}\n", + " edata_schemes={'feature': Scheme(shape=(286,), dtype=torch.float64), 'train_mask': Scheme(shape=(), dtype=torch.bool), 'test_mask': Scheme(shape=(), dtype=torch.bool)})" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "G = g6.DGL_graph\n", + "G" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "e237ce99-191d-473d-80bb-49211978fd5f", + "metadata": {}, + "outputs": [], + "source": [ + "# run a prediction task from https://docs.dgl.ai/tutorials/blitz/index.html" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4eb84597", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 649ac531e3d4e1546ef527b5f779ab278aeb875d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Jan 2023 18:29:42 -0800 Subject: [PATCH 127/432] adds cyber redteam demo improvements --- demos/ai/cyber/cyber-redteam-umap-demo.ipynb | 1366 +++++++++--------- 1 file changed, 661 insertions(+), 705 deletions(-) diff --git a/demos/ai/cyber/cyber-redteam-umap-demo.ipynb b/demos/ai/cyber/cyber-redteam-umap-demo.ipynb index 9e30bb5dff..b07a7403f8 100644 --- a/demos/ai/cyber/cyber-redteam-umap-demo.ipynb +++ b/demos/ai/cyber/cyber-redteam-umap-demo.ipynb @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "0215906c", "metadata": {}, "outputs": [], @@ -38,15 +38,12 @@ "from collections import Counter\n", "\n", "import numpy as np\n", - "import matplotlib.pylab as plt\n", - "\n", - "from sklearn.cluster import DBSCAN\n", - "from sknetwork.ranking import PageRank\n" + "import matplotlib.pylab as plt\n" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "8e1747b9-c903-4398-9aa0-b52b69fce021", "metadata": {}, "outputs": [], @@ -56,17 +53,17 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "6d2669fd-6164-4376-81bd-79c6c6f4112f", "metadata": {}, "outputs": [], "source": [ - "RENDER = False # set to True to render Graphistry UI inline" + "RENDER = True # set to True to render Graphistry UI inline" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "59e1cc0b", "metadata": {}, "outputs": [], @@ -96,12 +93,12 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "fe6e61b0", "metadata": {}, "outputs": [], "source": [ - "# cite data source\n", + "# data source citation\n", "# \"\"\"A. D. Kent, \"Cybersecurity Data Sources for Dynamic Network Research,\"\n", "# in Dynamic Networks in Cybersecurity, 2015.\n", "\n", @@ -125,7 +122,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "efe68cf8", "metadata": {}, "outputs": [ @@ -267,7 +264,7 @@ "28495743 0.0 C574 C523 Kerberos Network C574 C523 " ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -280,7 +277,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "03610297", "metadata": {}, "outputs": [ @@ -298,23 +295,13 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "66c5126e", "metadata": {}, "outputs": [], "source": [ "# here are the post-facto red team events\n", - "red_team = pd.read_csv('https://gist.githubusercontent.com/silkspace/5cf5a94b9ac4b4ffe38904f20d93edb1/raw/888dabd86f88ea747cf9ff5f6c44725e21536465/redteam_labels.csv', index_col=0)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "7b31d2b0-b123-4f7c-9157-03accce6a6c7", - "metadata": {}, - "outputs": [], - "source": [ - "# for later\n", + "red_team = pd.read_csv('https://gist.githubusercontent.com/silkspace/5cf5a94b9ac4b4ffe38904f20d93edb1/raw/888dabd86f88ea747cf9ff5f6c44725e21536465/redteam_labels.csv', index_col=0)\n", "red_team['feats2'] = red_team.feats" ] }, @@ -358,14 +345,6 @@ "print(ndf.shape)" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "b1c15b72-a355-48d5-9a4e-31dbb6a47b06", - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "markdown", "id": "32d1755d", @@ -717,54 +696,30 @@ "metadata": {}, "outputs": [], "source": [ - "# some enrichments\n", - "def pagerank(g):\n", - " from sknetwork.ranking import PageRank\n", - " adj = g._weighted_adjacency\n", - " pagerank = PageRank()\n", - " ranks = pagerank.fit_transform(adj)\n", - " g._nodes['pagerank'] = ranks\n", - " return g\n", - "\n", - "def cluster(g):\n", - " \"\"\"\n", - " Fits clustering on UMAP embeddings\n", - " \"\"\"\n", - " dbscan = DBSCAN()\n", - " labels = dbscan.fit_predict(g._node_embedding)\n", - " g._nodes['cluster'] = labels\n", - " cnt = Counter(labels)\n", - " return g, dbscan, cnt\n", - "\n", - "def get_confidences_per_cluster(g, cnt):\n", + "def get_confidences_per_cluster(g, col='RED', verbose=False):\n", " \"\"\"\n", " From DBSCAN clusters, will assess how many Red Team events exist,\n", " assessing confidence.\n", + " \n", " \"\"\"\n", " resses = []\n", " df = g._nodes\n", + " labels = df._dbscan\n", + " cnt = Counter(labels)\n", " for clust, count in cnt.most_common():\n", - " res = df[df.cluster==clust]\n", + " res = df[df._dbscan==clust]\n", " n = res.shape[0]\n", - " n_reds = res.RED.sum()\n", + " n_reds = res[col].sum()\n", " resses.append([clust, n_reds/n, n_reds, n])\n", - " if n_reds>0:\n", + " if n_reds>0 and verbose:\n", " print('-'*20)\n", " print(f'cluster: {clust}\\n red {100*n_reds/n:.2f}% or {n_reds} out of {count}')\n", - " conf_dict = {k[0]:k[1] for k in resses}\n", - " confidence = [conf_dict[k] for k in df.cluster.values]\n", + " conf_dict = {k[0]: k[1] for k in resses}\n", + " confidence = [conf_dict[k] for k in df._dbscan.values]\n", " g._nodes['confidence'] = confidence\n", - " return g, pd.DataFrame(resses, columns=['cluster', 'confidence', 'n_red', 'total_in_cluster'])\n", - "\n", - "\n", - "def enrich(g):\n", - " \"\"\"\n", - " Full Pipeline \n", - " \"\"\"\n", - " g = pagerank(g)\n", - " g, dbscan, cnt = cluster(g)\n", - " g, cluster_confidences = get_confidences_per_cluster(g, cnt)\n", - " return g, dbscan, cluster_confidences\n", + " conf_df = pd.DataFrame(resses, columns=['_dbscan', 'confidence', 'n_red', 'total_in_cluster'])\n", + " conf_df = conf_df.sort_values(by='confidence', ascending=False)\n", + " return g, conf_df\n", " " ] }, @@ -800,7 +755,7 @@ { "data": { "text/plain": [ - "{'kind': 'nodes', 'use_scaler': None, 'use_scaler_target': None, 'cardinality_threshold': 2, 'cardinality_threshold_target': 2, 'n_topics': 32, 'n_topics_target': 10, 'multilabel': False, 'embedding': False, 'use_ngrams': False, 'ngram_range': (1, 3), 'max_df': 0.2, 'min_df': 3, 'min_words': 40000000.0, 'model_name': 'sentence-transformers/msmarco-distilbert-base-v2', 'impute': 'median', 'n_quantiles': 100, 'output_distribution': 'normal', 'quantile_range': (25, 75), 'n_bins': 10, 'encode': 'ordinal', 'strategy': 'uniform', 'similarity': None, 'categories': 'auto', 'keep_n_decimals': 5, 'remove_node_column': True, 'inplace': False, 'feature_engine': 'auto', 'memoize': True, 'X': ['feats']}" + "{'cardinality_threshold': 2, 'cardinality_threshold_target': 2, 'n_topics': 32, 'n_topics_target': 10, 'min_words': 1000000000.0, 'X': ['feats']}" ] }, "execution_count": 16, @@ -827,7 +782,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "* Ignoring target column of shape (19762, 0) in UMAP fit, as it is not one dimensionalOMP: Info #273: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" + "* Ignoring target column of shape (19762, 0) in UMAP fit, as it is not one dimensionalOMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" ] }, { @@ -835,91 +790,127 @@ "output_type": "stream", "text": [ "--------------------\n", - "cluster: 0\n", - " red 2.59% or 95.0 out of 3665\n", + "cluster: 3\n", + " red 0.66% or 22.0 out of 3331\n", "--------------------\n", - "cluster: 27\n", + "cluster: 39\n", " red 0.41% or 3.0 out of 724\n", "--------------------\n", - "cluster: 26\n", + "cluster: 9\n", + " red 1.15% or 3.0 out of 260\n", + "--------------------\n", + "cluster: 38\n", " red 0.38% or 1.0 out of 260\n", "--------------------\n", - "cluster: 10\n", + "cluster: 13\n", " red 0.43% or 1.0 out of 234\n", "--------------------\n", - "cluster: 1\n", - " red 94.44% or 119.0 out of 126\n", - "--------------------\n", - "cluster: 6\n", + "cluster: 8\n", " red 95.06% or 77.0 out of 81\n", "--------------------\n", - "cluster: 9\n", - " red 84.42% or 65.0 out of 77\n", - "--------------------\n", - "cluster: 5\n", - " red 96.61% or 57.0 out of 59\n", + "cluster: 1\n", + " red 100.00% or 53.0 out of 53\n", "--------------------\n", - "cluster: 7\n", + "cluster: 10\n", " red 91.84% or 45.0 out of 49\n", "--------------------\n", - "cluster: 14\n", + "cluster: 12\n", + " red 82.61% or 38.0 out of 46\n", + "--------------------\n", + "cluster: 22\n", + " red 95.65% or 44.0 out of 46\n", + "--------------------\n", + "cluster: 18\n", " red 92.11% or 35.0 out of 38\n", "--------------------\n", - "cluster: 3\n", - " red 94.59% or 35.0 out of 37\n", + "cluster: 19\n", + " red 82.86% or 29.0 out of 35\n", "--------------------\n", - "cluster: 22\n", + "cluster: 15\n", + " red 86.67% or 26.0 out of 30\n", + "--------------------\n", + "cluster: 27\n", + " red 92.59% or 25.0 out of 27\n", + "--------------------\n", + "cluster: 32\n", " red 100.00% or 27.0 out of 27\n", "--------------------\n", - "cluster: 4\n", + "cluster: 28\n", + " red 100.00% or 26.0 out of 26\n", + "--------------------\n", + "cluster: 6\n", " red 84.00% or 21.0 out of 25\n", "--------------------\n", - "cluster: 23\n", + "cluster: 2\n", + " red 87.50% or 21.0 out of 24\n", + "--------------------\n", + "cluster: 35\n", " red 100.00% or 24.0 out of 24\n", "--------------------\n", - "cluster: 8\n", + "cluster: 0\n", " red 100.00% or 23.0 out of 23\n", "--------------------\n", - "cluster: 20\n", + "cluster: 11\n", + " red 100.00% or 23.0 out of 23\n", + "--------------------\n", + "cluster: 30\n", " red 81.25% or 13.0 out of 16\n", "--------------------\n", - "cluster: 13\n", + "cluster: 17\n", " red 93.33% or 14.0 out of 15\n", "--------------------\n", - "cluster: 16\n", + "cluster: 21\n", " red 100.00% or 15.0 out of 15\n", "--------------------\n", - "cluster: 2\n", + "cluster: 23\n", + " red 100.00% or 15.0 out of 15\n", + "--------------------\n", + "cluster: 4\n", " red 100.00% or 14.0 out of 14\n", "--------------------\n", - "cluster: 25\n", + "cluster: 29\n", + " red 100.00% or 14.0 out of 14\n", + "--------------------\n", + "cluster: 7\n", " red 100.00% or 13.0 out of 13\n", "--------------------\n", - "cluster: 11\n", + "cluster: 37\n", + " red 100.00% or 13.0 out of 13\n", + "--------------------\n", + "cluster: 14\n", " red 100.00% or 11.0 out of 11\n", "--------------------\n", - "cluster: 18\n", + "cluster: 5\n", + " red 100.00% or 10.0 out of 10\n", + "--------------------\n", + "cluster: 25\n", " red 100.00% or 9.0 out of 9\n", "--------------------\n", - "cluster: 15\n", + "cluster: 33\n", + " red 88.89% or 8.0 out of 9\n", + "--------------------\n", + "cluster: 20\n", " red 100.00% or 6.0 out of 6\n", "--------------------\n", - "cluster: 24\n", + "cluster: 36\n", " red 100.00% or 6.0 out of 6\n", "--------------------\n", - "cluster: 12\n", + "cluster: 16\n", " red 100.00% or 5.0 out of 5\n", "--------------------\n", - "cluster: 17\n", + "cluster: 24\n", " red 100.00% or 5.0 out of 5\n", "--------------------\n", - "cluster: 19\n", + "cluster: 26\n", " red 100.00% or 5.0 out of 5\n", "--------------------\n", - "cluster: 21\n", + "cluster: 31\n", " red 100.00% or 5.0 out of 5\n", - "CPU times: user 3min 16s, sys: 32 s, total: 3min 48s\n", - "Wall time: 1min 57s\n" + "--------------------\n", + "cluster: 34\n", + " red 100.00% or 1.0 out of 1\n", + "CPU times: user 3min 40s, sys: 39.2 s, total: 4min 19s\n", + "Wall time: 2min 7s\n" ] } ], @@ -929,16 +920,15 @@ "if process:\n", " # ##################################\n", " g = graphistry.nodes(tdf, 'node') # two lines does the heavy lifting\n", - " g5 = g.umap(**cyber_model)\n", + " g5 = g.umap(**cyber_model).dbscan(min_dist=0.2)\n", " # #########################\n", " \n", - " g5, dbscan, cluster_confidences = enrich(g5)\n", - "\n", + " g5, cluster_confidences = get_confidences_per_cluster(g5, verbose=True)\n", " g5.save_search_instance('auth-feat-topic.search')\n", "else:\n", " g = graphistry.bind()\n", " g5 = g.load_search_instance('auth-feat-topic.search')\n", - " g5, dbscan, cluster_confidences = enrich(g5)\n" + " g5, cluster_confidences = get_confidences_per_cluster(g5)" ] }, { @@ -958,8 +948,25 @@ "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=77ee7b6a4daa4539a93f0c60e34934c0&type=arrow&viztoken=4d612b44-1558-4398-8964-744cf5c9c632&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672345808&info=true&play=0'" + "" ] }, "execution_count": 18, @@ -1462,12 +1469,46 @@ "X" ] }, + { + "cell_type": "code", + "execution_count": 45, + "id": "87b32e09-3ca4-49de-b8c3-2b40ffa2b01d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 45, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXAAAAD4CAYAAAD1jb0+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAABG00lEQVR4nO2dd3hUxfrHv7ObpghKk4ugBpSaDjGACKh0uJZwudKuYEW9cm0/BRRF9FrAAogiiIggF+lNBZEWIXQCBggBQoAAgZCEQDopm31/f2zJtrO9nc37eZ482Z2dM/OeOee8Z+add94RRASGYRhGfih8LQDDMAzjHKzAGYZhZAorcIZhGJnCCpxhGEamsAJnGIaRKUHerKxJkyYUHh7uzSoZhmFkz6FDh64SUVPTdK8q8PDwcKSkpHizSoZhGNkjhDhvKZ1NKAzDMDKFFTjDMIxMYQXOMAwjU7xqA3cH1dXVyM7ORkVFha9FYRiGcSthYWFo2bIlgoOD7covOwWenZ2N+vXrIzw8HEIIX4vDMAzjFogIBQUFyM7ORqtWrew6RnYmlIqKCjRu3JiVN8MwAYUQAo0bN3bIuiA7BQ6AlTfDMAGJo7pNlgrckNKKalRW1/haDIZhGK9jU4ELIcKEEAeEEEeEEMeFEB9o01sJIfYLITKFEMuFECGeF9ecs1fLcCq3xKt1zpo1Cx06dMCoUaMcPjYrKws///yzB6Syn6eeegqrVq1y+vgpU6bgiy++AABMnjwZW7dutZr/zz//xJ49e5yuz14WLlyIy5cv678/99xzSE9P93i97qawsBDffvutXXnvv/9+m3lmzpyJ8vJyV8WyiqnMly9fxtChQ91eT107X1vY0wOvBPAwEcUAiAUwQAjRFcA0ADOI6F4A1wE86zEp/Yxvv/0WW7ZswZIlSxw+1h8UuDv58MMP0adPH6t5nFHgKpXKYVlMFfj8+fPRsWNHh8uxF2dktAdHFLg97eqMQqupcWxUayrzHXfc4VInQYq6dr42ISK7/wDcDOAwgC4ArgII0qZ3A/CHreM7d+5MrpKenm70/cjF63Tk4nWXy7WXF154gYKDgykyMpKmT59OpaWl9PTTT9N9991HsbGxtG7dOiIiOnfuHD3wwAMUFxdHcXFxtHv3biIi6tKlCzVo0IBiYmJo+vTplJaWRvfddx/FxMRQVFQUZWRkWK3/9OnT1Lt3b4qOjqa4uDjKzMykkpISevjhhykuLo4iIyP1MhARLVq0iKKioig6Opr+9a9/ERHRmDFj6D//+Q9169aNWrVqRStXrtTn/+yzzyg+Pp6ioqJo8uTJ+vSPPvqI2rRpQ927d6fhw4fT559/ri9Ld/zdd99NkydP1stx4sQJOnfuHDVr1ozuuOMOiomJoZ07d1JeXh4NGTKE4uPjKT4+nnbt2kVERO+//z7961//ovvvv5+GDx8u2YZERFOnTqXIyEiKjo6mCRMm0MqVK6levXrUtm1biomJofLycurVqxcdPHiQ5syZQ2+++ab+2B9//JFefvllIiJavHixvv3Hjh1LKpXKavv36tWLXn31VercuTN98cUXlJKSQj179qROnTpRv3796PLly/p8r732GnXu3Jnat29PBw4coMTERLr33ntp0qRJ+vK+/PJLioiIoIiICJoxYwYREQ0bNozCwsIoJiZGL7fUdalXrx4RESUlJVGvXr3oH//4B7Vr145GjhxJarWavvrqK/39+uCDDxIR0R9//EFdu3aluLg4Gjp0KJWUlOiv3/jx4ykuLo6WLl1K8+bNo/j4eIqOjqYhQ4ZQWVkZERFduXKFHn/8cYqOjqbo6GjavXu3mcznzp2jiIgIItLc82lpaUZtePDgQclnxxp14XxNdRwREYAUsqSTLSWaZQKUAFIBlELT824CINPg9zsBpEkcOxZACoCUu+66y+YFsoXhyU35JY0Gf7WTBn+1k56Yu8ctf1N+SbNSu4a7776b8vPziYjo7bffpsWLFxMR0fXr16lNmzZUWlpKZWVldOPGDSIiysjIIN3LKykpiQYPHqwva9y4cfS///2PiIgqKyupvLyciIgGDhxIly5dMqs7ISGB1qxZQ0REN27coLKyMqqurqaioiIiIsrPz6d77rmH1Go1paWlUZs2bfSyFhQUEJFG6Q4dOpRqamro+PHjdM899xCR5kZ//vnnSa1WU01NDQ0ePJh27NhBKSkpFBkZSWVlZVRUVET33HOPpAKfNWsWERHNnj2bnn32WSLSKGZdfiKiESNGUHJyMhERnT9/ntq3b6/P16lTJ30bSLXhxo0bqVu3bvoHTHdeugdFh+57Xl6e/hyJiAYMGEDJycmUnp5Of//736mqqoqIiF566SVatGiR+QU3oFevXvTSSy8REVFVVRV169aN8vLyiIho2bJl9PTTT+vzjR8/noiIZs6cSc2bN6fLly9TRUUFtWjRgq5evapv19LSUiopKaGOHTvS4cOHjZSBtetCZKzQGjRoQBcvXqSamhrq2rWrvo0N79f8/Hzq0aMHlZaWEpHmRfjBBx/o802bNk1f79WrV/WfJ02apL+2TzzxhP5lo1KpqLCw0Exmw+/Tp0/Xv3QuX75Mbdu2JSLpZ8cadeF8HVHgdvmBE1ENgFghxG0A1gJo70APfx6AeQAQHx8fcBtwbt68Gb/88oveJlxRUYELFy7gjjvuwLhx45CamgqlUomMjAyLx3fr1g0ff/wxsrOzMWTIELRp0wYAsHHjRrO8JSUluHTpEhITEwFonP4BzeKmd955Bzt37oRCocClS5eQm5uL7du345///CeaNGkCAGjUqJG+rMcffxwKhQIdO3ZEbm6u/lw2b96MuLg4AEBpaSlOnz6NkpISJCYm4uabbwYAPProo5LtMWTIEABA586dsWbNGot5tm7damSbLi4uRmlpqb7sm266SX9eltpw69atePrpp/XyGJ6XJZo2bYrWrVtj3759aNOmDU6ePInu3btj9uzZOHToEO677z4AwI0bN3D77bdbLQsAhg0bBgA4deoU0tLS0LdvXwCaYXjz5s31+XTtFBUVhYiICP1vrVu3xsWLF7Fr1y4kJiaiXr16+rZLTk42a1+p69KzZ0+jfAkJCWjZsiUAIDY2FllZWXjggQeM8uzbtw/p6eno3r07AKCqqgrdunUzOzcASEtLw7vvvovCwkKUlpaif//+AIDt27fjp59+AgAolUrceuutuH79umR7PfHEE+jXrx8++OADrFixQm8rlnp2OnToIFlWXT5fSzi0kIeICoUQSdCYTG4TQgQRkQpASwCXnJbCSd5/JAJHswsBANEtb/N29QA0I5jVq1ejXbt2RulTpkxBs2bNcOTIEajVar2yNWXkyJHo0qULNmzYgEGDBuG7777Dww8/7JAMS5YsQX5+Pg4dOoTg4GCEh4fb9CUNDQ01Ogfd/7fffhsvvPCCUd6ZM2faLYuuXKVSKWkjVqvV2Ldvn8U20SkzAJgxY4ZdbWgPw4cPx4oVK9C+fXskJiZCCAEiwpgxY/Dpp586VJZORiJCREQE9u7dazGfri0UCoVReysUCofs51LXRao+QLr9iQh9+/bF0qVLLZZh2P5PPfUU1q1bh5iYGCxcuBB//vmn3TIb0qJFCzRu3BhHjx7F8uXLMXfuXL0slp4de6lr52sJe7xQmmp73hBC3ASgL4ATAJIA6KZdxwBY7zapZET//v3x9ddf65XgX3/9BQAoKipC8+bNoVAosHjxYv0kSf369VFSUus1c/bsWbRu3RqvvPIKHnvsMRw9elSyrvr166Nly5ZYt24dAKCyshLl5eUoKirC7bffjuDgYCQlJeH8eU3kyYcffhgrV65EQUEBAODatWs2z2XBggX63vClS5eQl5eHnj17Yt26dbhx4wZKSkrw66+/OtRGpufcr18/fP311/rvqampFo+TasO+ffvixx9/1E9U6c7LtB5DEhMTsX79eixduhTDhw8HAPTu3RurVq1CXl6evhxd240ePRoHDhywel7t2rVDfn6+XoFXV1fj+PHjVo8xpEePHli3bh3Ky8tRVlaGtWvXokePHmbnIXVd7MWwvK5du2L37t3IzMwEAJSVlUmODktKStC8eXNUV1cbTdj37t0bc+bMAaAZdRQVFVlte0DT0/3ss89QVFSE6Oho/XlZenYuXbqE3r17231+cj9fV7DHC6U5gCQhxFEABwFsIaLfAEwA8IYQIhNAYwA/uCyNFZbsP48t6bmerMIp3nvvPVRXVyM6OhoRERF47733AAD//ve/sWjRIsTExODkyZP6N310dDSUSiViYmIwY8YMrFixApGRkYiNjUVaWhpGjx4NABg0aJCRR4WOxYsXY9asWYiOjsb999+PK1euYNSoUUhJSUFUVBR++ukntG+vsXBFRERg0qRJ6NWrF2JiYvDGG29YPZd+/fph5MiR6NatG6KiojB06FCUlJSgU6dOGDZsGGJiYjBw4EC9ycFeHnnkEaxduxaxsbFITk7GrFmzkJKSgujoaHTs2FHfQzFFqg0HDBiARx99FPHx8YiNjdUPSZ966im8+OKLiI2NxY0bN4zKatiwITp06IDz588jISEBANCxY0d89NFH6NevH6Kjo9G3b1/k5OQAAI4ePYo77rjD6nmFhIRg1apVmDBhAmJiYhAbG+uQt02nTp3w1FNPISEhAV26dMFzzz2HuLg4NG7cGN27d0dkZCTeeustyetiL2PHjsWAAQPw0EMPoWnTpli4cCFGjBiB6OhodOvWDSdPnrR43H//+1906dIF3bt3199TAPDVV18h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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "x = g5.get_matrix(['interactive', 'c17'])\n", + "x.plot()" + ] + }, { "cell_type": "markdown", "id": "632d6d0f-8212-4f4a-a920-7600d7456351", "metadata": {}, "source": [ - "## Predict/Online Mode\n", + "## Predict | Online Mode\n", "\n", "Once a model is fit, predict on new batches as we demonstrate here\n", "\n", @@ -1485,20 +1526,12 @@ "execution_count": 20, "id": "7b44d418", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "'SuperVectorizer' object has no attribute 'get_feature_names_in''SuperVectorizer' object has no attribute 'get_feature_names_in'" - ] - } - ], + "outputs": [], "source": [ "# first sample a batch from the normal data (auth=df)\n", - "emb_normal, xp_normal, _ = g5.transform_umap(df.sample(200), None, kind='nodes')\n", + "emb_normal, xp_normal, _ = g5.transform_umap(df.sample(200), None, kind='nodes', return_graph=False)\n", "# then transform all the red team data\n", - "emb_red, xp_red, _ = g5.transform_umap(red_team, None, kind='nodes')" + "emb_red, xp_red, _ = g5.transform_umap(red_team, None, kind='nodes', return_graph=False)" ] }, { @@ -1629,7 +1662,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 22, @@ -1658,6 +1691,16 @@ "plt.scatter(emb_normal.x, emb_normal.y, c='g') # batch of new data, to see if they occlude " ] }, + { + "cell_type": "code", + "execution_count": 23, + "id": "f9f98708-f18f-4248-96fb-498a4becad89", + "metadata": {}, + "outputs": [], + "source": [ + "#g5.transform_umap(df.sample(200).append(red_team)).plot()" + ] + }, { "cell_type": "markdown", "id": "b53dd8ed-39b2-4000-9ec7-139d1e2a6a85", @@ -1672,7 +1715,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "id": "14d207db-9a58-45a3-9876-058632389f17", "metadata": {}, "outputs": [ @@ -1680,7 +1723,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "93.92%\n" + "94.11%\n" ] } ], @@ -1692,17 +1735,17 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "id": "755a3f27-935d-4ba8-96cb-cbff11fdf00e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "19071" + "18998" ] }, - "execution_count": 24, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -1714,7 +1757,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "id": "5fd1cc50-0900-4694-8400-c426e314ec2e", "metadata": {}, "outputs": [ @@ -1722,7 +1765,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Alert Reduction 96.50%\n" + "Alert Reduction 96.13%\n" ] } ], @@ -1733,7 +1776,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 27, "id": "0ee508a5", "metadata": {}, "outputs": [ @@ -1746,7 +1789,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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\n", "text/plain": [ "
" ] @@ -1779,7 +1822,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 28, "id": "e0c6a16d-a899-43b6-a7ba-75b45f855a78", "metadata": {}, "outputs": [ @@ -1787,92 +1830,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "--------------------\n", - "cluster: 2\n", - " red 99.63% or 267.0 out of 268\n", - "--------------------\n", - "cluster: 6\n", - " red 100.00% or 58.0 out of 58\n", - "--------------------\n", - "cluster: 4\n", - " red 97.22% or 35.0 out of 36\n", - "--------------------\n", - "cluster: 8\n", - " red 97.14% or 34.0 out of 35\n", - "--------------------\n", - "cluster: 16\n", - " red 100.00% or 34.0 out of 34\n", - "--------------------\n", - "cluster: 11\n", - " red 100.00% or 31.0 out of 31\n", - "--------------------\n", - "cluster: 24\n", - " red 100.00% or 27.0 out of 27\n", - "--------------------\n", - "cluster: 25\n", - " red 100.00% or 24.0 out of 24\n", - "--------------------\n", - "cluster: 3\n", - " red 100.00% or 19.0 out of 19\n", - "--------------------\n", - "cluster: 7\n", - " red 100.00% or 18.0 out of 18\n", - "--------------------\n", - "cluster: 17\n", - " red 100.00% or 18.0 out of 18\n", - "--------------------\n", - "cluster: 0\n", - " red 100.00% or 17.0 out of 17\n", - "--------------------\n", - "cluster: 12\n", - " red 100.00% or 17.0 out of 17\n", - "--------------------\n", - "cluster: 18\n", - " red 94.12% or 16.0 out of 17\n", - "--------------------\n", - "cluster: 26\n", - " red 100.00% or 17.0 out of 17\n", - "--------------------\n", - "cluster: 14\n", - " red 100.00% or 15.0 out of 15\n", - "--------------------\n", - "cluster: 5\n", - " red 100.00% or 14.0 out of 14\n", - "--------------------\n", - "cluster: 10\n", - " red 100.00% or 14.0 out of 14\n", - "--------------------\n", - "cluster: 1\n", - " red 100.00% or 13.0 out of 13\n", - "--------------------\n", - "cluster: 13\n", - " red 100.00% or 9.0 out of 9\n", - "--------------------\n", - "cluster: 19\n", - " red 100.00% or 9.0 out of 9\n", - "--------------------\n", - "cluster: 15\n", - " red 100.00% or 8.0 out of 8\n", - "--------------------\n", - "cluster: 21\n", - " red 100.00% or 8.0 out of 8\n", - "--------------------\n", - "cluster: 23\n", - " red 87.50% or 7.0 out of 8\n", - "--------------------\n", - "cluster: -1\n", - " red 71.43% or 5.0 out of 7\n", - "--------------------\n", - "cluster: 9\n", - " red 100.00% or 5.0 out of 5\n", - "--------------------\n", - "cluster: 20\n", - " red 100.00% or 5.0 out of 5\n", - "--------------------\n", - "cluster: 22\n", - " red 100.00% or 5.0 out of 5\n", - "CPU times: user 2min 56s, sys: 33.1 s, total: 3min 29s\n", - "Wall time: 1min 24s\n" + "CPU times: user 3min 14s, sys: 38.6 s, total: 3min 52s\n", + "Wall time: 1min 34s\n" ] } ], @@ -1886,22 +1845,22 @@ " min_words=100000, # set high to bypass sbert encoding\n", " cardinality_threshold=2, # set low to force topic modeling\n", " n_topics=32,\n", - " use_scaler_target=None) # keep labels unscaled\n", + " use_scaler_target=None, # keep labels unscaled\n", + " dbscan=True) # add dbscan here\n", " # ##################################\n", " \n", - " g6, dbscan6, cluster_confidences6 = enrich(g6)\n", - " \n", + " g6, cluster_confidences6 = get_confidences_per_cluster(g6)\n", " g6.save_search_instance('auth-feat-supervised-topic.search')\n", "else:\n", " g = graphistry.bind()\n", " g6 = g.load_search_instance('auth-feat-supervised-topic.search')\n", - " \n", - " g6, dbscan6, cluster_confidences6 = enrich(g6)\n" + " g6, cluster_confidences6 = get_confidences_per_cluster(g6)\n", + " " ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 29, "id": "a98ef657-5307-41d9-ae31-79c1794b3728", "metadata": {}, "outputs": [ @@ -1996,13 +1955,13 @@ "[19762 rows x 1 columns]" ] }, - "execution_count": 28, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "g6._node_target.astype(int)" + "g6.get_matrix(target=True).astype(int)" ] }, { @@ -2013,22 +1972,39 @@ }, "source": [ "### Plot\n", - "Color by `confidence` and hover over `red` team histogram to see where events occur. Alternatively, color by `cluster` assignment" + "Color by `confidence` and hover over `red` team histogram to see where events occur. Alternatively, color by `_dbscan` assignment" ] }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 30, "id": "16e09a7d", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=426dd5f70ceb45f9bb8e8b8ac45a85ac&type=arrow&viztoken=bfeae91e-2f9e-4e1c-90a4-c968aed1a68e&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672345903&info=true&play=0'" + "" ] }, - "execution_count": 29, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -2048,7 +2024,39 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 31, + "id": "1731ae44-57e0-4c3e-bad0-ac486bba589c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 C17693 C1003\n", + "1 C17693 C305\n", + "2 C17693 C728\n", + "3 C17693 C1173\n", + "4 C17693 C294\n", + " ... \n", + "19008 C11843 C528\n", + "19009 C8470 C528\n", + "19010 C716 C716\n", + "19011 C16126 C586\n", + "19012 C6215 C6215\n", + "Name: feats2, Length: 19762, dtype: object" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tdf['feats2']" + ] + }, + { + "cell_type": "code", + "execution_count": 32, "id": "099b9d38", "metadata": {}, "outputs": [ @@ -2063,104 +2071,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "--------------------\n", - "cluster: 2\n", - " red 0.37% or 48.0 out of 12839\n", - "--------------------\n", - "cluster: 4\n", - " red 0.72% or 16.0 out of 2222\n", - "--------------------\n", - "cluster: 30\n", - " red 0.21% or 3.0 out of 1435\n", - "--------------------\n", - "cluster: 3\n", - " red 98.61% or 71.0 out of 72\n", - "--------------------\n", - "cluster: 10\n", - " red 100.00% or 51.0 out of 51\n", - "--------------------\n", - "cluster: 11\n", - " red 98.00% or 49.0 out of 50\n", - "--------------------\n", - "cluster: 14\n", - " red 97.67% or 42.0 out of 43\n", - "--------------------\n", - "cluster: 6\n", - " red 97.50% or 39.0 out of 40\n", - "--------------------\n", - "cluster: 12\n", - " red 97.44% or 38.0 out of 39\n", - "--------------------\n", - "cluster: 9\n", - " red 100.00% or 36.0 out of 36\n", - "--------------------\n", - "cluster: 0\n", - " red 100.00% or 33.0 out of 33\n", - "--------------------\n", - "cluster: 1\n", - " red 96.88% or 31.0 out of 32\n", - "--------------------\n", - "cluster: 18\n", - " red 100.00% or 30.0 out of 30\n", - "--------------------\n", - "cluster: 8\n", - " red 96.55% or 28.0 out of 29\n", - "--------------------\n", - "cluster: 20\n", - " red 96.43% or 27.0 out of 28\n", - "--------------------\n", - "cluster: 26\n", - " red 100.00% or 27.0 out of 27\n", - "--------------------\n", - "cluster: 29\n", - " red 96.00% or 24.0 out of 25\n", - "--------------------\n", - "cluster: 19\n", - " red 90.00% or 18.0 out of 20\n", - "--------------------\n", - "cluster: 5\n", - " red 100.00% or 17.0 out of 17\n", - "--------------------\n", - "cluster: 21\n", - " red 100.00% or 17.0 out of 17\n", - "--------------------\n", - "cluster: 25\n", - " red 100.00% or 13.0 out of 13\n", - "--------------------\n", - "cluster: 24\n", - " red 100.00% or 11.0 out of 11\n", - "--------------------\n", - "cluster: 16\n", - " red 100.00% or 10.0 out of 10\n", - "--------------------\n", - "cluster: 23\n", - " red 100.00% or 10.0 out of 10\n", - "--------------------\n", - "cluster: 7\n", - " red 100.00% or 9.0 out of 9\n", - "--------------------\n", - "cluster: 13\n", - " red 100.00% or 9.0 out of 9\n", - "--------------------\n", - "cluster: 15\n", - " red 88.89% or 8.0 out of 9\n", - "--------------------\n", - "cluster: 17\n", - " red 87.50% or 7.0 out of 8\n", - "--------------------\n", - "cluster: 27\n", - " red 100.00% or 8.0 out of 8\n", - "--------------------\n", - "cluster: 28\n", - " red 100.00% or 8.0 out of 8\n", - "--------------------\n", - "cluster: 31\n", - " red 85.71% or 6.0 out of 7\n", - "--------------------\n", - "cluster: 22\n", - " red 100.00% or 5.0 out of 5\n", - "CPU times: user 2min 39s, sys: 31.4 s, total: 3min 10s\n", - "Wall time: 1min 14s\n" + "CPU times: user 3min 5s, sys: 38.7 s, total: 3min 44s\n", + "Wall time: 1min 35s\n" ] } ], @@ -2174,14 +2086,15 @@ " min_words=100000, \n", " cardinality_threshold=2, \n", " n_topics=32,\n", - " use_scaler_target=None)\n", + " use_scaler=None,\n", + " use_scaler_target=None, \n", + " dbscan=True) # add dbscan here\n", " # ###################################\n", - " g7, dbscan7, cluster_confidences7 = enrich(g7)\n", - " g7.build_index()\n", + " g7, cluster_confidences7 = get_confidences_per_cluster(g7)\n", " g7.save_search_instance('auth-just-ip-topic.search')\n", "else:\n", " g7 = graphistry.bind().load_search_instance('auth-just-ip-topic.search')\n", - " g7, dbscan7, cluster_confidences7 = enrich(g7)\n" + " g7, cluster_confidences7 = get_confidences_per_cluster(g7)\n" ] }, { @@ -2197,28 +2110,46 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 33, "id": "c1e586a3", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=b85fc0d43e884508ad22d4a1e5daa03b&type=arrow&viztoken=75f5f3cc-b52a-4488-b0eb-9b9941208629&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672345982&info=true&play=0'" + "" ] }, - "execution_count": 31, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "g7.name('auth topic ips-ips only, no supervision').plot(render=RENDER)" + "g7.name('auth topic ips-ips only, no supervision').plot(render=RENDER)\n", + "# very similar to graph with metadata included, showing that ip-ip is strong indicator of phenomenon" ] }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 34, "id": "5f93d747", "metadata": {}, "outputs": [ @@ -2243,149 +2174,149 @@ " \n", " \n", " \n", - " feats2: c10555, c8555, c1055\n", - " feats2: c12222, c12226, c1227\n", - " feats2: c6665, c6667, c6653\n", - " feats2: c7703, c7701, c7707\n", - " feats2: c1992, c1922, c19932\n", - " feats2: c625, c612, c6125\n", - " feats2: c9028, c9904, c9283\n", - " feats2: c3073, c3037, c3074\n", - " feats2: c2106, c2626, c4210\n", - " feats2: c11196, c11918, c1111\n", + " feats2: c586, c585, c5864\n", + " feats2: c1961, c10196, c1901\n", + " feats2: c16916, c16169, c1616\n", + " feats2: c3636, c6363, c6365\n", + " feats2: c4944, c4444, c8444\n", + " feats2: c1065, c10652, c10585\n", + " feats2: c15556, c15550, c1555\n", + " feats2: c5999, c10999, c599\n", + " feats2: c17693, c6937, c3937\n", + " feats2: c8882, c8880, c8889\n", " ...\n", - " feats2: c4448, c4444, c4487\n", - " feats2: c17981, c2980, c1798\n", - " feats2: c4777, c14777, c4787\n", - " feats2: c7554, c7519, c5151\n", - " feats2: c10000, c10008, c10003\n", - " feats2: c25240, c2524, c2456\n", - " feats2: c809, c5099, c5809\n", - " feats2: c1065, c10658, c10656\n", - " feats2: c1550, c15034, c15615\n", - " feats2: c8182, c8882, c8889\n", + " feats2: c2890, c280, tgt\n", + " feats2: c3333, c3303, c3033\n", + " feats2: c11187, c1118, c1111\n", + " feats2: c1798, c1772, c1778\n", + " feats2: c3435, c3434, c3597\n", + " feats2: c2106, c210, c10000\n", + " feats2: c1085, c1080, c1081\n", + " feats2: c457, c222, c452\n", + " feats2: c1268, c1226, c12689\n", + " feats2: c6604, c16604, c16048\n", " \n", " \n", " \n", " \n", " 0\n", - " -0.34853\n", - " -0.29446\n", - " -0.34437\n", - " -0.31798\n", - " -0.34102\n", - " -0.46182\n", - " -0.33386\n", - " -0.32362\n", - " -0.31948\n", - " -0.32044\n", - " ...\n", - " -0.36251\n", - " -0.36984\n", - " -0.32987\n", - " -0.28672\n", - " 3.88640\n", - " -0.36522\n", - " -0.34670\n", - " -0.15082\n", - " -0.38757\n", - " -0.32410\n", + " 0.051269\n", + " 0.052370\n", + " 0.059563\n", + " 0.053012\n", + " 0.051056\n", + " 0.051073\n", + " 0.059119\n", + " 0.052026\n", + " 7.893405\n", + " 0.051389\n", + " ...\n", + " 0.050000\n", + " 0.101370\n", + " 0.059551\n", + " 1.219262\n", + " 0.051273\n", + " 2.699579\n", + " 3.244251\n", + " 0.051143\n", + " 0.059730\n", + " 0.051639\n", " \n", " \n", " 1\n", - " 0.09896\n", - " -0.29610\n", - " -0.34437\n", - " -0.31795\n", - " -0.34092\n", - " -0.46180\n", - " -0.33383\n", - " 2.52748\n", - " -0.32073\n", - " -0.32158\n", - " ...\n", - " -0.36238\n", - " -0.36982\n", - " -0.32984\n", - " -0.28669\n", - " -0.29198\n", - " -0.36519\n", - " -0.34666\n", - " -0.32204\n", - " -0.38862\n", - " -0.32407\n", + " 0.051271\n", + " 0.053484\n", + " 0.066529\n", + " 0.053060\n", + " 0.051057\n", + " 0.561800\n", + " 0.067701\n", + " 0.052046\n", + " 7.851341\n", + " 0.051392\n", + " ...\n", + " 0.050000\n", + " 2.741899\n", + " 0.068965\n", + " 1.368653\n", + " 1.024759\n", + " 0.051145\n", + " 0.108473\n", + " 0.051145\n", + " 0.070319\n", + " 0.052000\n", " \n", " \n", " 2\n", - " -0.34853\n", - " -0.29417\n", - " -0.34437\n", - " 3.43709\n", - " -0.33256\n", - " -0.46182\n", - " -0.00576\n", - " -0.32411\n", - " -0.32076\n", - " -0.32092\n", - " ...\n", - " -0.36235\n", - " -0.36985\n", - " -0.32988\n", - " -0.28673\n", - " -0.29119\n", - " -0.36523\n", - " -0.34668\n", - " -0.32147\n", - " -0.38820\n", - " -0.32411\n", + " 0.051264\n", + " 0.053257\n", + " 0.064578\n", + " 0.053130\n", + " 0.051051\n", + " 0.051067\n", + " 0.065562\n", + " 0.052079\n", + " 7.391063\n", + " 0.051386\n", + " ...\n", + " 0.063343\n", + " 0.051320\n", + " 0.066675\n", + " 1.671434\n", + " 0.051267\n", + " 0.051138\n", + " 0.097529\n", + " 0.051139\n", + " 0.067757\n", + " 0.051922\n", " \n", " \n", " 3\n", - " -0.34854\n", - " -0.29143\n", - " -0.34437\n", - " -0.31800\n", - " -0.34095\n", - " -0.46183\n", - " -0.33388\n", - " 0.60698\n", - " -0.32077\n", - " 2.87353\n", - " ...\n", - " -0.36240\n", - " -0.36985\n", - " -0.32989\n", - " -0.28674\n", - " -0.28688\n", - " -0.36524\n", - " -0.34671\n", - " -0.29361\n", - " -0.38584\n", - " -0.32412\n", + " 0.051251\n", + " 0.052477\n", + " 0.065590\n", + " 0.052994\n", + " 0.051041\n", + " 0.051057\n", + " 0.063403\n", + " 0.052009\n", + " 7.892231\n", + " 0.051369\n", + " ...\n", + " 0.050000\n", + " 0.051307\n", + " 3.263992\n", + " 3.747229\n", + " 0.053271\n", + " 0.051127\n", + " 0.192025\n", + " 0.051127\n", + " 0.063663\n", + " 0.051661\n", " \n", " \n", " 4\n", - " -0.34850\n", - " -0.27595\n", - " -0.34437\n", - " -0.31795\n", - " -0.34093\n", - " -0.46180\n", - " -0.32521\n", - " -0.32408\n", - " 0.41376\n", - " -0.32181\n", - " ...\n", - " -0.36239\n", - " 0.51644\n", - " -0.32984\n", - " -0.28668\n", - " -0.29225\n", - " -0.36184\n", - " -0.34668\n", - " -0.32224\n", - " 0.57418\n", - " -0.32407\n", + " 0.051314\n", + " 0.053518\n", + " 0.066485\n", + " 0.053112\n", + " 1.583872\n", + " 0.051108\n", + " 0.067694\n", + " 0.052094\n", + " 7.662636\n", + " 0.051438\n", + " ...\n", + " 0.050001\n", + " 0.051372\n", + " 0.069020\n", + " 1.370369\n", + " 0.051317\n", + " 2.325817\n", + " 0.109128\n", + " 0.051183\n", + " 0.070308\n", + " 0.052039\n", " \n", " \n", " ...\n", @@ -2413,123 +2344,123 @@ " \n", " \n", " 19008\n", - " -0.34820\n", - " -0.29900\n", - " -0.34437\n", - " -0.31436\n", - " -0.34085\n", - " -0.46158\n", - " 0.82235\n", - " -0.32377\n", - " -0.32049\n", - " 2.46634\n", - " ...\n", - " -0.36230\n", - " -0.36955\n", - " -0.32952\n", - " -0.28627\n", - " -0.29503\n", - " -0.36480\n", - " -0.34654\n", - " -0.32422\n", - " -0.39023\n", - " -0.28972\n", + " 0.065538\n", + " 0.052886\n", + " 0.057171\n", + " 0.051741\n", + " 0.566411\n", + " 0.051513\n", + " 0.057498\n", + " 0.051674\n", + " 0.064283\n", + " 0.051968\n", + " ...\n", + " 0.051546\n", + " 0.051878\n", + " 4.103773\n", + " 0.056866\n", + " 0.071395\n", + " 0.051617\n", + " 0.066317\n", + " 0.051617\n", + " 0.058158\n", + " 0.052165\n", " \n", " \n", " 19009\n", - " -0.34816\n", - " -0.30263\n", - " -0.34437\n", - " 0.18120\n", - " -0.34109\n", - " -0.46154\n", - " 0.81189\n", - " -0.32372\n", - " -0.32045\n", - " -0.32738\n", - " ...\n", - " -0.36260\n", - " -0.36951\n", - " 2.52929\n", - " -0.28621\n", - " -0.29878\n", - " -0.36475\n", - " -0.34650\n", - " -0.32700\n", - " -0.39251\n", - " 0.15501\n", + " 0.071523\n", + " 0.053326\n", + " 0.052552\n", + " 0.052867\n", + " 1.101434\n", + " 0.052483\n", + " 0.052288\n", + " 0.052754\n", + " 0.055194\n", + " 0.585756\n", + " ...\n", + " 4.655373\n", + " 0.053099\n", + " 0.052118\n", + " 0.052588\n", + " 0.052969\n", + " 0.052658\n", + " 0.051738\n", + " 0.052659\n", + " 0.052080\n", + " 0.053095\n", " \n", " \n", " 19010\n", - " -0.34800\n", - " -0.30251\n", - " -0.34437\n", - " 6.66342\n", - " -0.34094\n", - " -0.46143\n", - " -0.33312\n", - " -0.32356\n", - " -0.32032\n", - " -0.32727\n", - " ...\n", - " -0.36242\n", - " -0.36381\n", - " -0.32929\n", - " -0.28598\n", - " -0.29867\n", - " 0.69742\n", - " -0.34635\n", - " -0.32689\n", - " -0.39238\n", - " -0.32350\n", + " 0.052127\n", + " 0.052384\n", + " 3.672215\n", + " 1.093262\n", + " 0.051764\n", + " 0.051788\n", + " 0.051649\n", + " 0.051980\n", + " 0.053686\n", + " 0.052330\n", + " ...\n", + " 0.050001\n", + " 0.052224\n", + " 0.051528\n", + " 0.051865\n", + " 0.052132\n", + " 0.051912\n", + " 0.070156\n", + " 0.051913\n", + " 0.051500\n", + " 0.052221\n", " \n", " \n", " 19011\n", - " -0.34817\n", - " 1.28021\n", - " -0.34437\n", - " -0.31753\n", - " -0.34074\n", - " 0.23228\n", - " -0.33337\n", - " -0.32374\n", - " 0.70051\n", - " -0.32308\n", - " ...\n", - " -0.36216\n", - " -0.36952\n", - " -0.32948\n", - " -0.28623\n", - " -0.29381\n", - " 1.22123\n", - " -0.34651\n", - " -0.32332\n", - " -0.38951\n", - " -0.32370\n", + " 4.188590\n", + " 0.052729\n", + " 2.703301\n", + " 1.608644\n", + " 0.051619\n", + " 0.051642\n", + " 0.055151\n", + " 0.051817\n", + " 0.053484\n", + " 0.052138\n", + " ...\n", + " 0.050001\n", + " 0.052040\n", + " 0.055276\n", + " 0.054890\n", + " 0.051957\n", + " 0.051755\n", + " 0.059697\n", + " 0.051756\n", + " 4.386074\n", + " 0.052217\n", " \n", " \n", " 19012\n", - " 2.91613\n", - " -0.30214\n", - " -0.34437\n", - " -0.31754\n", - " -0.33919\n", - " 2.08866\n", - " -0.33338\n", - " -0.32375\n", - " 2.35113\n", - " -0.32740\n", - " ...\n", - " -0.36263\n", - " 0.56537\n", - " -0.32949\n", - " -0.28624\n", - " -0.29880\n", - " -0.36478\n", - " -0.34652\n", - " -0.32701\n", - " 0.50155\n", - " -0.32371\n", + " 0.051894\n", + " 0.052122\n", + " 0.051637\n", + " 0.051835\n", + " 0.051572\n", + " 2.638263\n", + " 2.734615\n", + " 0.051763\n", + " 0.053282\n", + " 0.052075\n", + " ...\n", + " 0.050001\n", + " 0.051980\n", + " 0.051362\n", + " 0.051660\n", + " 0.051899\n", + " 2.212243\n", + " 0.051121\n", + " 0.051704\n", + " 0.051338\n", + " 0.051977\n", " \n", " \n", "\n", @@ -2537,146 +2468,146 @@ "" ], "text/plain": [ - " feats2: c10555, c8555, c1055 feats2: c12222, c12226, c1227 \\\n", - "0 -0.34853 -0.29446 \n", - "1 0.09896 -0.29610 \n", - "2 -0.34853 -0.29417 \n", - "3 -0.34854 -0.29143 \n", - "4 -0.34850 -0.27595 \n", - "... ... ... \n", - "19008 -0.34820 -0.29900 \n", - "19009 -0.34816 -0.30263 \n", - "19010 -0.34800 -0.30251 \n", - "19011 -0.34817 1.28021 \n", - "19012 2.91613 -0.30214 \n", + " feats2: c586, c585, c5864 feats2: c1961, c10196, c1901 \\\n", + "0 0.051269 0.052370 \n", + "1 0.051271 0.053484 \n", + "2 0.051264 0.053257 \n", + "3 0.051251 0.052477 \n", + "4 0.051314 0.053518 \n", + "... ... ... \n", + "19008 0.065538 0.052886 \n", + "19009 0.071523 0.053326 \n", + "19010 0.052127 0.052384 \n", + "19011 4.188590 0.052729 \n", + "19012 0.051894 0.052122 \n", "\n", - " feats2: c6665, c6667, c6653 feats2: c7703, c7701, c7707 \\\n", - "0 -0.34437 -0.31798 \n", - "1 -0.34437 -0.31795 \n", - "2 -0.34437 3.43709 \n", - "3 -0.34437 -0.31800 \n", - "4 -0.34437 -0.31795 \n", - "... ... ... \n", - "19008 -0.34437 -0.31436 \n", - "19009 -0.34437 0.18120 \n", - "19010 -0.34437 6.66342 \n", - "19011 -0.34437 -0.31753 \n", - "19012 -0.34437 -0.31754 \n", + " feats2: c16916, c16169, c1616 feats2: c3636, c6363, c6365 \\\n", + "0 0.059563 0.053012 \n", + "1 0.066529 0.053060 \n", + "2 0.064578 0.053130 \n", + "3 0.065590 0.052994 \n", + "4 0.066485 0.053112 \n", + "... ... ... \n", + "19008 0.057171 0.051741 \n", + "19009 0.052552 0.052867 \n", + "19010 3.672215 1.093262 \n", + "19011 2.703301 1.608644 \n", + "19012 0.051637 0.051835 \n", "\n", - " feats2: c1992, c1922, c19932 feats2: c625, c612, c6125 \\\n", - "0 -0.34102 -0.46182 \n", - "1 -0.34092 -0.46180 \n", - "2 -0.33256 -0.46182 \n", - "3 -0.34095 -0.46183 \n", - "4 -0.34093 -0.46180 \n", - "... ... ... \n", - "19008 -0.34085 -0.46158 \n", - "19009 -0.34109 -0.46154 \n", - "19010 -0.34094 -0.46143 \n", - "19011 -0.34074 0.23228 \n", - "19012 -0.33919 2.08866 \n", + " feats2: c4944, c4444, c8444 feats2: c1065, c10652, c10585 \\\n", + "0 0.051056 0.051073 \n", + "1 0.051057 0.561800 \n", + "2 0.051051 0.051067 \n", + "3 0.051041 0.051057 \n", + "4 1.583872 0.051108 \n", + "... ... ... \n", + "19008 0.566411 0.051513 \n", + "19009 1.101434 0.052483 \n", + "19010 0.051764 0.051788 \n", + "19011 0.051619 0.051642 \n", + "19012 0.051572 2.638263 \n", "\n", - " feats2: c9028, c9904, c9283 feats2: c3073, c3037, c3074 \\\n", - "0 -0.33386 -0.32362 \n", - "1 -0.33383 2.52748 \n", - "2 -0.00576 -0.32411 \n", - "3 -0.33388 0.60698 \n", - "4 -0.32521 -0.32408 \n", - "... ... ... \n", - "19008 0.82235 -0.32377 \n", - "19009 0.81189 -0.32372 \n", - "19010 -0.33312 -0.32356 \n", - "19011 -0.33337 -0.32374 \n", - "19012 -0.33338 -0.32375 \n", + " feats2: c15556, c15550, c1555 feats2: c5999, c10999, c599 \\\n", + "0 0.059119 0.052026 \n", + "1 0.067701 0.052046 \n", + "2 0.065562 0.052079 \n", + "3 0.063403 0.052009 \n", + "4 0.067694 0.052094 \n", + "... ... ... \n", + "19008 0.057498 0.051674 \n", + "19009 0.052288 0.052754 \n", + "19010 0.051649 0.051980 \n", + "19011 0.055151 0.051817 \n", + "19012 2.734615 0.051763 \n", "\n", - " feats2: c2106, c2626, c4210 feats2: c11196, c11918, c1111 ... \\\n", - "0 -0.31948 -0.32044 ... \n", - "1 -0.32073 -0.32158 ... \n", - "2 -0.32076 -0.32092 ... \n", - "3 -0.32077 2.87353 ... \n", - "4 0.41376 -0.32181 ... \n", - "... ... ... ... \n", - "19008 -0.32049 2.46634 ... \n", - "19009 -0.32045 -0.32738 ... \n", - "19010 -0.32032 -0.32727 ... \n", - "19011 0.70051 -0.32308 ... \n", - "19012 2.35113 -0.32740 ... \n", + " feats2: c17693, c6937, c3937 feats2: c8882, c8880, c8889 ... \\\n", + "0 7.893405 0.051389 ... \n", + "1 7.851341 0.051392 ... \n", + "2 7.391063 0.051386 ... \n", + "3 7.892231 0.051369 ... \n", + "4 7.662636 0.051438 ... \n", + "... ... ... ... \n", + "19008 0.064283 0.051968 ... \n", + "19009 0.055194 0.585756 ... \n", + "19010 0.053686 0.052330 ... \n", + "19011 0.053484 0.052138 ... \n", + "19012 0.053282 0.052075 ... \n", "\n", - " feats2: c4448, c4444, c4487 feats2: c17981, c2980, c1798 \\\n", - "0 -0.36251 -0.36984 \n", - "1 -0.36238 -0.36982 \n", - "2 -0.36235 -0.36985 \n", - "3 -0.36240 -0.36985 \n", - "4 -0.36239 0.51644 \n", - "... ... ... \n", - "19008 -0.36230 -0.36955 \n", - "19009 -0.36260 -0.36951 \n", - "19010 -0.36242 -0.36381 \n", - "19011 -0.36216 -0.36952 \n", - "19012 -0.36263 0.56537 \n", + " feats2: c2890, c280, tgt feats2: c3333, c3303, c3033 \\\n", + "0 0.050000 0.101370 \n", + "1 0.050000 2.741899 \n", + "2 0.063343 0.051320 \n", + "3 0.050000 0.051307 \n", + "4 0.050001 0.051372 \n", + "... ... ... \n", + "19008 0.051546 0.051878 \n", + "19009 4.655373 0.053099 \n", + "19010 0.050001 0.052224 \n", + "19011 0.050001 0.052040 \n", + "19012 0.050001 0.051980 \n", "\n", - " feats2: c4777, c14777, c4787 feats2: c7554, c7519, c5151 \\\n", - "0 -0.32987 -0.28672 \n", - "1 -0.32984 -0.28669 \n", - "2 -0.32988 -0.28673 \n", - "3 -0.32989 -0.28674 \n", - "4 -0.32984 -0.28668 \n", + " feats2: c11187, c1118, c1111 feats2: c1798, c1772, c1778 \\\n", + "0 0.059551 1.219262 \n", + "1 0.068965 1.368653 \n", + "2 0.066675 1.671434 \n", + "3 3.263992 3.747229 \n", + "4 0.069020 1.370369 \n", "... ... ... \n", - "19008 -0.32952 -0.28627 \n", - "19009 2.52929 -0.28621 \n", - "19010 -0.32929 -0.28598 \n", - "19011 -0.32948 -0.28623 \n", - "19012 -0.32949 -0.28624 \n", + "19008 4.103773 0.056866 \n", + "19009 0.052118 0.052588 \n", + "19010 0.051528 0.051865 \n", + "19011 0.055276 0.054890 \n", + "19012 0.051362 0.051660 \n", "\n", - " feats2: c10000, c10008, c10003 feats2: c25240, c2524, c2456 \\\n", - "0 3.88640 -0.36522 \n", - "1 -0.29198 -0.36519 \n", - "2 -0.29119 -0.36523 \n", - "3 -0.28688 -0.36524 \n", - "4 -0.29225 -0.36184 \n", - "... ... ... \n", - "19008 -0.29503 -0.36480 \n", - "19009 -0.29878 -0.36475 \n", - "19010 -0.29867 0.69742 \n", - "19011 -0.29381 1.22123 \n", - "19012 -0.29880 -0.36478 \n", + " feats2: c3435, c3434, c3597 feats2: c2106, c210, c10000 \\\n", + "0 0.051273 2.699579 \n", + "1 1.024759 0.051145 \n", + "2 0.051267 0.051138 \n", + "3 0.053271 0.051127 \n", + "4 0.051317 2.325817 \n", + "... ... ... \n", + "19008 0.071395 0.051617 \n", + "19009 0.052969 0.052658 \n", + "19010 0.052132 0.051912 \n", + "19011 0.051957 0.051755 \n", + "19012 0.051899 2.212243 \n", "\n", - " feats2: c809, c5099, c5809 feats2: c1065, c10658, c10656 \\\n", - "0 -0.34670 -0.15082 \n", - "1 -0.34666 -0.32204 \n", - "2 -0.34668 -0.32147 \n", - "3 -0.34671 -0.29361 \n", - "4 -0.34668 -0.32224 \n", - "... ... ... \n", - "19008 -0.34654 -0.32422 \n", - "19009 -0.34650 -0.32700 \n", - "19010 -0.34635 -0.32689 \n", - "19011 -0.34651 -0.32332 \n", - "19012 -0.34652 -0.32701 \n", + " feats2: c1085, c1080, c1081 feats2: c457, c222, c452 \\\n", + "0 3.244251 0.051143 \n", + "1 0.108473 0.051145 \n", + "2 0.097529 0.051139 \n", + "3 0.192025 0.051127 \n", + "4 0.109128 0.051183 \n", + "... ... ... \n", + "19008 0.066317 0.051617 \n", + "19009 0.051738 0.052659 \n", + "19010 0.070156 0.051913 \n", + "19011 0.059697 0.051756 \n", + "19012 0.051121 0.051704 \n", "\n", - " feats2: c1550, c15034, c15615 feats2: c8182, c8882, c8889 \n", - "0 -0.38757 -0.32410 \n", - "1 -0.38862 -0.32407 \n", - "2 -0.38820 -0.32411 \n", - "3 -0.38584 -0.32412 \n", - "4 0.57418 -0.32407 \n", - "... ... ... \n", - "19008 -0.39023 -0.28972 \n", - "19009 -0.39251 0.15501 \n", - "19010 -0.39238 -0.32350 \n", - "19011 -0.38951 -0.32370 \n", - "19012 0.50155 -0.32371 \n", + " feats2: c1268, c1226, c12689 feats2: c6604, c16604, c16048 \n", + "0 0.059730 0.051639 \n", + "1 0.070319 0.052000 \n", + "2 0.067757 0.051922 \n", + "3 0.063663 0.051661 \n", + "4 0.070308 0.052039 \n", + "... ... ... \n", + "19008 0.058158 0.052165 \n", + "19009 0.052080 0.053095 \n", + "19010 0.051500 0.052221 \n", + "19011 4.386074 0.052217 \n", + "19012 0.051338 0.051977 \n", "\n", "[19762 rows x 32 columns]" ] }, - "execution_count": 32, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "X = g7._get_feature('nodes')\n", + "X = g7.get_matrix()\n", "X" ] }, @@ -2693,7 +2624,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 36, "id": "2d6f58dd", "metadata": { "tags": [] @@ -2705,7 +2636,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 37, "id": "f3b44db2-b34e-4398-8c5a-7a10bbe5d681", "metadata": { "tags": [] @@ -2719,7 +2650,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 38, "id": "3b2af6a2-4f10-4707-beb8-4f3447d3e3b8", "metadata": {}, "outputs": [ @@ -2838,7 +2769,7 @@ "[17836 rows x 3 columns]" ] }, - "execution_count": 35, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -2851,7 +2782,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 39, "id": "5258aee1", "metadata": {}, "outputs": [], @@ -2865,7 +2796,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 40, "id": "7ff921fc-3ecd-4404-acd7-8db943a4ebcc", "metadata": {}, "outputs": [ @@ -2996,7 +2927,7 @@ "[17836 rows x 4 columns]" ] }, - "execution_count": 37, + "execution_count": 40, "metadata": {}, "output_type": "execute_result" } @@ -3007,7 +2938,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 41, "id": "b4b10152-cac9-4497-b016-dd67b54cdcf2", "metadata": {}, "outputs": [], @@ -3018,17 +2949,34 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 42, "id": "9b3af1cd-6423-4484-8b99-81fad821f118", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=5e532500da8d459d8a3ef7832a6d6d9a&type=arrow&viztoken=4e311702-17ef-4563-b060-6d631e4a4101&usertag=f680a57a-pygraphistry-0.28.7&splashAfter=1672345993&info=true'" + "" ] }, - "execution_count": 39, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" } @@ -3055,6 +3003,14 @@ "Likewise the transpose conditional is even worse \n", "with prob_detection ~ 6e-5" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0cbef82-421d-489e-8666-84d412cae5a9", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From d87df3535a0d038b7fbbe908ffc77fd9d033ccd6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Jan 2023 20:32:32 -0800 Subject: [PATCH 128/432] updates Chavismo OSINT demo --- demos/ai/OSINT/Chavismo.ipynb | 1678 +++++++++++++++++---------------- 1 file changed, 882 insertions(+), 796 deletions(-) diff --git a/demos/ai/OSINT/Chavismo.ipynb b/demos/ai/OSINT/Chavismo.ipynb index 2457cfee06..3c3d92a215 100644 --- a/demos/ai/OSINT/Chavismo.ipynb +++ b/demos/ai/OSINT/Chavismo.ipynb @@ -45,16 +45,6 @@ "#! pip install --upgrade graphistry[ai]" ] }, - { - "cell_type": "code", - "execution_count": 29, - "id": "b6f55e41", - "metadata": {}, - "outputs": [], - "source": [ - "#cd .." - ] - }, { "cell_type": "code", "execution_count": 3, @@ -63,6 +53,7 @@ "outputs": [], "source": [ "import graphistry\n", + "from graphistry.features import ModelDict, topic_model, search_model, qa_model\n", "\n", "import requests\n", "import pandas as pd\n", @@ -317,9 +308,9 @@ " filename = f\"chavismo.xlsx\"\n", " open(filename, \"wb\").write(r.content)\n", " df = pd.read_excel(filename)\n", - " df.to_csv(\"data/chavismo.csv\", header=True)\n", + " df.to_csv(\"chavismo.csv\", header=True)\n", " else:\n", - " df = pd.read_csv('data/chavismo.csv', index_col=0)\n", + " df = pd.read_csv('chavismo.csv', index_col=0)\n", " return df\n", "\n", "df = download_chavismo_data(get_fresh=False) # set to True to get latest data\n", @@ -398,7 +389,7 @@ "metadata": {}, "outputs": [], "source": [ - "RENDER = False # set to True to have plots generated inline, or paste the URLs into a tab to see the graphs" + "RENDER = True # set to True to have plots generated inline, or paste the URLs into a tab to see the graphs" ] }, { @@ -409,8 +400,25 @@ "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=e17a8abbcddc472c932cdb5b2c0fc2c2&type=arrow&viztoken=c8e856ba-5b7b-4592-b22e-6dfac7b411a9&usertag=8a6d667e-pygraphistry-0.28.4+72.g2a02e2b.dirty&splashAfter=1668813426&info=true'" + "" ] }, "execution_count": 11, @@ -440,8 +448,25 @@ }, { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=9b8312e901dd47999a91f39f66880983&type=arrow&viztoken=5fc71180-3c6e-4ff7-adc9-38938b6da275&usertag=8a6d667e-pygraphistry-0.28.4+72.g2a02e2b.dirty&splashAfter=1668813431&info=true'" + "" ] }, "execution_count": 12, @@ -450,10 +475,10 @@ } ], "source": [ - "# one can also create a hypergraph with any number of columns of interest\n", + "# Create a hypergraph with any number of columns of interest\n", "hg = graphistry.hypergraph(df, ['Agent 1','Agent 2', 'Relationship'])\n", "gh = hg['graph']\n", - "gh.bind(point_title='Agent 1').plot(render=RENDER)" + "gh.bind(point_title='nodeID').plot(render=RENDER)" ] }, { @@ -477,39 +502,88 @@ { "cell_type": "code", "execution_count": 14, - "id": "9939d27f", + "id": "6699dc5c-16b2-4471-9a6f-9241c238d830", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 2min 34s, sys: 11.3 s, total: 2min 46s\n", - "Wall time: 2min 31s\n" + "_____________________________________________________________\n", + "\n", + "sentence-transformers/msmarco-distilbert-base-v2 Search Model\n", + "_____________________________________________________________\n", + "\n", + "Updated: {'cardinality_threshold_target': 2, 'n_topics_target': 11}\n", + "_____________________________________________________________\n", + "\n" ] + }, + { + "data": { + "text/plain": [ + "{'min_words': 0, 'model_name': 'sentence-transformers/msmarco-distilbert-base-v2', 'cardinality_threshold_target': 2, 'n_topics_target': 11}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "%%time\n", - "g = graphistry.nodes(df, 'Agent 1').edges(df, 'Agent 1', 'Agent 2')\n", - "# since we have edges, let's featurize (rather than umap, which would overwrite explicit edges)\n", - "# X = None will featurize ALL the columns and setting min_words=0 will treat them all as textual\n", - "g2 = g.featurize(y=['Relationship'], \n", - " model_name='msmarco-distilbert-base-v2', \n", - " min_words=0, # force textual encoding\n", - " cardinality_threshold_target=2, # force topic model (with low target cardinality)\n", - " n_topics_target=12)" + "search_model.update(dict(cardinality_threshold_target=2, n_topics_target=11))\n", + "search_model" ] }, { "cell_type": "code", "execution_count": 15, - "id": "bcf9a948", + "id": "9939d27f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2min 35s, sys: 9.96 s, total: 2min 45s\n", + "Wall time: 2min 27s\n", + "____________________________________________________________\n", + "\n", + "Search model over features with `y=Relationship` topic model\n", + "____________________________________________________________\n", + "\n" + ] + }, + { + "data": { + "text/plain": [ + "{'y': ['Relationship'], 'min_words': 0, 'model_name': 'sentence-transformers/msmarco-distilbert-base-v2', 'cardinality_threshold_target': 2, 'n_topics_target': 11}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# save search instance after featurization \n", - "# g2.save_search_instance('data/chavismo.search')" + "%%time\n", + "\n", + "model = ModelDict('Search model over features with `y=Relationship` topic model', y=['Relationship'], \n", + " **search_model)\n", + "process = True\n", + "if process:\n", + " g = graphistry.nodes(df.sample(len(df)), 'Agent 1').edges(df, 'Agent 1', 'Agent 2')\n", + " g2 = graphistry.bind().load_search_instance('chavismo.search')\n", + "\n", + " g._node_features = g2._node_features\n", + " # since we have edges, let's featurize (rather than umap, which would overwrite explicit edges)\n", + " # X = None will featurize ALL the columns and setting min_words=0 will treat them all as textual\n", + " g2 = g.featurize(**model)\n", + " g2.save_search_instance('chavismo.search')\n", + "else:\n", + " g2 = graphistry.bind().load_search_instance('chavismo.search')\n", + " \n", + "model" ] }, { @@ -564,124 +638,124 @@ " \n", " \n", " \n", - " 0\n", - " -0.253210\n", - " -0.092555\n", - " -0.086723\n", - " -0.157253\n", - " -0.204900\n", - " 0.122437\n", - " -0.529797\n", - " -0.015322\n", - " -0.564774\n", - " 0.769105\n", + " 1935\n", + " -0.187781\n", + " -0.387294\n", + " -0.160111\n", + " 0.533505\n", + " -0.012384\n", + " -0.036983\n", + " -0.227363\n", + " 0.305134\n", + " -0.053135\n", + " 0.242913\n", " ...\n", - " 0.170976\n", - " -0.685452\n", - " 0.295997\n", - " -0.593129\n", - " 0.390818\n", - " 0.249091\n", - " -0.099062\n", - " 0.051298\n", - " 0.319292\n", - " 0.244462\n", + " 0.295774\n", + " 0.358959\n", + " 0.566532\n", + " -0.852727\n", + " 0.118316\n", + " -0.122535\n", + " 0.347825\n", + " 0.101922\n", + " 0.386412\n", + " -0.644331\n", " \n", " \n", - " 1\n", - " 0.246941\n", - " 0.124175\n", - " 0.269999\n", - " 0.181342\n", - " -0.505962\n", - " 0.210211\n", - " 0.140408\n", - " 0.468697\n", - " 0.321780\n", - " -0.128226\n", + " 1583\n", + " -0.389066\n", + " -0.087771\n", + " -0.775325\n", + " 0.130028\n", + " 0.172289\n", + " -0.208622\n", + " -0.874529\n", + " 0.382446\n", + " 0.483579\n", + " -0.309687\n", " ...\n", - " 0.896930\n", - " -0.324542\n", - " -0.291376\n", - " -0.565900\n", - " 0.524987\n", - " -0.196209\n", - " -0.519527\n", - " -0.216939\n", - " 0.862336\n", - " 0.193277\n", + " 0.152333\n", + " -0.175365\n", + " 0.676532\n", + " -0.393536\n", + " 0.014875\n", + " -0.383976\n", + " 0.081081\n", + " 0.298928\n", + " 0.596412\n", + " -0.605296\n", " \n", " \n", - " 2\n", - " -0.068820\n", - " -0.323506\n", - " -0.206149\n", - " 0.559626\n", - " 0.145691\n", - " -0.100530\n", - " -0.492642\n", - " -0.207871\n", - " -0.306911\n", - " 0.365237\n", + " 2351\n", + " -0.423137\n", + " 0.182216\n", + " -0.133435\n", + " 0.217782\n", + " -0.094416\n", + " -0.097829\n", + " -0.876501\n", + " 0.797395\n", + " -0.277968\n", + " -0.045989\n", " ...\n", - " 0.702710\n", - " 0.152173\n", - " 0.309572\n", - " -0.461615\n", - " 0.666504\n", - " -0.016534\n", - " 0.877414\n", - " -0.203894\n", - " 0.450191\n", - " -0.544430\n", + " 1.015161\n", + " 0.453668\n", + " 0.440656\n", + " -0.040266\n", + " 0.693482\n", + " 0.169744\n", + " 0.060993\n", + " -0.500294\n", + " 0.649334\n", + " 0.195239\n", " \n", " \n", - " 3\n", - " -0.193803\n", - " -0.272143\n", - " -0.208212\n", - " 0.341019\n", - " -0.175586\n", - " 0.149454\n", - " -0.245428\n", - " -0.008559\n", - " 0.279371\n", - " 0.312212\n", + " 452\n", + " -0.095392\n", + " 0.142223\n", + " -0.686644\n", + " 0.674334\n", + " -0.228214\n", + " 0.109001\n", + " -0.440772\n", + " 0.124842\n", + " -0.066046\n", + " 0.171612\n", " ...\n", - " 0.451480\n", - " 0.413026\n", - " -0.211110\n", - " -0.550950\n", - " 0.556309\n", - " -0.521065\n", - " 0.327162\n", - " -0.200601\n", - " 0.148922\n", - " -0.181214\n", + " -0.009296\n", + " 0.564043\n", + " 0.178982\n", + " -0.222556\n", + " 0.523386\n", + " 0.331242\n", + " 0.360143\n", + " -0.011475\n", + " 0.105367\n", + " 0.146013\n", " \n", " \n", - " 4\n", - " 0.099981\n", - " -0.147359\n", - " -0.181383\n", - " 0.436541\n", - " 0.026173\n", - " -0.218141\n", - " -0.193041\n", - " 0.030840\n", - " 0.436779\n", - " 0.392634\n", + " 8\n", + " -0.477632\n", + " -0.784202\n", + " -0.383131\n", + " 0.669990\n", + " -0.100068\n", + " -0.294655\n", + " -0.627293\n", + " 0.058967\n", + " -0.455835\n", + " 0.461948\n", " ...\n", - " 0.505176\n", - " 0.505908\n", - " -0.286257\n", - " -0.614761\n", - " 0.811958\n", - " -0.615687\n", - " 0.479651\n", - " -0.345834\n", - " 0.185110\n", - " -0.311448\n", + " 0.381159\n", + " 0.021592\n", + " 0.348291\n", + " -0.903526\n", + " 0.694809\n", + " 0.018819\n", + " -0.002837\n", + " 0.023364\n", + " 0.385524\n", + " -0.437104\n", " \n", " \n", " ...\n", @@ -708,124 +782,124 @@ " ...\n", " \n", " \n", - " 2713\n", - " 0.149379\n", - " -0.222494\n", - " 0.303324\n", - " 0.306326\n", - " -0.254602\n", - " -0.536148\n", - " -0.470110\n", - " 0.565999\n", - " 0.020586\n", - " 0.609108\n", + " 334\n", + " -0.593350\n", + " 0.112945\n", + " 0.123807\n", + " 0.948721\n", + " -0.276408\n", + " -0.603181\n", + " -0.392917\n", + " 0.388957\n", + " 0.126814\n", + " 1.126764\n", " ...\n", - " -0.090419\n", - " 0.125347\n", - " 0.540859\n", - " -0.566799\n", - " 0.642269\n", - " 0.503141\n", - " -0.070126\n", - " -0.351428\n", - " 1.435632\n", - " 0.075295\n", + " -0.089558\n", + " -0.331218\n", + " 0.120006\n", + " -0.712097\n", + " -0.205538\n", + " 0.562405\n", + " 0.525867\n", + " -0.204776\n", + " -0.197538\n", + " -0.018287\n", " \n", " \n", - " 2714\n", - " 0.202084\n", - " -0.421907\n", - " 0.331125\n", - " 0.261079\n", - " -0.179134\n", - " -0.532431\n", - " -0.164233\n", - " 0.277176\n", - " -0.106925\n", - " 0.391598\n", + " 1264\n", + " 0.476763\n", + " 0.199641\n", + " 0.226375\n", + " 0.824276\n", + " -0.438520\n", + " -0.420465\n", + " 0.386475\n", + " 0.275413\n", + " -0.350755\n", + " 0.047801\n", " ...\n", - " -0.046802\n", - " 0.067547\n", - " 0.484514\n", - " -0.824181\n", - " 0.475694\n", - " 0.329913\n", - " -0.104037\n", - " -0.155081\n", - " 0.928627\n", - " -0.082177\n", + " 0.755453\n", + " 0.358454\n", + " 0.348341\n", + " -1.133082\n", + " -0.210484\n", + " -0.764606\n", + " 0.916726\n", + " -0.284010\n", + " -0.056577\n", + " 0.407399\n", " \n", " \n", - " 2715\n", - " -0.627377\n", - " -0.391843\n", - " 0.218678\n", - " -0.014267\n", - " -0.460290\n", - " -0.329174\n", - " -0.482513\n", - " 0.604713\n", - " 0.011763\n", - " 0.018444\n", + " 1171\n", + " -0.270009\n", + " 0.170292\n", + " 0.052011\n", + " 0.060369\n", + " -0.265923\n", + " 0.717389\n", + " -0.561747\n", + " 0.256904\n", + " 0.061957\n", + " 0.049001\n", " ...\n", - " 0.275013\n", - " 0.164754\n", - " 0.408749\n", - " -0.332821\n", - " 0.696610\n", - " 0.529032\n", - " 0.067081\n", - " -0.382973\n", - " 1.094504\n", - " 0.503912\n", + " -0.322604\n", + " -0.409145\n", + " 0.270570\n", + " -0.290132\n", + " 0.037762\n", + " 0.646035\n", + " -0.226534\n", + " 0.178052\n", + " 0.890680\n", + " -0.204422\n", " \n", " \n", - " 2716\n", - " 0.148124\n", - " -0.041698\n", - " 0.083552\n", - " 0.132032\n", - " -0.663597\n", - " 0.103373\n", - " -0.261431\n", - " -0.024310\n", - " 0.381935\n", - " 0.062252\n", + " 589\n", + " 0.217439\n", + " 0.188192\n", + " -0.186874\n", + " 0.108394\n", + " 0.318829\n", + " 0.049164\n", + " -0.460243\n", + " 0.502261\n", + " -0.304636\n", + " 0.176173\n", " ...\n", - " 0.362344\n", - " 0.360328\n", - " -0.020871\n", - " -0.640752\n", - " 0.638189\n", - " -0.118096\n", - " 0.245733\n", - " -0.404834\n", - " 0.754788\n", - " -0.139678\n", + " 0.700581\n", + " 0.730317\n", + " 0.152944\n", + " 0.017145\n", + " 0.597685\n", + " 0.536023\n", + " -0.020815\n", + " -0.774539\n", + " 0.354638\n", + " 0.153618\n", " \n", " \n", - " 2717\n", - " 0.148124\n", - " -0.041698\n", - " 0.083552\n", - " 0.132032\n", - " -0.663597\n", - " 0.103373\n", - " -0.261431\n", - " -0.024310\n", - " 0.381935\n", - " 0.062252\n", + " 2342\n", + " -0.477632\n", + " -0.784202\n", + " -0.383131\n", + " 0.669990\n", + " -0.100068\n", + " -0.294655\n", + " -0.627293\n", + " 0.058967\n", + " -0.455835\n", + " 0.461948\n", " ...\n", - " 0.362344\n", - " 0.360328\n", - " -0.020871\n", - " -0.640752\n", - " 0.638189\n", - " -0.118096\n", - " 0.245733\n", - " -0.404834\n", - " 0.754788\n", - " -0.139678\n", + " 0.381159\n", + " 0.021592\n", + " 0.348291\n", + " -0.903526\n", + " 0.694809\n", + " 0.018819\n", + " -0.002837\n", + " 0.023364\n", + " 0.385524\n", + " -0.437104\n", " \n", " \n", "\n", @@ -834,134 +908,134 @@ ], "text/plain": [ " Agent 2_Source_Evidence_0 Agent 2_Source_Evidence_1 \\\n", - "0 -0.253210 -0.092555 \n", - "1 0.246941 0.124175 \n", - "2 -0.068820 -0.323506 \n", - "3 -0.193803 -0.272143 \n", - "4 0.099981 -0.147359 \n", + "1935 -0.187781 -0.387294 \n", + "1583 -0.389066 -0.087771 \n", + "2351 -0.423137 0.182216 \n", + "452 -0.095392 0.142223 \n", + "8 -0.477632 -0.784202 \n", "... ... ... \n", - "2713 0.149379 -0.222494 \n", - "2714 0.202084 -0.421907 \n", - "2715 -0.627377 -0.391843 \n", - "2716 0.148124 -0.041698 \n", - "2717 0.148124 -0.041698 \n", + "334 -0.593350 0.112945 \n", + "1264 0.476763 0.199641 \n", + "1171 -0.270009 0.170292 \n", + "589 0.217439 0.188192 \n", + "2342 -0.477632 -0.784202 \n", "\n", " Agent 2_Source_Evidence_2 Agent 2_Source_Evidence_3 \\\n", - "0 -0.086723 -0.157253 \n", - "1 0.269999 0.181342 \n", - "2 -0.206149 0.559626 \n", - "3 -0.208212 0.341019 \n", - "4 -0.181383 0.436541 \n", + "1935 -0.160111 0.533505 \n", + "1583 -0.775325 0.130028 \n", + "2351 -0.133435 0.217782 \n", + "452 -0.686644 0.674334 \n", + "8 -0.383131 0.669990 \n", "... ... ... \n", - "2713 0.303324 0.306326 \n", - "2714 0.331125 0.261079 \n", - "2715 0.218678 -0.014267 \n", - "2716 0.083552 0.132032 \n", - "2717 0.083552 0.132032 \n", + "334 0.123807 0.948721 \n", + "1264 0.226375 0.824276 \n", + "1171 0.052011 0.060369 \n", + "589 -0.186874 0.108394 \n", + "2342 -0.383131 0.669990 \n", "\n", " Agent 2_Source_Evidence_4 Agent 2_Source_Evidence_5 \\\n", - "0 -0.204900 0.122437 \n", - "1 -0.505962 0.210211 \n", - "2 0.145691 -0.100530 \n", - "3 -0.175586 0.149454 \n", - "4 0.026173 -0.218141 \n", + "1935 -0.012384 -0.036983 \n", + "1583 0.172289 -0.208622 \n", + "2351 -0.094416 -0.097829 \n", + "452 -0.228214 0.109001 \n", + "8 -0.100068 -0.294655 \n", "... ... ... \n", - "2713 -0.254602 -0.536148 \n", - "2714 -0.179134 -0.532431 \n", - "2715 -0.460290 -0.329174 \n", - "2716 -0.663597 0.103373 \n", - "2717 -0.663597 0.103373 \n", + "334 -0.276408 -0.603181 \n", + "1264 -0.438520 -0.420465 \n", + "1171 -0.265923 0.717389 \n", + "589 0.318829 0.049164 \n", + "2342 -0.100068 -0.294655 \n", "\n", " Agent 2_Source_Evidence_6 Agent 2_Source_Evidence_7 \\\n", - "0 -0.529797 -0.015322 \n", - "1 0.140408 0.468697 \n", - "2 -0.492642 -0.207871 \n", - "3 -0.245428 -0.008559 \n", - "4 -0.193041 0.030840 \n", + "1935 -0.227363 0.305134 \n", + "1583 -0.874529 0.382446 \n", + "2351 -0.876501 0.797395 \n", + "452 -0.440772 0.124842 \n", + "8 -0.627293 0.058967 \n", "... ... ... \n", - "2713 -0.470110 0.565999 \n", - "2714 -0.164233 0.277176 \n", - "2715 -0.482513 0.604713 \n", - "2716 -0.261431 -0.024310 \n", - "2717 -0.261431 -0.024310 \n", + "334 -0.392917 0.388957 \n", + "1264 0.386475 0.275413 \n", + "1171 -0.561747 0.256904 \n", + "589 -0.460243 0.502261 \n", + "2342 -0.627293 0.058967 \n", "\n", " Agent 2_Source_Evidence_8 Agent 2_Source_Evidence_9 ... \\\n", - "0 -0.564774 0.769105 ... \n", - "1 0.321780 -0.128226 ... \n", - "2 -0.306911 0.365237 ... \n", - "3 0.279371 0.312212 ... \n", - "4 0.436779 0.392634 ... \n", + "1935 -0.053135 0.242913 ... \n", + "1583 0.483579 -0.309687 ... \n", + "2351 -0.277968 -0.045989 ... \n", + "452 -0.066046 0.171612 ... \n", + "8 -0.455835 0.461948 ... \n", "... ... ... ... \n", - "2713 0.020586 0.609108 ... \n", - "2714 -0.106925 0.391598 ... \n", - "2715 0.011763 0.018444 ... \n", - "2716 0.381935 0.062252 ... \n", - "2717 0.381935 0.062252 ... \n", + "334 0.126814 1.126764 ... \n", + "1264 -0.350755 0.047801 ... \n", + "1171 0.061957 0.049001 ... \n", + "589 -0.304636 0.176173 ... \n", + "2342 -0.455835 0.461948 ... \n", "\n", " Agent 2_Source_Evidence_758 Agent 2_Source_Evidence_759 \\\n", - "0 0.170976 -0.685452 \n", - "1 0.896930 -0.324542 \n", - "2 0.702710 0.152173 \n", - "3 0.451480 0.413026 \n", - "4 0.505176 0.505908 \n", + "1935 0.295774 0.358959 \n", + "1583 0.152333 -0.175365 \n", + "2351 1.015161 0.453668 \n", + "452 -0.009296 0.564043 \n", + "8 0.381159 0.021592 \n", "... ... ... \n", - "2713 -0.090419 0.125347 \n", - "2714 -0.046802 0.067547 \n", - "2715 0.275013 0.164754 \n", - "2716 0.362344 0.360328 \n", - "2717 0.362344 0.360328 \n", + "334 -0.089558 -0.331218 \n", + "1264 0.755453 0.358454 \n", + "1171 -0.322604 -0.409145 \n", + "589 0.700581 0.730317 \n", + "2342 0.381159 0.021592 \n", "\n", " Agent 2_Source_Evidence_760 Agent 2_Source_Evidence_761 \\\n", - "0 0.295997 -0.593129 \n", - "1 -0.291376 -0.565900 \n", - "2 0.309572 -0.461615 \n", - "3 -0.211110 -0.550950 \n", - "4 -0.286257 -0.614761 \n", + "1935 0.566532 -0.852727 \n", + "1583 0.676532 -0.393536 \n", + "2351 0.440656 -0.040266 \n", + "452 0.178982 -0.222556 \n", + "8 0.348291 -0.903526 \n", "... ... ... \n", - "2713 0.540859 -0.566799 \n", - "2714 0.484514 -0.824181 \n", - "2715 0.408749 -0.332821 \n", - "2716 -0.020871 -0.640752 \n", - "2717 -0.020871 -0.640752 \n", + "334 0.120006 -0.712097 \n", + "1264 0.348341 -1.133082 \n", + "1171 0.270570 -0.290132 \n", + "589 0.152944 0.017145 \n", + "2342 0.348291 -0.903526 \n", "\n", " Agent 2_Source_Evidence_762 Agent 2_Source_Evidence_763 \\\n", - "0 0.390818 0.249091 \n", - "1 0.524987 -0.196209 \n", - "2 0.666504 -0.016534 \n", - "3 0.556309 -0.521065 \n", - "4 0.811958 -0.615687 \n", + "1935 0.118316 -0.122535 \n", + "1583 0.014875 -0.383976 \n", + "2351 0.693482 0.169744 \n", + "452 0.523386 0.331242 \n", + "8 0.694809 0.018819 \n", "... ... ... \n", - "2713 0.642269 0.503141 \n", - "2714 0.475694 0.329913 \n", - "2715 0.696610 0.529032 \n", - "2716 0.638189 -0.118096 \n", - "2717 0.638189 -0.118096 \n", + "334 -0.205538 0.562405 \n", + "1264 -0.210484 -0.764606 \n", + "1171 0.037762 0.646035 \n", + "589 0.597685 0.536023 \n", + "2342 0.694809 0.018819 \n", "\n", " Agent 2_Source_Evidence_764 Agent 2_Source_Evidence_765 \\\n", - "0 -0.099062 0.051298 \n", - "1 -0.519527 -0.216939 \n", - "2 0.877414 -0.203894 \n", - "3 0.327162 -0.200601 \n", - "4 0.479651 -0.345834 \n", + "1935 0.347825 0.101922 \n", + "1583 0.081081 0.298928 \n", + "2351 0.060993 -0.500294 \n", + "452 0.360143 -0.011475 \n", + "8 -0.002837 0.023364 \n", "... ... ... \n", - "2713 -0.070126 -0.351428 \n", - "2714 -0.104037 -0.155081 \n", - "2715 0.067081 -0.382973 \n", - "2716 0.245733 -0.404834 \n", - "2717 0.245733 -0.404834 \n", + "334 0.525867 -0.204776 \n", + "1264 0.916726 -0.284010 \n", + "1171 -0.226534 0.178052 \n", + "589 -0.020815 -0.774539 \n", + "2342 -0.002837 0.023364 \n", "\n", " Agent 2_Source_Evidence_766 Agent 2_Source_Evidence_767 \n", - "0 0.319292 0.244462 \n", - "1 0.862336 0.193277 \n", - "2 0.450191 -0.544430 \n", - "3 0.148922 -0.181214 \n", - "4 0.185110 -0.311448 \n", + "1935 0.386412 -0.644331 \n", + "1583 0.596412 -0.605296 \n", + "2351 0.649334 0.195239 \n", + "452 0.105367 0.146013 \n", + "8 0.385524 -0.437104 \n", "... ... ... \n", - "2713 1.435632 0.075295 \n", - "2714 0.928627 -0.082177 \n", - "2715 1.094504 0.503912 \n", - "2716 0.754788 -0.139678 \n", - "2717 0.754788 -0.139678 \n", + "334 -0.197538 -0.018287 \n", + "1264 -0.056577 0.407399 \n", + "1171 0.890680 -0.204422 \n", + "589 0.354638 0.153618 \n", + "2342 0.385524 -0.437104 \n", "\n", "[2718 rows x 768 columns]" ] @@ -972,8 +1046,8 @@ } ], "source": [ - "# the resulting X = features matrix\n", - "X = g2._get_feature('nodes')\n", + "# the resulting X = features matrix, sbert encoding\n", + "X = g2.get_matrix()\n", "X" ] }, @@ -1004,95 +1078,89 @@ " \n", " \n", " \n", - " Relationship: sanctioned, sanction, violation\n", - " Relationship: smuggling, bribery, in\n", + " Relationship: occupied, functions, sanctions\n", + " Relationship: laundering, overpricing, international\n", " Relationship: integrates, company, in\n", + " Relationship: complaints, corruption, traffic\n", + " Relationship: sanctioned, sanction, evasion\n", + " Relationship: facilitators, colleagues, student\n", + " Relationship: designates, charge, rights\n", " Relationship: connection, business, in\n", - " Relationship: complaints, corruption, conspiracy\n", - " Relationship: suscribed, traffic, contract\n", - " Relationship: occupied, functions, sanctions\n", - " Relationship: overpricing, currency, illegal\n", - " Relationship: laundering, international, trials\n", " Relationship: members, family, enemies\n", - " Relationship: facilitators, extortion, friends\n", - " Relationship: designates, colleagues, charge\n", + " Relationship: smuggling, bribery, currency\n", + " Relationship: conspiracy, suscribed, contract\n", " \n", " \n", " \n", " \n", - " 0\n", - " 0.065003\n", - " 0.055670\n", - " 0.071783\n", - " 0.079073\n", - " 34.442679\n", - " 0.054766\n", - " 0.063643\n", - " 0.053862\n", - " 0.057605\n", - " 0.050000\n", - " 0.052458\n", - " 0.053457\n", + " 1935\n", + " 0.072898\n", + " 0.553233\n", + " 0.089311\n", + " 0.068496\n", + " 0.075216\n", + " 0.058883\n", + " 0.059063\n", + " 0.091499\n", + " 0.067527\n", + " 42.147305\n", + " 42.766569\n", " \n", " \n", - " 1\n", - " 0.050119\n", - " 0.050523\n", - " 0.051003\n", + " 1583\n", + " 23.982221\n", + " 0.053704\n", + " 0.050000\n", + " 0.055351\n", + " 0.090587\n", " 0.050000\n", - " 0.051689\n", - " 0.051122\n", + " 0.050001\n", + " 0.062470\n", " 0.050000\n", - " 0.050509\n", - " 0.050418\n", - " 0.054442\n", - " 15.039365\n", - " 0.050809\n", + " 0.052716\n", + " 0.052951\n", " \n", " \n", - " 2\n", - " 0.082737\n", - " 0.052691\n", - " 0.050000\n", - " 0.062643\n", - " 0.055349\n", - " 0.053101\n", - " 23.987324\n", - " 0.052330\n", - " 0.053825\n", + " 2351\n", + " 23.982221\n", + " 0.053704\n", " 0.050000\n", + " 0.055351\n", + " 0.090587\n", " 0.050000\n", + " 0.050001\n", + " 0.062470\n", " 0.050000\n", + " 0.052716\n", + " 0.052951\n", " \n", " \n", - " 3\n", - " 0.074388\n", - " 28.567443\n", - " 0.065518\n", - " 0.063843\n", - " 0.060323\n", - " 0.117900\n", - " 0.064166\n", - " 60.674413\n", - " 17.229793\n", - " 0.065414\n", - " 0.055578\n", - " 0.061222\n", + " 452\n", + " 23.982221\n", + " 0.053704\n", + " 0.050000\n", + " 0.055351\n", + " 0.090587\n", + " 0.050000\n", + " 0.050001\n", + " 0.062470\n", + " 0.050000\n", + " 0.052716\n", + " 0.052951\n", " \n", " \n", - " 4\n", - " 0.067568\n", - " 31.116316\n", - " 0.058412\n", - " 0.060998\n", - " 0.056463\n", - " 0.060142\n", - " 0.057811\n", - " 0.117512\n", - " 37.838020\n", - " 0.059851\n", - " 0.051573\n", - " 0.055335\n", + " 8\n", + " 0.072986\n", + " 0.053850\n", + " 0.051025\n", + " 0.053534\n", + " 16.497892\n", + " 0.050000\n", + " 0.050001\n", + " 0.065366\n", + " 0.050000\n", + " 0.053286\n", + " 0.052058\n", " \n", " \n", " ...\n", @@ -1107,246 +1175,227 @@ " ...\n", " ...\n", " ...\n", - " ...\n", " \n", " \n", - " 2713\n", - " 0.051717\n", - " 0.053141\n", - " 23.986537\n", - " 0.073292\n", - " 0.059816\n", - " 0.053714\n", - " 0.050000\n", - " 0.052808\n", + " 334\n", + " 0.072986\n", " 0.053850\n", + " 0.051025\n", + " 0.053534\n", + " 16.497892\n", " 0.050000\n", - " 0.051797\n", - " 0.063327\n", + " 0.050001\n", + " 0.065366\n", + " 0.050000\n", + " 0.053286\n", + " 0.052058\n", " \n", " \n", - " 2714\n", - " 0.072604\n", - " 0.052956\n", - " 0.092982\n", - " 23.941857\n", - " 0.063406\n", - " 0.054355\n", - " 0.061778\n", - " 0.052354\n", - " 0.054615\n", - " 0.052002\n", + " 1264\n", + " 0.072986\n", + " 0.053850\n", + " 0.051025\n", + " 0.053534\n", + " 16.497892\n", + " 0.050000\n", + " 0.050001\n", + " 0.065366\n", " 0.050000\n", - " 0.051090\n", + " 0.053286\n", + " 0.052058\n", " \n", " \n", - " 2715\n", - " 0.082737\n", - " 0.052691\n", - " 0.050000\n", - " 0.062643\n", - " 0.055349\n", - " 0.053101\n", - " 23.987324\n", - " 0.052330\n", - " 0.053825\n", + " 1171\n", " 0.050000\n", + " 0.051786\n", " 0.050000\n", + " 0.050037\n", + " 0.050001\n", + " 0.054220\n", " 0.050000\n", + " 0.050705\n", + " 18.039779\n", + " 0.052556\n", + " 0.050917\n", " \n", " \n", - " 2716\n", - " 0.071095\n", - " 0.299774\n", - " 0.070829\n", - " 0.064280\n", - " 0.060561\n", - " 0.064907\n", - " 0.062544\n", - " 25.575909\n", - " 20.649126\n", - " 0.051986\n", - " 0.070271\n", - " 0.058718\n", + " 589\n", + " 0.050000\n", + " 0.053772\n", + " 23.990746\n", + " 0.059230\n", + " 0.051608\n", + " 0.052662\n", + " 0.060380\n", + " 0.072165\n", + " 0.050000\n", + " 0.053006\n", + " 0.056433\n", " \n", " \n", - " 2717\n", - " 0.071095\n", - " 0.299774\n", - " 0.070829\n", - " 0.064280\n", - " 0.060561\n", - " 0.064907\n", - " 0.062544\n", - " 25.575909\n", - " 20.649126\n", - " 0.051986\n", - " 0.070271\n", - " 0.058718\n", + " 2342\n", + " 0.072986\n", + " 0.053850\n", + " 0.051025\n", + " 0.053534\n", + " 16.497892\n", + " 0.050000\n", + " 0.050001\n", + " 0.065366\n", + " 0.050000\n", + " 0.053286\n", + " 0.052058\n", " \n", " \n", "\n", - "

2718 rows × 12 columns

\n", + "

2718 rows × 11 columns

\n", "" ], "text/plain": [ - " Relationship: sanctioned, sanction, violation \\\n", - "0 0.065003 \n", - "1 0.050119 \n", - "2 0.082737 \n", - "3 0.074388 \n", - "4 0.067568 \n", - "... ... \n", - "2713 0.051717 \n", - "2714 0.072604 \n", - "2715 0.082737 \n", - "2716 0.071095 \n", - "2717 0.071095 \n", + " Relationship: occupied, functions, sanctions \\\n", + "1935 0.072898 \n", + "1583 23.982221 \n", + "2351 23.982221 \n", + "452 23.982221 \n", + "8 0.072986 \n", + "... ... \n", + "334 0.072986 \n", + "1264 0.072986 \n", + "1171 0.050000 \n", + "589 0.050000 \n", + "2342 0.072986 \n", "\n", - " Relationship: smuggling, bribery, in \\\n", - "0 0.055670 \n", - "1 0.050523 \n", - "2 0.052691 \n", - "3 28.567443 \n", - "4 31.116316 \n", - "... ... \n", - "2713 0.053141 \n", - "2714 0.052956 \n", - "2715 0.052691 \n", - "2716 0.299774 \n", - "2717 0.299774 \n", + " Relationship: laundering, overpricing, international \\\n", + "1935 0.553233 \n", + "1583 0.053704 \n", + "2351 0.053704 \n", + "452 0.053704 \n", + "8 0.053850 \n", + "... ... \n", + "334 0.053850 \n", + "1264 0.053850 \n", + "1171 0.051786 \n", + "589 0.053772 \n", + "2342 0.053850 \n", "\n", " Relationship: integrates, company, in \\\n", - "0 0.071783 \n", - "1 0.051003 \n", - "2 0.050000 \n", - "3 0.065518 \n", - "4 0.058412 \n", + "1935 0.089311 \n", + "1583 0.050000 \n", + "2351 0.050000 \n", + "452 0.050000 \n", + "8 0.051025 \n", "... ... \n", - "2713 23.986537 \n", - "2714 0.092982 \n", - "2715 0.050000 \n", - "2716 0.070829 \n", - "2717 0.070829 \n", - "\n", - " Relationship: connection, business, in \\\n", - "0 0.079073 \n", - "1 0.050000 \n", - "2 0.062643 \n", - "3 0.063843 \n", - "4 0.060998 \n", - "... ... \n", - "2713 0.073292 \n", - "2714 23.941857 \n", - "2715 0.062643 \n", - "2716 0.064280 \n", - "2717 0.064280 \n", + "334 0.051025 \n", + "1264 0.051025 \n", + "1171 0.050000 \n", + "589 23.990746 \n", + "2342 0.051025 \n", "\n", - " Relationship: complaints, corruption, conspiracy \\\n", - "0 34.442679 \n", - "1 0.051689 \n", - "2 0.055349 \n", - "3 0.060323 \n", - "4 0.056463 \n", - "... ... \n", - "2713 0.059816 \n", - "2714 0.063406 \n", - "2715 0.055349 \n", - "2716 0.060561 \n", - "2717 0.060561 \n", + " Relationship: complaints, corruption, traffic \\\n", + "1935 0.068496 \n", + "1583 0.055351 \n", + "2351 0.055351 \n", + "452 0.055351 \n", + "8 0.053534 \n", + "... ... \n", + "334 0.053534 \n", + "1264 0.053534 \n", + "1171 0.050037 \n", + "589 0.059230 \n", + "2342 0.053534 \n", "\n", - " Relationship: suscribed, traffic, contract \\\n", - "0 0.054766 \n", - "1 0.051122 \n", - "2 0.053101 \n", - "3 0.117900 \n", - "4 0.060142 \n", - "... ... \n", - "2713 0.053714 \n", - "2714 0.054355 \n", - "2715 0.053101 \n", - "2716 0.064907 \n", - "2717 0.064907 \n", + " Relationship: sanctioned, sanction, evasion \\\n", + "1935 0.075216 \n", + "1583 0.090587 \n", + "2351 0.090587 \n", + "452 0.090587 \n", + "8 16.497892 \n", + "... ... \n", + "334 16.497892 \n", + "1264 16.497892 \n", + "1171 0.050001 \n", + "589 0.051608 \n", + "2342 16.497892 \n", "\n", - " Relationship: occupied, functions, sanctions \\\n", - "0 0.063643 \n", - "1 0.050000 \n", - "2 23.987324 \n", - "3 0.064166 \n", - "4 0.057811 \n", - "... ... \n", - "2713 0.050000 \n", - "2714 0.061778 \n", - "2715 23.987324 \n", - "2716 0.062544 \n", - "2717 0.062544 \n", + " Relationship: facilitators, colleagues, student \\\n", + "1935 0.058883 \n", + "1583 0.050000 \n", + "2351 0.050000 \n", + "452 0.050000 \n", + "8 0.050000 \n", + "... ... \n", + "334 0.050000 \n", + "1264 0.050000 \n", + "1171 0.054220 \n", + "589 0.052662 \n", + "2342 0.050000 \n", "\n", - " Relationship: overpricing, currency, illegal \\\n", - "0 0.053862 \n", - "1 0.050509 \n", - "2 0.052330 \n", - "3 60.674413 \n", - "4 0.117512 \n", - "... ... \n", - "2713 0.052808 \n", - "2714 0.052354 \n", - "2715 0.052330 \n", - "2716 25.575909 \n", - "2717 25.575909 \n", + " Relationship: designates, charge, rights \\\n", + "1935 0.059063 \n", + "1583 0.050001 \n", + "2351 0.050001 \n", + "452 0.050001 \n", + "8 0.050001 \n", + "... ... \n", + "334 0.050001 \n", + "1264 0.050001 \n", + "1171 0.050000 \n", + "589 0.060380 \n", + "2342 0.050001 \n", "\n", - " Relationship: laundering, international, trials \\\n", - "0 0.057605 \n", - "1 0.050418 \n", - "2 0.053825 \n", - "3 17.229793 \n", - "4 37.838020 \n", - "... ... \n", - "2713 0.053850 \n", - "2714 0.054615 \n", - "2715 0.053825 \n", - "2716 20.649126 \n", - "2717 20.649126 \n", + " Relationship: connection, business, in \\\n", + "1935 0.091499 \n", + "1583 0.062470 \n", + "2351 0.062470 \n", + "452 0.062470 \n", + "8 0.065366 \n", + "... ... \n", + "334 0.065366 \n", + "1264 0.065366 \n", + "1171 0.050705 \n", + "589 0.072165 \n", + "2342 0.065366 \n", "\n", " Relationship: members, family, enemies \\\n", - "0 0.050000 \n", - "1 0.054442 \n", - "2 0.050000 \n", - "3 0.065414 \n", - "4 0.059851 \n", + "1935 0.067527 \n", + "1583 0.050000 \n", + "2351 0.050000 \n", + "452 0.050000 \n", + "8 0.050000 \n", "... ... \n", - "2713 0.050000 \n", - "2714 0.052002 \n", - "2715 0.050000 \n", - "2716 0.051986 \n", - "2717 0.051986 \n", + "334 0.050000 \n", + "1264 0.050000 \n", + "1171 18.039779 \n", + "589 0.050000 \n", + "2342 0.050000 \n", "\n", - " Relationship: facilitators, extortion, friends \\\n", - "0 0.052458 \n", - "1 15.039365 \n", - "2 0.050000 \n", - "3 0.055578 \n", - "4 0.051573 \n", - "... ... \n", - "2713 0.051797 \n", - "2714 0.050000 \n", - "2715 0.050000 \n", - "2716 0.070271 \n", - "2717 0.070271 \n", + " Relationship: smuggling, bribery, currency \\\n", + "1935 42.147305 \n", + "1583 0.052716 \n", + "2351 0.052716 \n", + "452 0.052716 \n", + "8 0.053286 \n", + "... ... \n", + "334 0.053286 \n", + "1264 0.053286 \n", + "1171 0.052556 \n", + "589 0.053006 \n", + "2342 0.053286 \n", "\n", - " Relationship: designates, colleagues, charge \n", - "0 0.053457 \n", - "1 0.050809 \n", - "2 0.050000 \n", - "3 0.061222 \n", - "4 0.055335 \n", - "... ... \n", - "2713 0.063327 \n", - "2714 0.051090 \n", - "2715 0.050000 \n", - "2716 0.058718 \n", - "2717 0.058718 \n", + " Relationship: conspiracy, suscribed, contract \n", + "1935 42.766569 \n", + "1583 0.052951 \n", + "2351 0.052951 \n", + "452 0.052951 \n", + "8 0.052058 \n", + "... ... \n", + "334 0.052058 \n", + "1264 0.052058 \n", + "1171 0.050917 \n", + "589 0.056433 \n", + "2342 0.052058 \n", "\n", - "[2718 rows x 12 columns]" + "[2718 rows x 11 columns]" ] }, "execution_count": 17, @@ -1356,7 +1405,7 @@ ], "source": [ "# we've reorganized 68 relationships into N topics and we see it has understood the semantics correctly\n", - "y = g2._get_target('nodes')\n", + "y = g2.get_matrix(target=True)\n", "y" ] }, @@ -1378,7 +1427,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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vzK5tz0JCQlkZ//9/64zZswgMTGxJsIFKJUI/Epjx44tVXdN7bcmOTs7V/szDz30UI1kKGhoFi9ezEcffVTu+z/99BPDhg2r8Xr37t1bKmenPXl5ebz77rs1Xrc9tf1vBmru+q71pz4VRXEG1gLPq6r6h6Io7wFzALXov28BY2qgnvHAeID77rvvWncnhLgZzGxZw/s7U2mR4hRS8HfuzeJE4FcrISGBgQMH4uHhAdiSadeVZcuW1VlddUFVVVRV5csvv6zvUKqkOF4Hh8rHXQoKCipd8Lht27ZaNoea5OPjg4+P3WXCNMUdtUmTJlV5v9U5/pLq899MddXqiJqiKE2wddLiVFVdB6Cq6klVVS+rqloILOXv25u5wL0lPt6uaFt520tRVXWJqqo+qqr63HHHHTV/MEIIUcP8/f3JzbX9OcvJyaFfv354e3sTGBhoN3XT0qVL8fX1xWg0MnToUM6fP09ycjIbNmwgIiICk8lETk4O4eHh2pdtUlISZrMZvV7PmDFjtGTvLi4uREZG4uXlhV6v1+rbtm0bJpMJk8mE2WzW0vzk5+czbNgw3NzcCAsL01br79WrF8ULjDs7OzN16lQ8PT0JCgri1KlTVW6LlJQU/P39MZvNdOvWjUOHDgEQGxtbau2ygQMHajlOnZ2defXVVzEajXTt2lVL0v3999/j7++PXq/XVt8vFhUVha+vLwaDgcjISACsViudO3fmySefRKfTcfz4cVxcXPj111+xWq24u7szbtw4PD096dOnj5abc8+ePRgMBkwmExEREeh0ukqP8+2330an06HT6ViwYAFgy68aExOjlSk5ilrVeMtr+169evH888/j4+PDwoULS+37yJEjPPjggxiNRry8vMjJycFqtWrHERsbS0hICP369aNjx46lkqMvX76cTp064efnx7hx4ypdX67k6NLMmTMZM2YMvXr1on379kRHR2vtkJOTo7VnVY9/x44d5Z6juvg3U961W1Nq86lPBVgOHFBV9e0S29uUKDYE2F/0+wZghKIoTRVFcQU6AinAHqCjoiiuiqL8A9sDBxtqK24hhKgLly9fJikpSVvhfvz48SxatIjU1FTmzZtnd1QhJCSEPXv2kJ6ejru7O8uXL6dbt24MHjyYqKgoLBYLHTr8nTHl4sWLhIeHEx8fT2ZmJgUFBbz33nva+61btyYtLY2JEydqX97z5s0jJiYGi8XCjh07aNasGQD79u1jwYIFZGdnc/ToUXbt2lUmvnPnzuHj40NWVhY9e/Zk1qxZQPkppEpyc3Njx44d7Nu3j9mzZ/PPf/6z0jY8d+4cXbt2JT09nR49erB06VIAnnvuOSZOnEhmZiZt2vz9lbN582YOHz5MSkoKFouF1NRUtm/fDsDhw4eZNGkSWVlZ3H///aXqOXz4MM8884yWB3Tt2rUAjB49mvfffx+LxUKjRo0qjTc1NZUVK1bwv//9j927d7N06VL27dvH8OHD+fTTT7Vyn376KcOHD69WvOW1PcBff/3F3r17efHFF0vFExYWxjPPPEN6ejrJycml2qqYxWLRrp/4+HiOHz/OTz/9xJw5c9i9eze7du2qVj7YYgcPHmTTpk2kpKQwa9YsLl26xNy5c+nQoQMWi4WoqKhqHX9556gu/s1czbVbHbU5ohYAPAE8cMVSHP9WFCVTUZQMoDcwFUBV1SzgU2wPCXwFPFM08lYATAY2AQeAT4vKCiHEdefChQuYTCbuvvtuTp48SXBwMPn5+SQnJxMaGorJZOLpp5/mxIkTZT67f/9+AgMD0ev1xMXFkZVV8Z/CQ4cO4erqquWZHDVqlPZFB7YvMQBvb2+sVisAAQEBvPDCC0RHR5OXl6fltfTz86Ndu3Y4ODhgMpm08iU5ODgwfPhwAEaOHKklNZ8wYUKlt9zOnDlDaGgoOp2OqVOnVnpsYMsjWTxKU/IYdu3axWOPPQbAE088oZXfvHkzmzdvxmw24+XlxcGDBzl8+DAA999/P127drVbj6urKyaTqVQ9eXl5nD17Fn9/fwAef/zxSuPduXMnQ4YMoXnz5jg7OxMSEsKOHTswm8388ssv/PTTT6Snp3Prrbdy7733Vive8toe0LaXdPbsWXJzcxkyZAhgywvr5ORUplxQUBAtW7bE0dERDw8Pjh07RkpKCj179uS2226jSZMmhIaGVnrsVxowYABNmzaldevW3HnnndpoaEnVOX575wjq5t/M1Vy71VGbT33uBBQ7b5V7419V1TeAN+xs/7KizwkhxPWieI7a+fPn6du3LzExMYSHh9OqVStt7lp5wsPDSUhIwGg0Ehsbq90CvFpNmzYFbA84FBQUALbbTwMGDODLL78kICBASy9UXPbK8hWx3Vipmv/7v/+jd+/erF+/HqvVSq9evQBo3LgxhYWFWrmLFy9qvzdp0kSr48qY7NWtqiqvvPIKTz/9dKntVqu1wuTiVx578W21mhQaGspnn33Gzz//rHWsrjZeKH38lZWtyNWc95rab3WOv7xzVBf/Zsq7dmuKZCYQQoh64OTkRHR0NG+99RZOTk64urqyZs0awPYFlZ6eXuYzZ8+epU2bNly6dIm4uDhte4sWLbS5ZCV17twZq9XKkSNHAFi5ciU9e/asMK6cnBz0ej3Tpk3D19e3Wre1CgsLtXk+q1atonv37lX+7JkzZ7jnHtsD/bGxsdp2FxcXLBYLhYWFHD9+nJSUlEr3FRAQwCeffAJQqp369u3LBx98oKWWys3N5ZdffqlyjCW1atWKFi1a8L//2RYzKK6veL9BQUFlPhMYGEhCQgLnz5/n3LlzrF+/nsDAQMA26vXJJ5/w2WefaSNU1Ym3um3fokUL2rVrR0JCAgB//vkn58+fr9Kx+/r6sm3bNn7//XcKCgq024wA69ev55VXXqnSfuzFVPI6ronzVRf/Zsq7dmuKdNSEEKKemM1mDAYDq1evJi4ujuXLl2M0GvH09OTzzz8vU37OnDl06dKFgIAA3NzctO0jRowgKioKs9lMTk6Ott3R0ZEVK1YQGhqKXq/HwcGh0luQCxYsQKfTYTAYaNKkCf3796/y8TRv3pyUlBR0Oh1btmxhxowZQNXmqL388su88sormM3mUqMrAQEBuLq64uHhwZQpU/Dy8qo0joULFxITE4Ner9ce1gDo06cPjz/+uPagwbBhw+x+WVfV8uXLGTduHCaTiXPnztGype0p4hMnTmi3jEvy8vIiPDwcPz8/unTpwtixYzGbzQB4enpy9uxZ7rnnHm2uWHXiLa/tK7Jy5Uqio6MxGAx069aNn3/+uUrHfc899/DPf/4TPz8/AgICcHFx0Y49JyeHW265pUr7udLtt99OQEAAOp2OiIiIGjlfdfFvprxrt6YoxU/u3Eh8fHzU4qeQapPL9C9qvY66Yp07oL5DEKLWHThwAHd39/oO44bl7Ox8UyVCz8/P19Zomzt3LidOnGDhwoW888473HfffdqDInWhrtu++NgLCgoYMmQIY8aMYciQIYwcOZL58+cjqy+Uz97fIUVRUlVVtbt+Sa2voyaEEELciL744gv+9a9/UVBQwP3336/d9qpsqYobwcyZM0lMTOTixYv06dOHRx55BICPP/64fgO7AUlHTQghRI24mUbTwDavzN4TlfWhrtu+smwZoubIHDUhhBBCiAZKOmpCCCGEEA2UdNSEEEIIIRoo6agJIYQQQjRQ0lETQog61KhRI0wmEzqdjkGDBpGXl1dh+ZJJtMuTkJBAdna29nrGjBkkJibWRLhA6YTaVxo7dmypuqtj7969TJkypcIyeXl5vPvuu1e1/+raunUrycnJdVLX9eyhhx6q9LoVNUee+hRC3LT0H+prdH+ZozIrLVOcQgpseQRjYmJ49dVXr6nehIQEBg4ciIeHBwCzZ8++pv1Vx7Jly676sz4+Pvj42F06SlPcUbOXpP5qFBQU2F2MFmwdNWdnZ7p161Yjdd2ovvxSMjrWJRlRE0KIeuLv76+tnJ+Tk0O/fv3w9vYmMDDQbuqmpUuX4uvri9FoZOjQoZw/f57k5GQ2bNhAREQEJpOJnJwcwsPDtXRCSUlJmM1m9Ho9Y8aM4c8//wRsqZkiIyPx8vJCr9dr9W3btg2TyYTJZMJsNmsrwefn5zNs2DDc3NwICwujeLH0Xr16UbzAuLOzM1OnTsXT05OgoCBOnTpV4fGXHKmbOXMmY8aMoVevXrRv357o6GjAlns0JycHk8lEREQEAFFRUfj6+mIwGIiMjNT2N2fOHDp37kz37t157LHHtJHIXr168fzzz+Pj48PChQv5z3/+Q5cuXTCbzTz44IOcPHkSq9XK4sWLmT9/PiaTiR07dnDq1CmGDh2Kr68vvr6+7Nq1q8I2Ks9XX32Fl5cXRqNRSy3122+/8cgjj2AwGOjatSsZGRlaO4waNYrAwEDuv/9+1q1bx8svv4xer6dfv35cunRJO3/F2/38/LSUR/aOraL2nTFjBgsWLNBiffXVV1m4cGGFx+Pi4sKvv/6K1WrF3d2dcePG4enpSZ8+fWolD+rNTjpqQghRDy5fvkxSUpK2ev348eNZtGgRqampzJs3z+4IUkhICHv27CE9PR13d3eWL19Ot27dGDx4MFFRUVgsFjp06KCVv3jxIuHh4cTHx5OZmUlBQQHvvfee9n7r1q1JS0tj4sSJWqdm3rx5xMTEYLFY2LFjB82aNQNg3759LFiwgOzsbI4ePap1Wko6d+4cPj4+ZGVl0bNnT2bNmgVULYUUwMGDB9m0aRMpKSnMmjWLS5cuMXfuXDp06IDFYiEqKorNmzdz+PBhUlJSsFgspKamsn37dvbs2cPatWtJT0/nv//9L1dmp/nrr7/Yu3cvL774It27d2f37t3s27ePESNG8O9//xsXFxcmTJjA1KlTsVgsBAYG8txzzzF16lRt32PHjq2wjew5deoU48aN02IrzucaGRmJ2WwmIyODN998kyeffFL7TE5ODlu2bGHDhg2MHDmS3r17k5mZSbNmzfjii78z4rRs2ZLMzEwmT57M888/D2D32Cpq3zFjxvDRRx8Btnyhn3zyCSNHjqz0XBU7fPgwzzzzDFlZWbRq1apU3k9RM+TWpxBC1KELFy5gMpnIzc3F3d2d4OBg8vPzSU5O1pJxA9rIV0n79+/ntddeIy8vj/z8fPr27VthXYcOHcLV1ZVOnToBf99qLf5SDwkJAcDb25t169YBttyaL7zwAmFhYYSEhNCuXTsA/Pz8tN9NJhNWq7VM4m8HBwdtAdiRI0dq+68sV2KxAQMG0LRpU5o2bcqdd96pjQaVtHnzZjZv3qzlyMzPz+fw4cOcPXuWhx9+GEdHRxwdHRk0aFCpz5VcmPbHH39k+PDhnDhxgr/++gtXV1e78SQmJpaaf/fHH3+Qn59fbhvZs3v3bnr06KHVcdtttwGwc+dOrVPzwAMPcPr0af744w8A+vfvT5MmTdDr9Vy+fJl+/foBoNfrsVqt2r4fe+wx7b9Tp06t9Njsta+Liwu33347+/bt4+TJk5jNZm6//fZyj+dKrq6umEwmwHYdlYxP1AwZURNCiDpUPEft2LFjqKpKTEwMhYWFtGrVCovFov0cOHCgzGfDw8N55513yMzMJDIykosXL15TLE2bNgVsDzgUJ5OePn06y5Yt48KFCwQEBGi3RIvLXlm+IoqiXFU8FdWhqiqvvPKK1k5HjhzhqaeeqnTfzZs3135/9tlnmTx5MpmZmbz//vvltmNhYSG7d+/W6srNzcXZ2bncNqopxe3g4OBAkyZNtHZ0cHAo1SYl27f494qOrbz2HTt2LLGxsaxYsYIxY8ZcVaxX7lPUHOmoCSFEPXByciI6Opq33noLJycnXF1dtdtiqqqSnp5e5jNnz56lTZs2XLp0ibi4OG17ixYt7M6T6ty5M1arVZu/tHLlSnr27FlhXDk5Oej1eqZNm4avr2+1OiGFhYXa3LhVq1aVGXG7GlceW9++ffnggw+0lEm5ubn88ssvBAQE8J///IeLFy+Sn5/Pxo0by93nmTNnuOeeewD48MMPy62rT58+LFq0SHtd/BBIeW3k5uZWpq6uXbuyfft2vv/+e8A2Nw0gMDBQO4dbt26ldevW3HLLLVVvGCA+Pl77r7+/f4XHVpEhQ4bw1VdfsWfPnlKjtPaOR9Q96agJIUQ9MZvNGAwGVq9eTVxcHMuXL8doNOLp6cnnn39epvycOXPo0qULAQEBpb5ER4wYQVRUFGazmZycHG27o6MjK1asIDQ0FL1ej4ODQ6W3IRcsWIBOp8NgMNCkSRP69+9f5eNp3rw5KSkp6HQ6tmzZwowZM4Cqz1Gz5/bbbycgIACdTkdERAR9+vTh8ccfx9/fH71ez7Bhwzh79iy+vr4MHjwYg8FA//790ev1tGzZ0u4+Z86cSWhoKN7e3rRu3VrbPmjQINavX689TBAdHc3evXsxGAx4eHhox2CvjX799VftAYuS7rjjDpYsWUJISAhGo1G7BTtz5kxSU1MxGAxMnz69yp2qkn7//XcMBgMLFy5k/vz5FR5bRf7xj3/Qu3dvHn30URo1agRQ7vGIuqfciCfCx8dHvXIiaW1wmf5F5YWuE9a5A+o7BCFq3YEDB3B3d6/vMG5Yzs7O9ZqYPT8/H2dnZ86fP0+PHj1YsmQJXl5edVL3xo0bOXr0aKXrwtUUFxcX9u7dW+XOWEUKCwvx8vJizZo1dOzYEaj747mZ2Ps7pChKqqqqdteqkYcJhBBC3BDGjx9PdnY2Fy9eZNSoUXXWSQPKXRC4ocvOzmbgwIEMGTJE66TB9Xs8NyLpqAkhhKgR9TmaBrZ5cTeLmnq60sPDg6NHj9bIvkTtkDlqQgghhBANlHTUhBBCCCEaKOmoCSGEEEI0UNJRE0IIIYRooKSjJoQQdahRo0aYTCZ0Oh2DBg0iLy+vwvIzZ87U8nCWJyEhoVSqoxkzZpCYmFgT4QKlk6dfaezYsaXqbsiKk4lX5KGHHqr0nMTGxvLTTz/VYGR1Jy8vj3fffVd7/dNPPzFs2LAar+dq2+jUqVNaUvkdO3awZs0a3N3d6d27N3v37r0plwuRpz6FEDetA241u6aa+8GyaZ+uVJxCCv7Ovfnqq69eU70JCQkMHDgQDw8PAGbPnn1N+6uOZcuW1VlddeHLL7+stExsbCw6nY62bdvWQUR/u3z5srYgrb3XVVHcUZs0aRIAbdu21bJJ1KSK2qiiuJOSktDr9dp11a9fP5YuXaplufDxsbvU2A1NRtSEEKKe+Pv7k5ubC9jSEvXr1w9vb28CAwPtpm5aunQpvr6+GI1Ghg4dyvnz50lOTmbDhg1ERERgMpnIyckhPDxc+/JNSkrCbDaj1+sZM2aMluzdxcWFyMhIvLy80Ov1Wn3btm3DZDJhMpkwm81aSqX8/HyGDRuGm5sbYWFh2qr1vXr1oniBcWdnZ6ZOnYqnpydBQUGcOnWqwuO/fPkyL730krbKf3G6popifuWVVzCZTPj4+JCWlkbfvn3p0KGDljVg69at9OjRgwEDBtC5c2cmTJhAYWFhmbofeeQRvL298fT0ZMmSJdr24lE3q9WKu7s748aNw9PTkz59+nDhwgU+++wz9u7dS1hYGCaTiQsXLjB9+nQ8PDwwGAy89NJLNX7M06ZN0xakvfJ1yfb/9ddfcXFxAWwdpYcffphevXrRsWNHZs2aBdhyuebk5GAymYiIiMBqtaLT6QC4ePEio0ePRq/XYzab+eabb7R9hYSE0K9fPzp27MjLL79c4THaa6Mr47Z3LVssFl5++WU+//xzTCYTs2bNYufOnTz11FNERESUGtnNz8/XYjUYDFqC+xuRdNSEEKIeXL58maSkJAYPHgzYFmtdtGgRqampzJs3TxvxKCkkJIQ9e/aQnp6Ou7s7y5cvp1u3bgwePJioqCgsFgsdOnTQyl+8eJHw8HDi4+PJzMykoKCA9957T3u/devWpKWlMXHiRO326rx584iJicFisbBjxw6aNWsGwL59+1iwYAHZ2dkcPXqUXbt2lYnv3Llz+Pj4kJWVRc+ePbXOQXkppJYsWYLVasVisZCRkUFYWFilMd93331YLBYCAwO1Dunu3buJjIzUyqSkpLBo0SKys7PJyclh3bp1Zer+4IMPSE1NZe/evURHR3P69OkyZQ4fPswzzzxDVlYWrVq1Yu3atQwbNgwfHx/i4uKwWCycP3+e9evXk5WVRUZGBq+99lqZ/VzrMd9+++2kpaUxYsQIu6/Lk5KSwtq1a8nIyGDNmjXs3buXuXPn0qFDBywWC1FRUaXKx8TEoCgKmZmZrF69mlGjRmlJ3S0WixZffHw8x48fL7feK9uo+BoqGbe9a9lkMjF79myGDx+OxWIhMjJS28+Vsc6ZM4eWLVuSmZlJRkYGDzzwQIVtcT2TjpoQQtShCxcuYDKZuPvuuzl58iTBwcHk5+eTnJxMaGgoJpOJp59+mhMnTpT57P79+wkMDESv1xMXF0dWVlaFdR06dAhXV1c6deoE2G61bt++XXs/JCQEAG9vb20B1YCAAF544QWio6PJy8ujcWPbDBk/Pz/atWuHg4MDJpPJ7oKrDg4OWi7LkSNHsnPnTgAmTJhgN8doYmIiTz/9tFbHbbfdVmnMxR1bvV5Ply5daNGiBXfccQdNmzbV5pb5+fnRvn17GjVqxGOPPabFUVJ0dDRGo5GuXbty/PhxDh8+XKaMq6srJpOpTBuV1LJlSxwdHXnqqadYt24dTk5OZcpc6zEXt2l5r8sTHBzM7bffTrNmzQgJCbHbDiXt3LmTkSNHAraE7Pfffz/fffcdAEFBQdqxenh4cOzYsSrFUF7c1b2Wr5SYmMgzzzyjvb711lurHc/1QjpqQghRh4rnqB07dgxVVYmJiaGwsJBWrVphsVi0nwMHys53Cw8P55133iEzM5PIyEhttONqNW3aFLA94FBQUADYbo0tW7aMCxcuEBAQoN0SLS57ZfmKKIpyTfFVFLODg0OpmBwcHLSYrqz3ytdbt24lMTGRb7/9lvT0dMxms922rMoxN27cmJSUFIYNG8bGjRvp16/f1R9cOZo3b17u68aNG2u3dq88hsraoTqu5vxfqWTcNX0t38ikoyaEEPXAycmJ6Oho3nrrLZycnHB1dWXNmjUAqKpKenp6mc+cPXuWNm3acOnSJeLi4rTtLVq00OaSldS5c2esVitHjhwBYOXKlfTs2bPCuHJyctDr9UybNg1fX1+7c+XKU1hYqM2NW7VqlTYBvDzBwcG8//772pf+b7/9dlUxXyklJYXvv/+ewsJC4uPjy8Rx5swZbr31VpycnDh48CC7d++u1v5Ltnd+fj5nzpzhoYceYv78+dp5W79+Pa+88kqtH7OLiwupqakAZR4K+Prrr/ntt9+4cOECCQkJBAQElHutAAQGBmrX1XfffccPP/xA586dK6z/ySefJCUlpcz2iuqB8q/lqgoODiYmJkZ7/fvvv1d7H9cL6agJIUQ9MZvNGAwGVq9eTVxcHMuXL8doNOLp6cnnn39epvycOXPo0qULAQEBuLm5adtHjBhBVFQUZrOZnJwcbbujoyMrVqwgNDQUvV6Pg4OD3VuQJS1YsECb6N6kSRP69+9f5eNp3rw5KSkp6HQ6tmzZwowZM4Dy56iNHTuW++67D4PBgNFoZNWqVVcV85V8fX2ZPHky7u7uuLq6MmTIkFLv9+vXj4KCAtzd3Zk+fTpdu3at1v7Dw8OZMGECJpOJs2fPMnDgQAwGA927d+ftt98GbB3eW265pdaP+aWXXuK9997DbDaXWXrEz8+PoUOHYjAYGDp0KD4+Ptx+++0EBASg0+mIiIgoVX7SpEkUFhai1+sZPnw4sbGxpUbS7MnIyLD7ZGfJNrpw4UKZ98u7lqvqtdde4/fff0en02E0GrUHH25ESvGTOzcSHx8ftfgpmNrkMv2LWq+jrljnDqjvEISodQcOHMDdvWaX5BB/c3Z2rvfE7Fu3bmXevHls3LixXuMYOXIk8+fP54477qiX+mNjY9m7dy/vvPNOrdXxxx9/8NRTT2kjwaJq7P0dUhQlVVVVu2uPyDpqQgghRA37+OOP6zuEWnfLLbdIJ60OSEdNCCFEjajv0TSwrevWq1ev+g6j3oWHhxMeHl7fYYgaIHPUhBBCCCEaKOmoCSGEEEI0UNJRE0IIIYRooKSjJoQQQgjRQElHTQgh6lCjRo0wmUzodDoGDRqkpT0qz8yZM7U8nOVJSEggOztbez1jxgwSExNrIlyAUsmwrzR27NhSdTcUeXl5vPvuu9rrn376iWHDhtVqnRW1U1158803S73u1q1bPUVyderiPF1vau2pT0VR7gU+Au4CVGCJqqoLFUW5DYgHXAAr8Kiqqr8rttwWC4GHgPNAuKqqaUX7GgUUZ7p9XVXVD2srbiHEzSNmwpYa3d8ziytPDF2cQgpsOR1jYmJ49dVXr6nehIQEBg4ciIeHBwCzZ8++pv1Vx7Jly+qsruoo7qgVJ7dv27ZtmZX7b0Rvvvkm//znP7XXycnJ9RhN9d0s56k6anNErQB4UVVVD6Ar8IyiKB7AdCBJVdWOQFLRa4D+QMein/HAewBFHbtIoAvgB0QqinLjZl8VQtw0/P39yc3NBWwr2ffr1w9vb28CAwPtpm5aunQpvr6+GI1Ghg4dyvnz50lOTmbDhg1ERERgMpnIyckhPDxc+7JLSkrCbDaj1+sZM2YMf/75J2BLPRQZGYmXlxd6vV6rb9u2bZhMJkwmE2azuVSqpGHDhuHm5kZYWBjFi6X36tWL4gXGnZ2dmTp1Kp6engQFBXHq1KkKjz8rKws/Pz9MJhMGg0FLjP7II4/g7e2Np6cnS5Ys0co7Ozvz6quvasnUT548CcDJkycZMmQIRqMRo9FIcnIy06dPJycnB5PJREREBFarFZ1OB9hyYo4ePRq9Xo/ZbNZWtY+NjSUkJIR+/frRsWNHXn75Za3uzZs34+/vj5eXF6GhodpSJF999RVubm54eXmxbt26Ss/5iRMn6NGjhzaqumPHDgAmTpyIj48Pnp6eREZGauXLO0/5+fnaMRgMBtauXcv06dO5cOECJpOJsLAwrc3AlpYsIiICnU6HXq8nPj4esI0C9urVy+65Lc+pU6cYOnQovr6++Pr6smvXLgoLC3FxcSk1QtyxY0dOnjzJf/7zH7p06YLZbObBBx/Uzpu9a+1az9ONqNY6aqqqnigeEVNV9SxwALgHeBgoHhH7EHik6PeHgY9Um91AK0VR2gB9ga9VVf1NVdXfga+Bms96K4QQdejy5cskJSUxePBgAMaPH8+iRYtITU1l3rx52khQSSEhIezZs4f09HTc3d1Zvnw53bp1Y/DgwURFRWGxWOjQoYNW/uLFi4SHhxMfH09mZiYFBQW899572vutW7cmLS2NiRMnardX582bR0xMDBaLhR07dtCsWTMA9u3bx4IFC8jOzubo0aPs2rWrTHznzp3Dx8eHrKwsevbsyaxZs4DyU0gtXryY5557DovFwt69e2nXrh0AH3zwAampqezdu5fo6GhOnz6t7b9r166kp6fTo0cPli5dCsCUKVPo2bMn6enppKWl4enpydy5c+nQoQMWi4WoqKhS9cbExKAoCpmZmaxevZpRo0ZpScEtFovWXvHx8Rw/fpxff/2V119/ncTERNLS0vDx8eHtt9/m4sWLjBs3jv/85z+kpqby888/V3reV61aRd++fbFYLKSnp2MymQB444032Lt3LxkZGWzbto2MjIwKz9OcOXNo2bIlmZmZZGRk8MADDzB37lxtxPbK/Jnr1q3T6kxMTCQiIoITJ05U+dyW9NxzzzF16lT27NnD2rVrGTt2LA4ODjz88MOsX78egP/973/cf//93HXXXXTv3p3du3ezb98+RowYwb///W+g/Gvtas/TjapO5qgpiuICmIH/AXepqnqi6K2fsd0aBVsnrmRL/1i0rbztQghx3Ske8bj77rs5efIkwcHB5Ofnk5ycTGhoKCaTiaef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/JHk5OQyZZ977jmee+45MjMzadeuXan37F0jV7ajxWIhNzeX/fv3k5mZyejRoyuN5WYnHTUhhKhDFy5cwGQycffdd3Py5EmCg4PJz88nOTmZ0NBQTCYTTz/9NCdOnCjz2f379xMYGIherycuLo6srKwK6zp06BCurq506tQJsN1q3b59u/Z+SEgIAN7e3litVsCWDPuFF14gOjqavLw87Zaan58f7dq1w8HBAZPJpJUvycHBQcthOXLkSHbu3AnYbk3auz25fft2Ro4cCYDBYMBgMACwe/dusrOzCQgIwGQy8eGHH3Ls2DFatmyJo6MjTz31FOvWrStzKy0vL4+zZ8/i7+8PwOOPP17q/aCgIG0fHh4eHDt2DIBPP/0ULy8vzGYzWVlZdufcXW1MV0pMTOTpp5/W2vW2227T3rN3Ps6cOUNoaCg6nY6pU6eWOufBwcHa53fu3MmIESMAtNG6iuKuTGhoaJmOLsC3335LaGgoULZ9q3KNtG/fnqNHj/Lss8/y1VdflZuAXvxN5qgJIUQdKp6jdv78efr27UtMTAzh4eG0atWq0hyM4eHhJCQkYDQaiY2NZevWrdcUS9OmTQHbAw4FBQWA7TbegAED+PLLLwkICGDTpk2lyl5ZviKKolxVXKqqEhwczOrVq8u8l5KSQlJSEp999hnvvPMOW7ZsqfJ+7R3D999/z7x589izZw+33nor4eHhXLx4sc5ishdfyfb9v//7P3r37s369euxWq306tVLK9+8efNK91lR3BWpyr6vVJVr5NZbbyU9PZ1NmzaxePFiPv30Uz744INq13UzkRE1IYSoB05OTkRHR/PWW2/h5OSEq6sra9asAWxfrunp6WU+c/bsWdq0acOlS5eIi4vTtrdo0UKbS1ZS586dsVqtHDlyBICVK1fSs2fPCuPKyclBr9czbdo0fH197c6VK09hYaE2N27VqlV07969wvI9evRg1apVgG20MCMjA4CuXbuya9cuLe5z587x3XffkZ+fz5kzZ3jooYeYP39+mTZq1aoVLVq04H//+x8An3zySaUx//HHHzRv3pyWLVty8uRJ/vvf/2rvlWzX6sa0fv16XnnllTL1BQcH8/7772udmN9++63C+M6cOcM999wD2OallScgIIBPP/0UgOzsbDIzMyuMG+CVV15h/fr1FTfQFbp27cratWuBqrUvlG7H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\n", "text/plain": [ "
" ] @@ -1393,6 +1442,58 @@ "y.plot(kind='hist', figsize=(10,5)) " ] }, + { + "cell_type": "code", + "execution_count": 19, + "id": "46d25a40-deb2-4aac-96c6-d5b3e84a8d1d", + "metadata": {}, + "outputs": [], + "source": [ + "g2._nodes['rel_topic'] = [y.columns[k].replace('Relationship: ', '') for k in y.values.argmax(1)]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "e81a96ce-407a-47ad-a31f-d6ac1798c4d2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1935 conspiracy, suscribed, contract\n", + "1583 occupied, functions, sanctions\n", + "2351 occupied, functions, sanctions\n", + "452 occupied, functions, sanctions\n", + "8 sanctioned, sanction, evasion\n", + " ... \n", + "334 sanctioned, sanction, evasion\n", + "1264 sanctioned, sanction, evasion\n", + "1171 members, family, enemies\n", + "589 integrates, company, in\n", + "2342 sanctioned, sanction, evasion\n", + "Name: rel_topic, Length: 2718, dtype: object" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g2._nodes['rel_topic']" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "feba6d2b-12a9-4143-ba22-7a3fadd3d430", + "metadata": {}, + "outputs": [], + "source": [ + "g2 = g2.nodes(g2._nodes, g2._node)" + ] + }, { "cell_type": "markdown", "id": "53574e47", @@ -1403,143 +1504,29 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 22, "id": "ce53de1d", - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [ { "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Agent 1RelationshipAgent 2SourceEvidence_distance
2497Gustavo Adolfo Hernández Frieri (USA, Colombia)International trials. Money laundering. Bribery. Illegal Currency traffic. Company connectionGlobal Securities Trade Finance (Cayman Islands)Court documentThe Operation Money Flight case mentions that Frieri laundered money from Ortega through a false structure of mutual funds.12.718029
2498Abraham Edgardo Ortega (Venezuela)International trials. Money laundering. Bribery. Illegal Currency traffic. Company connectionGlobal Securities Trade Finance (Cayman Islands)Court documentThe Operation Money Flight case mentions that Frieri laundered money from Ortega through a false structure of mutual funds.12.718029
109Alex Nain Saab Morán (Colombia)International trials. Money laundering. FraudDevis José Mendoza (Colombia)Recognized communication media (trustable)79th Court of Guarantee control of Colombia investigates money laundering, conspiracy to commit a crime, illicit enrichment, fake export or import and aggravated scam.13.068510
1744Nervis Gerardo Villalobos Cárdenas (Venezuela)International trials. Money launderingGrupo Swissinvest (No information available)Court documentJudicial investigation in Spain points that through \"the structure of transnational character\" of Swissinvest group, \"money laundering operations were made\" inside and outside Spain to “raise capital from crimes of corruption” commited through Pdvsa.13.110929
659Diosdado Cabello Rondón (Venezuela)Complaints for corruptionPedro Fritz Morejon Carrillo (Venezuela)Recognized communication media (trustable)Accused of laundering around USD $1.300 millions in Panama, Costa Rica, Madrid and USA through companies, using money from corruption, drug traffic and terrorism.13.302184
\n", - "
" - ], "text/plain": [ - " Agent 1 \\\n", - "2497 Gustavo Adolfo Hernández Frieri (USA, Colombia) \n", - "2498 Abraham Edgardo Ortega (Venezuela) \n", - "109 Alex Nain Saab Morán (Colombia) \n", - "1744 Nervis Gerardo Villalobos Cárdenas (Venezuela) \n", - "659 Diosdado Cabello Rondón (Venezuela) \n", - "\n", - " Relationship \\\n", - "2497 International trials. Money laundering. Bribery. Illegal Currency traffic. Company connection \n", - "2498 International trials. Money laundering. Bribery. Illegal Currency traffic. Company connection \n", - "109 International trials. Money laundering. Fraud \n", - "1744 International trials. Money laundering \n", - "659 Complaints for corruption \n", - "\n", - " Agent 2 \\\n", - "2497 Global Securities Trade Finance (Cayman Islands) \n", - "2498 Global Securities Trade Finance (Cayman Islands) \n", - "109 Devis José Mendoza (Colombia) \n", - "1744 Grupo Swissinvest (No information available) \n", - "659 Pedro Fritz Morejon Carrillo (Venezuela) \n", - "\n", - " Source \\\n", - "2497 Court document \n", - "2498 Court document \n", - "109 Recognized communication media (trustable) \n", - "1744 Court document \n", - "659 Recognized communication media (trustable) \n", - "\n", - " Evidence \\\n", - "2497 The Operation Money Flight case mentions that Frieri laundered money from Ortega through a false structure of mutual funds. \n", - "2498 The Operation Money Flight case mentions that Frieri laundered money from Ortega through a false structure of mutual funds. \n", - "109 79th Court of Guarantee control of Colombia investigates money laundering, conspiracy to commit a crime, illicit enrichment, fake export or import and aggravated scam. \n", - "1744 Judicial investigation in Spain points that through \"the structure of transnational character\" of Swissinvest group, \"money laundering operations were made\" inside and outside Spain to “raise capital from crimes of corruption” commited through Pdvsa. \n", - "659 Accused of laundering around USD $1.300 millions in Panama, Costa Rica, Madrid and USA through companies, using money from corruption, drug traffic and terrorism. \n", - "\n", - " _distance \n", - "2497 12.718029 \n", - "2498 12.718029 \n", - "109 13.068510 \n", - "1744 13.110929 \n", - "659 13.302184 " + "2498 smuggling, bribery, currency\n", + "2497 smuggling, bribery, currency\n", + "2482 connection, business, in\n", + "1744 laundering, overpricing, international\n", + "2487 connection, business, in\n", + "Name: rel_topic, dtype: object" ] }, - "execution_count": 19, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "res, query_vector = g2.search('money laundering', top_n=5)\n", - "res" + "res.rel_topic" ] }, { @@ -1547,7 +1534,7 @@ "id": "d81d93d3", "metadata": {}, "source": [ - "# Search to Graph\n", + "## Search to Graph\n", "\n", "Pull in neighborhood data from a given search\n", "* the resulting graph will contain Agents connected to Agents that have been involved in Money Laundering (or whatever you wish to search for)\n", @@ -1556,17 +1543,34 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 23, "id": "01278b51", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=5392c6cefae944b38d192740ece8db4a&type=arrow&viztoken=d8174842-dda1-4b4a-9ce9-973a97e2e136&usertag=8a6d667e-pygraphistry-0.28.4+72.g2a02e2b.dirty&splashAfter=1668813586&info=true'" + "" ] }, - "execution_count": 20, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -1578,7 +1582,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 24, "id": "69143805", "metadata": {}, "outputs": [ @@ -1608,6 +1612,7 @@ " Agent 2\n", " Source\n", " Evidence\n", + " rel_topic\n", " _distance\n", " \n", " \n", @@ -1619,6 +1624,7 @@ " Hermágoras González Polanco (Colombia)\n", " Recognized communication media (trustable)\n", " Colombian druf trafficker, leader of Guajira Cartel, according to the Narcogram, he is linked to Tareck El Aissami.\n", + " complaints, corruption, traffic\n", " 13.883661\n", " \n", " \n", @@ -1628,6 +1634,7 @@ " Robinson Ruíz Guerrero (Colombia)\n", " Recognized communication media (trustable)\n", " 79th Court of Guarantee control of Colombia investigates money laundering, conspiracy to commit a crime, illicit enrichment, fake export or import and aggravated scam.\n", + " laundering, overpricing, international\n", " 13.998826\n", " \n", " \n", @@ -1637,6 +1644,7 @@ " Luis Alberto Saab Morán (Colombia)\n", " Recognized communication media (trustable)\n", " 79th Court of Guarantee control of Colombia investigates money laundering, conspiracy to commit a crime, illicit enrichment, fake export or import and aggravated scam.\n", + " laundering, overpricing, international\n", " 14.068014\n", " \n", " \n", @@ -1646,6 +1654,7 @@ " Jaime Alberto Marín Zamora (Colombia)\n", " Recognized communication media (trustable)\n", " Denounces links between drug lords, scam groups, money laundering and a network of SAIME offices. Ditter José Marcano was pointed.\n", + " complaints, corruption, traffic\n", " 14.180918\n", " \n", " \n", @@ -1655,6 +1664,7 @@ " Amir Luis Saab Morán (Colombia)\n", " Recognized communication media (trustable)\n", " 79th Court of Guarantee control of Colombia investigates money laundering, conspiracy to commit a crime, illicit enrichment, fake export or import and aggravated scam.\n", + " laundering, overpricing, international\n", " 14.181194\n", " \n", " \n", @@ -1697,15 +1707,15 @@ "668 Denounces links between drug lords, scam groups, money laundering and a network of SAIME offices. Ditter José Marcano was pointed. \n", "110 79th Court of Guarantee control of Colombia investigates money laundering, conspiracy to commit a crime, illicit enrichment, fake export or import and aggravated scam. \n", "\n", - " _distance \n", - "2233 13.883661 \n", - "108 13.998826 \n", - "111 14.068014 \n", - "668 14.180918 \n", - "110 14.181194 " + " rel_topic _distance \n", + "2233 complaints, corruption, traffic 13.883661 \n", + "108 laundering, overpricing, international 13.998826 \n", + "111 laundering, overpricing, international 14.068014 \n", + "668 complaints, corruption, traffic 14.180918 \n", + "110 laundering, overpricing, international 14.181194 " ] }, - "execution_count": 21, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -1717,17 +1727,34 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 25, "id": "09685c42", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=710e32ad2c574cf1a4ae0e11e13909ab&type=arrow&viztoken=cc06093d-e095-4474-b5d6-db43f7b33759&usertag=8a6d667e-pygraphistry-0.28.4+72.g2a02e2b.dirty&splashAfter=1668813588&info=true'" + "" ] }, - "execution_count": 22, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -1738,7 +1765,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 26, "id": "bcfa273d", "metadata": {}, "outputs": [ @@ -1782,8 +1809,8 @@ " Included him on the list of sanctioned officials for being \"responsibles or accomplices of serious violations\" to Human Rights, \"important acts of corruption or both\".\n", " \n", " \n", - " 1191\n", - " Jesús Rafael Suárez Chourio (Venezuela)\n", + " 2363\n", + " Xavier Antonio Moreno Reandes (Venezuela)\n", " Sanctioned by\n", " On June 25th, 2018 the European Union included him on the list of 11 officials sanctioned\n", " \n", @@ -1795,15 +1822,15 @@ " Agent 1 Relationship \\\n", "1384 Katherine Nayartih Haringhton Padrón (Venezuela) Sanctioned by \n", "1265 José Miguel Montoanda Rodríguez (Venezuela) Sanctioned by \n", - "1191 Jesús Rafael Suárez Chourio (Venezuela) Sanctioned by \n", + "2363 Xavier Antonio Moreno Reandes (Venezuela) Sanctioned by \n", "\n", " Evidence \n", "1384 The only non military officer included on the decree 03/09/2015, in which President Barack Obama suspended visas and froze assets of government officials, for Human Rights violation. \n", "1265 Included him on the list of sanctioned officials for being \"responsibles or accomplices of serious violations\" to Human Rights, \"important acts of corruption or both\". \n", - "1191 On June 25th, 2018 the European Union included him on the list of 11 officials sanctioned " + "2363 On June 25th, 2018 the European Union included him on the list of 11 officials sanctioned " ] }, - "execution_count": 23, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -1815,7 +1842,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 27, "id": "653acdfc", "metadata": {}, "outputs": [ @@ -1871,8 +1898,8 @@ " National Audience and the Anti - Corruption Prosecutor of Spain investigate alleged bribery and money laundering. Involved: Ministry of Energy and Mining , CORPOELEC.\n", " \n", " \n", - " 1126\n", - " Ingeniería Gestión de Proyectos de Energía, C.A. (Ingespre) (No information available)\n", + " 2252\n", + " Técnicas Reunidas Terca Ca (Venezuela)\n", " National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved Ministry of Energy and Mining , CORPOELEC.\n", " \n", " \n", @@ -1881,8 +1908,8 @@ " National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved Ministry of Energy and Mining , CORPOELEC.\n", " \n", " \n", - " 2252\n", - " Técnicas Reunidas Terca Ca (Venezuela)\n", + " 1126\n", + " Ingeniería Gestión de Proyectos de Energía, C.A. (Ingespre) (No information available)\n", " National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved Ministry of Energy and Mining , CORPOELEC.\n", " \n", " \n", @@ -1906,9 +1933,9 @@ "1165 Javier Andrés Alvarado Ochoa (Venezuela) \n", "1330 Juan Carlos Torres Inclán (Spain) \n", "676 Duro Felguera (Spain) \n", - "1126 Ingeniería Gestión de Proyectos de Energía, C.A. (Ingespre) (No information available) \n", - "1468 Luís Barrios Melean (No information available) \n", "2252 Técnicas Reunidas Terca Ca (Venezuela) \n", + "1468 Luís Barrios Melean (No information available) \n", + "1126 Ingeniería Gestión de Proyectos de Energía, C.A. (Ingespre) (No information available) \n", "2283 Víctor Eduardo Aular Blanco (Venezuela) \n", "360 Carlos Eduardo Borges Polar (Venezuela) \n", "\n", @@ -1918,14 +1945,14 @@ "1165 National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved: Ministry of Energy and Mining , CORPOELEC. \n", "1330 National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved: Ministry of Energy and Mining , CORPOELEC. \n", "676 National Audience and the Anti - Corruption Prosecutor of Spain investigate alleged bribery and money laundering. Involved: Ministry of Energy and Mining , CORPOELEC. \n", - "1126 National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved Ministry of Energy and Mining , CORPOELEC. \n", - "1468 National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved Ministry of Energy and Mining , CORPOELEC. \n", "2252 National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved Ministry of Energy and Mining , CORPOELEC. \n", + "1468 National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved Ministry of Energy and Mining , CORPOELEC. \n", + "1126 National Audience and Anti - corruption Prosecutor of Spain investigates alleged bribery and money laundering. Involved Ministry of Energy and Mining , CORPOELEC. \n", "2283 Member of the Strategic Execution Committee of the Financial area. Official Gazzette 39.182, May 20th, 2009. \n", "360 Director of the Internal Operations Office (in charge), of the Sectoral Vice - Presidency of Public Works and Services. Official Gazzette 41.182 of June 28th, 2017. " ] }, - "execution_count": 24, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -1937,17 +1964,34 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 28, "id": "f103792c", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=a936df2d05dd4196addee7b8527d6a46&type=arrow&viztoken=d4ed2c99-1df4-4a35-a032-f398f4580209&usertag=8a6d667e-pygraphistry-0.28.4+72.g2a02e2b.dirty&splashAfter=1668813591&info=true'" + "" ] }, - "execution_count": 25, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1958,38 +2002,72 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 29, "id": "423315b6", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=b810f623437f48b4aa82a3b005810eed&type=arrow&viztoken=bccdca8f-215c-41b7-a1c5-28f14f95acb5&usertag=8a6d667e-pygraphistry-0.28.4+72.g2a02e2b.dirty&splashAfter=1668813594&info=true'" + "" ] }, - "execution_count": 26, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "g2.search_graph('drug trafficking').plot(render=RENDER)" + "g2.search_graph('drug trafficking').dbscan().plot(render=RENDER)" ] }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 32, "id": "e2604442", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=fc826e3132da4cd391f0b4c5aeea939e&type=arrow&viztoken=07862a3c-a473-4e11-b8d1-750a6d4dba93&usertag=8a6d667e-pygraphistry-0.28.4+72.g2a02e2b.dirty&splashAfter=1668813596&info=true'" + "" ] }, - "execution_count": 27, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -2000,23 +2078,39 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 33, "id": "b4f5ebcf", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], "text/plain": [ - "'https://hub.graphistry.com/graph/graph.html?dataset=3ea11287101d47d7ac6748a04a669e98&type=arrow&viztoken=be9ab400-53d0-4b08-9a1e-c0cafc7fc5b7&usertag=8a6d667e-pygraphistry-0.28.4+72.g2a02e2b.dirty&splashAfter=1668813599&info=true'" + "" ] }, - "execution_count": 28, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# paste in url to see in new tab \n", "g2.search_graph('oil and energy companies').plot(render=RENDER)" ] }, @@ -2034,14 +2128,6 @@ "\n", "Join the Graphistry-Community Slack! \n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "91380fec", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 2bf558186b88e9bb2d8055a8192b5e66af479a23 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Jan 2023 20:36:11 -0800 Subject: [PATCH 129/432] edit --- demos/ai/Introduction/simple-power-of-umap.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb index 19538a5b60..90cd94cbf7 100644 --- a/demos/ai/Introduction/simple-power-of-umap.ipynb +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -702,7 +702,7 @@ ], "source": [ "# suppose we want to cluster and color by these variables,\n", - "g2.dbscan(cols='worst', min_dist=0.3, verbose=True, fit_umap_embedding=True).plot()" + "g2.dbscan(cols='worst', min_dist=0.3, verbose=True).plot()" ] }, { From d8ceaa132d63b17523bafc8f80f4de365c6724e8 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 20 Jan 2023 21:00:06 -0800 Subject: [PATCH 130/432] edit --- .../Introduction/simple-power-of-umap.ipynb | 12227 ++++------------ 1 file changed, 2559 insertions(+), 9668 deletions(-) diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb index 90cd94cbf7..5d22c3730a 100644 --- a/demos/ai/Introduction/simple-power-of-umap.ipynb +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -1,13 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": 44, - "id": "0270b0aa-7eea-4915-a3c5-601c0edd34e3", - "metadata": {}, - "outputs": [], - "source": [] - }, { "cell_type": "code", "execution_count": 2, @@ -345,8 +337,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 15.7 s, sys: 927 ms, total: 16.7 s\n", - "Wall time: 17.6 s\n" + "CPU times: user 15.2 s, sys: 745 ms, total: 15.9 s\n", + "Wall time: 16 s\n" ] } ], @@ -370,7 +362,7 @@ "data": { "text/html": [ "\n", - " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# see all the data\n", "g2.plot()" @@ -218,34 +504,541 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "id": "22ed4eec", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
title_0title_1title_2title_3title_4title_5title_6title_7title_8title_9...title_758title_759title_760title_761title_762title_763title_764title_765title_766title_767
16540.6044880.101652-0.0634970.3075420.8443740.1975610.8966310.6318570.315805-0.578581...-0.042918-0.322729-0.277031-0.319512-0.165631-0.5843830.2618680.429799-0.303072-0.377494
1538-1.103507-0.835217-1.2375200.5491960.3972460.199831-1.1968740.290311-1.1710760.513540...0.006427-0.731422-0.750713-0.4866370.841622-0.1986520.195885-0.5702500.050978-0.436235
27080.3260920.0457120.3082240.803963-0.063246-0.123905-0.7314680.2276430.261804-0.048012...-0.2065010.0344020.796114-0.2370420.1177020.649347-0.2994330.995765-0.009557-0.119748
620.926326-0.3926180.035194-0.161504-0.326212-0.1667370.0709370.950549-0.228309-0.056017...-0.5511320.639072-0.468963-0.2904770.117795-0.8035800.8048260.423588-0.092650-0.687976
14810.384588-0.8327600.0338760.2154920.593188-0.432190-0.2835620.4008130.045255-0.430429...0.028084-0.152095-0.2266460.2087030.1870910.1336190.4862500.5752100.730881-0.129466
..................................................................
1264-0.098364-0.2760860.5505860.5420780.321339-0.601650-0.540975-0.3333770.0940110.031201...-0.266734-1.1711370.190349-1.094334-0.9390850.294115-0.118376-0.473456-0.3218700.111786
11710.702274-0.0917610.348669-0.4317061.1911160.006005-1.105823-0.625805-0.1680520.075096...0.838124-0.3052360.3982990.1562320.1468670.339570-0.152106-0.456346-0.3934800.293989
589-0.406301-0.531044-0.563821-0.0126610.3802320.1879000.1690930.475025-0.7724570.188258...-0.478902-0.7819220.1352310.8473670.4511990.4208090.683643-0.7132180.390578-0.141390
23420.1289660.1684800.055048-0.287427-0.069591-0.533780-0.401158-0.270016-0.3983770.062334...1.068983-0.483162-0.373780-0.4115170.0445800.6025510.4239180.028719-0.1603960.211980
2782-0.1442290.703746-0.852380-0.084720-0.654991-0.3746480.142915-0.072289-0.0828890.965485...0.0686120.432348-0.718999-0.4656701.0386470.308591-0.3692320.004829-0.0208010.027217
\n", + "

3000 rows × 768 columns

\n", + "
" + ], + "text/plain": [ + " title_0 title_1 title_2 title_3 title_4 title_5 title_6 \\\n", + "1654 0.604488 0.101652 -0.063497 0.307542 0.844374 0.197561 0.896631 \n", + "1538 -1.103507 -0.835217 -1.237520 0.549196 0.397246 0.199831 -1.196874 \n", + "2708 0.326092 0.045712 0.308224 0.803963 -0.063246 -0.123905 -0.731468 \n", + "62 0.926326 -0.392618 0.035194 -0.161504 -0.326212 -0.166737 0.070937 \n", + "1481 0.384588 -0.832760 0.033876 0.215492 0.593188 -0.432190 -0.283562 \n", + "... ... ... ... ... ... ... ... \n", + "1264 -0.098364 -0.276086 0.550586 0.542078 0.321339 -0.601650 -0.540975 \n", + "1171 0.702274 -0.091761 0.348669 -0.431706 1.191116 0.006005 -1.105823 \n", + "589 -0.406301 -0.531044 -0.563821 -0.012661 0.380232 0.187900 0.169093 \n", + "2342 0.128966 0.168480 0.055048 -0.287427 -0.069591 -0.533780 -0.401158 \n", + "2782 -0.144229 0.703746 -0.852380 -0.084720 -0.654991 -0.374648 0.142915 \n", + "\n", + " title_7 title_8 title_9 ... title_758 title_759 title_760 \\\n", + "1654 0.631857 0.315805 -0.578581 ... -0.042918 -0.322729 -0.277031 \n", + "1538 0.290311 -1.171076 0.513540 ... 0.006427 -0.731422 -0.750713 \n", + "2708 0.227643 0.261804 -0.048012 ... -0.206501 0.034402 0.796114 \n", + "62 0.950549 -0.228309 -0.056017 ... -0.551132 0.639072 -0.468963 \n", + "1481 0.400813 0.045255 -0.430429 ... 0.028084 -0.152095 -0.226646 \n", + "... ... ... ... ... ... ... ... \n", + "1264 -0.333377 0.094011 0.031201 ... -0.266734 -1.171137 0.190349 \n", + "1171 -0.625805 -0.168052 0.075096 ... 0.838124 -0.305236 0.398299 \n", + "589 0.475025 -0.772457 0.188258 ... -0.478902 -0.781922 0.135231 \n", + "2342 -0.270016 -0.398377 0.062334 ... 1.068983 -0.483162 -0.373780 \n", + "2782 -0.072289 -0.082889 0.965485 ... 0.068612 0.432348 -0.718999 \n", + "\n", + " title_761 title_762 title_763 title_764 title_765 title_766 \\\n", + "1654 -0.319512 -0.165631 -0.584383 0.261868 0.429799 -0.303072 \n", + "1538 -0.486637 0.841622 -0.198652 0.195885 -0.570250 0.050978 \n", + "2708 -0.237042 0.117702 0.649347 -0.299433 0.995765 -0.009557 \n", + "62 -0.290477 0.117795 -0.803580 0.804826 0.423588 -0.092650 \n", + "1481 0.208703 0.187091 0.133619 0.486250 0.575210 0.730881 \n", + "... ... ... ... ... ... ... \n", + "1264 -1.094334 -0.939085 0.294115 -0.118376 -0.473456 -0.321870 \n", + "1171 0.156232 0.146867 0.339570 -0.152106 -0.456346 -0.393480 \n", + "589 0.847367 0.451199 0.420809 0.683643 -0.713218 0.390578 \n", + "2342 -0.411517 0.044580 0.602551 0.423918 0.028719 -0.160396 \n", + "2782 -0.465670 1.038647 0.308591 -0.369232 0.004829 -0.020801 \n", + "\n", + " title_767 \n", + "1654 -0.377494 \n", + "1538 -0.436235 \n", + "2708 -0.119748 \n", + "62 -0.687976 \n", + "1481 -0.129466 \n", + "... ... \n", + "1264 0.111786 \n", + "1171 0.293989 \n", + "589 -0.141390 \n", + "2342 0.211980 \n", + "2782 0.027217 \n", + "\n", + "[3000 rows x 768 columns]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# get the encoded features, and use in downstream models (clf.fit(x, y), etc)\n", "x=g2._get_feature('nodes')\n", + "# same as \n", + "x = g2._node_features\n", + "# same as\n", + "x = g2.get_matrix()\n", "x" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "67b15408", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
score
1654-0.38835
1538-0.33530
2708-0.71150
622.70326
1481-0.31601
......
1264-0.19060
1171-0.13273
5890.44605
2342-0.62951
2782-0.72115
\n", + "

3000 rows × 1 columns

\n", + "
" + ], + "text/plain": [ + " score\n", + "1654 -0.38835\n", + "1538 -0.33530\n", + "2708 -0.71150\n", + "62 2.70326\n", + "1481 -0.31601\n", + "... ...\n", + "1264 -0.19060\n", + "1171 -0.13273\n", + "589 0.44605\n", + "2342 -0.62951\n", + "2782 -0.72115\n", + "\n", + "[3000 rows x 1 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# likewise with the (scaled) targets\n", "y = g2._get_target('nodes')\n", + "# same as \n", + "y = g2._node_target\n", + "# same as\n", + "y = g2.get_matrix(target=True)\n", "y" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "id": "f43b7806", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# visualize the results where we prune edges using the `filter_weighted_edges` method\n", "# this keeps all weights that are (more similar) 0.5 and above. The initial layout is the same (given by umap in 2d)\n", @@ -263,10 +1056,73 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "id": "e79eabfc", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
title
1647Is it normal to fall out of love with coding?
1412What landing page do you love?
2854Hackers falling in love
2770What do you love/hate about terminals? Would you change them?
1182Have you found something you love to do? If yes how?
\n", + "
" + ], + "text/plain": [ + " title\n", + "1647 Is it normal to fall out of love with coding?\n", + "1412 What landing page do you love?\n", + "2854 Hackers falling in love\n", + "2770 What do you love/hate about terminals? Would you change them?\n", + "1182 Have you found something you love to do? If yes how?" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# direct keyword search when fuzzy=False and a set of columns are given, does not require featurization\n", "g.search('love', fuzzy=False, cols=['title'])[0][['title']]" @@ -274,10 +1130,250 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, + "id": "c9a8e3bb-faf0-432f-be9e-b173528af866", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
title
2532How did you find your passion?
1182Have you found something you love to do? If yes how?
2509After almost 30 years the romance is over - What now?
2669Is it better to be good at many things or great at one thing?
1164My wife needs something to do from home to make money...
2469Does success in work bring you happiness?
2177As an adult introvertish nerd what makes you happy?
1650Anxiety is limiting my enjoyment of a wonderful career. Can you relate?
1853What do you wish you had done/known in your 30s?
1360Turning 40 soon – seeking personal and professional life advice
\n", + "
" + ], + "text/plain": [ + " title\n", + "2532 How did you find your passion?\n", + "1182 Have you found something you love to do? If yes how?\n", + "2509 After almost 30 years the romance is over - What now?\n", + "2669 Is it better to be good at many things or great at one thing?\n", + "1164 My wife needs something to do from home to make money...\n", + "2469 Does success in work bring you happiness?\n", + "2177 As an adult introvertish nerd what makes you happy?\n", + "1650 Anxiety is limiting my enjoyment of a wonderful career. Can you relate?\n", + "1853 What do you wish you had done/known in your 30s?\n", + "1360 Turning 40 soon – seeking personal and professional life advice" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g2.search('love')[0][['title']]" + ] + }, + { + "cell_type": "code", + "execution_count": 20, "id": "85cf9c06", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "*********************************\n", + "Is true love possible?\n", + "******************************\n", + "1182 Have you found something you love to do? If yes how?\n", + "2043 Why aren't there many credible online bachelors programs?\n", + "2509 After almost 30 years the romance is over - What now?\n", + "2469 Does success in work bring you happiness?\n", + "2532 How did you find your passion?\n", + "1569 What do you wish you had known before you turned 40?\n", + "2669 Is it better to be good at many things or great at one thing?\n", + "1853 What do you wish you had done/known in your 30s?\n", + "1198 What are the books you wish your colleagues had read?\n", + "400 What Lived Up to the Hype?\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n", + "*********************************\n", + "How to create deep learning models?\n", + "******************************\n", + "35 How to get started with machine learning?\n", + "959 How to Seriously Start with Machine Learning and AI\n", + "2172 Why TensorFlow instead of Theano for deep learning?\n", + "1833 How do you manage multiple learning projects?\n", + "1739 How to incorporate machine learning into day job?\n", + "208 Good ways to capture institutional knowledge?\n", + "1726 What do you use Machine Learning for?\n", + "2219 How do I start with test driven development?\n", + "1988 How to develop a growth mindset?\n", + "704 Best introductory video courses on ML and Deep Learning?\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n", + "*********************************\n", + "Best tech careers\n", + "******************************\n", + "1328 What tech that's right around the corner are you most excited about?\n", + "366 Joining Big Tech in One’s 40s\n", + "3 What tech job would let me get away with the least real work possible?\n", + "247 What is the most exciting development in your field right now?\n", + "2428 Companies of one, what is your tech stack?\n", + "748 Companies of one, what is your tech stack?\n", + "981 Who here has built a profitable startup while keeping their day job?\n", + "831 What company environment has enabled your best work?\n", + "801 What are some of the best job boards you have seen (any industry)?\n", + "259 What was your experience starting a tech consultancy?\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n", + "*********************************\n", + "How do I make more money?\n", + "******************************\n", + "1302 How do you earn your money?\n", + "975 Why can't I make as much as I make?\n", + "500 Ways to generate income when you're at home without pay?\n", + "1164 My wife needs something to do from home to make money...\n", + "1034 How do you motivate yourself to keep working on a project?\n", + "2496 Should I find a job or try to build a profitable project?\n", + "844 How do you decide when you've done enough work for the day?\n", + "402 How to optimize your career for happiness?\n", + "1870 How to get out of Tech and still make a decent living?\n", + "1987 How do you stay productive after work?\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n", + "*********************************\n", + "Advances in particle physics\n", + "******************************\n", + "1277 What are the greatest discoveries in the last few years?\n", + "1200 Will there ever be a resurgence of interest in symbolic AI?\n", + "438 What tech were you convinced would take the world by storm but didn't?\n", + "850 Any scientifically proven techniques to boost concentration?\n", + "738 Has any progress been made on large format E-ink displays?\n", + "2528 Why aren't there any real alternatives to Electron?\n", + "1029 I'm looking for a good book on the fundamentals of CS\n", + "2399 What is the emerging state of the art in fuzzing techniques?\n", + "522 What are some interesting projects to reuse your old devices?\n", + "650 What things do you wish you discovered earlier?\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n", + "*********************************\n", + "Best apps and gadgets\n", + "******************************\n", + "2638 What are the best web tools to build basic web apps as of October 2016?\n", + "817 Best-architected open-source business applications worth studying?\n", + "2769 What Android apps do you use?\n", + "1032 What are the best technologies you've worked with this year?\n", + "1439 What is the best enterprise software you use every day?\n", + "2826 Inspirational money making web apps made by hackers.\n", + "211 What's your favorite way of getting a web app up quickly in 2018?\n", + "2773 Best tech for a web site 2018? (PHP, Rails, Django, Node, Go, etc.)?\n", + "1923 What is good business advice for independent mobile app developers?\n", + "2658 What is the best way to promote your new fancy web application?\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n", + "*********************************\n", + "Graph Neural Networks\n", + "******************************\n", + "1827 What was your experience using a graph database?\n", + "1825 Why GraphQL APIs but no Datalog APIs?\n", + "1155 Why are relational DBs are the standard instead of graph-based DBs?\n", + "1540 If you've used a graph database, would you use it again?\n", + "799 Were you happy moving your API from REST to GraphQL?\n", + "919 What's the best algorithms and data structures online course?\n", + "2907 What are the best resources for learning about algorithmic trading?\n", + "1436 Looking for a book on algorithms and data structures\n", + "377 What are some examples of good database schema designs?\n", + "2498 Building a game for AI Research\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n", + "*********************************\n", + "recommend impactful books\n", + "******************************\n", + "113 Great fiction books that have had a positive impact on your life?\n", + "2507 What book impacted your life the most and how?\n", + "104 What books have made the biggest impact on your mental models?\n", + "2737 Which books have helped you the most professionally?\n", + "523 Which non-technology book has influenced you the most and why?\n", + "1099 Recommendations of good cybercrime novels?\n", + "1933 Recommend books that give you insight into other professions\n", + "2837 What are the best books for professional effectiveness?\n", + "1764 What is one book you would recommend everyone to read?\n", + "815 What makes a good technical leader – any recommended books?\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n", + "*********************************\n", + "lamenting about life\n", + "******************************\n", + "1218 Words of encouragement for someone lost in life?\n", + "1337 What do you regret in life?\n", + "2155 The Internet is getting lame. What's next?\n", + "741 I'm So Lonely\n", + "2165 Coping with Loneliness\n", + "514 When you feel stuck in life\n", + "1526 What should I say to my manager when my performance starts suffering?\n", + "969 How to cope with the death of a dear person?\n", + "2195 I'm a solopreneur and I feel demoralised\n", + "520 Failed interview, feeling unemployable and depressed – what do I do?\n", + "Name: title, dtype: object\n", + "------------------------------------------------------------\n" + ] + } + ], "source": [ "# Query semantically instead of strict keyword matching\n", "\n", @@ -313,10 +1409,38 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "302a0b53", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "gr = g2.search_graph('How to create deep learning models', thresh=15, top_n=50, scale=0.25, broader=False) \n", "gr.plot()" @@ -324,20 +1448,76 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "543f7b83", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "g2.search_graph('Graph Neural Networks', thresh=50, top_n=50, scale=0.1, broader=False).plot()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "id": "6f2f9157", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "g2.search_graph('fraud detection algorithms', thresh=50, top_n=50, scale=0.1, broader=False).plot() # works better if you encode 'text' column as well" ] @@ -352,12 +1532,360 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "id": "09b941fe", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
title_0title_1title_2title_3title_4title_5title_6title_7title_8title_9...title_758title_759title_760title_761title_762title_763title_764title_765title_766title_767
15170.9791800.041072-0.5930980.2143040.962338-0.713328-0.9896970.1453880.0756080.488215...-0.6967080.139317-0.846404-0.025490-0.120986-0.0558430.6379300.1435140.585826-0.303448
16050.6886530.019184-0.227892-0.2392110.1897950.341207-0.120877-1.2968780.2069080.103815...-0.236452-0.3842940.3940590.244530-0.3649240.675864-0.271245-0.173634-0.298026-0.017564
1451-0.1847700.235804-0.400443-0.2115110.1148180.160413-0.1312620.500900-0.2752310.190890...0.091999-0.233226-0.072699-0.7134600.4236841.398612-0.2034360.473697-0.219005-0.128714
1372-0.9836800.548434-0.5843510.3537030.117870-0.0988791.095775-0.385954-0.5412510.007578...0.0470790.485933-0.285741-0.035659-0.1018070.1101451.122281-0.237854-0.5323320.939817
964-0.431375-0.915085-0.5808610.3954720.406366-0.1311931.074949-0.996813-0.183665-0.006735...-0.9463000.433078-0.1901540.137894-0.198106-0.261280-0.6958570.226295-0.670496-0.423368
11040.096140-0.0561720.0631500.2348380.117753-0.346006-0.744430-0.1511070.1430600.241910...0.500574-0.4781880.296040-0.612845-0.324935-0.4394640.4697100.5394910.906302-0.175188
370.281097-0.6086630.4235990.4207871.051380-0.0272900.602898-0.2847270.099539-1.925934...-0.300906-0.1112351.1238930.459886-0.2181240.5902450.296381-0.609109-0.147541-1.250704
228-0.131974-0.062444-0.8378200.162044-0.4514660.3191390.052473-0.631871-0.020183-0.478724...-0.1010251.011868-0.704747-0.454947-0.2272430.9617580.6868370.5102590.270457-1.069947
13400.6343220.3514940.0380980.234291-0.872616-0.458497-0.1796050.2568170.1226790.471110...0.6821620.1848810.3820030.2360480.035794-0.4627130.3330540.4479520.9125960.432614
1681-0.244261-0.050637-0.4746880.063758-0.309980-0.171460-0.609836-0.007839-0.3715130.509530...-0.222305-1.502060-0.571068-1.054443-0.434218-0.1450710.131197-0.6852010.0558740.055352
\n", + "

10 rows × 768 columns

\n", + "
" + ], + "text/plain": [ + " title_0 title_1 title_2 title_3 title_4 title_5 title_6 \\\n", + "1517 0.979180 0.041072 -0.593098 0.214304 0.962338 -0.713328 -0.989697 \n", + "1605 0.688653 0.019184 -0.227892 -0.239211 0.189795 0.341207 -0.120877 \n", + "1451 -0.184770 0.235804 -0.400443 -0.211511 0.114818 0.160413 -0.131262 \n", + "1372 -0.983680 0.548434 -0.584351 0.353703 0.117870 -0.098879 1.095775 \n", + "964 -0.431375 -0.915085 -0.580861 0.395472 0.406366 -0.131193 1.074949 \n", + "1104 0.096140 -0.056172 0.063150 0.234838 0.117753 -0.346006 -0.744430 \n", + "37 0.281097 -0.608663 0.423599 0.420787 1.051380 -0.027290 0.602898 \n", + "228 -0.131974 -0.062444 -0.837820 0.162044 -0.451466 0.319139 0.052473 \n", + "1340 0.634322 0.351494 0.038098 0.234291 -0.872616 -0.458497 -0.179605 \n", + "1681 -0.244261 -0.050637 -0.474688 0.063758 -0.309980 -0.171460 -0.609836 \n", + "\n", + " title_7 title_8 title_9 ... title_758 title_759 title_760 \\\n", + "1517 0.145388 0.075608 0.488215 ... -0.696708 0.139317 -0.846404 \n", + "1605 -1.296878 0.206908 0.103815 ... -0.236452 -0.384294 0.394059 \n", + "1451 0.500900 -0.275231 0.190890 ... 0.091999 -0.233226 -0.072699 \n", + "1372 -0.385954 -0.541251 0.007578 ... 0.047079 0.485933 -0.285741 \n", + "964 -0.996813 -0.183665 -0.006735 ... -0.946300 0.433078 -0.190154 \n", + "1104 -0.151107 0.143060 0.241910 ... 0.500574 -0.478188 0.296040 \n", + "37 -0.284727 0.099539 -1.925934 ... -0.300906 -0.111235 1.123893 \n", + "228 -0.631871 -0.020183 -0.478724 ... -0.101025 1.011868 -0.704747 \n", + "1340 0.256817 0.122679 0.471110 ... 0.682162 0.184881 0.382003 \n", + "1681 -0.007839 -0.371513 0.509530 ... -0.222305 -1.502060 -0.571068 \n", + "\n", + " title_761 title_762 title_763 title_764 title_765 title_766 \\\n", + "1517 -0.025490 -0.120986 -0.055843 0.637930 0.143514 0.585826 \n", + "1605 0.244530 -0.364924 0.675864 -0.271245 -0.173634 -0.298026 \n", + "1451 -0.713460 0.423684 1.398612 -0.203436 0.473697 -0.219005 \n", + "1372 -0.035659 -0.101807 0.110145 1.122281 -0.237854 -0.532332 \n", + "964 0.137894 -0.198106 -0.261280 -0.695857 0.226295 -0.670496 \n", + "1104 -0.612845 -0.324935 -0.439464 0.469710 0.539491 0.906302 \n", + "37 0.459886 -0.218124 0.590245 0.296381 -0.609109 -0.147541 \n", + "228 -0.454947 -0.227243 0.961758 0.686837 0.510259 0.270457 \n", + "1340 0.236048 0.035794 -0.462713 0.333054 0.447952 0.912596 \n", + "1681 -1.054443 -0.434218 -0.145071 0.131197 -0.685201 0.055874 \n", + "\n", + " title_767 \n", + "1517 -0.303448 \n", + "1605 -0.017564 \n", + "1451 -0.128714 \n", + "1372 0.939817 \n", + "964 -0.423368 \n", + "1104 -0.175188 \n", + "37 -1.250704 \n", + "228 -1.069947 \n", + "1340 0.432614 \n", + "1681 0.055352 \n", + "\n", + "[10 rows x 768 columns]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "x, y = g2.transform(df.sample(10), df.sample(10), kind='nodes') # or edges if given or already produced through umap-ing the nodes, \n", + "sdf = df.sample(10)\n", + "x, y = g2.transform(sdf, sdf, return_graph=False) # or edges if given or already produced through umap-ing the ny_nodes=\n", " #and if neither, set `embedding=True` for random embedding of size `n_topics`\n", "x" ] @@ -372,13 +1900,82 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "e68126cd", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
xy
217812.0678985.299919
122711.8311994.594526
684-3.1081384.623506
173310.2674676.992209
70211.2295307.411973
\n", + "
" + ], + "text/plain": [ + " x y\n", + "2178 12.067898 5.299919\n", + "1227 11.831199 4.594526\n", + "684 -3.108138 4.623506\n", + "1733 10.267467 6.992209\n", + "702 11.229530 7.411973" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "emb, x, y = g2.transform_umap(df.sample(10), df.sample(10))\n", - "emb" + "emb, x, y = g2.transform_umap(df.sample(10), df.sample(10), return_graph=False)\n", + "emb.head()" ] }, { @@ -391,80 +1988,446 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "id": "d148348e", "metadata": {}, "outputs": [], "source": [ "# this inherets all the arguments from the g.featurize api for both nodes and edges, see g.build_gnn? for details\n", - "g3 = g25.build_gnn() # we use the filtered edges graphistry instance as it has higher fidelity similarity scores on edges\n", + "g3 = g25.build_gnn(y_nodes='score') # we use the filtered edges graphistry instance as it has higher fidelity similarity scores on edges\n", " # ie, less edges" ] }, { "cell_type": "code", - "execution_count": null, - "id": "5989c286", - "metadata": {}, - "outputs": [], - "source": [ - "# notice the difference in edge dataframes between g2/5 and g3\n", - "g25._edges" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "59af921c", - "metadata": {}, - "outputs": [], - "source": [ - "# versus\n", - "g3._edges" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af1cd73e", - "metadata": {}, - "outputs": [], - "source": [ - "# Edges come from data supplied by umap on nodes\n", - "g3._edge_encoder.feature_names_in" - ] - }, - { - "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "764e7ba7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0123456789...299129922993299429952996299729982999_weight
21.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.797920
41.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.547040
61.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.862698
71.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.801896
81.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.653791
\n", + "

5 rows × 3001 columns

\n", + "
" + ], + "text/plain": [ + " 0 1 2 3 4 5 6 7 8 9 ... 2991 2992 2993 \\\n", + "2 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 \n", + "4 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 \n", + "6 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 \n", + "7 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 \n", + "8 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 \n", + "\n", + " 2994 2995 2996 2997 2998 2999 _weight \n", + "2 0.0 0.0 0.0 0.0 0.0 0.0 0.797920 \n", + "4 0.0 0.0 0.0 0.0 0.0 0.0 0.547040 \n", + "6 0.0 0.0 0.0 0.0 0.0 0.0 0.862698 \n", + "7 0.0 0.0 0.0 0.0 0.0 0.0 0.801896 \n", + "8 0.0 0.0 0.0 0.0 0.0 0.0 0.653791 \n", + "\n", + "[5 rows x 3001 columns]" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "g3._edge_features.head()" + "g3.get_matrix(kind='edges').head()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "id": "fc1955b1", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0123456789...299129922993299429952996299729982999_weight
70430.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.01.000000
106330.00.00.00.00.00.00.00.01.00.0...0.00.00.00.00.00.00.00.00.01.000000
11800.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.01.000000
67690.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.724311
472010.00.00.00.00.00.00.00.00.00.0...0.00.00.00.00.00.00.00.00.00.514489
\n", + "

5 rows × 3001 columns

\n", + "
" + ], + "text/plain": [ + " 0 1 2 3 4 5 6 7 8 9 ... 2991 2992 \\\n", + "7043 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n", + "10633 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 ... 0.0 0.0 \n", + "1180 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n", + "6769 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n", + "47201 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n", + "\n", + " 2993 2994 2995 2996 2997 2998 2999 _weight \n", + "7043 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.000000 \n", + "10633 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.000000 \n", + "1180 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.000000 \n", + "6769 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.724311 \n", + "47201 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.514489 \n", + "\n", + "[5 rows x 3001 columns]" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Since edges are featurized, we can transform on \"unseen/batch\" ones\n", - "# y_edges will be none since we don't have a label for the implicit edges. One could supply it via enrichment (like clustering, annotation etc)\n", "edge_data = g3._edges.sample(10)\n", "\n", - "x_edges, _ = g3.transform(edge_data, None, kind='edges')\n", - "x_edges" + "# y_edges will be None since we don't have a label for the implicit edges. One could supply it via enrichment (like clustering, annotation etc)\n", + "x_edges, _ = g3.transform(edge_data, None, kind='edges', return_graph=False)\n", + "x_edges.head()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "id": "59d403f9", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Graph(num_nodes=3000, num_edges=19100,\n", + " ndata_schemes={'feature': Scheme(shape=(768,), dtype=torch.float32), 'target': Scheme(shape=(1,), dtype=torch.float64), 'train_mask': Scheme(shape=(), dtype=torch.bool), 'test_mask': Scheme(shape=(), dtype=torch.bool)}\n", + " edata_schemes={'feature': Scheme(shape=(3001,), dtype=torch.float64), 'train_mask': Scheme(shape=(), dtype=torch.bool), 'test_mask': Scheme(shape=(), dtype=torch.bool)})" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# once built, we can get the DGL graph itself\n", "G = g3.DGL_graph\n", @@ -473,10 +2436,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "id": "8380122a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'feature': tensor([[ 0.6045, 0.1017, -0.0635, ..., 0.4298, -0.3031, -0.3775],\n", + " [-1.1035, -0.8352, -1.2375, ..., -0.5702, 0.0510, -0.4362],\n", + " [ 0.3261, 0.0457, 0.3082, ..., 0.9958, -0.0096, -0.1197],\n", + " ...,\n", + " [-0.4063, -0.5310, -0.5638, ..., -0.7132, 0.3906, -0.1414],\n", + " [ 0.1290, 0.1685, 0.0550, ..., 0.0287, -0.1604, 0.2120],\n", + " [-0.1442, 0.7037, -0.8524, ..., 0.0048, -0.0208, 0.0272]]), 'target': tensor([[-0.3883],\n", + " [-0.3353],\n", + " [-0.7115],\n", + " ...,\n", + " [ 0.4461],\n", + " [-0.6295],\n", + " [-0.7211]], dtype=torch.float64), 'train_mask': tensor([True, True, True, ..., True, True, True]), 'test_mask': tensor([False, False, False, ..., False, False, False])}" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# the features, targets, and masks\n", "G.ndata" @@ -484,10 +2470,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "id": "63beefab", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([19100, 3001])" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# `build_gnn()` will turn edges gotten from umap into bonafide feature matrices, \n", "# and make features out of explicit edges with `build_gnn(X_edges=[...], ..)`\n", @@ -496,10 +2493,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "id": "45d3a37a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA20AAAHVCAYAAABi9BP7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAApOElEQVR4nO3de5hlZ10n+u/vdHcS2ASaQMxdAtiCICZhmhABBaIgZDgmOMjFCxEZIwwc4XgZQQ+POMiAHgSMHNEAGQIHgYgGYg6jBIhCBgl0yIUkEAkxTO4BkiZhx4amec8ftYM1oZqu6rq8a9f+fJ5nP7X2u9Ze69vJ6q761rpVay0AAAAM0//WOwAAAAC7p7QBAAAMmNIGAAAwYEobAADAgCltAAAAA6a0AQAADNiqlbaqekpVXVlVV1XVy1ZrOwAAAOtZrcZz2qpqQ5J/TvKkJNcl+XSS57TWrljxjQEAAKxjq3Wk7dgkV7XWrm6tfTPJe5KcuErbAgAAWLc2rtJ6D0ty7bz31yV59PwFquqUJKckyWg0+ncPvXOci1b+oB8AS1BJjv53hyXXXJ+Lvto7DQDMjm8nX2mtHbjQvNUqbXvUWjstyWlJsnXr1rbt0gsz2tkrDQB3ufnC6/ONJPv1DgKwzj0ryentC3lvbckv9w5Dd3cmX9rdvNUqbdcnOWLe+8MnYwAM3K29AwDMiA8k+anakkt6B2HwVuuatk8n2VJVD6yqfZI8O8nZq7QtAACYOjuSnJ/kjt5BGLxVOdLWWvtWVb04yd8n2ZDk9Nba5auxLQAAgPVs1a5pa619MMkHV2v9AAAAs2DVHq4NAADA8iltAAAAAzaQ0nZJ8pDkd3vHAAByYJK3JHlQ7yAAJBlIabvowm8lr0p+px3bOwoAzLzHJ/m59vI8r3cQgHVqc5LnTL4uxiBKGwAwHO9LMqrX5BW9gwCsU89N8tb28bxwkcuv2t0jAQAA+G6nJjm1fmzRyzvSBgAAMGCDKW2bn578RX0q43Zbfq13mBkzbvfM+MO9UwAAAAsZzOmRO5PsSJJszoa+UWbOw+vOfLN3CAAAYEGDKW30c03vAAAAwG4N5vRIAAAAvtugStt7k4yr8gdvS8b79k6zPjwqyfjDcZ0gAABMqUGVtkuSfF+S/PLPJn/YOcw6cXuS/FHypRVa3+VJxj+0QisDAAD2aFCljZV3ZZLRh5IPrND6HpAkj1ihlQEAg/HOzP1yFhgepY0luVeS0Zm9UwAAK+1ntiZHvrF3CmAh7h4JAEBG25Js650CWIgjbQAAAAOmtAEAAAyY0gYAADBgShsAAMCAKW0AAAADprQBAAAMmNIGAAAwYIMsbX9bf5Wck4zPS/ZfwfXekmR8ZrJhBdcJAACwmgZZ2p6dJNckecJ52W8F13tGkvz8Cq4QAABglQ2ytK2W30oy2pns6h0EAABgkWaqtAEAAEwbpQ0AAGDAlDYAAIABU9oAAAAGTGkDAAAYMKUNAABgwAZb2jZflRxfT8w17dO5rncYAIAV8s4k43ZtntE7CDA1Blvadia5I0ly/+zTNwoAwIp5QZLH1BH5QO8gwNTY2DsAAMAsGSe5pHcIYKoM9khbknwpyd/UAzP642T8x73TrH/7JxlfnVzeOwgAAPAdgy5t4yS/mCSPTPLrh/YNMwN2JLntQcmbegcBps5+ScZvTP6hcw4AWI+cHsl37ExyeO8QwFTalSTvTa7oHQQA1iGlDYBl25lk9E+9UwDA+jTo0yMBAABmndIGAAAwYNNR2q5O8vUb8qjMXewOAOydo5Ic3DsEAEsyFaVt9Pzknfsn/9BantM7DABMqVGST7TX5otP7J1ktmzK3GN1APbWVJQ2AGD5xklG9bKMzuudZLZs35rc1N6fzb2DAFNLaQMAWEWjbcm966Rs7x0EWDXjds+Mz1+99bvlPwDAKtvVOwCwqh5cd67q3/NllbaquibJHZn7t+hbrbWtVXVAkvcmOTLJNUme2Vq7bXkxAQAAhummVV7/Spwe+cTW2tGtta2T9y9L8pHW2pYkH5m8BwAAYC+sxjVtJyY5YzJ9RpKTVmEbAAAAM2G5pa0l+VBVXVhVp0zGDmqt3TiZvinJQQt9sKpOqaptVbWtLWJDZybJIytvelly0TJDAwAATIvl3ojkca2166vq+5KcW1Wfnz+ztdaqasFO1lo7LclpSbJhN8vM99Eko4uS8Wf+j/zgfn+avHKZyQEAAKbAso60tdaun3y9JclZSY5NcnNVHZIkk6+3LDckAADArNrr0lZVo6ra/67pJE9OclmSs5OcPFns5CQfWG5IAACAWbWc0yMPSnJWVd21nr9srf1dVX06yZlV9fwkX0ryzOXHBAAAmE17Xdpaa1cnOWqB8a8m+YnlhPrePp/sTA5NcsPqbQRmwsFJdiTZ3jkHAAC7txq3/F9Vozo3+fvkC+392dw7DEy5L7Yn5vo3904Bs23D5AUAuzN1pQ1YOaM6L6MX9k4Bs+329vTcfmrvFAAM2XJv+Q8ALMPBdVa+3TsEAIM2lUfaHrwteUmdlOvbn+d/9A4DAMtwR5Jx7xDAmvhQknH78zy6dxCmzlSWtpuSXJIkOdF1bQAATIVTkvx2vWDyc+z697dJxu1JeUTvIOuA0yMBAGANXJPkTb1DrKHXJNlV5+aa3kHWAaUNAABYcZ9IclLvEOvEVJ4eCQAAMCuUNgAAgAFT2gAAAAZMaQMAABgwpQ0AAGDAlDYAAIABU9oAAAAGbGpL26eT3K8OyZEXJeNv904DADBn/KFk3A7oHQNYR6b64do7kmRTktonyTf7hgEASPLgJyebcmvvGMA6MtWlDQBgaG7qHQBYd6b29EgAAIBZMPWl7Z0/nGTLNzM+P3l07zDAmnl7kvE5yaG9gwAArLKpL20vSPL+q5I8dmeO7JwFWDufSZJXJHf2DgIAsMqmvrQBs+nUJKOLku29gwAArDKlDQAAYMCUNgAAgAFT2gAAAAZMaQMAABgwpQ0AAGDAlDYAAIABWxel7ZeSbKlNOb39SsbP7Z0GWA/G5yfjO3qnAABYJ6VtZ5KbkyQHJ/v3zQKsD498XHKcf0/obNwOzfiXeqcAoLeNvQMADNGVvQNAkpfUDbm6dwgAulPaAGCg3to7AACDsC5OjwQAAFivlDYAAIABU9oAAAAGTGkDAAAYMKUNAABgwJQ2AACAAVPaAAAABkxpAwAAGDClDQAAYMCUNgAAgAFT2gAAAAZs3ZS2XUneVK9KbkjGVyb79Q4EADBjxucn48N7p4D1Z92UtiT57STZkeQH354NnbMAAMyayx+X/NfreqeAZPzDyfg5vVOsnI29AwAAsD4c2zsATHzxsuRBl/VOsXL2eKStqk6vqluq6rJ5YwdU1blV9YXJ1/tOxquqTq2qq6rq0qp65GqGBwAAuLsfSXKv3iFW0GJOj3x7kqfcbexlST7SWtuS5COT90ny1CRbJq9Tkrx5ZWICAADMpj2Wttbax5LcerfhE5OcMZk+I8lJ88bf0eZ8MsnmqjpkhbICAADMnL29EclBrbUbJ9M3JTloMn1YkmvnLXfdZOy7VNUpVbWtqra1vQwBAACw3i377pGttZZkyb2rtXZaa21ra21rLTfEfOMkuSkHJtm0kusFAADoYG9L2813nfY4+XrLZPz6JEfMW+7wydiaGX0sOaFelsvbbfn4Wm4YAABgFextaTs7ycmT6ZOTfGDe+HMnd5E8LsnX5p1GCQAAwBLt8TltVfXuJE9Icv+qui7J7yV5bZIzq+r5Sb6U5JmTxT+Y5IQkVyW5M8nzViEzAADAzNhjaWut7e5Z4j+xwLItyYuWGwoAAIA5y74RCQAAAKtHaQMAABgwpQ0AAGDA1mVpuzZJ/vN984h/n/xz7zAAAADLUHP3DulrQ1XbbxXWO/5Sks3J6D6rsHIAAIAVcmdyYWtt60Lz1uWRNgAAgPVCaQMAABgwpQ0AAGDA1ndp25lkV7Kpdw4YsA3xdwQAYMjWdWkb/UDy8QOS7e1fc0LvMDBQtx+abG//lEN7BwEAYEEbewcA+tp8Q7Jf/Wju6B0EAIAFKW0w43ZOXgAADNO6Pj0SAABg2iltAAAAA6a0AQAADNi6L21nJMmz7pG/em7ynt5hAGDKPTrJ+LnJT/YOAjBD1n1pe3eSzWcmOeON+d9/vncaAJhu+yfJTyebO+cAmCWDLW0nJHlXVvChvx97aZLkLSu1PgCYQR9OMnpG8r7eQQBmyGBL2zuSnNQeNvcbvWXamWT0+Lnpn2uvW7kiyKoZJXlhkiN6BwEAgM4GW9run2RUV+TW3kHo4pgkr2tvz5/1DgIArCsbkjwyyQG9g8ASeLg2g3R+klH9Uu8YAMA684AkH28fzq76ydy7d5gpMJp8HXdNgdIGAMDMuDrJqNz/dLFuuTrJgcloJa5ZYq8pbQAAwILu/aDeCUiUNgAAYDd29Q5AkgHfiAQAAAClDQAAYNCUNgAAgAFT2gAAAAZMaQMAABgwpQ0AAGDAlDYAAIABU9oAAAAGTGkDAAAYMKUNAABgwJQ2AACAAVPaAAAABkxpAwAAGLCZKm3Hvyv5q/rNbG8/mz/rHQYAAGARZqq0XZDkfUmSd+QBfaMAAAAsykyVNgAAgGkzc6XtyiQ5+R55wjOT63qHGYjxpuRzvUMAAAALqtZa7wzZUNX2W+Ntju9Icmsycp5kxv8hySXJ6KreSQAAYDbdmVzYWtu60LyNax2G4Rn9de8EAADA7szc6ZEAAADTZHaPtF2d5NvJUZO3NyT5csc4AAAAC9njkbaqOr2qbqmqy+aNvbKqrq+qiyevE+bNe3lVXVVVV1bVT61W8OUaHZV89pjkE63lE+22XPPjvRNBst/kBQAAd1nM6ZFvT/KUBcbf0Fo7evL6YJJU1cOSPDvJwyef+bOq2rBSYVfH9pxQ983oY71zQPLVryVfvaF3CgAAhmSPpa219rEkty5yfScmeU9r7RuttX9JclWSY5eRD2bK5vskmw/tnQJgth2aZNz+LuP7904CMGc5NyJ5cVVdOjl98r6TscOSXDtvmesmY8Ai7Jy8AOjnhiSPqKfk4K/0TgJ7dnuScfvzHNE7CKtqb0vbm5M8OMnRSW5M8sdLXUFVnVJV26pqW/8nxQEA/Jurk9zROwQswo8k+bl6QVxdsb7t1d0jW2s33zVdVW9Jcs7k7fXJ/1L0D5+MLbSO05Kclsw9XHtvcgAAwCy7ZvJifdurI21Vdci8t09PctedJc9O8uyq2reqHphkS5JPLS8iAADA7NrjkbaqeneSJyS5f1Vdl+T3kjyhqo5O0jJX7n81SVprl1fVmUmuSPKtJC9qre1aleQAAAAzoFrrf2bihqrW49lUn0zyiHZbTqj75h87bB8AACBJ7kwubK1tXWjecu4eCQAAwCpT2gAAAAZMaUvyuMSzLQAAgEGa6dK2I0nylfxOa/n8kzuHgSm3KcnBSTb0DgIAsM7MdGl7QpLvqy1JzuicBKbfXyb5YrsjT+sdBABmjF+Yrn8zXdqAlfOzSUa1fz7QOwgAzJDxvZLb26dzZO8grCqlDQAAptT9vp4cWY/KNb2DLMP4Vcm4PaZ3jEFT2gAAYErtSPLl3iGW6RGvSH6sPtE7xqBt7B0AAACYXVf3DjAFHGkDAAAYMKUNAABgwGa+tH0zSZ72S8n9kvHhncN0cnuSC3uHAAAAFjTzpW1nktH/l+TAJNe+Yiafc7HhOclDf6h3CgAAYCFuREJG7+6dAAAA2J2ZP9IGAAAwZEobAADAgCltAAAAA6a0AQAADJjSBgAAMGBK28SRpya/X6/K7e3DeUvvMAAATIXxjybj9rzs1zsI65pb/k98OcmXkiSPzz37RgEAYEoc90/JD9R/y47eQVjXlDYAANhLn528YDU5PRIAAGDAlDYAAIABU9oAAAAGTGkDAAAYMKVtnu1Jkp/JY5I8rWsSAACAOdVa650hG6rakJ5tMW77JBd/M6NjeicBAABmwZ3Jha21rQvNc6QNAABgwJQ2AACAAVPaAAAABkxpW8AR9c3cdkwybuflkb3DzKDxE5Nxe3mGdJ0jAAD0orQt4NYkdyZJHto3yIx6+HnJT9VrsqN3EFghT0sybm/Kn/UOAgBMJaWNwbkmyfm9Q8AKOj/JH9aL80e9g6yg45OM22Pyh72DAMAMUNoAVtn2JP8lc7+QWC+uSPLZ+kTe2zsIAKyAo5KMr0z+a+8gu7GxdwAAps9NSY7rHQIAVsitSfKU5ILeQXZDaQMAAGbatUlG/9I7xe45PRIAAGDAlDYAAIABU9oAAAAGTGkDAAAYMKUNAABgwJQ2AACAAVPaAAAABkxpAwAAGDClDQAAYMD2WNqq6oiqOq+qrqiqy6vqJZPxA6rq3Kr6wuTrfSfjVVWnVtVVVXVpVT1ytf8Qq+EXk3y0DsnHb0j+uXcYAABgZi3mSNu3kvxGa+1hSY5L8qKqeliSlyX5SGttS5KPTN4nyVOTbJm8Tkny5hVPvQYuyFxxyyHn5LBjOocBAABm1h5LW2vtxtbaZybTdyT5XJLDkpyY5IzJYmckOWkyfWKSd7Q5n0yyuaoOWengAAAAs2BJ17RV1ZFJjsncgaiDWms3TmbdlOSgyfRhSa6d97HrJmN3X9cpVbWtqra1paYGAACYEYsubVV1ryR/neSlrbXb589rrbUkS+perbXTWmtbW2tbaykfBAAAmCGLKm1VtSlzhe1drbW/mQzffNdpj5Ovt0zGr09yxLyPHz4Zmzq7kiT/Kdk/eU7nLAAAwGxazN0jK8nbknyutfb6ebPOTnLyZPrkJB+YN/7cyV0kj0vytXmnUU6VO5KM6n8mT0re2l7YOw4AADCDNi5imcdm7kaKn62qiydjv5PktUnOrKrnJ/lSkmdO5n0wyQlJrkpyZ5LnrWRgAACAWbLH0tZaOz/J7i47+4kFlm9JXrTMXAAAAGSJd48EAABgbSltAAAAA6a0LcKjXpG8s96ccXtdXtU7zDJ8KMm4vTyP7B0EAABYNKVtEa5Isi1J8hvZ3DXJ8rw0yZ/Ua3Jl7yAAAMCiLebukawTV2Tutp8AAMD0cKQNAABgwJQ2AACAAVPaAAAABkxpAwAAGDClDQAAYMCUNgAAgAFT2pZkRzYk2dA7BgAAMDOUtkV6a5JR3SMnt+/P7R/qnQYAAJgVStuSbfRfDQAAWDPqBwAAwIApbUt0cl2dPDcZtx/Pg9Zwu0clGben5q/XcJsAAEB/StsSvS/Jv96QJG/LpjXc7peSnFv/PX+0htsElm98n2R8jhsYAQB7T2mbEtuTnJTkgr4xgCV609eSW56W7OodBPbS65OMT0sO7R0EYIYpbQCr6LeTPLB3CFiGm5Lkw8nOjhk2JBn/UPKujhkAelLaAIDd+qMkozOTL/cO8nPJo3pnAOhkY+8AAADfy64ko1f0TgHQjyNtAAAAA6a0AQAADJjSBgAAMGBKGwAAwIApbXvhoCQPry35TPu7jA/vnQaAtbY5ybi9L+Mf7p0EgFng7pF7YVeSHUmSUbKpbxYA1t72JI+oZ+TW3kEAmAlKGwDshat7BwBgZjg9EgAAYMCUNgAAgAFT2gAAAAZMaQMAABgwpQ0AAGDAlDYAAIABU9r20q4kyTuSUfKozlkAYC1sie95AD0obXvpy0lG9ZbklOQf2tN7xwGAVXfxK5N/aMf2jgFTa1OSg3uHYCopbQDAooxemYzqU71jwNT6H0m+2G7M43oHYeps7B0AAABmwbFJUof0jsEUcqQNAABgwJQ2AACAAVPaAAAABkxpAwAAGDClDQAAYMCUtmX6519L8sKzMn5y3L6VmfL6JOMfmnvmDAAAq0dpW6Zjkvzenyf5+5af6h0G1tBjkuT/TDbsZv7BSW5McvyaJQIAWJ/2WNqq6oiqOq+qrqiqy6vqJZPxV1bV9VV18eR1wrzPvLyqrqqqK6tKl4F16Lgko1OSHbuZf0SSe7d75ulrmAkAYD1azMO1v5XkN1prn6mq/ZNcWFXnTua9obX2uvkLV9XDkjw7ycOTHJrkw1X1g621XSsZHBi2TycZ1Z29YwAATL09Hmlrrd3YWvvMZPqOJJ9Lctj3+MiJSd7TWvtGa+1fklyVyQPgAQAAWJolXdNWVUdm7jKuCyZDL66qS6vq9Kq672TssCTXzvvYdVmg5FXVKVW1raq2taXnBgAAmAmLLm1Vda8kf53kpa2125O8OcmDkxydufsN/PFSNtxaO621trW1trWW8kEAAIAZsqjSVlWbMlfY3tVa+5skaa3d3Frb1Vr7dpK35N9Ogbw+c/cguMvhkzEAAACWaDF3j6wkb0vyudba6+eNHzJvsacnuWwyfXaSZ1fVvlX1wCRbknxq5SIDAADMjsXcPfKxSX4xyWer6uLJ2O8keU5VHZ2kJbkmya8mSWvt8qo6M8kVmbvz5IvcORIAAGDv7LG0tdbOT7LQZWcf/B6feXWSVy8jFwAAAFni3SNZ2N8mGVfl19+cjA/tnQYAAFhPlLYVcGWS70uSFzwmObVzGAAAYF1R2gAAAAZMaQMAABgwpW0lffITyXXJryXZ1DsLAACwLlRrrXeGbKhq+/UOsULG+ybZcVEeXsfkmt5hAACAqXBncmFrbetC8xxpAwAAGDClDQAAYMCUNgAAgAFT2lbY/b6RHF/H5PL2noz91wUAAJZJrVhhO5LcnCR5SHLPvlkAAIDpp7QBAAAMmNIGAAAwYErbKvhqkhx8TPJzyfgne6cBWHlHJRn/cfK83kEAYAYobavgjiSjm5M8Mcm5P945DcDK+3aSfDnZ2TsIAMyAjb0DADB9Pptk9NreKQBgNjjSBgAAMGBK22r6TJJ//VienmT/3lkGZkuSE3qHAACAKaC0raLR/5287p7J/9taXtg7zMBc/EPJX7WXK7MAALAHrmmji9HnktRrescAAIDBc6QNAABgwJQ2AACAAVPaVtlrkvxYVX6vfX/Gv9s7zewan5KM2w/2jgEAAEumtK2yHZm7iWRyVHJ43yyz7PjTkpPrn3vHWDXjf0zGr+idAgCA1eBGJMyECyav9er1j0++3DsEAACrQmmDdcBBNgCA9cvpkQAAAAOmtAEAAAyY0gYAADBgShsAAMCAKW0AAAADprQBAAAMmNIGAAAwYErbGrl3/W3y6mTcPpyDe4cBAACmhtK2RnZ9Z2rfjikAAIBpo7QBAAAMmNIGAAAwYEobAADAgCltAAAAA6a0raG/vC7JH/xYvnhMckrvMMys9yf5Su8QAAAsWrXWemfIhqq2X+8Qa+QhST7TvpBdtSX37h2GmTT+gSS/kIxe2TsJALBenZ5ke5Jf75xjmtyZXNha27rQPEfaYMaMrlLYAIDV9ay3Jb/6W71TrB8bewcAAADWl9HzeydYXxxpAwAAGDClDQAAYMD2WNqqar+q+lRVXVJVl1fV70/GH1hVF1TVVVX13qraZzK+7+T9VZP5R67ynwEAAGDdWsyRtm8kOb61dlSSo5M8paqOS/KHSd7QWvuBJLcluevM1ecnuW0y/obJckxcmeSw2pINZyfj5v6RAAAszW8mGbcb87u9g7Bm9lja2pyvT95umrxakuOTvG8yfkaSkybTJ07eZzL/J6qqVirwerA9mfuvmIO75gAAYPq8KcnxdUje2DsIa2ZR17RV1YaqujjJLUnOTfLFJNtba9+aLHJdksMm04cluTZJJvO/luR+K5gZAABm1o4kFyQZ9w7CmllUaWut7WqtHZ3k8CTHJnnocjdcVadU1baq2tb/8d4AAADDtKS7R7bWtic5L8mPJtlcVXc95+3wJNdPpq9PckSSTObfJ8lXF1jXaa21ra21rc6dBAAAWNhi7h55YFVtnkzfI8mTknwuc+XtGZPFTk7ygcn02ZP3mcz/aGvNwTQAAIC9sHHPi+SQJGdU1YbMlbwzW2vnVNUVSd5TVX+Q5KIkb5ss/7Yk76yqq5LcmuTZq5AbAABgJtQQDoJtqGr79Q6xxsY/neTFyfufnPxGkpt6B5piG5L8aZIPJjmncxYAANgbdyYXtta2LjRvSde0sXJGZydveXJyUmv56d5hptx+SU5u35/33r93EgAAWHmLOT0SBm2cZFT/s3cMAABYFY60dfTtJMlXMsrkWdvMvP0mLwAAuIvS1tGvJxnVgfmD9ivZ/treaRiCr34t+eoNvVMAADAkTo+EAdl8n94JAAAYGqUNBmRn7wAAAAyO0yMBAAAGTGkDAAAYMKUNAABgwJQ2AACAAVPahuBZb0k+n4zvtb6f0fX4JOMHJA/pHQQAAKaI0jYAozOTbEtyxzm5Z+8wq+gRSfKx5LDeQQAAYIoobayZNyUZPSD5aO8gAAAwRZQ2AACAAVPaAAAABkxpAwAAGDClDQAAYMCUtoG432XJifW0XNs+ns/1DgMAAAyG0jYQO5LcmiQ5Mvv0jQIAAAyI0gYAADBgShsAAMCAKW0Dck2S3O+IHPzSZPy4vllWwy8kGZ+WPLp3EAAAmCJK24DcmmR0a+bazccP6Jxm5Y2T5LJkZ+8gMKMuTXJb7xAAwJJt7B2A2XFWktGpvVPA7HrwUUkeluTdvZMAAEuhtAHMiNElSS7pnQIAWCqnRwIAAAyY0gYAADBgShsAAMCAKW0AAAADprQBAAAMmNIGAAAwYEobAADAgCltAAAAA6a0AQAADJjSNkDXbE3yM7dm/ILkqN5hAACW6Igk4+cnL+4dBNYJpW2AHp7kvWclefPOPKJ3GACAJdqUJFuT+3Ta/ijJeFPywk7bh5WmtAEAsKKuTjJ6YfLqTtvfN0m2Jc/qtH1YaRt7BwAAgJV0a5KRa0xYRxxpAwAAGDClDQAAYMCUNgAAgAFT2gAAAAZMaQMAABgwpQ0AAGDAlDYAAIABU9oG6k+SfL425S8+lIw39U4DAAD0ssfSVlX7VdWnquqSqrq8qn5/Mv72qvqXqrp48jp6Ml5VdWpVXVVVl1bVI1f5z7AuXZLk2CR50m8lv9Y5DAAA0M3GRSzzjSTHt9a+XlWbkpxfVf99Mu+3Wmvvu9vyT02yZfJ6dJI3T74CAACwRHs80tbmfH3ydtPk1b7HR05M8o7J5z6ZZHNVHbL8qAAAALNnUde0VdWGqro4yS1Jzm2tXTCZ9erJKZBvqKp9J2OHJbl23sevm4zdfZ2nVNW2qtr2vRogAADALFtUaWut7WqtHZ3k8CTHVtUPJ3l5kocmeVSSA5L89lI23Fo7rbW2tbW2tZaWGQAAYGYs6e6RrbXtSc5L8pTW2o2TUyC/keS/ZXLfjCTXJzli3scOn4wBAACwRIu5e+SBVbV5Mn2PJE9K8vm7rlOrqkpyUpLLJh85O8lzJ3eRPC7J11prN65C9hmxI8nchYTA9PB3FgBYKYs50nZIkvOq6tIkn87cNW3nJHlXVX02yWeT3D/JH0yW/2CSq5NcleQtSf7TiqeeEbuSjOpPk88k29t7sn/vQMCifDLJ9nZbHt87CACwLuzxlv+ttUuTHLPA+PG7Wb4ledHyowFMp8cmuWfdN3f0DgIArAtLuqYNgD3blShsAMCKUdoAAAAGTGkDAAAYMKUNAABgwJQ2AACAAVPa9sJzknw9yeY12t6nz0ty+rNz072Sp63RNgEAgGFQ2vbCM5LUDVmz56Y9IcmTnp/kji/nVWu0TVgNpyd5Vu8QAABTRmnbC/8hyejQ5NreQWCKbEryrHbPnH547yRr69FJfqF3CABgqiltwJrYmWRUd2Z0Xe8ka+ujT0/+oh2bDb2DwF46IMmjEvswQEdKG8AqGp2VjOpT2dU7COylS5P8Q/unPKR3EGAm7J9k1DvEACltAMBuHZ5kVD+aK3oHAWbCTe3Y3HJW7xTDs7F3AAAAgCS5d32qd4RBUtoAAIBBcDnBwpweCQAAMGBKGwAAwIApbQAAAAOmtAEAAAyY0gYAADBgShsAAMCAKW1T4o4kyWPzg/dK/mPnLAAAvZyS5Dm9Q8Aaq9Za7wzZUNX26x1iSoyvTHJkMtq3dxIAgLU3vizJR5PRr63seo9I8pAkH17Z1cKi3Zlc2FrbutA8D9cGAGBqjH54ddb7+YOS3PTGHFEvza2rswnYa0obAAAzb3Rzknpp7xiwINe0AQAADJjSBgAAMGBKGwAAwIApbVPmWQ9Jbts3GbefzfG9wwAAzLjxocm47RN3Qmc1KW1T5pwk25Ik78j+faMAAMy8X74h+Zv6Znb2DsK65u6RAACwl947ecFqcqQNAABgwJS2KfSPSfJ398hfHpC8vHcYAABgVSltU+gNSTY/NclXX5v/67m90wAAAKtJaQMAABgwpQ0AAGDAlDYAAIABU9oAAAAGTGmbUjuTbK6XJTuScXtd9usdCNahdyYZt9vyjN5BAICZprRNsZ3fmVLZYDX8SpKj6745q3cQAGCmbewdAGCodiT5Qu8QAMDMc6QNAABgwJS2KffyM5Ps8+J89fzkd3uHAQAAVpzSNuVOTfIzO5M89o48vncYAABgxSltAAAAA6a0AQAADJjSBgAAMGBK2zqwI0lyRo5I8oi+URiAxyU5tHcIAABWTLXWemfIhqrm8dDLN741yY5k5Cf2mXVgkmvaW5PD/mNGN/ROA8BaOCDJt5Ns75wDWJ47kwtba1sXmudIG6wjX04yKoUNYJZc2/bJ9Rf1TgGspkEcaauqLycZJ/lK7yxMhfvHvsLi2V9YLPsKS2F/YbHsKyzWA1prBy40YxClLUmqatvuDgfCfPYVlsL+wmLZV1gK+wuLZV9hJTg9EgAAYMCUNgAAgAEbUmk7rXcApoZ9haWwv7BY9hWWwv7CYtlXWLbBXNMGAADAdxvSkTYAAADuRmkDAAAYsO6lraqeUlVXVtVVVfWy3nnor6pOr6pbquqyeWMHVNW5VfWFydf7Tsarqk6d7D+XVtUj+yVnrVXVEVV1XlVdUVWXV9VLJuP2F75LVe1XVZ+qqksm+8vvT8YfWFUXTPaL91bVPpPxfSfvr5rMP7LrH4A1V1Ubquqiqjpn8t6+woKq6pqq+mxVXVxV2yZjvhexYrqWtqrakOT/SfLUJA9L8pyqeljPTAzC25M85W5jL0vykdbaliQfmbxP5vadLZPXKUnevEYZGYZvJfmN1trDkhyX5EWTf0PsLyzkG0mOb60dleToJE+pquOS/GGSN7TWfiDJbUmeP1n++Ulum4y/YbIcs+UlST437719he/lia21o+c9k833IlZM7yNtxya5qrV2dWvtm0nek+TEzpnorLX2sSS33m34xCRnTKbPSHLSvPF3tDmfTLK5qg5Zk6B011q7sbX2mcn0HZn74eqw2F9YwOT/+9cnbzdNXi3J8UneNxm/+/5y1370viQ/UVW1NmnpraoOT/Lvk7x18r5iX2FpfC9ixfQubYcluXbe++smY3B3B7XWbpxM35TkoMm0fYgkyeR0pGOSXBD7C7sxOd3t4iS3JDk3yReTbG+tfWuyyPx94jv7y2T+15Lcb00D09Mbk/znJN+evL9f7CvsXkvyoaq6sKpOmYz5XsSK2dg7ACxVa61VlWdV8B1Vda8kf53kpa212+f/gtv+wnyttV1Jjq6qzUnOSvLQvokYoqp6WpJbWmsXVtUTOsdhOjyutXZ9VX1fknOr6vPzZ/pexHL1PtJ2fZIj5r0/fDIGd3fzXacOTL7eMhm3D824qtqUucL2rtba30yG7S98T6217UnOS/KjmTs16a5fYs7fJ76zv0zm3yfJV9c2KZ08NslPV9U1mbt04/gkfxL7CrvRWrt+8vWWzP1C6Nj4XsQK6l3aPp1ky+RuTPskeXaSsztnYpjOTnLyZPrkJB+YN/7cyZ2YjkvytXmnIrDOTa4ZeVuSz7XWXj9vlv2F71JVB06OsKWq7pHkSZm7DvK8JM+YLHb3/eWu/egZST7aWvOb8hnQWnt5a+3w1tqRmfvZ5KOttZ+PfYUFVNWoqva/azrJk5NcFt+LWEHV+9+Uqjohc+eNb0hyemvt1V0D0V1VvTvJE5LcP8nNSX4vyfuTnJnk+5N8KckzW2u3Tn5of1Pm7jZ5Z5Lntda2dYhNB1X1uCQfT/LZ/Nt1J7+Tueva7C/8L6rqRzJ3M4ANmful5Zmttf9SVQ/K3NGUA5JclOQXWmvfqKr9krwzc9dK3prk2a21q/ukp5fJ6ZG/2Vp7mn2FhUz2i7Mmbzcm+cvW2qur6n7xvYgV0r20AQAAsHu9T48EAADge1DaAAAABkxpAwAAGDClDQAAYMCUNgAAgAFT2gAAAAZMaQMAABiw/x8Q78uOap2HFgAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# see the edge features which are shape (n_edges, n_nodes + weight)\n", "# notice that had we used filter_weighted_edges to create a new graphistry instance and then .build_gnn() we would get\n", @@ -510,10 +2530,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "id": "6c150a8e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], "source": [ "# see the way edges are related across the first 500 edges.\n", "plt.figure(figsize=(15,8))\n", @@ -522,21 +2565,13 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 35, "id": "9ab619bf", "metadata": {}, "outputs": [], "source": [ "# to see how to train a GNN, see the cyber or influence tutorial" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f170fa3a", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 8dba7c45d07577e8398c1d82fed12b1c1184cb7a Mon Sep 17 00:00:00 2001 From: Alex Morrise Date: Mon, 23 Jan 2023 09:46:34 -0800 Subject: [PATCH 133/432] Update README.md Adds README for new features --- README.md | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f3f64aea83..7587b5a72b 100644 --- a/README.md +++ b/README.md @@ -358,11 +358,11 @@ Automatically and intelligently transform text, numbers, booleans, and other for g = g.umap() # UMAP, GNNs, use features if already provided, otherwise will compute # other pydata libraries - X = g._node_features # g._get_feature('nodes') - y = g._node_target # g._get_target('nodes') + X = g._node_features # g._get_feature('nodes') or g.get_matrix() + y = g._node_target # g._get_target('nodes') or g.get_matrix(target=True) from sklearn.ensemble import RandomForestRegressor - model = RandomForestRegressor().fit(X, y) #assumes train/test split - new_df = pandas.read_csv(...) + model = RandomForestRegressor().fit(X, y) # assumes train/test split + new_df = pandas.read_csv(...) # mini batch X_new, _ = g.transform(new_df, None, kind='nodes') preds = model.predict(X_new) ``` @@ -371,7 +371,7 @@ Automatically and intelligently transform text, numbers, booleans, and other for ```python # graphistry - from graphistry.features import search_model, topic_model, ngrams_model, ModelDict, default_featurize_parameters + from graphistry.features import search_model, topic_model, ngrams_model, ModelDict, default_featurize_parameters, default_umap_parameters g = graphistry.nodes(df) g2 = g.umap(X=[..], y=[..], **search_model) @@ -381,7 +381,7 @@ Automatically and intelligently transform text, numbers, booleans, and other for new_model.update(dict( y=[...], kind='edges', - model_name='sbert/hf/a_cool_transformer_model', + model_name='sbert/cool_transformer_model', use_scaler_target='kbins', n_bins=11, strategy='normal')) @@ -411,8 +411,20 @@ See `help(g.featurize)` for more options ```python new_df = pd.read_csv(...) - embeddings, X_new, _ = g.transform_umap(new_df, None, kind='nodes') + embeddings, X_new, _ = g.transform_umap(new_df, None, kind='nodes', return_graph=False) ``` +* Infer a new graph from new data using the old umap coordinates to run inference without having to train a new umap fit. + + ```python + new_df = pd.read_csv(...) + g2 = g.transform_umap(new_df, return_graph=True) # return_graph=True is default + g2.plot() # + + # or if you want the new minibatch to cluster to closest points in previous fit: + g3 = g.transform_umap(new_df, return_graph=True, merge_policy=True) + g3.plot() # useful to see how new data connects to old -- can play with `sample` and `n_neighbors` to control how much of old to include + ``` + * UMAP supports many options, such as supervised mode, working on a subset of columns, and passing arguments to underlying `featurize()` and UMAP implementations (see `help(g.umap)`): @@ -564,13 +576,13 @@ See `help(g.embed)`, `help(g.predict_links)` , `help(g.predict_links_all)` for o # cluster by UMAP embeddings kind = 'nodes' | 'edges' g2 = g.umap(kind=kind).dbscan(kind=kind) - print(g2._nodes['_cluster']) | print(g2._edges['_cluster']) + print(g2._nodes['_dbscan']) | print(g2._edges['_dbscan']) - # dbscan with fixed parameters is default in umap - g2 = g.umap(dbscan=True) + # dbscan in umap or featurize via flag + g2 = g.umap(dbscan=True, min_dist=0.2, min_samples=1) - # and with greater control over parameters via chaining, - g2 = g.umap().dbscan(eps=1.2, min_samples=2, **kwargs) + # or via chaining, + g2 = g.umap().dbscan(min_dist=1.2, min_samples=2, **kwargs) # cluster by feature embeddings g2 = g.featurize().dbscan(**kwargs) @@ -580,10 +592,14 @@ See `help(g.embed)`, `help(g.predict_links)` , `help(g.predict_links_all)` for o # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) g2 = g.umap().dbscan(cols=['ip_172', 'location', 'alert'], umap=True | False, **kwargs) - g2.plot() # color by `_cluster` + g2.plot() # color by `_dbscan` + + new_df = pd.read_csv(..) + # transform on new data according to fit dbscan model + g3 = g.transform_dbscan(new_df) ``` -See `help(g.dbscan)` for options +See `help(g.dbscan)` or `help(g.transform_dbscan)` for options ### Quickly configurable From a52dd1a0a71a6cc98836a23d380991aa75a3d133 Mon Sep 17 00:00:00 2001 From: Alex Morrise Date: Mon, 23 Jan 2023 09:51:38 -0800 Subject: [PATCH 134/432] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7587b5a72b..850ab54704 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,7 @@ Automatically and intelligently transform text, numbers, booleans, and other for from sklearn.ensemble import RandomForestRegressor model = RandomForestRegressor().fit(X, y) # assumes train/test split new_df = pandas.read_csv(...) # mini batch - X_new, _ = g.transform(new_df, None, kind='nodes') + X_new, _ = g.transform(new_df, None, kind='nodes', return_graph=False) preds = model.predict(X_new) ``` From a9d067af504691defae1896b6dcde152e317ed5c Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 13:31:43 -0500 Subject: [PATCH 135/432] fix(readme):optional if the change changes context --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 850ab54704..14eeea9315 100644 --- a/README.md +++ b/README.md @@ -397,7 +397,7 @@ See `help(g.featurize)` for more options ### [sklearn-based UMAP](https://umap-learn.readthedocs.io/en/latest/), [cuML-based UMAP](https://docs.rapids.ai/api/cuml/stable/api.html?highlight=umap#cuml.UMAP) -* Reduce dimensionality and plot a similarity graph from feature vectors: +* Reduce dimensionality by plotting a similarity graph from feature vectors: ```python # automatic feature engineering, UMAP From 4df7a63d4a74ce77a86950ab5081076d76d25f34 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 13:33:02 -0500 Subject: [PATCH 136/432] fix(readme): made line more concise --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14eeea9315..f517447d15 100644 --- a/README.md +++ b/README.md @@ -403,7 +403,7 @@ See `help(g.featurize)` for more options # automatic feature engineering, UMAP g = graphistry.nodes(df).umap() - # plot the similarity graph even though there was no explicit edge_dataframe passed in -- it is created during UMAP. + # plot the similarity graph without any explicit edge_dataframe passed in -- it is created during UMAP. g.plot() ``` From 2a7b9a1f1d04f9a4cccf48e70a38e436c622ded3 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 13:51:24 -0500 Subject: [PATCH 137/432] docs(readme): added conjunction --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f517447d15..10dd07c77a 100644 --- a/README.md +++ b/README.md @@ -564,7 +564,7 @@ See `help(g.search_graph)` for options g2.predict_links_all(threshold=0.95).plot() ``` -See `help(g.embed)`, `help(g.predict_links)` , `help(g.predict_links_all)` for options +See `help(g.embed)`, `help(g.predict_links)` , or `help(g.predict_links_all)` for options ### DBSCAN From 08b549b4ff4266d1ccddb9d44a83d03bd449e533 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:28:23 -0500 Subject: [PATCH 138/432] fix(power-umap): maybe no article? optional change --- demos/ai/Introduction/simple-power-of-umap.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb index 5d22c3730a..596c1dfcd2 100644 --- a/demos/ai/Introduction/simple-power-of-umap.ipynb +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -36,7 +36,7 @@ "metadata": {}, "source": [ "# Explore Data in a Whole New Way\n", - "PyGraphistry is a GPU Graph AI visualization tool that unlocks the graph in your data. \n", + "PyGraphistry is a GPU Graph AI visualization tool that unlocks graphing in your data. \n", "\n", "In the past loading, transforming and interacting with large multivariate datasets took time to set up pipelines and processes. Graphistry makes time-to-graph + AI + interactivity 100x faster. \n", "\n", @@ -7138,7 +7138,7 @@ }, { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] From 4dc610be7e1a4474aa3952371142760befb1c81e Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:29:37 -0500 Subject: [PATCH 139/432] fix(power-umap): optional fix --- demos/ai/Introduction/simple-power-of-umap.ipynb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb index 596c1dfcd2..d87928e254 100644 --- a/demos/ai/Introduction/simple-power-of-umap.ipynb +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -38,7 +38,7 @@ "# Explore Data in a Whole New Way\n", "PyGraphistry is a GPU Graph AI visualization tool that unlocks graphing in your data. \n", "\n", - "In the past loading, transforming and interacting with large multivariate datasets took time to set up pipelines and processes. Graphistry makes time-to-graph + AI + interactivity 100x faster. \n", + "In the past loading, transforming and interacting with large multivariate datasets took a vast amount of time to set up pipelines and processes. Graphistry makes time-to-graph + AI + interactivity 100x faster. \n", "\n", "We will explore how to see data, explore relationships and create new graphs from batches using sci-kits like api, and even build a GNN model one could use in downstream DGL models. \n", "\n", @@ -7511,7 +7511,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3.9.7 64-bit", "language": "python", "name": "python3" }, @@ -7525,7 +7525,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.9" + "version": "3.9.7" + }, + "vscode": { + "interpreter": { + "hash": "aee8b7b246df8f9039afb4144a1f6fd8d2ca17a180786b69acc140d282b71a49" + } } }, "nbformat": 4, From 8b95af8c54cde6a494ec7718e730da0670ae51f7 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:31:24 -0500 Subject: [PATCH 140/432] fix(power-umap): condensed sentence --- demos/ai/Introduction/simple-power-of-umap.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb index d87928e254..b01de96f02 100644 --- a/demos/ai/Introduction/simple-power-of-umap.ipynb +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -40,7 +40,7 @@ "\n", "In the past loading, transforming and interacting with large multivariate datasets took a vast amount of time to set up pipelines and processes. Graphistry makes time-to-graph + AI + interactivity 100x faster. \n", "\n", - "We will explore how to see data, explore relationships and create new graphs from batches using sci-kits like api, and even build a GNN model one could use in downstream DGL models. \n", + "We will explore how to see data, explore relationships, create new graphs from batches using sci-kits like api, and build a GNN model one could use in downstream DGL models. \n", "\n", "We will quickly analyze breast cancer, diabetes and digits datasets from sklearn.data\n", "\n", From 83fe25d7f9c668cb4dbf6999fbe609a77f90499d Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:38:22 -0500 Subject: [PATCH 141/432] fix(power-umap): cleaned up sentence for clarity --- demos/ai/Introduction/simple-power-of-umap.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb index b01de96f02..1c93aa1a9d 100644 --- a/demos/ai/Introduction/simple-power-of-umap.ipynb +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -303,7 +303,7 @@ "\n", "Reduce the data into a 2 dimensional graph -- the edges come from similarity in features. \n", "\n", - "UMAP is a powerful way to see the parts of the dataset -- one can not only visually confirm if a predictive model will 'separate' the data, one can explore relationships that can help feed insights and potential treatment strategies. \n", + "UMAP is a powerful way to see the parts of the dataset. One can not simply confirm if a predictive model will 'separate' the data off of visuals alone. UMAP provides the tools to explore relationships that can help feed insights and potential treatment strategies. \n", "\n", "What can you find in the data?" ] From 5886606a807b7ac572b87b99737185f1fa0a66a1 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:40:10 -0500 Subject: [PATCH 142/432] fix(power-umap):added comma --- demos/ai/Introduction/simple-power-of-umap.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb index 1c93aa1a9d..c89b8c8819 100644 --- a/demos/ai/Introduction/simple-power-of-umap.ipynb +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -403,7 +403,7 @@ "By setting `cols` one may pick out features from the matrix and have dbscan only focus on those.\n", "Coloring by the `_dbscan` label in the UI finds clusters across those variables. \n", "\n", - "Contrasting UMAP coordinates versus dbscan labels is a useful way to see total behavior against some part/pivot of interest. In the following we see k-clusters in the `worst` (case sensitive column selection) meta variable. " + "Contrasting UMAP coordinates versus dbscan labels is a useful way to see total behavior against some part/pivot of interest. In the following, we see k-clusters in the `worst` (case sensitive column selection) meta variable. " ] }, { From fb734c3395f94755835cbbd2f9bf8d199caa0da4 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:42:13 -0500 Subject: [PATCH 143/432] fix(power-umap): sentence restructuring --- demos/ai/Introduction/simple-power-of-umap.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/ai/Introduction/simple-power-of-umap.ipynb b/demos/ai/Introduction/simple-power-of-umap.ipynb index c89b8c8819..c61a4c8c04 100644 --- a/demos/ai/Introduction/simple-power-of-umap.ipynb +++ b/demos/ai/Introduction/simple-power-of-umap.ipynb @@ -914,8 +914,8 @@ "metadata": {}, "source": [ "# Transform Test Data into Graph\n", - "Can add batch onto closest neighbors of existing graph (from fit above) if merge_policy=True\n", - "otherwise, will create a new graph from the batch. " + "Can add batch onto closest neighbors of existing graph (from fit above). Otherwise, if merge_policy=True\n", + "it will create a new graph from the batch. " ] }, { From cd278909127b6a24d62d018a142d1b988609d43f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Jan 2023 10:25:40 -0800 Subject: [PATCH 144/432] adds(ci and tests for text search), adds changelog --- .github/workflows/ci.yml | 5 +++++ CHANGELOG.md | 5 ++++- bin/test-text.sh | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100755 bin/test-text.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61a1447280..2834540916 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -209,6 +209,11 @@ jobs: source pygraphistry/bin/activate ./bin/test-features.sh + - name: Full search tests (rich featurize) + run: | + source pygraphistry/bin/activate + ./bin/test-text.sh + - name: Full umap tests (rich featurize) run: | source pygraphistry/bin/activate diff --git a/CHANGELOG.md b/CHANGELOG.md index 119150b9c1..5313c7e0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed * AI: moves public `g.g_dgl` from KG `embed` method to private method `g._kg_dgl` +* AI: BREAKING CHANGES: to return matrices during transform, set the flag: `X, y = g.transform(df, return_graph=False)` default behavior is ~ `g2 = g.transform(df)` returning a Plottable instance. ### Added +* AI: all `transform_*` methods return graphistry Plottable instances, using an infer_graph method. To return matrices, set the `return_graph=False` flag. +* AI: adds `g.get_matrix(**kwargs)` general method to retrieve (sub)-feature/target matrices * AI: DBSCAN -- `g.featurize().dbscan()` and `g.umap().dbscan()` with options to use UMAP embedding, feature matrix, or subset of feature matrix via `g.dbscan(cols=[...])` -* AI: Demo cleanup using ModelDict & new features +* AI: Demo cleanup using ModelDict & new features, refactoring demos using `dbscan` and `transform` methods. * Tests: dbscan tests * AI: Easy import of featurization kwargs for `g.umap(**kwargs)` and `g.featurize(**kwargs)` * AI: `g.get_features_by_cols` returns featurized submatrix with `col_part` in their columns diff --git a/bin/test-text.sh b/bin/test-text.sh new file mode 100755 index 0000000000..949bd68735 --- /dev/null +++ b/bin/test-text.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -ex + +# Run from project root +# - Args get passed to pytest phase +# Non-zero exit code on fail + +# Assume [umap-learn,test] + +python -m pytest --version + +python -B -m pytest -vv \ + graphistry/tests/test_text_utils.py + +# chmod +x bin/test-text.sh \ No newline at end of file From ed9c57ba3f8bb884190404a8d1e6956043243a91 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Jan 2023 10:27:04 -0800 Subject: [PATCH 145/432] removes bug in __len__ call, more bug fixes --- graphistry/compute/cluster.py | 4 +++- graphistry/dgl_utils.py | 9 --------- graphistry/feature_utils.py | 6 ++---- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index af1724d8d6..ff789c844c 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -123,6 +123,9 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ kind = "node" if kind == "nodes" else "edge" setattr(g, f"_{kind}_dbscan", dbscan) + + if cols is not None: # set False since we used the features for verbose + use_umap_embedding = False if verbose: cnt = Counter(labels) @@ -141,7 +144,6 @@ def dbscan_predict(X: pd.DataFrame, model): DBSCAN has no predict per se, so we reverse engineer one here from https://stackoverflow.com/questions/27822752/scikit-learn-predicting-new-points-with-dbscan - """ n_samples = X.shape[0] diff --git a/graphistry/dgl_utils.py b/graphistry/dgl_utils.py index f82614dff1..2303a5aacd 100644 --- a/graphistry/dgl_utils.py +++ b/graphistry/dgl_utils.py @@ -519,15 +519,6 @@ def _mask_edges(self): self.DGL_graph.edata[config.TEST_MASK], ) = get_torch_train_test_mask(n, self.train_split) - def __getitem__(self, idx): - # get one example by index, here we have only one graph. #todo parameterize case if we have RGNN - if self.DGL_graph is None: - logger.warning("DGL graph is not built, run `g.build_gnn(...)` first") - return self.DGL_graph - - # def __len__(self): # this messes up scope. - # # number of data examples - # return 1 # if __name__ == "__main__": diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 10be319062..eebbb4a4df 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1734,11 +1734,9 @@ def transform(self, df, ydf=None): def _transform_scaled(self, df, ydf, scaling_pipeline, scaling_pipeline_target): """Transform with scaling fit durning fit.""" X, y = transform(df, ydf, self.res, self.kind, self.src, self.dst) - if scaling_pipeline is not None: - #print("scaling") + if scaling_pipeline is not None and not X.empty: X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns, index=X.index) - if scaling_pipeline_target is not None: - #print("scaling target") + if scaling_pipeline_target is not None and y is not None and not y.empty: y = pd.DataFrame(scaling_pipeline_target.transform(y), columns=y.columns, index=y.index) return X, y From 2124324cf7e7a20e1d72cd2a171ed0ac70202d03 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Jan 2023 10:28:50 -0800 Subject: [PATCH 146/432] adds hydration for needed attributes on Plottable generated via search_graph --- graphistry/text_utils.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 40c72f939f..1ee9bfcfd3 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -243,10 +243,23 @@ def search_graph( pass found_indices = pd.concat([edges[src], edges[dst], indices], axis=0).unique() + emb = None try: tdf = rdf.iloc[found_indices] - except: # for explicit relabeled nodes + feats = res._node_features.iloc[found_indices] + if res._umap is not None: + emb = res._node_embedding.iloc[found_indices] + except Exception as e: # for explicit relabeled nodes + #print(e) tdf = rdf[df[node].isin(found_indices)] + feats = res._node_features.loc[tdf.index] + #print(node, feats.shape) + if res._umap is not None: + emb = res._node_embedding[df[node].isin(found_indices)] + #print('working') + + #print(tdf.shape, feats.shape) + #print(emb.shape) if emb is not None else None logger.info(f" - Returning edge dataframe of size {edges.shape[0]}") # get all the unique nodes logger.info( @@ -254,6 +267,8 @@ def search_graph( ) g = res.edges(edges, src, dst).nodes(tdf, node) + g._node_features = feats + g._node_embedding = emb if g._name is not None: name = f"{g._name}-query:{query}" From cef4cf448e3b57e48e30c3efd43401ed3c0fdf63 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Jan 2023 10:39:51 -0800 Subject: [PATCH 147/432] lint --- graphistry/text_utils.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 1ee9bfcfd3..9e67a95184 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -250,16 +250,11 @@ def search_graph( if res._umap is not None: emb = res._node_embedding.iloc[found_indices] except Exception as e: # for explicit relabeled nodes - #print(e) + logger.exception(e) tdf = rdf[df[node].isin(found_indices)] feats = res._node_features.loc[tdf.index] - #print(node, feats.shape) if res._umap is not None: emb = res._node_embedding[df[node].isin(found_indices)] - #print('working') - - #print(tdf.shape, feats.shape) - #print(emb.shape) if emb is not None else None logger.info(f" - Returning edge dataframe of size {edges.shape[0]}") # get all the unique nodes logger.info( From 0316ef09de3105b467f9f9de8ee4baaa8e594682 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Jan 2023 10:51:07 -0800 Subject: [PATCH 148/432] lint --- graphistry/text_utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 9e67a95184..1378b01f91 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -246,15 +246,15 @@ def search_graph( emb = None try: tdf = rdf.iloc[found_indices] - feats = res._node_features.iloc[found_indices] + feats = res._node_features.iloc[found_indices] # type: ignore if res._umap is not None: - emb = res._node_embedding.iloc[found_indices] + emb = res._node_embedding.iloc[found_indices] # type: ignore except Exception as e: # for explicit relabeled nodes logger.exception(e) tdf = rdf[df[node].isin(found_indices)] - feats = res._node_features.loc[tdf.index] + feats = res._node_features.loc[tdf.index] # type: ignore if res._umap is not None: - emb = res._node_embedding[df[node].isin(found_indices)] + emb = res._node_embedding[df[node].isin(found_indices)] # type: ignore logger.info(f" - Returning edge dataframe of size {edges.shape[0]}") # get all the unique nodes logger.info( @@ -262,6 +262,7 @@ def search_graph( ) g = res.edges(edges, src, dst).nodes(tdf, node) + # add them back so they sync with .dbscan etc calls g._node_features = feats g._node_embedding = emb From 31307d70f7b1b49bbd276b263424385fe84621d7 Mon Sep 17 00:00:00 2001 From: Alex Morrise Date: Tue, 24 Jan 2023 11:56:41 -0800 Subject: [PATCH 149/432] Update README.md --- README.md | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 10dd07c77a..4756c86654 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ Automatically and intelligently transform text, numbers, booleans, and other for g = graphistry.nodes(df) g2 = g.umap(X=[..], y=[..], **search_model) - # set custom encoding model with any feature kwargs + # set custom encoding model with any feature/umap/dbscan kwargs new_model = ModelDict(message='encoding new model parameters is easy', **default_featurize_parameters) new_model.update(dict( y=[...], @@ -389,7 +389,6 @@ Automatically and intelligently transform text, numbers, booleans, and other for g3 = g.umap(X=[..], **new_model) # compare g2 vs g3 or add to different pipelines - # ... ``` @@ -413,7 +412,7 @@ See `help(g.featurize)` for more options new_df = pd.read_csv(...) embeddings, X_new, _ = g.transform_umap(new_df, None, kind='nodes', return_graph=False) ``` -* Infer a new graph from new data using the old umap coordinates to run inference without having to train a new umap fit. +* Infer a new graph from new data using the old umap coordinates to run inference without having to train a new umap model. ```python new_df = pd.read_csv(...) @@ -422,7 +421,7 @@ See `help(g.featurize)` for more options # or if you want the new minibatch to cluster to closest points in previous fit: g3 = g.transform_umap(new_df, return_graph=True, merge_policy=True) - g3.plot() # useful to see how new data connects to old -- can play with `sample` and `n_neighbors` to control how much of old to include + g3.plot() # useful to see how new data connects to old -- play with `sample` and `n_neighbors` to control how much of old to include ``` @@ -463,11 +462,11 @@ See `help(g.umap)` for more options from [your_training_pipeline] import train, model # Train - g = graphistry.nodes(df).build_gnn(y=`target`) + g = graphistry.nodes(df).build_gnn(y_nodes=`target`) G = g.DGL_graph train(G, model) # predict on new data - X_new, _ = g.transform(new_df, None, kind='nodes' or 'edges') # no targets + X_new, _ = g.transform(new_df, None, kind='nodes' or 'edges', return_graph=False) # no targets predictions = model.predict(G_new, X_new) ``` @@ -492,12 +491,21 @@ GNN support is rapidly evolving, please contact the team directly or on Slack fo #encode text as paraphrase embeddings, supports any sbert model model_name = "paraphrase-MiniLM-L6-v2") + # or use convienence `ModelDict` to store parameters + + from graphistry.features import search_model + g2 = g.featurize(X = ['text_col_1', .., 'text_col_n'], kind='nodes', **search_model) + + # query using the power of transformers to find richly relevant results + results_df, query_vector = g2.search('my natural language query', ...) - print(results_df[['_distance', ..., 'text_col_n']]) #sorted by relevancy + print(results_df[['_distance', 'text_col', ..]]) #sorted by relevancy + + # or see graph of matching entities and original edges - # or see graph of matching entities and similarity edges (or optional original edges) g2.search_graph('my natural language query', ...).plot() + ``` @@ -578,7 +586,7 @@ See `help(g.embed)`, `help(g.predict_links)` , or `help(g.predict_links_all)` fo g2 = g.umap(kind=kind).dbscan(kind=kind) print(g2._nodes['_dbscan']) | print(g2._edges['_dbscan']) - # dbscan in umap or featurize via flag + # dbscan in `umap` or `featurize` via flag g2 = g.umap(dbscan=True, min_dist=0.2, min_samples=1) # or via chaining, @@ -587,7 +595,7 @@ See `help(g.embed)`, `help(g.predict_links)` , or `help(g.predict_links_all)` fo # cluster by feature embeddings g2 = g.featurize().dbscan(**kwargs) - # cluster by a given set of feature column attributes + # cluster by a given set of feature column attributes, inhereted from `g.get_matrix(cols)` g2 = g.featurize().dbscan(cols=['ip_172', 'location', 'alert'], **kwargs) # equivalent to above (ie, cols != None and umap=True will still use features dataframe, rather than UMAP embeddings) @@ -596,7 +604,7 @@ See `help(g.embed)`, `help(g.predict_links)` , or `help(g.predict_links_all)` fo new_df = pd.read_csv(..) # transform on new data according to fit dbscan model - g3 = g.transform_dbscan(new_df) + g3 = g2.transform_dbscan(new_df) ``` See `help(g.dbscan)` or `help(g.transform_dbscan)` for options From 647ab0663fb650884934a95ba662f356fed55a0e Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Jan 2023 13:29:38 -0800 Subject: [PATCH 150/432] breaking change: DGL_graph -> _dgl_graph --- graphistry/dgl_utils.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/graphistry/dgl_utils.py b/graphistry/dgl_utils.py index 2303a5aacd..6792073ec8 100644 --- a/graphistry/dgl_utils.py +++ b/graphistry/dgl_utils.py @@ -229,7 +229,7 @@ def dgl_lazy_init(self, train_split: float = 0.8, device: str = "cpu"): self.train_split = train_split self.device = device self._removed_edges_previously = False - self.DGL_graph = None + self._dgl_graph = None self.dgl_initialized = True def _prune_edge_target(self): @@ -335,7 +335,7 @@ def _convert_edge_dataframe_to_DGL( 'destination column not set, try running g.bind(destination="my_col") or g.edges(df, destination="my_col")' ) - res.DGL_graph, res._adjacency, res._entity_to_index = pandas_to_dgl_graph( + res._dgl_graph, res._adjacency, res._entity_to_index = pandas_to_dgl_graph( res._edges, res._source, res._destination, @@ -370,7 +370,7 @@ def _featurize_nodes_to_dgl( ndata = convert_to_torch(X_enc, y_enc) # add ndata to the graph - res.DGL_graph.ndata.update(ndata) + res._dgl_graph.ndata.update(ndata) res._mask_nodes() return res @@ -396,7 +396,7 @@ def _featurize_edges_to_dgl( edata = convert_to_torch(X_enc, y_enc) # add edata to the graph - res.DGL_graph.edata.update(edata) + res._dgl_graph.edata.update(edata) res._mask_edges() return res @@ -504,19 +504,19 @@ def build_gnn( return res def _mask_nodes(self): - if config.FEATURE in self.DGL_graph.ndata: - n = self.DGL_graph.ndata[config.FEATURE].shape[0] + if config.FEATURE in self._dgl_graph.ndata: + n = self._dgl_graph.ndata[config.FEATURE].shape[0] ( - self.DGL_graph.ndata[config.TRAIN_MASK], - self.DGL_graph.ndata[config.TEST_MASK], + self._dgl_graph.ndata[config.TRAIN_MASK], + self._dgl_graph.ndata[config.TEST_MASK], ) = get_torch_train_test_mask(n, self.train_split) def _mask_edges(self): - if config.FEATURE in self.DGL_graph.edata: - n = self.DGL_graph.edata[config.FEATURE].shape[0] + if config.FEATURE in self._dgl_graph.edata: + n = self._dgl_graph.edata[config.FEATURE].shape[0] ( - self.DGL_graph.edata[config.TRAIN_MASK], - self.DGL_graph.edata[config.TEST_MASK], + self._dgl_graph.edata[config.TRAIN_MASK], + self._dgl_graph.edata[config.TEST_MASK], ) = get_torch_train_test_mask(n, self.train_split) @@ -598,7 +598,7 @@ def _mask_edges(self): # use_edge_scaler="zscale", # ) # # the DGL graph -# G = g2.DGL_graph +# G = g2._dgl_graph # print('G', G) # # to get a sense of the different parts in training loop above # # labels = torch.tensor(T.values, dtype=torch.float) From da1a82bb8449c6379bb69488c985f077bf99c28d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 25 Jan 2023 19:56:05 -0800 Subject: [PATCH 151/432] typing --- graphistry/compute/cluster.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index ff789c844c..d3328fdf96 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -2,13 +2,12 @@ import pandas as pd import numpy as np -from typing import Any, List, Union, TYPE_CHECKING, Tuple, Optional, Dict, Callable +from typing import Any, List, Union, TYPE_CHECKING, Tuple, Optional from typing_extensions import Literal from collections import Counter -from graphistry.Engine import Engine from graphistry.Plottable import Plottable -from graphistry.constants import CUML, UMAP_LEARN, DBSCAN, DBSCAN_PARAMS # noqa type: ignore +from graphistry.constants import CUML, UMAP_LEARN, DBSCAN # noqa type: ignore from graphistry.features import ModelDict from graphistry.feature_utils import get_matrix_by_column_parts @@ -67,7 +66,7 @@ def resolve_cpu_gpu_engine( ) -def get_model_matrix(g, kind, cols, umap, target): +def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, target): """ Allows for a single function to get the model matrix for both nodes and edges as well as targets, embeddings, and features @@ -95,7 +94,7 @@ def get_model_matrix(g, kind, cols, umap, target): return df -def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, target=False, verbose=False): +def dbscan_fit(g: Any, dbscan: Any, kind:str="nodes", cols: Optional[Union[List, str]]=None, use_umap_embedding:bool=True, target:bool=False, verbose:bool=False): """ Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe or target dataframe if target is True. @@ -104,7 +103,7 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ g: graphistry graph kind: 'nodes' or 'edges' cols: list of columns to use for clustering given `g.featurize` has been run - umap: whether to use UMAP embeddings or features dataframe + use_umap_embedding: whether to use UMAP embeddings or features dataframe for clustering (default: True) """ X = get_model_matrix(g, kind, cols, use_umap_embedding, target) @@ -139,7 +138,7 @@ def dbscan_fit(g, dbscan, kind="nodes", cols=None, use_umap_embedding=True, targ return g -def dbscan_predict(X: pd.DataFrame, model): +def dbscan_predict(X: pd.DataFrame, model: Any): """ DBSCAN has no predict per se, so we reverse engineer one here from https://stackoverflow.com/questions/27822752/scikit-learn-predicting-new-points-with-dbscan @@ -162,14 +161,6 @@ def dbscan_predict(X: pd.DataFrame, model): return y_new -# def dbscan_predict2(g, kind="nodes", cols=None, umap=True): -# X = g._get_feature(kind) -# dbscan = g._node_dbscan if kind == "nodes" else g._edge_dbscan - -# preds = dbscan_predict(X, dbscan) -# return X, preds - - class ClusterMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): pass @@ -195,9 +186,9 @@ def _cluster_dbscan( ) dbscan = ( - cuDBSCAN(eps=min_dist, min_samples=min_samples, **kwargs) + cuDBSCAN(eps=min_dist, min_samples=min_samples, *args, **kwargs) if res.engine == CUML - else DBSCAN(eps=min_dist, min_samples=min_samples, **kwargs) + else DBSCAN(eps=min_dist, min_samples=min_samples, *args, **kwargs) ) res = dbscan_fit( @@ -210,11 +201,11 @@ def dbscan( self, min_dist: float = 0.2, min_samples: int = 1, - cols=None, - kind="nodes", - fit_umap_embedding=True, - target=False, - verbose=False, + cols: Optional[Union[List, str]] = None, + kind: str = "nodes", + fit_umap_embedding: bool = True, + target: bool = False, + verbose: bool = False, *args, **kwargs, ): From 844674cdbc9b056a264cb4d474b4f4774d46a47c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 25 Jan 2023 21:57:23 -0800 Subject: [PATCH 152/432] lint --- graphistry/compute/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index d3328fdf96..15b7cf0ed3 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -94,7 +94,7 @@ def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, targe return df -def dbscan_fit(g: Any, dbscan: Any, kind:str="nodes", cols: Optional[Union[List, str]]=None, use_umap_embedding:bool=True, target:bool=False, verbose:bool=False): +def dbscan_fit(g: Any, dbscan: Any, kind: str = "nodes", cols: Optional[Union[List, str]] = None, use_umap_embedding: bool = True, target: bool = False, verbose: bool = False): """ Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe or target dataframe if target is True. From 5ebfc8e3c145652956ff4d5e2a7dd7ad4bbfd0ac Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 11 Feb 2023 13:50:30 +0800 Subject: [PATCH 153/432] added cuCat feature_engine --- graphistry/feature_utils.py | 43 +++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index ba6227da29..6b3c200dbe 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -47,6 +47,16 @@ SuperVectorizer = Any GapEncoder = Any SimilarityEncoder = Any + try: + from cuCat import ( + SuperVectorizer, + GapEncoder, + SimilarityEncoder, + ) + except: + SuperVectorizer = Any + GapEncoder = Any + SimilarityEncoder = Any try: from sklearn.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin @@ -91,6 +101,20 @@ def lazy_import_has_min_dependancy(): except ModuleNotFoundError as e: return False, e +def lazy_import_has_cuml_dependancy(): + import warnings + warnings.filterwarnings("ignore") + try: + import scipy.sparse # noqa + from scipy import __version__ as scipy_version + from cuCat import __version__ as cuCat_version + from sklearn import __version__ as sklearn_version + logger.debug(f"SCIPY VERSION: {scipy_version}") + logger.debug(f"cuCat VERSION: {cuCat_version}") + logger.debug(f"sklearn VERSION: {sklearn_version}") + return True, 'ok' + except ModuleNotFoundError as e: + return False, e def assert_imported_text(): has_dependancy_text_, import_text_exn, _ = lazy_import_has_dependancy_text() @@ -135,7 +159,7 @@ def assert_imported(): # # _featurize_or_get_edges_dataframe_if_X_is_None -FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch"] +FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cuCat"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] @@ -143,13 +167,16 @@ def resolve_feature_engine( feature_engine: FeatureEngine, ) -> FeatureEngineConcrete: # noqa - if feature_engine in ["none", "pandas", "dirty_cat", "torch"]: + if feature_engine in ["none", "pandas", "dirty_cat", "torch", "cuCat"]: return feature_engine # type: ignore if feature_engine == "auto": has_dependancy_text_, _, _ = lazy_import_has_dependancy_text() if has_dependancy_text_: return "torch" + has_cuml_dependancy_, _ = lazy_import_has_cuml_dependancy() + if has_cuml_dependancy_: + return "cuCat" has_min_dependancy_, _ = lazy_import_has_min_dependancy() if has_min_dependancy_: return "dirty_cat" @@ -157,7 +184,7 @@ def resolve_feature_engine( raise ValueError( # noqa f'feature_engine expected to be "none", ' - '"pandas", "dirty_cat", "torch", or "auto"' + '"pandas", "dirty_cat", "torch", "cuCat", or "auto"' f'but received: {feature_engine} :: {type(feature_engine)}' ) @@ -890,6 +917,11 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ + if feature_engine=='dirty_cat': + from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + elif feature_engine=='cuCat': + from cuCat import SuperVectorizer, GapEncoder, SimilarityEncoder + from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder from sklearn.preprocessing import FunctionTransformer t = time() @@ -2331,7 +2363,10 @@ def featurize( default True. :return: self, with new attributes set by the featurization process. """ - assert_imported() + if feature_engine == 'dirty_cat': + assert_imported() + elif feature_engine == 'cuCat': + assert_cuml_imported() if inplace: res = self else: From be038f112b1f16178d1434017b2305825e3e562a Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 11 Feb 2023 15:13:15 +0800 Subject: [PATCH 154/432] functional --- graphistry/feature_utils.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 6b3c200dbe..41c23bdc65 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -135,7 +135,14 @@ def assert_imported(): ) raise import_min_exn - +def assert_cuml_imported(): + has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cuml_dependancy() + if not has_cuml_dependancy_: + logger.error( # noqa + "cunl not found, trying running" # noqa + "`pip install rapids`" # noqa + ) + raise import_cuml_exn # ############################################################################ # # Rough calltree @@ -891,6 +898,7 @@ def process_dirty_dataframes( similarity: Optional[str] = None, # "ngram", categories: Optional[str] = "auto", multilabel: bool = False, + feature_engine: Optional[str] = "dirty_cat", ) -> Tuple[ pd.DataFrame, Optional[pd.DataFrame], @@ -922,7 +930,6 @@ def process_dirty_dataframes( elif feature_engine=='cuCat': from cuCat import SuperVectorizer, GapEncoder, SimilarityEncoder - from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder from sklearn.preprocessing import FunctionTransformer t = time() @@ -1164,7 +1171,8 @@ def process_nodes_dataframes( n_topics_target=n_topics_target, similarity=similarity, categories=categories, - multilabel=multilabel + multilabel=multilabel, + feature_engine=feature_engine, ) if embedding: From 86993ef8eec97bf82acae58e2ccc15a2a1daa0dc Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 16 Feb 2023 15:11:58 -0500 Subject: [PATCH 155/432] fix(graphistry.rst): now produces plotter modules --- docs/source/graphistry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index c9fbcfa4dd..91aa7f08f9 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -12,7 +12,7 @@ graphistry package graphistry.plotter module ------------------------- -.. automodule:: graphistry.plotter +.. automodule:: graphistry.PlotterBase :members: :undoc-members: :show-inheritance: From 27dee7360dcd4ebd71a57d7b96d3568a32a30b6f Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 21 Feb 2023 16:42:41 +0800 Subject: [PATCH 156/432] working umap from cudf, via pandas --- graphistry/feature_utils.py | 5 +++-- graphistry/umap_utils.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index ba6227da29..97c0632350 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2,6 +2,7 @@ import numpy as np import os import pandas as pd +import cudf from time import time import warnings from functools import partial @@ -167,7 +168,7 @@ def resolve_feature_engine( def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: - if isinstance(y, pd.DataFrame): + if isinstance(y, pd.DataFrame) or isinstance(y, cudf.DataFrame): return y if df is None: @@ -188,7 +189,7 @@ def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: def resolve_X(df: Optional[pd.DataFrame], X: XSymbolic) -> pd.DataFrame: - if isinstance(X, pd.DataFrame): + if isinstance(X, pd.DataFrame) or isinstance(X, cudf.DataFrame): return X if df is None: diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 5cd092f29d..1dd60c4aec 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union import pandas as pd +import cudf from . import constants as config from .feature_utils import (FeatureMixin, Literal, XSymbolic, YSymbolic, @@ -478,7 +479,6 @@ def umap( ) nodes = res._nodes[res._node].values - index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) logger.debug("propagating with featurize_kwargs: %s", featurize_kwargs) ( @@ -492,8 +492,14 @@ def umap( logger.debug("umap X_: %s", X_) logger.debug("umap y_: %s", y_) + if isinstance(X_,pd.DataFrame): + index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) + elif isinstance(X_,cudf.DataFrame): + index_to_nodes_dict=cudf.DataFrame(nodes).reset_index() + + res = res._process_umap( - res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs + res, pd.DataFrame(X_), y_, kind, memoize, featurize_kwargs, **umap_kwargs ) res._weighted_adjacency_nodes = res._weighted_adjacency @@ -521,7 +527,7 @@ def umap( ) res = res._process_umap( - res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs + res, pd.DataFrame(X_.to_numpy()), y_, kind, memoize, featurize_kwargs, **umap_kwargs ) res._weighted_adjacency_edges = res._weighted_adjacency if res._xy is None: From a35fe0a5e5dd39a0271de4d3fd936a3aa8fc4e62 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 21 Feb 2023 16:51:06 +0800 Subject: [PATCH 157/432] working umap from cudf, via pandas --- graphistry/umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 1dd60c4aec..1911891bfd 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -499,7 +499,7 @@ def umap( res = res._process_umap( - res, pd.DataFrame(X_), y_, kind, memoize, featurize_kwargs, **umap_kwargs + res, pd.DataFrame(X_.to_numpy()), y_, kind, memoize, featurize_kwargs, **umap_kwargs ) res._weighted_adjacency_nodes = res._weighted_adjacency From 935a9f9277a6fe64af7c36402dae0d133143acf1 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 21 Feb 2023 17:06:00 +0800 Subject: [PATCH 158/432] working umap from cudf, via pandas --- graphistry/umap_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 1911891bfd..6e61a8aa69 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -496,10 +496,10 @@ def umap( index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) elif isinstance(X_,cudf.DataFrame): index_to_nodes_dict=cudf.DataFrame(nodes).reset_index() - + X_=pd.DataFrame(X_.to_numpy()) res = res._process_umap( - res, pd.DataFrame(X_.to_numpy()), y_, kind, memoize, featurize_kwargs, **umap_kwargs + res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs ) res._weighted_adjacency_nodes = res._weighted_adjacency @@ -527,7 +527,7 @@ def umap( ) res = res._process_umap( - res, pd.DataFrame(X_.to_numpy()), y_, kind, memoize, featurize_kwargs, **umap_kwargs + res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs ) res._weighted_adjacency_edges = res._weighted_adjacency if res._xy is None: From 261f1221807e008457b20cb8331861245b120eb7 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Tue, 21 Feb 2023 11:09:34 -0500 Subject: [PATCH 159/432] feat(docs-ui): added a bio --- docs/source/index.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 1943a5cf72..cc734f5581 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,8 +1,11 @@ PyGraphistry's documentation (|version|) ======================================== -Quickstart: -`Read our tutorial `_ +.. Quickstart: +.. `Read our tutorial `_ + +PyGraphistry is a Python visual graph AI library to extract, transform, analyze, model, and visualize big graphs, and especially alongside Graphistry end-to-end GPU server sessions. Installing optional graphistry[ai] dependencies adds graph autoML, including automatic feature engineering, UMAP, and graph neural net support. Combined, PyGraphistry reduces your time to graph for going from raw data to visualizations and AI models down to three lines of code. +Here in our docstrings you can find useful packages, modules, commands to maximize your graph AI experience with PyGraphistry. For a full tutorial, refer to our `PyGraphistry `_ repo. .. toctree:: :maxdepth: 3 From 882eb94fb6173d5eafe372b474bac35e7ad1cb13 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 22 Feb 2023 08:33:12 +0800 Subject: [PATCH 160/432] working umap from cudf, via pandas --- graphistry/umap_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 6e61a8aa69..bfe035f96e 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -495,8 +495,8 @@ def umap( if isinstance(X_,pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) elif isinstance(X_,cudf.DataFrame): - index_to_nodes_dict=cudf.DataFrame(nodes).reset_index() - X_=pd.DataFrame(X_.to_numpy()) + index_to_nodes_dict = cudf.DataFrame(nodes).reset_index() + X_= pd.DataFrame(X_.to_numpy()) res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs From d0779731a932613ba21df806acbac35ef1c1a2f3 Mon Sep 17 00:00:00 2001 From: dc Date: Wed, 22 Feb 2023 12:35:53 +0800 Subject: [PATCH 161/432] remove explicit cudf import --- graphistry/feature_utils.py | 63 +++++++++++++++++++++++++++++++------ graphistry/umap_utils.py | 5 +-- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 97c0632350..9ff6080a12 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -6,6 +6,7 @@ from time import time import warnings from functools import partial +from inspect import getmodule from typing import ( Hashable, @@ -48,6 +49,16 @@ SuperVectorizer = Any GapEncoder = Any SimilarityEncoder = Any + try: + from cuCat import ( + SuperVectorizer, + GapEncoder, + SimilarityEncoder, + ) + except: + SuperVectorizer = Any + GapEncoder = Any + SimilarityEncoder = Any try: from sklearn.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin @@ -92,6 +103,20 @@ def lazy_import_has_min_dependancy(): except ModuleNotFoundError as e: return False, e +def lazy_import_has_cuml_dependancy(): + import warnings + warnings.filterwarnings("ignore") + try: + import scipy.sparse # noqa + from scipy import __version__ as scipy_version + from cuCat import __version__ as cuCat_version + from sklearn import __version__ as sklearn_version + logger.debug(f"SCIPY VERSION: {scipy_version}") + logger.debug(f"cuCat VERSION: {cuCat_version}") + logger.debug(f"sklearn VERSION: {sklearn_version}") + return True, 'ok' + except ModuleNotFoundError as e: + return False, e def assert_imported_text(): has_dependancy_text_, import_text_exn, _ = lazy_import_has_dependancy_text() @@ -112,7 +137,14 @@ def assert_imported(): ) raise import_min_exn - +def assert_cuml_imported(): + has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cuml_dependancy() + if not has_cuml_dependancy_: + logger.error( # noqa + "cunl not found, trying running" # noqa + "`pip install rapids`" # noqa + ) + raise import_cuml_exn # ############################################################################ # # Rough calltree @@ -136,7 +168,7 @@ def assert_imported(): # # _featurize_or_get_edges_dataframe_if_X_is_None -FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch"] +FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cuCat"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] @@ -144,13 +176,16 @@ def resolve_feature_engine( feature_engine: FeatureEngine, ) -> FeatureEngineConcrete: # noqa - if feature_engine in ["none", "pandas", "dirty_cat", "torch"]: + if feature_engine in ["none", "pandas", "dirty_cat", "torch", "cuCat"]: return feature_engine # type: ignore if feature_engine == "auto": has_dependancy_text_, _, _ = lazy_import_has_dependancy_text() if has_dependancy_text_: return "torch" + has_cuml_dependancy_, _ = lazy_import_has_cuml_dependancy() + if has_cuml_dependancy_: + return "cuCat" has_min_dependancy_, _ = lazy_import_has_min_dependancy() if has_min_dependancy_: return "dirty_cat" @@ -158,7 +193,7 @@ def resolve_feature_engine( raise ValueError( # noqa f'feature_engine expected to be "none", ' - '"pandas", "dirty_cat", "torch", or "auto"' + '"pandas", "dirty_cat", "torch", "cuCat", or "auto"' f'but received: {feature_engine} :: {type(feature_engine)}' ) @@ -168,7 +203,7 @@ def resolve_feature_engine( def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: - if isinstance(y, pd.DataFrame) or isinstance(y, cudf.DataFrame): + if isinstance(y, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(y)): return y if df is None: @@ -189,7 +224,7 @@ def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: def resolve_X(df: Optional[pd.DataFrame], X: XSymbolic) -> pd.DataFrame: - if isinstance(X, pd.DataFrame) or isinstance(X, cudf.DataFrame): + if isinstance(X, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(X)): return X if df is None: @@ -865,6 +900,7 @@ def process_dirty_dataframes( similarity: Optional[str] = None, # "ngram", categories: Optional[str] = "auto", multilabel: bool = False, + feature_engine: Optional[str] = "dirty_cat", ) -> Tuple[ pd.DataFrame, Optional[pd.DataFrame], @@ -891,7 +927,11 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ - from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + if feature_engine=='dirty_cat': + from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + elif feature_engine=='cuCat': + from cuCat import SuperVectorizer, GapEncoder, SimilarityEncoder + from sklearn.preprocessing import FunctionTransformer t = time() @@ -1133,7 +1173,8 @@ def process_nodes_dataframes( n_topics_target=n_topics_target, similarity=similarity, categories=categories, - multilabel=multilabel + multilabel=multilabel, + feature_engine=feature_engine, ) if embedding: @@ -1789,6 +1830,7 @@ def prune_weighted_edges_df_and_relabel_nodes( f"from {len(wdf):,} to {len(wdf2):,} edges." ) if index_to_nodes_dict is not None: + wdf2 = wdf2.replace( { config.SRC: index_to_nodes_dict, @@ -2332,7 +2374,10 @@ def featurize( default True. :return: self, with new attributes set by the featurization process. """ - assert_imported() + if feature_engine == 'dirty_cat': + assert_imported() + elif feature_engine == 'cuCat': + assert_cuml_imported() if inplace: res = self else: diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index bfe035f96e..112c7d111d 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -1,9 +1,9 @@ import copy from time import time from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union +from inspect import getmodule import pandas as pd -import cudf from . import constants as config from .feature_utils import (FeatureMixin, Literal, XSymbolic, YSymbolic, @@ -494,7 +494,8 @@ def umap( if isinstance(X_,pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) - elif isinstance(X_,cudf.DataFrame): + elif 'cudf.core.dataframe' in str(getmodule(X_)): + import cudf index_to_nodes_dict = cudf.DataFrame(nodes).reset_index() X_= pd.DataFrame(X_.to_numpy()) From 62fa5fdcde196aba6c0682c74aed0aefe9f27850 Mon Sep 17 00:00:00 2001 From: dc Date: Wed, 22 Feb 2023 18:12:49 +0800 Subject: [PATCH 162/432] opt-in for setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5817609f6a..2db477787a 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] + ['git+https://github.com/graphistry/cuCat/@0.01.0'] base_extras = {**base_extras_light, **base_extras_heavy} From 5ba336ea6bb63aec8bcce17e192ec9a71eecdec9 Mon Sep 17 00:00:00 2001 From: dc Date: Fri, 24 Feb 2023 16:11:46 +0800 Subject: [PATCH 163/432] lint --- graphistry/feature_utils.py | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 41c23bdc65..a6f2856277 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -925,9 +925,9 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ - if feature_engine=='dirty_cat': + if feature_engine =='dirty_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - elif feature_engine=='cuCat': + elif feature_engine =='cuCat': from cuCat import SuperVectorizer, GapEncoder, SimilarityEncoder from sklearn.preprocessing import FunctionTransformer diff --git a/setup.py b/setup.py index 2db477787a..68d0dbe70a 100755 --- a/setup.py +++ b/setup.py @@ -38,9 +38,9 @@ def unique_flatten_dict(d): } base_extras_heavy = { - 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], + 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'] + ['cuCat @ git+https://github.com/graphistry/cuCat/@0.01.0'] , } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] + ['git+https://github.com/graphistry/cuCat/@0.01.0'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From 9520cbcc8ea869c2f4a3afe7d78de8a56ef5dca4 Mon Sep 17 00:00:00 2001 From: dc Date: Fri, 24 Feb 2023 16:13:35 +0800 Subject: [PATCH 164/432] lint --- graphistry/feature_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index a6f2856277..bcbc7ca0bb 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -925,9 +925,9 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ - if feature_engine =='dirty_cat': + if feature_engine == 'dirty_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - elif feature_engine =='cuCat': + elif feature_engine == 'cuCat': from cuCat import SuperVectorizer, GapEncoder, SimilarityEncoder from sklearn.preprocessing import FunctionTransformer From ab0b019f5c259ecfe9a4a320f8656878d0536c3c Mon Sep 17 00:00:00 2001 From: dc Date: Fri, 24 Feb 2023 16:16:51 +0800 Subject: [PATCH 165/432] lint --- graphistry/feature_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index bcbc7ca0bb..4697b6795e 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -143,6 +143,8 @@ def assert_cuml_imported(): "`pip install rapids`" # noqa ) raise import_cuml_exn + + # ############################################################################ # # Rough calltree From d12f2db527d0f99e146cbd5b254c15c64c2fa57c Mon Sep 17 00:00:00 2001 From: dc Date: Fri, 24 Feb 2023 16:43:02 +0800 Subject: [PATCH 166/432] branches got confused, removed cudf import --- graphistry/feature_utils.py | 64 ++++++------------------------------- 1 file changed, 9 insertions(+), 55 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 9ff6080a12..2937ca7565 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2,11 +2,9 @@ import numpy as np import os import pandas as pd -import cudf from time import time import warnings from functools import partial -from inspect import getmodule from typing import ( Hashable, @@ -49,16 +47,6 @@ SuperVectorizer = Any GapEncoder = Any SimilarityEncoder = Any - try: - from cuCat import ( - SuperVectorizer, - GapEncoder, - SimilarityEncoder, - ) - except: - SuperVectorizer = Any - GapEncoder = Any - SimilarityEncoder = Any try: from sklearn.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin @@ -103,20 +91,6 @@ def lazy_import_has_min_dependancy(): except ModuleNotFoundError as e: return False, e -def lazy_import_has_cuml_dependancy(): - import warnings - warnings.filterwarnings("ignore") - try: - import scipy.sparse # noqa - from scipy import __version__ as scipy_version - from cuCat import __version__ as cuCat_version - from sklearn import __version__ as sklearn_version - logger.debug(f"SCIPY VERSION: {scipy_version}") - logger.debug(f"cuCat VERSION: {cuCat_version}") - logger.debug(f"sklearn VERSION: {sklearn_version}") - return True, 'ok' - except ModuleNotFoundError as e: - return False, e def assert_imported_text(): has_dependancy_text_, import_text_exn, _ = lazy_import_has_dependancy_text() @@ -137,14 +111,7 @@ def assert_imported(): ) raise import_min_exn -def assert_cuml_imported(): - has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cuml_dependancy() - if not has_cuml_dependancy_: - logger.error( # noqa - "cunl not found, trying running" # noqa - "`pip install rapids`" # noqa - ) - raise import_cuml_exn + # ############################################################################ # # Rough calltree @@ -168,7 +135,7 @@ def assert_cuml_imported(): # # _featurize_or_get_edges_dataframe_if_X_is_None -FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cuCat"] +FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] @@ -176,16 +143,13 @@ def resolve_feature_engine( feature_engine: FeatureEngine, ) -> FeatureEngineConcrete: # noqa - if feature_engine in ["none", "pandas", "dirty_cat", "torch", "cuCat"]: + if feature_engine in ["none", "pandas", "dirty_cat", "torch"]: return feature_engine # type: ignore if feature_engine == "auto": has_dependancy_text_, _, _ = lazy_import_has_dependancy_text() if has_dependancy_text_: return "torch" - has_cuml_dependancy_, _ = lazy_import_has_cuml_dependancy() - if has_cuml_dependancy_: - return "cuCat" has_min_dependancy_, _ = lazy_import_has_min_dependancy() if has_min_dependancy_: return "dirty_cat" @@ -193,7 +157,7 @@ def resolve_feature_engine( raise ValueError( # noqa f'feature_engine expected to be "none", ' - '"pandas", "dirty_cat", "torch", "cuCat", or "auto"' + '"pandas", "dirty_cat", "torch", or "auto"' f'but received: {feature_engine} :: {type(feature_engine)}' ) @@ -203,7 +167,7 @@ def resolve_feature_engine( def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: - if isinstance(y, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(y)): + if isinstance(y, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(y): return y if df is None: @@ -224,7 +188,7 @@ def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: def resolve_X(df: Optional[pd.DataFrame], X: XSymbolic) -> pd.DataFrame: - if isinstance(X, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(X)): + if isinstance(X, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(X): return X if df is None: @@ -900,7 +864,6 @@ def process_dirty_dataframes( similarity: Optional[str] = None, # "ngram", categories: Optional[str] = "auto", multilabel: bool = False, - feature_engine: Optional[str] = "dirty_cat", ) -> Tuple[ pd.DataFrame, Optional[pd.DataFrame], @@ -927,11 +890,7 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ - if feature_engine=='dirty_cat': - from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - elif feature_engine=='cuCat': - from cuCat import SuperVectorizer, GapEncoder, SimilarityEncoder - + from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder from sklearn.preprocessing import FunctionTransformer t = time() @@ -1173,8 +1132,7 @@ def process_nodes_dataframes( n_topics_target=n_topics_target, similarity=similarity, categories=categories, - multilabel=multilabel, - feature_engine=feature_engine, + multilabel=multilabel ) if embedding: @@ -1830,7 +1788,6 @@ def prune_weighted_edges_df_and_relabel_nodes( f"from {len(wdf):,} to {len(wdf2):,} edges." ) if index_to_nodes_dict is not None: - wdf2 = wdf2.replace( { config.SRC: index_to_nodes_dict, @@ -2374,10 +2331,7 @@ def featurize( default True. :return: self, with new attributes set by the featurization process. """ - if feature_engine == 'dirty_cat': - assert_imported() - elif feature_engine == 'cuCat': - assert_cuml_imported() + assert_imported() if inplace: res = self else: From 736744e632136b356ba62a5790423e747f92ea90 Mon Sep 17 00:00:00 2001 From: dc Date: Fri, 24 Feb 2023 16:45:38 +0800 Subject: [PATCH 167/432] branches got confused, removed cudf import --- graphistry/feature_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 2937ca7565..467d71bb79 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -3,6 +3,7 @@ import os import pandas as pd from time import time +from inspect import getmodule import warnings from functools import partial @@ -167,7 +168,7 @@ def resolve_feature_engine( def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: - if isinstance(y, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(y): + if isinstance(y, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(y)): return y if df is None: @@ -188,7 +189,7 @@ def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: def resolve_X(df: Optional[pd.DataFrame], X: XSymbolic) -> pd.DataFrame: - if isinstance(X, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(X): + if isinstance(X, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(X)): return X if df is None: From dc8914884c85a05fba8c5b4abfbf5f8f72148242 Mon Sep 17 00:00:00 2001 From: dc Date: Fri, 24 Feb 2023 16:47:58 +0800 Subject: [PATCH 168/432] branches got confused, removed cudf import --- graphistry/umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 112c7d111d..11f0b64c06 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -497,7 +497,7 @@ def umap( elif 'cudf.core.dataframe' in str(getmodule(X_)): import cudf index_to_nodes_dict = cudf.DataFrame(nodes).reset_index() - X_= pd.DataFrame(X_.to_numpy()) + X_ = pd.DataFrame(X_.to_numpy()) res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs From 4d2ff19ea66671d329d53f887d6677897ed91552 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:25:21 -0500 Subject: [PATCH 169/432] fix(docstring): made blockcode visible --- graphistry/PlotterBase.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 7d22469d8f..f397b70631 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -295,7 +295,7 @@ def style(self, fg=None, bg=None, page=None, logo=None): :param fg: Dictionary {'blendMode': str} of any valid CSS blend mode :type fg: dict - :param bg: Nested dictionary of page background properties. {'color': str, 'gradient': {'kind': str, 'position': str, 'stops': list }, 'image': { 'url': str, 'width': int, 'height': int, 'blendMode': str } + :param bg: Nested dictionary of page background properties. { 'color': str, 'gradient': {'kind': str, 'position': str, 'stops': list }, 'image': { 'url': str, 'width': int, 'height': int, 'blendMode': str } :type bg: dict :param logo: Nested dictionary of logo properties. { 'url': str, 'autoInvert': bool, 'position': str, 'dimensions': { 'maxWidth': int, 'maxHeight': int }, 'crop': { 'top': int, 'left': int, 'bottom': int, 'right': int }, 'padding': { 'top': int, 'left': int, 'bottom': int, 'right': int}, 'style': str} @@ -845,7 +845,8 @@ def bind(self, source=None, destination=None, node=None, edge=None, :param edge: Attribute containing an edge's ID :type edge: str - :param edge_title: Attribute overriding edge's minimized label text. By default, the edge source and destination is used. + :param edge_title: Attribute overriding edge's minimized label text. + By default, the edge source and destination is used. :type edge_title: str :param edge_label: Attribute overriding edge's expanded label text. By default, scrollable list of attribute/value mappings. @@ -1002,6 +1003,7 @@ def nodes(self, nodes: Union[Callable, Any], node=None, *args, **kwargs) -> Plot **Example** :: + import graphistry def sample_nodes(g, n): @@ -1101,6 +1103,7 @@ def edges(self, edges: Union[Callable, Any], source=None, destination=None, edge **Example** :: + import graphistry def sample_edges(g, n): From 9e7b08665fd2e3aba62304ddb1a896ff5e1ef65b Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:26:20 -0500 Subject: [PATCH 170/432] fix(docstring): added methods to correct place --- graphistry/layout/graph/graph.py | 112 ++++++++++++++++++++------ graphistry/layout/graph/graphBase.py | 91 ++++++++++++--------- graphistry/layout/graph/vertexBase.py | 37 ++++++--- 3 files changed, 164 insertions(+), 76 deletions(-) diff --git a/graphistry/layout/graph/graph.py b/graphistry/layout/graph/graph.py index b04dd4842e..4cbeac9182 100644 --- a/graphistry/layout/graph/graph.py +++ b/graphistry/layout/graph/graph.py @@ -8,31 +8,55 @@ class Graph(object): - """ - The graph is stored in disjoint-sets holding each connected component in `components` as a list of graph_core objects. - - **Attributes** - C (list[GraphBase]): list of graph_core components. - - **Methods** - add_vertex(v): add vertex v into the Graph as a new component - add_edge(e): add edge e and its vertices into the Graph possibly merging the - associated graph_core components - get_vertices_count(): see order() - vertices(): see graph_core - edges(): see graph_core - remove_edge(e): remove edge e possibly spawning two new cores - if the graph_core that contained e gets disconnected. - remove_vertex(v): remove vertex v and all its edges. - order(): the order of the graph (number of vertices) - norm(): the norm of the graph (number of edges) - deg_min(): the minimum degree of vertices - deg_max(): the maximum degree of vertices - deg_avg(): the average degree of vertices - eps(): the graph epsilon value (norm/order), average number of edges per vertex. - connected(): returns True if the graph is connected (i.e. it has only one component). - components(): returns the list of components - """ + # """ + # The graph is stored in disjoint-sets holding each connected component in `components` as a list of graph_core objects. + + # **Attributes** + # C (list[GraphBase]): list of graph_core components. + + + # **add_edge(e):** + # add edge e and its vertices into the Graph possibly merging the associated graph_core components + + # **get_vertices_count():** + # see order() + + # **vertices():** + # see graph_core + + # **edges():** + # see graph_core + + # **remove_edge(e):** + # remove edge e possibly spawning two new cores if the graph_core that contained e gets disconnected. + + # **remove_vertex(v):** + # remove vertex v and all its edges. + + # **order():** + # the order of the graph (number of vertices) + + # **norm():** + # the norm of the graph (number of edges) + + # **deg_min():** + # the minimum degree of vertices + + # **deg_max():** + # the maximum degree of vertices + + # **deg_avg():** + # the average degree of vertices + + # **eps():** + # the graph epsilon value (norm/order), average number of edges per vertex. + + # **connected():** + # returns True if the graph is connected (i.e. it has only one component). + + # **components():** + # returns the list of components + # """ component_class = GraphBase @@ -73,16 +97,22 @@ def __init__(self, vertices = None, edges = None, directed = True): self.components.append(self.component_class(vertices, edge_set, directed)) def add_vertex(self, v): + """ + add vertex v into the Graph as a new component + """ for c in self.components: if v in c.verticesPoset: return c.verticesPoset.get(v) g = self.component_class(directed = self.directed) v = g.add_single_vertex(v) self.components.append(g) + print("add vertex v into the Graph as a new component") return v def add_edge(self, e): - + """ + add edge e and its vertices into the Graph possibly merging the associated graph_core components + """ x = e.v[0] y = e.v[1] x = self.add_vertex(x) @@ -116,6 +146,9 @@ def get_vertex_from_data(self, data): return None def vertices(self): + """ + see graph_core + """ for c in self.components: vertices = c.verticesPoset for v in vertices: @@ -128,6 +161,9 @@ def edges(self): yield e def remove_edge(self, e): + """ + remove edge e possibly spawning two new cores if the graph_core that contained e gets disconnected. + """ # get the GraphBase: c = e.v[0].component assert c == e.v[1].component @@ -147,6 +183,9 @@ def remove_edge(self, e): return e def remove_vertex(self, x): + """ + remove vertex v and all its edges. + """ c = x.component if c not in self.components: return None @@ -165,24 +204,42 @@ def remove_vertex(self, x): return x def order(self): + """ + the order of the graph (number of vertices) + """ return sum([c.order() for c in self.components]) def norm(self): + """ + the norm of the graph (number of edges) + """ return sum([c.norm() for c in self.components]) def deg_min(self): + """ + the minimum degree of vertices + """ return min([c.deg_min() for c in self.components]) def deg_max(self): + """ + the maximum degree of vertices + """ return max([c.deg_max() for c in self.components]) def deg_avg(self): + """ + the average degree of vertices + """ t = 0.0 for c in self.components: t += sum([v.degree() for v in c.verticesPoset]) return t / float(self.order()) def eps(self): + """ + the graph epsilon value (norm/order), average number of edges per vertex. + """ return float(self.norm()) / self.order() def path(self, x, y, f_io = 0, hook = None): @@ -203,4 +260,7 @@ def __contains__(self, G): return r def connected(self): + """ + returns the list of components + """ return len(self.components) == 1 diff --git a/graphistry/layout/graph/graphBase.py b/graphistry/layout/graph/graphBase.py index 0e1b8f51e4..725f45daf9 100644 --- a/graphistry/layout/graph/graphBase.py +++ b/graphistry/layout/graph/graphBase.py @@ -13,41 +13,6 @@ class GraphBase(object): loops (set[Edge]): the set of *loop* edges (of degree 0). directed (bool): indicates if the graph is considered *oriented* or not. - Methods: - vertices(cond=None): generates an iterator over vertices, with optional filter - edges(cond=None): generates an iterator over edges, with optional filter - matrix(cond=None): returns the associativity matrix of the graph component - order(): the order of the graph (number of vertices) - norm(): the norm of the graph (number of edges) - deg_min(): the minimum degree of vertices - deg_max(): the maximum degree of vertices - deg_avg(): the average degree of vertices - eps(): the graph epsilon value (norm/order), average number of edges per vertex. - path(x,y,f_io=0,hook=None): shortest path between vertices x and y by breadth-first descent, - contrained by f_io direction if provided. The path is returned as a list of Vertex objects. - If a *hook* function is provided, it is called at every vertex added to the path, passing - the vertex object as argument. - roots(): returns the list of *roots* (vertices with no inward edges). - leaves(): returns the list of *leaves* (vertices with no outward edges). - add_single_vertex(v): allow a GraphBase to hold a single vertex. - add_edge(e): add edge e. At least one of its vertex must belong to the graph, - the other being added automatically. - remove_edge(e): remove Edge e, asserting that the resulting graph is still connex. - remove_vertex(x): remove Vertex x and all associated edges. - dijkstra(x,f_io=0,hook=None): shortest weighted-edges paths between x and all other vertices - by dijkstra's algorithm with heap used as priority queue. - get_scs_with_feedback(): returns the set of strongly connected components - ("scs") by using Tarjan algorithm. - These are maximal sets of vertices such that there is a path from each - vertex to every other vertex. - The algorithm performs a DFS from the provided list of root vertices. - A cycle is of course a strongly connected component, - but a strongly connected component can include several cycles. - The Feedback Acyclic Set of edge to be removed/reversed is provided by - marking the edges with a "feedback" flag. - Complexity is O(V+E). - partition(): returns a *partition* of the connected graph as a list of lists. - neighbors(v): returns neighbours of a vertex v. """ def __init__(self, vertices = None, edges = None, directed = True): @@ -96,12 +61,21 @@ def __init__(self, vertices = None, edges = None, directed = True): v.component = self def roots(self): + """ + returns the list of *roots* (vertices with no inward edges). + """ return list(filter(lambda v: len(v.e_in()) == 0, self.verticesPoset)) def leaves(self): + """ + returns the list of *leaves* (vertices with no outward edges). + """ return list(filter(lambda v: len(v.e_out()) == 0, self.verticesPoset)) def add_single_vertex(self, v): + """ + allow a GraphBase to hold a single vertex. + """ if len(self.edgesPoset) == 0 and len(self.verticesPoset) == 0: v = self.verticesPoset.add(v) v.component = self @@ -109,6 +83,9 @@ def add_single_vertex(self, v): return None def add_edge(self, e): + """ + add edge e. At least one of its vertex must belong to the graph, the other being added automatically. + """ if e in self.edgesPoset: return self.edgesPoset.get(e) x = e.v[0] @@ -127,6 +104,9 @@ def add_edge(self, e): return e def remove_edge(self, e): + """ + remove Edge e, asserting that the resulting graph is still connex. + """ if e not in self.edgesPoset: return e.detach() @@ -143,6 +123,9 @@ def remove_edge(self, e): return e def remove_vertex(self, x): + """ + remove Vertex x and all associated edges. + """ if x not in self.verticesPoset: return vertices = x.neighbors() # get all neighbor vertices to check paths @@ -168,6 +151,9 @@ def constant_function(self, value): return lambda x: value def vertices(self, cond = None): + """ + generates an iterator over vertices, with optional filter + """ vertices = self.verticesPoset if cond is None: cond = self.constant_function(True) @@ -176,6 +162,9 @@ def vertices(self, cond = None): yield v def edges(self, cond = None): + """ + generates an iterator over edges, with optional filter + """ edges = self.edgesPoset if cond is None: cond = self.constant_function(True) @@ -185,7 +174,7 @@ def edges(self, cond = None): def matrix(self, cond = None): """ - This associativity matrix is like the adjacency matrix but antisymmetric. + This associativity matrix is like the adjacency matrix but antisymmetric. Returns the associativity matrix of the graph component :param cond: same a the condition function in vertices(). :return: array @@ -207,27 +196,46 @@ def matrix(self, cond = None): return mat def order(self): + """ + the order of the graph (number of vertices) + """ return len(self.verticesPoset) def norm(self): """ - The size of the edge poset. + The size of the edge poset (number of edges). """ return len(self.edgesPoset) def deg_min(self): + """ + the minimum degree of vertices + """ return min([v.degree() for v in self.verticesPoset]) def deg_max(self): + """ + the maximum degree of vertices + """ return max([v.degree() for v in self.verticesPoset]) def deg_avg(self): + """ + the average degree of vertices + """ return sum([v.degree() for v in self.verticesPoset]) / float(self.order()) def eps(self): + """ + the graph epsilon value (norm/order), average number of edges per vertex. + """ return float(self.norm()) / self.order() def path(self, x, y, f_io = 0, hook = None): + """ + shortest path between vertices x and y by breadth-first descent, contrained by f_io direction if provided. The path is returned as a list of Vertex objects. + If a *hook* function is provided, it is called at every vertex added to the path, passing the vertex object as argument. + """ assert x in self.verticesPoset assert y in self.verticesPoset x = self.verticesPoset.get(x) @@ -263,6 +271,9 @@ def path(self, x, y, f_io = 0, hook = None): return p def dijkstra(self, x, f_io = 0, hook = None): + """ + shortest weighted-edges paths between x and all other vertices by dijkstra's algorithm with heap used as priority queue. + """ from collections import defaultdict from heapq import heappop, heappush @@ -300,7 +311,11 @@ def dijkstra(self, x, f_io = 0, hook = None): def get_scs_with_feedback(self, roots = None): """ - Minimum FAS algorithm (feedback arc set) creating a DAG. + Minimum FAS algorithm (feedback arc set) creating a DAG. Returns the set of strongly connected components + ("scs") by using Tarjan algorithm. These are maximal sets of vertices such that there is a path from each vertex to every other vertex. + The algorithm performs a DFS from the provided list of root vertices. A cycle is of course a strongly connected component,but a strongly connected component can include several cycles. + The Feedback Acyclic Set of edge to be removed/reversed is provided by marking the edges with a "feedback" flag. + Complexity is O(V+E). :param roots: :return: diff --git a/graphistry/layout/graph/vertexBase.py b/graphistry/layout/graph/vertexBase.py index 07cb8d6794..1a950273f0 100644 --- a/graphistry/layout/graph/vertexBase.py +++ b/graphistry/layout/graph/vertexBase.py @@ -7,17 +7,6 @@ class VertexBase(object): **Attributes** e (list[Edge]): list of edges associated with this vertex. - **Methods** - degree() : degree of the vertex (number of edges). - e_in() : list of edges directed toward this vertex. - e_out(): list of edges directed outward this vertex. - e_dir(int): either e_in, e_out or all edges depending on provided direction parameter (>0 means outward). - neighbors(f_io=0): list of neighbor vertices in all directions (default) or in filtered f_io direction (>0 means outward). - e_to(v): returns the Edge from this vertex directed toward vertex v. - e_from(v): returns the Edge from vertex v directed toward this vertex. - e_with(v): return the Edge with both this vertex and vertex v - detach(): removes this vertex from all its edges and returns this list of edges. - """ def __init__(self): @@ -25,15 +14,27 @@ def __init__(self): self.e = [] def degree(self): + """ + degree() : degree of the vertex (number of edges). + """ return len(self.e) def e_in(self): + """ + e_in() : list of edges directed toward this vertex. + """ return list(filter((lambda e: e.v[1] == self), self.e)) def e_out(self): + """ + e_out(): list of edges directed outward this vertex. + """ return list(filter((lambda e: e.v[0] == self), self.e)) def e_dir(self, dir): + """ + either e_in, e_out or all edges depending on provided direction parameter (>0 means outward). + """ if dir > 0: return self.e_out() if dir < 0: @@ -42,7 +43,7 @@ def e_dir(self, dir): def neighbors(self, direction = 0): """ - Returns the neighbors of this vertex. + Returns the neighbors of this vertex. List of neighbor vertices in all directions (default) or in filtered f_io direction (>0 means outward). :param direction: - 0: parent and children @@ -58,24 +59,36 @@ def neighbors(self, direction = 0): return arr def e_to(self, y): + """ + returns the Edge from this vertex directed toward vertex v. + """ for e in self.e_out(): if e.v[1] == y: return e return None def e_from(self, x): + """ + returns the Edge from vertex v directed toward this vertex. + """ for e in self.e_in(): if e.v[0] == x: return e return None def e_with(self, v): + """ + return the Edge with both this vertex and vertex v + """ for e in self.e: if v in e.v: return e return None def detach(self): + """ + removes this vertex from all its edges and returns this list of edges. + """ E = self.e[:] for e in E: e.detach() From 12d65c36e2110bf15b5c4febb67c7520e3cd207c Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:26:47 -0500 Subject: [PATCH 171/432] fix(docstring): fixed homepage menu and bio --- docs/source/graphistry.rst | 28 ++++++++++++++++++---------- docs/source/index.rst | 4 ++-- docs/source/modules.rst | 12 ++++++------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index 91aa7f08f9..7255d2752c 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -1,42 +1,50 @@ -graphistry package +Overview ================== .. toctree:: :maxdepth: 3 - graphistry.compute + graphistry.layout graphistry.plugins graphistry.plugins_types -graphistry.plotter module -------------------------- +Plotter Module +================== .. automodule:: graphistry.PlotterBase :members: :undoc-members: :show-inheritance: -graphistry.pygraphistry module ------------------------------- +Pygraphistry Module +================== .. automodule:: graphistry.pygraphistry :members: :undoc-members: :show-inheritance: -graphistry.arrow_uploader module --------------------------------- +Arrow uploader Module +================== .. automodule:: graphistry.arrow_uploader :members: :undoc-members: :show-inheritance: -graphistry.ArrowFileUploader module ------------------------------------ +Arrow File Uploader Module +================== .. automodule:: graphistry.ArrowFileUploader :members: :undoc-members: :show-inheritance: + +Versioneer +================== + +.. automodule:: graphistry._version + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index cc734f5581..b45393c266 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,11 +1,11 @@ -PyGraphistry's documentation (|version|) +PyGraphistry[ai]'s documentation ======================================== .. Quickstart: .. `Read our tutorial `_ PyGraphistry is a Python visual graph AI library to extract, transform, analyze, model, and visualize big graphs, and especially alongside Graphistry end-to-end GPU server sessions. Installing optional graphistry[ai] dependencies adds graph autoML, including automatic feature engineering, UMAP, and graph neural net support. Combined, PyGraphistry reduces your time to graph for going from raw data to visualizations and AI models down to three lines of code. -Here in our docstrings you can find useful packages, modules, commands to maximize your graph AI experience with PyGraphistry. For a full tutorial, refer to our `PyGraphistry `_ repo. +Here in our docstrings you can find useful packages, modules, and commands to maximize your graph AI experience with PyGraphistry. In the navbar you can find an overview of all the packages and modules we provided and a few useful highlighted ones as well. You can search for them on our Search page. For a full tutorial, refer to our `PyGraphistry `_ repo. .. toctree:: :maxdepth: 3 diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 2d0d70fd92..71e0a12335 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -1,9 +1,9 @@ -doc -=== +.. doc +.. === -.. toctree:: - :maxdepth: 4 - :caption: Contents: +.. .. toctree:: +.. :maxdepth: 4 +.. :caption: Contents: - versioneer +.. versioneer From 75dd67f41c566b608be2815de564c07c8a7f527c Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:27:12 -0500 Subject: [PATCH 172/432] fix(docstring): made block code visible --- graphistry/plugins/cugraph.py | 3 +++ graphistry/plugins/igraph.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/graphistry/plugins/cugraph.py b/graphistry/plugins/cugraph.py index b5f070af72..d769ba89ce 100644 --- a/graphistry/plugins/cugraph.py +++ b/graphistry/plugins/cugraph.py @@ -239,16 +239,19 @@ def compute_cugraph( **Example: Pagerank** :: + g2 = g.compute_cugraph('pagerank') assert 'pagerank' in g2._nodes.columns **Example: Katz centrality with rename** :: + g2 = g.compute_cugraph('katz_centrality', out_col='katz_centrality_renamed') assert 'katz_centrality_renamed' in g2._nodes.columns **Example: Pass params to cugraph** :: + g2 = g.compute_cugraph('k_truss', params={'k': 2}) assert 'k_truss' in g2._nodes.columns diff --git a/graphistry/plugins/igraph.py b/graphistry/plugins/igraph.py index a5bab3ac19..5fe5c2f8a6 100644 --- a/graphistry/plugins/igraph.py +++ b/graphistry/plugins/igraph.py @@ -53,6 +53,7 @@ def from_igraph(self, **Example: Convert from igraph, including all node/edge properties** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a', 'b', 'c', 'd'], 'd': ['b', 'c', 'd', 'e'], 'v': [101, 102, 103, 104]}) g = graphistry.edges(edges, 's', 'd').materialize_nodes().get_degrees() @@ -62,6 +63,7 @@ def from_igraph(self, **Example: Enrich from igraph, but only load in 1 node attribute** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a', 'b', 'c', 'd'], 'd': ['b', 'c', 'd', 'e'], 'v': [101, 102, 103, 104]}) g = graphistry.edges(edges, 's', 'd').materialize_nodes().get_degree() @@ -198,7 +200,8 @@ def from_igraph(self, return g -def to_igraph(self: Plottable, +def to_igraph( + self: Plottable, directed: bool = True, include_nodes: bool = True, node_attributes: Optional[List[str]] = None, @@ -309,8 +312,8 @@ def compute_igraph( :rtype: Plotter **Example: Pagerank** - :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['c','c','e','e']}) g = graphistry.edges(edges, 's', 'd') @@ -319,6 +322,7 @@ def compute_igraph( **Example: Pagerank with custom name** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['c','c','e','e']}) g = graphistry.edges(edges, 's', 'd') @@ -327,6 +331,7 @@ def compute_igraph( **Example: Pagerank on an undirected** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['c','c','e','e']}) g = graphistry.edges(edges, 's', 'd') @@ -334,7 +339,8 @@ def compute_igraph( assert 'pagerank' in g2._nodes.columns **Example: Pagerank with custom parameters** - :: + :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['c','c','e','e']}) g = graphistry.edges(edges, 's', 'd') @@ -447,6 +453,7 @@ def layout_igraph( **Example: Sugiyama layout** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['b','c','d','e']}) g = graphistry.edges(edges, 's', 'd') @@ -456,6 +463,7 @@ def layout_igraph( **Example: Change which column names are generated** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['b','c','d','e']}) g = graphistry.edges(edges, 's', 'd') @@ -466,6 +474,7 @@ def layout_igraph( **Example: Pass parameters to layout methods - Sort nodes by degree** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['b','c','d','e']}) g = graphistry.edges(edges, 's', 'd') From bbd8d55eb5504a9512f77fdbf47becf33873d2ad Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:27:34 -0500 Subject: [PATCH 173/432] fix(docstring): made versioneer populate --- docs/source/versioneer.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/versioneer.rst b/docs/source/versioneer.rst index 804c171da3..a34edfc48d 100644 --- a/docs/source/versioneer.rst +++ b/docs/source/versioneer.rst @@ -1,2 +1,2 @@ -versioneer module -================= +.. versioneer module +.. ================= From 5e0f979c08147e74f0ee20601d1ecab3d3d3b196 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:46:18 -0500 Subject: [PATCH 174/432] fix(docstring): added featurize and umap --- docs/source/graphistry.rst | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index 7255d2752c..9af86845c9 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -25,6 +25,28 @@ Pygraphistry Module :undoc-members: :show-inheritance: +Featurize +================== +.. automodule:: graphistry.feature_utils + :members: + :undoc-members: + :show-inheritance: + + +UMAP +================== +.. automodule:: graphistry.umap_utils + :members: + :undoc-members: + :show-inheritance: + +DB Scan +================== +.. automodule:: graphistry.compute. + :members: + :undoc-members: + :show-inheritance: + Arrow uploader Module ================== @@ -47,4 +69,5 @@ Versioneer .. automodule:: graphistry._version :members: :undoc-members: - :show-inheritance: \ No newline at end of file + :show-inheritance: + From 1d4e73a3ba67d34959521f19d0c4c4a5daafd92e Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 27 Feb 2023 17:12:18 +0800 Subject: [PATCH 175/432] Update setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 68d0dbe70a..7f2efad62b 100755 --- a/setup.py +++ b/setup.py @@ -38,9 +38,9 @@ def unique_flatten_dict(d): } base_extras_heavy = { - 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'] + ['cuCat @ git+https://github.com/graphistry/cuCat/@0.01.0'] , + 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] + ['cuCat @ git+https://github.com/graphistry/cuCat/@0.01.0'] base_extras = {**base_extras_light, **base_extras_heavy} From eb7e61e6e5f2cb017c97933547062f08fdd2293a Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 27 Feb 2023 17:44:58 +0800 Subject: [PATCH 176/432] partial response to comments --- graphistry/feature_utils.py | 35 ++++++++++++++++++++--------------- setup.py | 5 ++++- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 4697b6795e..9e3775968a 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -48,15 +48,15 @@ GapEncoder = Any SimilarityEncoder = Any try: - from cuCat import ( + from cu_cat import ( SuperVectorizer, GapEncoder, SimilarityEncoder, ) except: - SuperVectorizer = Any - GapEncoder = Any - SimilarityEncoder = Any + SuperVectorizer = object + GapEncoder = object + SimilarityEncoder = object try: from sklearn.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin @@ -101,17 +101,22 @@ def lazy_import_has_min_dependancy(): except ModuleNotFoundError as e: return False, e -def lazy_import_has_cuml_dependancy(): +def lazy_import_has_cu_cat_dependancy(): import warnings warnings.filterwarnings("ignore") try: import scipy.sparse # noqa from scipy import __version__ as scipy_version - from cuCat import __version__ as cuCat_version + from cu_cat import __version__ as cu_cat_version from sklearn import __version__ as sklearn_version + from cuml import __verison__ as cuml_version + from cudf import __verison__ as cudf_version logger.debug(f"SCIPY VERSION: {scipy_version}") - logger.debug(f"cuCat VERSION: {cuCat_version}") + logger.debug(f"Cuda CAT VERSION: {cu_cat_version}") logger.debug(f"sklearn VERSION: {sklearn_version}") + logger.debug(f"cuml VERSION: {cuml_version}") + logger.debug(f"cudf VERSION: {cudf_version}") + return True, 'ok' except ModuleNotFoundError as e: return False, e @@ -139,7 +144,7 @@ def assert_cuml_imported(): has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cuml_dependancy() if not has_cuml_dependancy_: logger.error( # noqa - "cunl not found, trying running" # noqa + "cuml not found, trying running" # noqa "`pip install rapids`" # noqa ) raise import_cuml_exn @@ -168,7 +173,7 @@ def assert_cuml_imported(): # # _featurize_or_get_edges_dataframe_if_X_is_None -FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cuCat"] +FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] @@ -176,7 +181,7 @@ def resolve_feature_engine( feature_engine: FeatureEngine, ) -> FeatureEngineConcrete: # noqa - if feature_engine in ["none", "pandas", "dirty_cat", "torch", "cuCat"]: + if feature_engine in ["none", "pandas", "dirty_cat", "torch", "cu_cat"]: return feature_engine # type: ignore if feature_engine == "auto": @@ -185,7 +190,7 @@ def resolve_feature_engine( return "torch" has_cuml_dependancy_, _ = lazy_import_has_cuml_dependancy() if has_cuml_dependancy_: - return "cuCat" + return "cu_cat" has_min_dependancy_, _ = lazy_import_has_min_dependancy() if has_min_dependancy_: return "dirty_cat" @@ -193,7 +198,7 @@ def resolve_feature_engine( raise ValueError( # noqa f'feature_engine expected to be "none", ' - '"pandas", "dirty_cat", "torch", "cuCat", or "auto"' + '"pandas", "dirty_cat", "torch", "cu_cat", or "auto"' f'but received: {feature_engine} :: {type(feature_engine)}' ) @@ -929,8 +934,8 @@ def process_dirty_dataframes( """ if feature_engine == 'dirty_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - elif feature_engine == 'cuCat': - from cuCat import SuperVectorizer, GapEncoder, SimilarityEncoder + elif feature_engine == 'cu_cat': + from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder from sklearn.preprocessing import FunctionTransformer t = time() @@ -2375,7 +2380,7 @@ def featurize( """ if feature_engine == 'dirty_cat': assert_imported() - elif feature_engine == 'cuCat': + elif feature_engine == 'cu_cat': assert_cuml_imported() if inplace: res = self diff --git a/setup.py b/setup.py index 7f2efad62b..e4a757b4b5 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,10 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] + ['cuCat @ git+https://github.com/graphistry/cuCat/@0.01.0'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] + +base_extras_heavy['cu-cat'] = + ['cu-cat @ git+https://github.com/graphistry/cuCat/@0.01.0'] + base_extras = {**base_extras_light, **base_extras_heavy} From f22f56575112a83b008bf24561fa5b50ab676e22 Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 27 Feb 2023 20:10:10 +0800 Subject: [PATCH 177/432] more comments fixes --- graphistry/feature_utils.py | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 9e3775968a..98008ee803 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -141,7 +141,7 @@ def assert_imported(): raise import_min_exn def assert_cuml_imported(): - has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cuml_dependancy() + has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cu_cat_dependancy() if not has_cuml_dependancy_: logger.error( # noqa "cuml not found, trying running" # noqa @@ -188,7 +188,7 @@ def resolve_feature_engine( has_dependancy_text_, _, _ = lazy_import_has_dependancy_text() if has_dependancy_text_: return "torch" - has_cuml_dependancy_, _ = lazy_import_has_cuml_dependancy() + has_cuml_dependancy_, _ = lazy_import_has_cu_cat_dependancy() if has_cuml_dependancy_: return "cu_cat" has_min_dependancy_, _ = lazy_import_has_min_dependancy() diff --git a/setup.py b/setup.py index e4a757b4b5..582378259d 100755 --- a/setup.py +++ b/setup.py @@ -40,9 +40,9 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] + base_extras_heavy['cu-cat'] -base_extras_heavy['cu-cat'] = + ['cu-cat @ git+https://github.com/graphistry/cuCat/@0.01.0'] +base_extras_heavy['cu-cat'] = ['cu-cat @ git+https://github.com/graphistry/cuCat/@0.01.0'] base_extras = {**base_extras_light, **base_extras_heavy} From 5e0e08c993ec43cbda87085ab0d0070e12141703 Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 27 Feb 2023 20:12:08 +0800 Subject: [PATCH 178/432] more comments fixes --- graphistry/feature_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 98008ee803..a900c85986 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -54,9 +54,9 @@ SimilarityEncoder, ) except: - SuperVectorizer = object - GapEncoder = object - SimilarityEncoder = object + SuperVectorizer = Any + GapEncoder = Any + SimilarityEncoder = Any try: from sklearn.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin From 708baa3dea3c3dfaf9a5a3484eba535cc882ac42 Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 27 Feb 2023 20:52:24 +0800 Subject: [PATCH 179/432] more comments fixes --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 582378259d..8b6db91972 100755 --- a/setup.py +++ b/setup.py @@ -40,10 +40,10 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] + base_extras_heavy['cu-cat'] - base_extras_heavy['cu-cat'] = ['cu-cat @ git+https://github.com/graphistry/cuCat/@0.01.0'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] + base_extras_heavy['cu-cat'] + base_extras = {**base_extras_light, **base_extras_heavy} From a6e1ab011b01c55196c3c2b8eb401c73ead70056 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 27 Feb 2023 14:33:50 -0500 Subject: [PATCH 180/432] fix(docstring): formatting changes/updates --- graphistry/compute/cluster.py | 10 ++++ graphistry/feature_utils.py | 98 +++++++++++++++++++++-------------- graphistry/text_utils.py | 9 ++-- 3 files changed, 74 insertions(+), 43 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 15b7cf0ed3..be181bc938 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -212,6 +212,8 @@ def dbscan( """DBSCAN clustering on cpu or gpu infered automatically. Adds a `_dbscan` column to nodes or edges. Examples: + :: + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') # cluster by UMAP embeddings @@ -333,22 +335,30 @@ def transform_dbscan( Graph nodes | edges will be colored by '_dbscan' column. Examples: + :: + fit: g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') g2 = g.featurize().dbscan() predict: + :: + emb, X, _, ndf = g2.transform_dbscan(ndf, return_graph=False) # or g3 = g2.transform_dbscan(ndf, return_graph=True) g3.plot() likewise for umap: + :: + fit: g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node') g2 = g.umap(X=.., y=..).dbscan() predict: + :: + emb, X, y, ndf = g2.transform_dbscan(ndf, ndf, return_graph=False) # or g3 = g2.transform_dbscan(ndf, ndf, return_graph=True) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index eebbb4a4df..462ca18a11 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -499,7 +499,6 @@ class Embedding: """ Generates random embeddings of a given dimension that aligns with the index of the dataframe - _____________________________________________________________________ """ def __init__(self, df: pd.DataFrame): @@ -1754,10 +1753,11 @@ def fit_transform(self, src=None, dst=None, *args, **kwargs): def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): """Fits new scaling functions on df, y via args-kwargs - example: + **Example:** + :: + from graphisty.features import SCALERS, SCALER_OPTIONS print(SCALERS) - g = graphistry.nodes(df) # set a scaling strategy for features and targets -- umap uses those and produces different results depending. g2 = g.umap(use_scaler='standard', use_scaler_target=None) @@ -1770,6 +1770,8 @@ def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): clf.fit(X_scaled, y_scaled) args: + :: + X: pd.DataFrame of features y: pd.DataFrame of target features kind: str, one of 'nodes' or 'edges' @@ -1880,14 +1882,20 @@ class FeatureMixin(MIXIN_BASE): Subclasses UMAPMixin for umap-ing of automatic features. Usage: + :: + g = graphistry.nodes(df, 'node_column') g2 = g.featurize() or for edges, + :: + g = graphistry.edges(df, 'src', 'dst') g2 = g.featurize(kind='edges') or chain them for both nodes and edges, + :: + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node_column') g2 = g.featurize().featurize(kind='edges') @@ -2202,21 +2210,24 @@ def transform(self, df: pd.DataFrame, """ Transform new data and append to existing graph, or return dataframes - args: - df: pd.DataFrame, raw data to transform - ydf: pd.DataFrame, optional - kind: str # one of `nodes`, `edges` - return_graph: bool, if True, will return a graph with inferred edges. - merge_policy: bool, if True, adds batch to existing graph nodes via nearest neighbors. - If False, will infer edges only between nodes in the batch, default False - min_dist: float, if return_graph is True, will use this value in NN search, or 'auto' to infer a good value - min_dist represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. - sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph - n_neighbors: int, if return_graph is True, will use this value for n_neighbors in Nearest Neighbors search - scaled: bool, if True, will use scaled transformation of data set during featurization, default True - verbose: bool, if True, will print metadata about the graph construction, default False - returns: - X, y: pd.DataFrame, transformed data if return_graph is False + **args:** + :: + + # df: pd.DataFrame, raw data to transform + # ydf: pd.DataFrame, optional + # kind: str # one of `nodes`, `edges` + # return_graph: bool, if True, will return a graph with inferred edges. + # merge_policy: bool, if True, adds batch to existing graph nodes via nearest neighbors. + # If False, will infer edges only between nodes in the batch, default False + # min_dist: float, if return_graph is True, will use this value in NN search, or 'auto' to infer a good value + # min_dist represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. + # sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph + # n_neighbors: int, if return_graph is True, will use this value for n_neighbors in Nearest Neighbors search + # scaled: bool, if True, will use scaled transformation of data set during featurization, default True + # verbose: bool, if True, will print metadata about the graph construction, default False + **Returns:** + + X, y: pd.DataFrame, transformed data if return_graph is False or a graphistry Plottable with inferred edges if return_graph is True """ if kind == "nodes": @@ -2255,7 +2266,9 @@ def scale( ): """Scale data using the same scalers as used in the featurization step. - example usage: + **Example** + :: + g = graphistry.nodes(df) X, y = g.featurize().scale(kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) @@ -2271,25 +2284,29 @@ def scale( clf.fit(X_scaled, y_scaled) - args: - df: pd.DataFrame, raw data to transform, if None, will use data from featurization fit - y: pd.DataFrame, optional target data - kind: str, one of `nodes`, `edges` - use_scaler: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` - use_scaler_target: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` - impute: bool, if True, will impute missing values - n_quantiles: int, number of quantiles to use for quantile scaler - output_distribution: str, one of `normal`, `uniform`, `lognormal` - quantile_range: tuple, range of quantiles to use for quantile scaler - n_bins: int, number of bins to use for KBinsDiscretizer - encode: str, one of `ordinal`, `onehot`, `onehot-dense`, `binary` - strategy: str, one of `uniform`, `quantile`, `kmeans` - keep_n_decimals: int, number of decimals to keep after scaling - return_scalers: bool, if True, will return the scalers used to scale the data - returns: - (X, y) transformed data if return_graph is False + **Args:** + :: + + # df: pd.DataFrame, raw data to transform, if None, will use data from featurization fit + # y: pd.DataFrame, optional target data + # kind: str, one of `nodes`, `edges` + # use_scaler: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` + # use_scaler_target: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` + # impute: bool, if True, will impute missing values + # n_quantiles: int, number of quantiles to use for quantile scaler + # output_distribution: str, one of `normal`, `uniform`, `lognormal` + # quantile_range: tuple, range of quantiles to use for quantile scaler + # n_bins: int, number of bins to use for KBinsDiscretizer + # encode: str, one of `ordinal`, `onehot`, `onehot-dense`, `binary` + # strategy: str, one of `uniform`, `quantile`, `kmeans` + # keep_n_decimals: int, number of decimals to keep after scaling + # return_scalers: bool, if True, will return the scalers used to scale the data + + **Returns:** + + (X, y) transformed data if return_graph is False or a graph with inferred edges if return_graph is True, - or (X, y, scaler, scaler_target) if return_scalers is True + or (X, y, scaler, scaler_target) if return_scalers is True """ if df is None: # use the original data @@ -2774,7 +2791,8 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False) -> pd.DataFrame: - """Returns feature matrix, and if columns are specified, returns matrix with only the columns that contain + """ + Returns feature matrix, and if columns are specified, returns matrix with only the columns that contain the string `column_part` in their name. `X = g.get_matrix(['feature1', 'feature2'])` @@ -2786,7 +2804,9 @@ def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'no Powerful way to retrieve features from a featurized graph by column or (top) features of interest. - example: + **Example:** + :: + # get the full feature matrices X = g.get_matrix() y = g.get_matrix(target=True) diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 1378b01f91..ff6f2eabbb 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -125,18 +125,19 @@ def search( If node data is not yet feature-encoded (and explicit edges are given), run automatic feature engineering: - ``` + :: + g2 = g.featurize(kind='nodes', X=['text_col_1', ..], min_words=0 # forces all named columns are textually encoded ) - ``` If edges do not yet exist, generate them via - ``` + :: + g2 = g.umap(kind='nodes', X=['text_col_1', ..], min_words=0 # forces all named columns are textually encoded ) - ``` + If an index is not yet built, it is generated `g2.build_index()` on the fly at search time. Otherwise, can set `g2.build_index()` and then subsequent `g2.search(...)` calls will be not rebuilt index. From 4780f5e1c1f3b9892d1df0a9508fbbdc73120325 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 27 Feb 2023 14:34:26 -0500 Subject: [PATCH 181/432] fix(docstring): added umap, SS, dbscan to navbar --- docs/source/graphistry.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index 9af86845c9..71cf81bb41 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -40,9 +40,17 @@ UMAP :undoc-members: :show-inheritance: -DB Scan + +Semantic Search +================== +.. automodule:: graphistry.text_utils + :members: + :undoc-members: + :show-inheritance: + +DBScan ================== -.. automodule:: graphistry.compute. +.. automodule:: graphistry.compute.cluster :members: :undoc-members: :show-inheritance: From 5f2c28c646d4bee3d296684ae5c34041349ec7a5 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 27 Feb 2023 15:48:04 -0500 Subject: [PATCH 182/432] fix(docstring): fixed arg formatting on dbscan --- graphistry/compute/cluster.py | 52 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index be181bc938..f19fbfbe38 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -71,11 +71,11 @@ def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, targe Allows for a single function to get the model matrix for both nodes and edges as well as targets, embeddings, and features Args: - g: graphistry graph - kind: 'nodes' or 'edges' - cols: list of columns to use for clustering given `g.featurize` has been run - umap: whether to use UMAP embeddings or features dataframe - target: whether to use the target dataframe or features dataframe + :g: graphistry graph + :kind: 'nodes' or 'edges' + :cols: list of columns to use for clustering given `g.featurize` has been run + :umap: whether to use UMAP embeddings or features dataframe + :target: whether to use the target dataframe or features dataframe Returns: pd.DataFrame: dataframe of model matrix given the inputs @@ -99,11 +99,11 @@ def dbscan_fit(g: Any, dbscan: Any, kind: str = "nodes", cols: Optional[Union[Li Fits clustering on UMAP embeddings if umap is True, otherwise on the features dataframe or target dataframe if target is True. - args: - g: graphistry graph - kind: 'nodes' or 'edges' - cols: list of columns to use for clustering given `g.featurize` has been run - use_umap_embedding: whether to use UMAP embeddings or features dataframe for clustering (default: True) + Args: + :g: graphistry graph + :kind: 'nodes' or 'edges' + :cols: list of columns to use for clustering given `g.featurize` has been run + :use_umap_embedding: whether to use UMAP embeddings or features dataframe for clustering (default: True) """ X = get_model_matrix(g, kind, cols, use_umap_embedding, target) @@ -246,14 +246,14 @@ def dbscan( https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb Args: - min_dist float: The maximum distance between two samples for them to be considered as in the same neighborhood. - kind str: 'nodes' or 'edges' - cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features or targets by + :min_dist float: The maximum distance between two samples for them to be considered as in the same neighborhood. + :kind str: 'nodes' or 'edges' + :cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features or targets by fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] - fit_umap_embedding bool: whether to use UMAP embeddings or features dataframe to cluster DBSCAN - min_samples: The number of samples in a neighborhood for a point to be considered as a core point. + :fit_umap_embedding bool: whether to use UMAP embeddings or features dataframe to cluster DBSCAN + :min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. - target: whether to use the target column as the clustering feature + :target: whether to use the target column as the clustering feature """ @@ -358,28 +358,28 @@ def transform_dbscan( predict: :: - + emb, X, y, ndf = g2.transform_dbscan(ndf, ndf, return_graph=False) # or g3 = g2.transform_dbscan(ndf, ndf, return_graph=True) g3.plot() - args: - df: dataframe to transform - y: optional labels dataframe - min_dist: The maximum distance between two samples for them to be considered as in the same neighborhood. + Args: + :df: dataframe to transform + :y: optional labels dataframe + :min_dist: The maximum distance between two samples for them to be considered as in the same neighborhood. smaller values will result in less edges between the minibatch and the original graph. Default 'auto', infers min_dist from the mean distance and std of new points to the original graph - fit_umap_embedding: whether to use UMAP embeddings or features dataframe when inferring edges between + :fit_umap_embedding: whether to use UMAP embeddings or features dataframe when inferring edges between the minibatch and the original graph. Default False, uses the features dataframe - sample: number of samples to use when inferring edges between the minibatch and the original graph, + :sample: number of samples to use when inferring edges between the minibatch and the original graph, if None, will only use closest point to the minibatch. If greater than 0, will sample the closest `sample` points in existing graph to pull in more edges. Default None - kind: 'nodes' or 'edges' - return_graph: whether to return a graph or the (emb, X, y, minibatch df enriched with DBSCAN labels), default True + :kind: 'nodes' or 'edges' + :return_graph: whether to return a graph or the (emb, X, y, minibatch df enriched with DBSCAN labels), default True infered graph supports kind='nodes' only. - verbose: whether to print out progress, default False + :verbose: whether to print out progress, default False """ emb, X, y, df = self._transform_dbscan(df, y, kind=kind, verbose=verbose) From c4fab96b072e49bcbb8852b9e102a401242b2d57 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:03:01 -0500 Subject: [PATCH 183/432] fix(docstring): fixed arg formatting on UMAP --- graphistry/umap_utils.py | 64 ++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 2107710a3d..38ad606f26 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -282,17 +282,17 @@ def transform_umap(self, df: pd.DataFrame, """Transforms data into UMAP embedding args: - df: Dataframe to transform - y: Target column - kind: One of `nodes` or `edges` - min_dist: Epsilon for including neighbors in infer_graph - n_neighbors: Number of neighbors to use for contextualization - merge_policy: if True, use previous graph, adding new batch to existing graph's neighbors + :df: Dataframe to transform + :y: Target column + :kind: One of `nodes` or `edges` + :min_dist: Epsilon for including neighbors in infer_graph + :n_neighbors: Number of neighbors to use for contextualization + :merge_policy: if True, use previous graph, adding new batch to existing graph's neighbors useful to contextualize new data against existing graph. If False, `sample` is irrelevant. - sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs - return_graph: Whether to return a graph or just the embeddings - fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data - verbose: Whether to print information about the graph inference + :sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs + :return_graph: Whether to return a graph or just the embeddings + :fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data + :verbose: Whether to print information about the graph inference """ X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) emb = self._umap.transform(X) # type: ignore @@ -437,47 +437,47 @@ def umap( Parameters ---------- - X: either a dataframe ndarray of features, or column names to featurize - y: either an dataframe ndarray of targets, or column names to featurize + :X: either a dataframe ndarray of features, or column names to featurize + :y: either an dataframe ndarray of targets, or column names to featurize targets - kind: `nodes` or `edges` or None. + :kind: `nodes` or `edges` or None. If None, expects explicit X, y (optional) matrices, and will Not associate them to nodes or edges. If X, y (optional) is given, with kind = [nodes, edges], it will associate new matrices to nodes or edges attributes. - scale: multiplicative scale for pruning weighted edge DataFrame + :scale: multiplicative scale for pruning weighted edge DataFrame gotten from UMAP, between [0, ..) with high end meaning keep all edges - n_neighbors: UMAP number of nearest neighbors to include for + :n_neighbors: UMAP number of nearest neighbors to include for UMAP connectivity, lower makes more compact layouts. Minimum 2 - min_dist: UMAP float between 0 and 1, lower makes more compact + :min_dist: UMAP float between 0 and 1, lower makes more compact layouts. - spread: UMAP spread of values for relaxation - local_connectivity: UMAP connectivity parameter - repulsion_strength: UMAP repulsion strength - negative_sample_rate: UMAP negative sampling rate - n_components: number of components in the UMAP projection, + :spread: UMAP spread of values for relaxation + :local_connectivity: UMAP connectivity parameter + :repulsion_strength: UMAP repulsion strength + :negative_sample_rate: UMAP negative sampling rate + :n_components: number of components in the UMAP projection, default 2 - metric: UMAP metric, default 'euclidean'. + :metric: UMAP metric, default 'euclidean'. see (UMAP-LEARN)[https://umap-learn.readthedocs.io/ en/latest/parameters.html] documentation for more. - suffix: optional suffix to add to x, y attributes of umap. - play: Graphistry play parameter, default 0, how much to evolve + :suffix: optional suffix to add to x, y attributes of umap. + :play: Graphistry play parameter, default 0, how much to evolve the network during clustering. 0 preserves the original UMAP layout. - encode_weight: if True, will set new edges_df from + :encode_weight: if True, will set new edges_df from implicit UMAP, default True. - encode_position: whether to set default plotting bindings + :encode_position: whether to set default plotting bindings -- positions x,y from umap for .plot(), default True - dbscan: whether to run DBSCAN on the UMAP embedding, default False. - engine: selects which engine to use to calculate UMAP: + :dbscan: whether to run DBSCAN on the UMAP embedding, default False. + :engine: selects which engine to use to calculate UMAP: default "auto" will use cuML if available, otherwise UMAP-LEARN. - feature_engine: How to encode data + :feature_engine: How to encode data ("none", "auto", "pandas", "dirty_cat", "torch") - inplace: bool = False, whether to modify the current object, default False. + :inplace: bool = False, whether to modify the current object, default False. when False, returns a new object, useful for chaining in a functional paradigm. - memoize: whether to memoize the results of this method, + :memoize: whether to memoize the results of this method, default True. - verbose: whether to print out extra information, default False. + :verbose: whether to print out extra information, default False. :return: self, with attributes set with new data """ if engine == UMAP_LEARN: From 329cbc69c239b9dc1c192c3aaa36c06005aa6ee0 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:22:13 -0500 Subject: [PATCH 184/432] fix(docstr): fixed args formatting on seman search --- graphistry/text_utils.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index ff6f2eabbb..c714c42a9b 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -143,16 +143,16 @@ def search( calls will be not rebuilt index. Args: - query (str): natural language query. - cols (list or str, optional): if fuzzy=False, select which column to query. + :query (str): natural language query. + :cols (list or str, optional): if fuzzy=False, select which column to query. Defaults to None since fuzzy=True by defaul. - thresh (float, optional): distance threshold from query vector to returned results. + :thresh (float, optional): distance threshold from query vector to returned results. Defaults to 5000, set large just in case, but could be as low as 10. - fuzzy (bool, optional): if True, uses embedding + annoy index for recall, + :fuzzy (bool, optional): if True, uses embedding + annoy index for recall, otherwise does string matching over given `cols` Defaults to True. - top_n (int, optional): how many results to return. Defaults to 100. + :top_n (int, optional): how many results to return. Defaults to 100. Returns: pd.DataFrame, vector_encoding_of_query: @@ -194,15 +194,15 @@ def search_graph( See help(g.search) for more information Args: - query (str): query input eg "coding best practices" - scale (float, optional): edge weigh threshold, Defaults to 0.5. - top_n (int, optional): how many results to return. Defaults to 100. - thresh (float, optional): distance threshold from query vector to returned results. + :query (str): query input eg "coding best practices" + :scale (float, optional): edge weigh threshold, Defaults to 0.5. + :top_n (int, optional): how many results to return. Defaults to 100. + :thresh (float, optional): distance threshold from query vector to returned results. Defaults to 5000, set large just in case, but could be as low as 10. - broader (bool, optional): if True, will retrieve entities connected via an edge + :broader (bool, optional): if True, will retrieve entities connected via an edge that were not necessarily bubbled up in the results_dataframe. Defaults to False. - inplace (bool, optional): whether to return new instance (default) or mutate self. + :inplace (bool, optional): whether to return new instance (default) or mutate self. Defaults to False. Returns: From dc5f0cb113b9d65bd24c8face0b879f726f3be26 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:28:10 -0500 Subject: [PATCH 185/432] fix(docstr): minor formatting fix --- graphistry/text_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index c714c42a9b..314d37c025 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -155,10 +155,10 @@ def search( :top_n (int, optional): how many results to return. Defaults to 100. Returns: - pd.DataFrame, vector_encoding_of_query: - * rank ordered dataframe of results matching query - * vector encoding of query via given transformer/ngrams model if fuzzy=True - else None + **pd.DataFrame, vector_encoding_of_query:** + rank ordered dataframe of results matching query + + vector encoding of query via given transformer/ngrams model if fuzzy=True else None """ if not fuzzy: if cols is None: From 5f5102aee57b442c647fa7124ac3e3b6658b8f0a Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:28:34 -0500 Subject: [PATCH 186/432] fix(docstr): changed overview nav name --- docs/source/graphistry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index 71cf81bb41..a6fdf5cf15 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -1,4 +1,4 @@ -Overview +Layout & Plugins ================== .. toctree:: :maxdepth: 3 From 72d7c2667d10a6fe38ca17d64062a325187a94ab Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Feb 2023 16:09:04 -0800 Subject: [PATCH 187/432] swaps out ANNOY for FAISS --- graphistry/ai_utils.py | 89 +++++++++++++++++-------------------- graphistry/feature_utils.py | 2 +- graphistry/text_utils.py | 18 +++----- 3 files changed, 47 insertions(+), 62 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index e29c334785..66d2b868e0 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -3,8 +3,7 @@ import graphistry -from .constants import N_TREES, DISTANCE, WEIGHT, BATCH -from .features import N_NEIGHBORS +from .constants import DISTANCE, WEIGHT, BATCH from logging import getLogger logger = getLogger(__name__) @@ -133,49 +132,41 @@ def get_graphistry_from_milieu_search( return g + + # ######################################################################################################################### # # Graphistry Vector Search Index # ########################################################################################################################## - - -def build_annoy_index(X, angular, n_trees=None): - """Builds an Annoy Index for fast vector search - - Args: - X (_type_): _description_ - angular (_type_): _description_ - n_trees (_type_, optional): _description_. Defaults to None. - - Returns: - _type_: _description_ - """ - from annoy import AnnoyIndex # type: ignore - - logger.info(f"Building Index of size {X.shape}") - - if angular: - logger.info("-using angular metric") - metric = "angular" - else: - logger.info("-using euclidean metric") - metric = "euclidean" - - search_index = AnnoyIndex(X.shape[1], metric) - # Add all the feature vectors to the search index - for i in range(len(X)): - search_index.add_item(i, X.values[i]) - if n_trees is None: - n_trees = N_TREES - - logger.info(f"-building index with {n_trees} trees") - search_index.build(n_trees) - return search_index - - -def query_by_vector(vect, df, search_index, top_n): - """ Query by vector using annoy index and append distance to results +# import faiss +# import numpy as np + +class FaissVectorSearch: + def __init__(self, M): + import faiss + import numpy as np + self.index = faiss.IndexFlatL2(M.shape[1]) + self.index.add(M) + + def search(self, q, k=5): + """ + Search for the k nearest neighbors of a query vector q. + + Parameters: + - q: the query vector to search for + - k: the number of nearest neighbors to return (default: 5) + + Returns: + - I: a numpy array of size (k,) containing the indices of the k nearest neighbors + - D: a numpy array of size (k,) containing the distances to the k nearest neighbors + """ + q = np.asarray(q, dtype=np.float32) + D, I = self.index.search(q.reshape(1, -1), k) + return I[0], D[0] + + def search_df(self, q, df, k): + """ Query by vector using annoy index and append distance to results it is assumed len(vect) == len(df) == len(search_index) args: @@ -185,16 +176,15 @@ def query_by_vector(vect, df, search_index, top_n): top_n: number of results to return returns: sorted dataframe with top_n results and distance - """ - indices, distances = search_index.get_nns_by_vector( - vect.values[0], top_n, include_distances=True - ) + """ + + indices, distances = self.search(q.values[0], k=k) - results = df.iloc[indices] - results[DISTANCE] = distances - results = results.sort_values(by=[DISTANCE]) + results = df.iloc[indices] + results.loc[:, DISTANCE] = distances + results = results.sort_values(by=[DISTANCE]) - return results + return results # ######################################################################################################################### @@ -479,3 +469,6 @@ def infer_self_graph(res, # ######################################################### print("-" * 50) if verbose else None return hydrate_graph(res, df, new_edges, node, src, dst, emb, X, y) + + + diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index eebbb4a4df..c9b4a9174c 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1944,7 +1944,7 @@ def _featurize_nodes( memoize: bool = True, verbose: bool = False, ): - res = self.bind() # was self.copy() but changing to test + res = self.copy() ndf = res._nodes node = res._node diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 1378b01f91..eb1f8cab86 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -1,7 +1,7 @@ import pandas as pd from .feature_utils import FeatureMixin -from .ai_utils import search_to_df, build_annoy_index, query_by_vector +from .ai_utils import search_to_df, FaissVectorSearch from .constants import WEIGHT, DISTANCE from logging import getLogger @@ -37,24 +37,18 @@ def assert_features_line_up_with_nodes(self): f"found nodes: {a}, feats: {b}. Did you mutate nodes between fit?" ) - def _build_search_index(self, X, angular=False, n_trees=None): - # builds local index from X - return build_annoy_index(X, angular, n_trees) - def build_index(self, angular=False, n_trees=None): # builds local index self.assert_fitted() self.assert_features_line_up_with_nodes() - X = self._get_feature("nodes") - - self.search_index = self._build_search_index(X, angular, n_trees) + self.search_index = FaissVectorSearch(X.values) #self._build_search_index(X, angular, n_trees, faiss=False) def _query_from_dataframe(self, qdf: pd.DataFrame, top_n: int, thresh: float): # Use the loaded featurizers to transform the dataframe vect, _ = self.transform(qdf, None, kind="nodes", return_graph=False) - results = query_by_vector(vect, self._nodes, self.search_index, top_n) + results = self.search_index.search_df(vect, self._nodes, top_n) results = results.query(f"{DISTANCE} < {thresh}") return results, vect @@ -138,9 +132,7 @@ def search( ) ``` If an index is not yet built, it is generated `g2.build_index()` on the fly at search time. - Otherwise, can set `g2.build_index()` and then subsequent `g2.search(...)` - calls will be not rebuilt index. - + Otherwise, can set `g2.build_index()` to build it ahead of time. Args: query (str): natural language query. cols (list or str, optional): if fuzzy=False, select which column to query. @@ -250,7 +242,7 @@ def search_graph( if res._umap is not None: emb = res._node_embedding.iloc[found_indices] # type: ignore except Exception as e: # for explicit relabeled nodes - logger.exception(e) + #logger.exception(e) tdf = rdf[df[node].isin(found_indices)] feats = res._node_features.loc[tdf.index] # type: ignore if res._umap is not None: From ea3b9a61407ca93459971fe06315b43a4d6db35d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Feb 2023 16:15:48 -0800 Subject: [PATCH 188/432] lint --- graphistry/ai_utils.py | 11 ++++------- graphistry/text_utils.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 66d2b868e0..5c9186159b 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -158,12 +158,12 @@ def search(self, q, k=5): - k: the number of nearest neighbors to return (default: 5) Returns: - - I: a numpy array of size (k,) containing the indices of the k nearest neighbors - - D: a numpy array of size (k,) containing the distances to the k nearest neighbors + - Index: a numpy array of size (k,) containing the indices of the k nearest neighbors + - Distances: a numpy array of size (k,) containing the distances to the k nearest neighbors """ q = np.asarray(q, dtype=np.float32) - D, I = self.index.search(q.reshape(1, -1), k) - return I[0], D[0] + Distances, Index = self.index.search(q.reshape(1, -1), k) + return Index[0], Distances[0] def search_df(self, q, df, k): """ Query by vector using annoy index and append distance to results @@ -469,6 +469,3 @@ def infer_self_graph(res, # ######################################################### print("-" * 50) if verbose else None return hydrate_graph(res, df, new_edges, node, src, dst, emb, X, y) - - - diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index eb1f8cab86..d5b579b593 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -17,6 +17,7 @@ logger = getLogger(__name__) + class SearchToGraphMixin(MIXIN_BASE): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -42,7 +43,9 @@ def build_index(self, angular=False, n_trees=None): self.assert_fitted() self.assert_features_line_up_with_nodes() X = self._get_feature("nodes") - self.search_index = FaissVectorSearch(X.values) #self._build_search_index(X, angular, n_trees, faiss=False) + self.search_index = FaissVectorSearch( + X.values + ) # self._build_search_index(X, angular, n_trees, faiss=False) def _query_from_dataframe(self, qdf: pd.DataFrame, top_n: int, thresh: float): # Use the loaded featurizers to transform the dataframe @@ -205,9 +208,9 @@ def search_graph( res = self.bind() edf = edges = res._edges - #print('shape of edges', edf.shape) + # print('shape of edges', edf.shape) rdf = df = res._nodes - #print('shape of nodes', rdf.shape) + # print('shape of nodes', rdf.shape) node = res._node indices = rdf[node] src = res._source @@ -242,7 +245,7 @@ def search_graph( if res._umap is not None: emb = res._node_embedding.iloc[found_indices] # type: ignore except Exception as e: # for explicit relabeled nodes - #logger.exception(e) + logger.exception(e) tdf = rdf[df[node].isin(found_indices)] feats = res._node_features.loc[tdf.index] # type: ignore if res._umap is not None: @@ -267,6 +270,7 @@ def search_graph( def save_search_instance(self, savepath): from joblib import dump # type: ignore # need to make this onnx or similar + self.build_index() search = self.search_index del self.search_index # can't pickle Annoy From 371481deb347d23c262d9d5938345e3e69baf6c8 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Feb 2023 16:20:38 -0800 Subject: [PATCH 189/432] adds faiss-cpu in AI deps build --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5817609f6a..09600be3dc 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'annoy', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From 30c57c906a8a72634a844294cf134d726498b834 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Feb 2023 16:25:35 -0800 Subject: [PATCH 190/432] adds faiss in AI deps build --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 09600be3dc..4cc9590401 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss', 'faiss-cpu', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From c639f837e3900fd893c215c688c8913c66a2d144 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Feb 2023 16:34:28 -0800 Subject: [PATCH 191/432] lint moved import --- graphistry/ai_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 5c9186159b..5234c3d6c2 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -6,6 +6,11 @@ from .constants import DISTANCE, WEIGHT, BATCH from logging import getLogger +try: + import faiss +except: + faiss = None + logger = getLogger(__name__) @@ -144,8 +149,7 @@ def get_graphistry_from_milieu_search( class FaissVectorSearch: def __init__(self, M): - import faiss - import numpy as np + # import faiss self.index = faiss.IndexFlatL2(M.shape[1]) self.index.add(M) From 18c282b4eccfa46772bde365739b7ee7d75a3ae0 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Feb 2023 16:39:18 -0800 Subject: [PATCH 192/432] lint --- graphistry/ai_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 5234c3d6c2..fb5a4de516 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -7,7 +7,7 @@ from logging import getLogger try: - import faiss + import faiss # type ignore except: faiss = None From dc675b535687145bfb264fc6afff7619b6a2c080 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Feb 2023 16:55:06 -0800 Subject: [PATCH 193/432] removes package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4cc9590401..09600be3dc 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss', 'faiss-cpu', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From a124ed6ef916b6248661214c35bb565e57395026 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 27 Feb 2023 17:20:08 -0800 Subject: [PATCH 194/432] adds mypy ignore --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index 01cff103e8..898e001146 100644 --- a/mypy.ini +++ b/mypy.ini @@ -31,6 +31,9 @@ ignore_missing_imports = True [mypy-dgl.*] ignore_missing_imports = True +[mypy-faiss.*] +ignore_missing_imports = True + [mypy-igraph.*] ignore_missing_imports = True From ebe0f1635329f127f1f35ec324388a3088091338 Mon Sep 17 00:00:00 2001 From: dc Date: Tue, 28 Feb 2023 17:44:14 +0800 Subject: [PATCH 195/432] check cucat cuml --- graphistry/feature_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index a900c85986..f376c5f791 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -140,7 +140,7 @@ def assert_imported(): ) raise import_min_exn -def assert_cuml_imported(): +def assert_cuml_cucat(): has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cu_cat_dependancy() if not has_cuml_dependancy_: logger.error( # noqa @@ -2381,7 +2381,7 @@ def featurize( if feature_engine == 'dirty_cat': assert_imported() elif feature_engine == 'cu_cat': - assert_cuml_imported() + assert_cuml_cucat() if inplace: res = self else: From 297dcc9d17e058cce6dfee127b3c7b3d106b23b9 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Tue, 28 Feb 2023 09:16:40 -0500 Subject: [PATCH 196/432] fix(docstr): minor formatting fixes on featurize --- graphistry/feature_utils.py | 72 ++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 38 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 462ca18a11..6b845576e6 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -627,10 +627,9 @@ def fit_pipeline( which helps for when transformer pipeline is scaling or imputer which sometime introduce small negative numbers, and umap metrics like Hellinger need to be positive - :param X, DataFrame to transform. + :param X: DataFrame to transform. :param transformer: Pipeline object to fit and transform - :param keep_n_decimals: Int of how many decimal places to keep in - rounded transformed data + :param keep_n_decimals: Int of how many decimal places to keep in rounded transformed data """ columns = X.columns index = X.index @@ -1772,9 +1771,10 @@ def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): args: :: - X: pd.DataFrame of features - y: pd.DataFrame of target features - kind: str, one of 'nodes' or 'edges' + + ;X: pd.DataFrame of features + :y: pd.DataFrame of target features + :kind: str, one of 'nodes' or 'edges' *args, **kwargs: passed to smart_scaler pipeline returns: scaled X, y @@ -2211,20 +2211,17 @@ def transform(self, df: pd.DataFrame, Transform new data and append to existing graph, or return dataframes **args:** - :: - # df: pd.DataFrame, raw data to transform - # ydf: pd.DataFrame, optional - # kind: str # one of `nodes`, `edges` - # return_graph: bool, if True, will return a graph with inferred edges. - # merge_policy: bool, if True, adds batch to existing graph nodes via nearest neighbors. - # If False, will infer edges only between nodes in the batch, default False - # min_dist: float, if return_graph is True, will use this value in NN search, or 'auto' to infer a good value - # min_dist represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. - # sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph - # n_neighbors: int, if return_graph is True, will use this value for n_neighbors in Nearest Neighbors search - # scaled: bool, if True, will use scaled transformation of data set during featurization, default True - # verbose: bool, if True, will print metadata about the graph construction, default False + :df: pd.DataFrame, raw data to transform + :ydf: pd.DataFrame, optional + :kind: str # one of `nodes`, `edges` + :return_graph: bool, if True, will return a graph with inferred edges. + :merge_policy: bool, if True, adds batch to existing graph nodes via nearest neighbors. If False, will infer edges only between nodes in the batch, default False + :min_dist: float, if return_graph is True, will use this value in NN search, or 'auto' to infer a good value. min_dist represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. + :sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph + :n_neighbors: int, if return_graph is True, will use this value for n_neighbors in Nearest Neighbors search + :scaled: bool, if True, will use scaled transformation of data set during featurization, default True + :verbose: bool, if True, will print metadata about the graph construction, default False **Returns:** X, y: pd.DataFrame, transformed data if return_graph is False @@ -2285,22 +2282,21 @@ def scale( **Args:** - :: - # df: pd.DataFrame, raw data to transform, if None, will use data from featurization fit - # y: pd.DataFrame, optional target data - # kind: str, one of `nodes`, `edges` - # use_scaler: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` - # use_scaler_target: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` - # impute: bool, if True, will impute missing values - # n_quantiles: int, number of quantiles to use for quantile scaler - # output_distribution: str, one of `normal`, `uniform`, `lognormal` - # quantile_range: tuple, range of quantiles to use for quantile scaler - # n_bins: int, number of bins to use for KBinsDiscretizer - # encode: str, one of `ordinal`, `onehot`, `onehot-dense`, `binary` - # strategy: str, one of `uniform`, `quantile`, `kmeans` - # keep_n_decimals: int, number of decimals to keep after scaling - # return_scalers: bool, if True, will return the scalers used to scale the data + :df: pd.DataFrame, raw data to transform, if None, will use data from featurization fit + :y: pd.DataFrame, optional target data + :kind: str, one of `nodes`, `edges` + :use_scaler: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` + :use_scaler_target: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` + :impute: bool, if True, will impute missing values + :n_quantiles: int, number of quantiles to use for quantile scaler + :output_distribution: str, one of `normal`, `uniform`, `lognormal` + :quantile_range: tuple, range of quantiles to use for quantile scaler + :n_bins: int, number of bins to use for KBinsDiscretizer + :encode: str, one of `ordinal`, `onehot`, `onehot-dense`, `binary` + :strategy: str, one of `uniform`, `quantile`, `kmeans` + :keep_n_decimals: int, number of decimals to keep after scaling + :return_scalers: bool, if True, will return the scalers used to scale the data **Returns:** @@ -2792,7 +2788,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False) -> pd.DataFrame: """ - Returns feature matrix, and if columns are specified, returns matrix with only the columns that contain + Returns feature matrix, and if columns are specified, returns matrix with only the columns that contain the string `column_part` in their name. `X = g.get_matrix(['feature1', 'feature2'])` @@ -2824,10 +2820,10 @@ def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'no Caveats: - if you have a column name that is a substring of another column name, you may get unexpected results. Args: - columns (Union[List, str]): list of column names or a single column name that may exist in columns + :columns (Union[List, str]): list of column names or a single column name that may exist in columns of the feature matrix. If None, returns original feature matrix - kind (str, optional): Node or Edge features. Defaults to 'nodes'. - target (bool, optional): If True, returns the target matrix. Defaults to False. + :kind (str, optional): Node or Edge features. Defaults to 'nodes'. + :target (bool, optional): If True, returns the target matrix. Defaults to False. Returns: pd.DataFrame: feature matrix with only the columns that contain the string `column_part` in their name. From fa749a2dc76479ebdda7897fb1559fb3c04c7156 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Tue, 28 Feb 2023 09:30:55 -0500 Subject: [PATCH 197/432] fix(docstr): fixed hyperlink for palettes --- graphistry/PlotterBase.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 555c9e580c..badb060b19 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -314,15 +314,18 @@ def style(self, fg=None, bg=None, page=None, logo=None): **Example: Chained merge - results in url and blendMode being set, while color is dropped** :: + g2 = g.style(bg={'color': 'black'}, fg={'blendMode': 'screen'}) g3 = g2.style(bg={'image': {'url': 'http://site.com/watermark.png'}}) **Example: Gradient background** :: + g.style(bg={'gradient': {'kind': 'linear', 'position': 45, 'stops': [['rgb(0,0,0)', '0%'], ['rgb(255,255,255)', '100%']]}}) **Example: Page settings** :: + g.style(page={'title': 'Site - {{ name }}', 'favicon': 'http://site.com/logo.ico'}) """ @@ -857,7 +860,7 @@ def bind(self, source=None, destination=None, node=None, edge=None, :param edge_label: Attribute overriding edge's expanded label text. By default, scrollable list of attribute/value mappings. :type edge_label: str - :param edge_color: Attribute overriding edge's color. rgba (int64) or int32 palette index, see palette definitions `_ for values. Based on Color Brewer. + :param edge_color: Attribute overriding edge's color. rgba (int64) or int32 palette index, see `palette `_ definitions for values. Based on Color Brewer. :type edge_color: str :param edge_source_color: Attribute overriding edge's source color if no edge_color, as an rgba int64 value. @@ -875,7 +878,7 @@ def bind(self, source=None, destination=None, node=None, edge=None, :param point_label: Attribute overriding node's expanded label text. By default, scrollable list of attribute/value mappings. :type point_label: str - :param point_color: Attribute overriding node's color.rgba (int64) or int32 palette index, see palette definitions `_ for values. Based on Color Brewer. + :param point_color: Attribute overriding node's color.rgba (int64) or int32 palette index, see `palette `_ definitions for values. Based on Color Brewer. :type point_color: str :param point_size: Attribute overriding node's size. By default, uses the node degree. The visualization will normalize point sizes and adjust dynamically using semantic zoom. From ca09bea30aaa0ddb9238955f16ee7005b3ce4e48 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Tue, 28 Feb 2023 09:31:16 -0500 Subject: [PATCH 198/432] fix(docstr): fixed spacing for codeblock --- graphistry/plugins/cugraph.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graphistry/plugins/cugraph.py b/graphistry/plugins/cugraph.py index d769ba89ce..5e7a656b25 100644 --- a/graphistry/plugins/cugraph.py +++ b/graphistry/plugins/cugraph.py @@ -363,6 +363,7 @@ def layout_cugraph( **Example: ForceAtlas2 layout** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['b','c','d','e']}) g = graphistry.edges(edges, 's', 'd') @@ -370,6 +371,7 @@ def layout_cugraph( **Example: Change which column names are generated** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['b','c','d','e']}) g = graphistry.edges(edges, 's', 'd') @@ -380,6 +382,7 @@ def layout_cugraph( **Example: Pass parameters to layout methods** :: + import graphistry, pandas as pd edges = pd.DataFrame({'s': ['a','b','c','d'], 'd': ['b','c','d','e']}) g = graphistry.edges(edges, 's', 'd') From 3cabd769e921ee08d17be43303dc8f92688e35ff Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Tue, 28 Feb 2023 09:43:39 -0500 Subject: [PATCH 199/432] fix(docstr):minor text edit --- graphistry/umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 38ad606f26..633f941c55 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -281,7 +281,7 @@ def transform_umap(self, df: pd.DataFrame, ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: """Transforms data into UMAP embedding - args: + Args: :df: Dataframe to transform :y: Target column :kind: One of `nodes` or `edges` From 0b4702ac67e6dae02effe7790c382b9d1c69fa64 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Tue, 28 Feb 2023 09:43:59 -0500 Subject: [PATCH 200/432] fix(docstr): fixed codeblock indentation --- graphistry/pygraphistry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphistry/pygraphistry.py b/graphistry/pygraphistry.py index 0a8dd76310..2051a32523 100644 --- a/graphistry/pygraphistry.py +++ b/graphistry/pygraphistry.py @@ -1942,6 +1942,7 @@ def nodes(nodes: Union[Callable, Any], node=None, *args, **kwargs) -> Plottable: **Example** :: + import graphistry def sample_nodes(g, n): @@ -1992,6 +1993,7 @@ def edges( **Example** :: + import graphistry def sample_edges(g, n): From 9b3e2b1724dfb19be32b9551b8d70e3311714ffc Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 28 Feb 2023 10:28:37 -0800 Subject: [PATCH 201/432] adds pinned faiss version --- graphistry/ai_utils.py | 5 ++--- setup.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index fb5a4de516..93e24532a7 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -144,12 +144,11 @@ def get_graphistry_from_milieu_search( # Graphistry Vector Search Index # ########################################################################################################################## -# import faiss -# import numpy as np + class FaissVectorSearch: def __init__(self, M): - # import faiss + import faiss self.index = faiss.IndexFlatL2(M.shape[1]) self.index.add(M) diff --git a/setup.py b/setup.py index 09600be3dc..2a11b08fad 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu==1.6.5', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From 24e7c759c3659e806fe5add30cd2bd6bc73e03e6 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 28 Feb 2023 10:37:08 -0800 Subject: [PATCH 202/432] adds pinned setuptools version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2a11b08fad..f50b28c852 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu==1.6.5', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['setuptools==67.4.0', 'scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu==1.6.5', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From b0d7a18946a848dd861e318ca7ce482918c8e017 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 28 Feb 2023 10:41:32 -0800 Subject: [PATCH 203/432] adds pinned faiss 1.6.1 --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f50b28c852..3b07d22380 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,8 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['setuptools==67.4.0', 'scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu==1.6.5', 'joblib'] +# https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu==1.6.1', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From 9252b09c8c1b587ee9c6de2a166c191f61aeb96b Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 28 Feb 2023 10:48:59 -0800 Subject: [PATCH 204/432] unpins FAISS to see why swig worked then --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3b07d22380..71b6052048 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def unique_flatten_dict(d): 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu==1.6.1', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From ddba63f637cabefb95224b15f5d82d6f2f0183cf Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Wed, 1 Mar 2023 01:08:39 +0530 Subject: [PATCH 205/432] Update setup.py --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 71b6052048..2e09266ac3 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,8 @@ def unique_flatten_dict(d): 'requests', 'squarify', 'typing-extensions', - 'packaging >= 20.1' + 'packaging >= 20.1', + 'setuptools < 60.0.0', ] stubs = [ From a272ea3af4626f118e5cc52a0d9a888f226d7995 Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Wed, 1 Mar 2023 01:32:28 +0530 Subject: [PATCH 206/432] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2e09266ac3..1c5304d74d 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def unique_flatten_dict(d): 'networkx': ['networkx>=2.5'], 'gremlin': ['gremlinpython'], 'bolt': ['neo4j', 'neotime'], - 'nodexl': ['openpyxl', 'xlrd'], + 'nodexl': ['openpyxl==3.1.0', 'xlrd'], 'jupyter': ['ipython'], } From 21dac04e349a7842cbbbecd988fe1e05bdf111cb Mon Sep 17 00:00:00 2001 From: dc Date: Wed, 1 Mar 2023 14:51:51 +0800 Subject: [PATCH 207/432] flag and typo fix --- graphistry/feature_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index f376c5f791..618a516374 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -109,8 +109,8 @@ def lazy_import_has_cu_cat_dependancy(): from scipy import __version__ as scipy_version from cu_cat import __version__ as cu_cat_version from sklearn import __version__ as sklearn_version - from cuml import __verison__ as cuml_version - from cudf import __verison__ as cudf_version + from cuml import __version__ as cuml_version + from cudf import __version__ as cudf_version logger.debug(f"SCIPY VERSION: {scipy_version}") logger.debug(f"Cuda CAT VERSION: {cu_cat_version}") logger.debug(f"sklearn VERSION: {sklearn_version}") @@ -173,7 +173,7 @@ def assert_cuml_cucat(): # # _featurize_or_get_edges_dataframe_if_X_is_None -FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat"] +FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat", "cu_cat|torch"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] From 23df5bc9792b7b20312707e1cb21cb91c71a54cb Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 6 Mar 2023 14:36:37 +0800 Subject: [PATCH 208/432] cudf all the way thru, cuda cannot handle nulls so few more ifs --- graphistry/feature_utils.py | 2 +- graphistry/umap_utils.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 467d71bb79..7de9bebd66 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1891,7 +1891,7 @@ def _featurize_nodes( ndf = res._nodes node = res._node - if remove_node_column: + if remove_node_column and 'cudf.core.dataframe' not in str(getmodule(ndf)): ndf = remove_node_column_from_symbolic(ndf, node) X = remove_node_column_from_symbolic(X, node) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 11f0b64c06..6a1cadb473 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -283,8 +283,11 @@ def transform_umap( # noqa: E303 def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 - if emb.shape[1] == 2: + if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) + elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): + import cudf + emb = cudf.DataFrame(emb, columns=[config.X, config.Y], index=index) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1] - 2) @@ -497,7 +500,6 @@ def umap( elif 'cudf.core.dataframe' in str(getmodule(X_)): import cudf index_to_nodes_dict = cudf.DataFrame(nodes).reset_index() - X_ = pd.DataFrame(X_.to_numpy()) res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs @@ -593,11 +595,11 @@ def _bind_xy_from_umap( emb = res._node_embedding else: emb = res._edge_embedding + if 'cudf.core.dataframe' not in str(getmodule(emb)): ## cuda cannot support nulls https://github.com/cupy/cupy/issues/5918#issuecomment-946327237 + df[x_name] = emb.values.T[0] # if embedding is greater + # than two dimensions will only take first two coordinates + df[y_name] = emb.values.T[1] - df[x_name] = emb.values.T[0] # if embedding is greater - # than two dimensions will only take first two coordinates - df[y_name] = emb.values.T[1] - # res = res.nodes(df) if kind == "nodes" else res.edges(df) if encode_weight and kind == "nodes": From a2640cd61d559e246d29f69cdc308b128749bf04 Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 6 Mar 2023 17:43:07 +0800 Subject: [PATCH 209/432] cudf+umap working on numerics --- graphistry/feature_utils.py | 2 +- graphistry/umap_utils.py | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 7de9bebd66..467d71bb79 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1891,7 +1891,7 @@ def _featurize_nodes( ndf = res._nodes node = res._node - if remove_node_column and 'cudf.core.dataframe' not in str(getmodule(ndf)): + if remove_node_column: ndf = remove_node_column_from_symbolic(ndf, node) X = remove_node_column_from_symbolic(X, node) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 6a1cadb473..83a767e775 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -286,8 +286,7 @@ def _bundle_embedding(self, emb, index): if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): - import cudf - emb = cudf.DataFrame(emb, columns=[config.X, config.Y], index=index) + emb = pd.DataFrame(emb.to_numpy(), columns=[config.X, config.Y], index=index.to_numpy()) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1] - 2) @@ -326,7 +325,6 @@ def _process_umap( # have to set _raw_data attribute on umap? fresh_res._umap = old_res._umap # this saves the day! return fresh_res - emb = res.umap_fit_transform(X_, y_) res._xy = emb return res @@ -509,6 +507,7 @@ def umap( if res._xy is None: raise RuntimeError("This should not happen") res._node_embedding = res._xy + # TODO add edge filter so graph doesn't have double edges # TODO user-guidable edge merge policies like upsert? res._weighted_edges_df_from_nodes = ( @@ -595,10 +594,9 @@ def _bind_xy_from_umap( emb = res._node_embedding else: emb = res._edge_embedding - if 'cudf.core.dataframe' not in str(getmodule(emb)): ## cuda cannot support nulls https://github.com/cupy/cupy/issues/5918#issuecomment-946327237 - df[x_name] = emb.values.T[0] # if embedding is greater - # than two dimensions will only take first two coordinates - df[y_name] = emb.values.T[1] + + df[x_name] = emb.values.T[0] + df[y_name] = emb.values.T[1] res = res.nodes(df) if kind == "nodes" else res.edges(df) From ec151b8c4059821d6cff9eec2363f1a4fdecd284 Mon Sep 17 00:00:00 2001 From: dc Date: Tue, 7 Mar 2023 09:08:29 +0800 Subject: [PATCH 210/432] full numeric cudf-- needs hack to plot --- graphistry/umap_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 83a767e775..00acfc2c89 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -286,12 +286,17 @@ def _bundle_embedding(self, emb, index): if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): - emb = pd.DataFrame(emb.to_numpy(), columns=[config.X, config.Y], index=index.to_numpy()) + import cudf + emb = cudf.DataFrame(emb.to_cupy(), columns=[config.X, config.Y], index=index.to_cupy()) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1] - 2) ] - emb = pd.DataFrame(emb, columns=columns, index=index) + if 'cudf.core.dataframe' not in str(getmodule(emb)): + emb = pd.DataFrame(emb, columns=columns, index=index) + elif 'cudf.core.dataframe' in str(getmodule(emb)): + import cudf + emb = cudf.DataFrame(emb.to_cupy(), columns=columns, index=index.to_cupy()) return emb def _process_umap( From 0ec412a6852e78f9152473b5cdb4e73f7ecc359c Mon Sep 17 00:00:00 2001 From: dc Date: Tue, 7 Mar 2023 09:10:46 +0800 Subject: [PATCH 211/432] full numeric cudf-- needs hack to plot --- graphistry/umap_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 00acfc2c89..2b0d71f4fc 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -287,7 +287,7 @@ def _bundle_embedding(self, emb, index): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): import cudf - emb = cudf.DataFrame(emb.to_cupy(), columns=[config.X, config.Y], index=index.to_cupy()) + emb = cudf.DataFrame(emb.values, columns=[config.X, config.Y], index=index.values) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1] - 2) @@ -296,7 +296,7 @@ def _bundle_embedding(self, emb, index): emb = pd.DataFrame(emb, columns=columns, index=index) elif 'cudf.core.dataframe' in str(getmodule(emb)): import cudf - emb = cudf.DataFrame(emb.to_cupy(), columns=columns, index=index.to_cupy()) + emb = cudf.DataFrame(emb.values, columns=columns, index=index.values) return emb def _process_umap( From 55c2b07862d768f8bf994e75cbce763e5b706b4e Mon Sep 17 00:00:00 2001 From: dc Date: Tue, 7 Mar 2023 14:04:40 +0800 Subject: [PATCH 212/432] use rename if 2 columns, otherwise = to columns list --- graphistry/umap_utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 2b0d71f4fc..5d0041230e 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -286,8 +286,7 @@ def _bundle_embedding(self, emb, index): if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): - import cudf - emb = cudf.DataFrame(emb.values, columns=[config.X, config.Y], index=index.values) + emb.rename(columns={0:config.X,1: config.Y},inplace=True) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1] - 2) @@ -295,8 +294,7 @@ def _bundle_embedding(self, emb, index): if 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=columns, index=index) elif 'cudf.core.dataframe' in str(getmodule(emb)): - import cudf - emb = cudf.DataFrame(emb.values, columns=columns, index=index.values) + emb.columns=columns return emb def _process_umap( From 0a4209068fd30c895d4929ec5e59811038a40835 Mon Sep 17 00:00:00 2001 From: dc Date: Fri, 10 Mar 2023 08:42:18 +0900 Subject: [PATCH 213/432] umap cudf --- graphistry/umap_utils.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 5d0041230e..5c92661ae1 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -495,12 +495,11 @@ def umap( logger.debug("umap X_: %s", X_) logger.debug("umap y_: %s", y_) - - if isinstance(X_,pd.DataFrame): + logger.debug("data is type :: %s", (type(X_))) + if isinstance(X_, pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) elif 'cudf.core.dataframe' in str(getmodule(X_)): - import cudf - index_to_nodes_dict = cudf.DataFrame(nodes).reset_index() + index_to_nodes_dict = nodes res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs @@ -598,8 +597,12 @@ def _bind_xy_from_umap( else: emb = res._edge_embedding - df[x_name] = emb.values.T[0] - df[y_name] = emb.values.T[1] + if type(df) == type(emb): + df[x_name] = emb.values.T[0] + df[y_name] = emb.values.T[1] + elif isinstance(df, pd.DataFrame) and 'cudf.core.dataframe' in str(getmodule(emb)): + df[x_name] = emb.to_numpy().T[0] + df[y_name] = emb.to_numpy().T[1] res = res.nodes(df) if kind == "nodes" else res.edges(df) From 1a013a80b3e8e400974e767296a921a1fd820915 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 9 Mar 2023 20:37:03 -0800 Subject: [PATCH 214/432] untested, adds decorator for cuml and pandas dataframes, standardizing umap input and outputs if engine=cuml or pandas --- graphistry/umap_utils.py | 107 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 5c92661ae1..99697d8922 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -12,6 +12,8 @@ from .PlotterBase import Plottable, WeakValueDictionary from .util import check_set_memoize, setup_logger +#logger = logging.getLogger(__name__) + logger = setup_logger(name=__name__, verbose=config.VERBOSE) if TYPE_CHECKING: @@ -98,6 +100,98 @@ def resolve_umap_engine( ) +logger = logging.getLogger(__name__) + +def convert_pandas_to_cudf(func): + def wrapper(*args, **kwargs): + new_args = [] + new_kwargs = {} + for arg in args: + if isinstance(arg, pd.DataFrame): + new_args.append(cudf.DataFrame.from_pandas(arg)) + else: + new_args.append(arg) + for key, value in kwargs.items(): + if isinstance(value, pd.DataFrame): + new_kwargs[key] = cudf.DataFrame.from_pandas(value) + else: + new_kwargs[key] = value + return func(*new_args, **new_kwargs) + return wrapper + +def convert_cudf_to_pandas(func): + def wrapper(*args, **kwargs): + new_args = [] + new_kwargs = {} + for arg in args: + if isinstance(arg, cudf.DataFrame): + new_args.append(arg.to_pandas()) + else: + new_args.append(arg) + for key, value in kwargs.items(): + if isinstance(value, cudf.DataFrame): + new_kwargs[key] = value.to_pandas() + else: + new_kwargs[key] = value + try: + result = func(*new_args, **new_kwargs) + if isinstance(result, cudf.DataFrame): + result = result.to_pandas() + except Exception as e: + logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") + raise e + return result + return wrapper + + +def safe_gpu_dataframes(func, engine_in=None, engine_out=None): + """Decorator function that safely wraps methods given the engine, + specifically flexibility in what part of pipeline to convert to or from pd or cudf + engine_in asserts the dtype of the input (converting if necessary) + while engine_out asserts the output dtype + """ + def wrapper(*args, **kwargs): + new_args = [] + new_kwargs = {} + for arg in args: + if isinstance(arg, cudf.DataFrame) and engine_in == "cuml": + new_args.append(arg) + elif isinstance(arg, pd.DataFrame) and engine_in == "pandas": + new_args.append(arg) + elif isinstance(arg, cudf.DataFrame) and engine_in == "pandas": + new_args.append(arg.to_pandas()) + elif isinstance(arg, pd.DataFrame) and engine_in == "cuml": + new_args.append(cudf.from_pandas(arg)) + else: + new_args.append(arg) + for key, value in kwargs.items(): + if isinstance(value, cudf.DataFrame) and engine_in == "cuml": + new_kwargs[key] = value + elif isinstance(value, pd.DataFrame) and engine_in == "pandas": + new_kwargs[key] = value.to_pandas() + elif isinstance(value, cudf.DataFrame) and engine_in == "pandas": + new_kwargs[key] = value.to_pandas() + elif isinstance(value, pd.DataFrame) and engine_in == "cuml": + new_kwargs[key] = cudf.from_pandas(value) + else: + new_kwargs[key] = value + try: + result = func(*new_args, **new_kwargs) + if isinstance(result, cudf.DataFrame) and engine_out == "cuml": + result = result + elif isinstance(result, pd.DataFrame) and engine_out == "pandas": + result = result + elif isinstance(result, cudf.DataFrame) and engine_out == "pandas": + result = result.to_pandas() + elif isinstance(result, pd.DataFrame) and engine_out == "cuml": + result = cudf.from_pandas(result) + else: + raise ValueError("Unknown engine specified.") + except Exception as e: + logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") + raise e + return result + return wrapper ############################################################################### @@ -165,6 +259,7 @@ class UMAPMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): self.umap_initialized = False + self.engine = 'umap_learn' def umap_lazy_init( self, @@ -217,6 +312,7 @@ def umap_lazy_init( self.engine = engine_resolved self.suffix = suffix + @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): if y is None: return None @@ -231,6 +327,7 @@ def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): ) return None + @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: raise ValueError("UMAP is not initialized") @@ -261,6 +358,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info(f" - or {X.shape[0]/mins:.2f} rows per minute") return self + @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: raise ValueError("UMAP is not initialized") @@ -269,6 +367,7 @@ def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = Non emb = self._bundle_embedding(emb, index=X.index) return emb + @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def transform_umap( # noqa: E303 self, df: pd.DataFrame, ydf: pd.DataFrame, kind: str = "nodes" ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: @@ -281,6 +380,7 @@ def transform_umap( # noqa: E303 emb = self._bundle_embedding(emb, index=df.index) return emb, x, y + @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): @@ -297,6 +397,7 @@ def _bundle_embedding(self, emb, index): emb.columns=columns return emb + @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _process_umap( self, res, @@ -499,7 +600,7 @@ def umap( if isinstance(X_, pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) elif 'cudf.core.dataframe' in str(getmodule(X_)): - index_to_nodes_dict = nodes + index_to_nodes_dict = nodes # {}? res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs @@ -549,7 +650,7 @@ def umap( "kind should be one of `nodes` or `edges` unless" "you are passing explicit matrices" ) - if X is not None and isinstance(X, pd.DataFrame): + if X is not None and isinstance(X, pd.DataFrame) or '': logger.info("New Matrix `X` passed in for UMAP-ing") xy = res.umap_fit_transform(X, y) res._xy = xy @@ -579,6 +680,7 @@ def umap( if not inplace: return res + #@safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _bind_xy_from_umap( self, res: Any, @@ -638,6 +740,7 @@ def filter_weighted_edges( ): """ Filter edges based on _weighted_edges_df (ex: from .umap()) + """ if inplace: res = self From c23529baff61e4c681a160bd233d3306c1fdf99b Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Fri, 10 Mar 2023 19:03:31 +0530 Subject: [PATCH 215/432] rst change PlotterBase to plotter --- docs/source/graphistry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index a6fdf5cf15..ee7acf9851 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -12,7 +12,7 @@ Layout & Plugins Plotter Module ================== -.. automodule:: graphistry.PlotterBase +.. automodule:: graphistry.plotter :members: :undoc-members: :show-inheritance: From c79fac68ce0e8b331cfeab161189638840add348 Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Fri, 10 Mar 2023 19:41:13 +0530 Subject: [PATCH 216/432] add graphistry.compute to graphistry.rst --- docs/source/graphistry.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index ee7acf9851..e399710768 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -47,6 +47,13 @@ Semantic Search :members: :undoc-members: :show-inheritance: + +Compute +================== +.. automodule:: graphistry.compute + :members: + :undoc-members: + :show-inheritance: DBScan ================== From fbc33e3d0d82d78b91eceb15b71d3046f6afcfad Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Fri, 10 Mar 2023 19:52:07 +0530 Subject: [PATCH 217/432] Delete graphistry.compute.rst --- docs/source/graphistry.compute.rst | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 docs/source/graphistry.compute.rst diff --git a/docs/source/graphistry.compute.rst b/docs/source/graphistry.compute.rst deleted file mode 100644 index 6ea4bdedbd..0000000000 --- a/docs/source/graphistry.compute.rst +++ /dev/null @@ -1,29 +0,0 @@ -graphistry.layout package -========================= - -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - -Submodules ----------- - -graphistry.compute.ComputeMixin module ------------------------------------------------- - -.. automodule:: graphistry.compute.ComputeMixin - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: graphistry.compute - :members: - :undoc-members: - :show-inheritance: From 35345ff0d936831eb97bc0e5378920aec851944f Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Fri, 10 Mar 2023 19:58:56 +0530 Subject: [PATCH 218/432] Update modules.rst --- docs/source/modules.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 71e0a12335..92efcdb8bc 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -1,5 +1,5 @@ -.. doc -.. === +Modules +=========== .. .. toctree:: .. :maxdepth: 4 From 811d3e576a00bef859f913ea8a4291ea0cb63744 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 20:07:57 +0530 Subject: [PATCH 219/432] add graphistry.compute to toctree --- docs/source/graphistry.compute.rst | 29 +++++++++++++++++++++++++++++ docs/source/graphistry.rst | 7 ------- docs/source/index.rst | 1 + 3 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 docs/source/graphistry.compute.rst diff --git a/docs/source/graphistry.compute.rst b/docs/source/graphistry.compute.rst new file mode 100644 index 0000000000..6ea4bdedbd --- /dev/null +++ b/docs/source/graphistry.compute.rst @@ -0,0 +1,29 @@ +graphistry.layout package +========================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + +Submodules +---------- + +graphistry.compute.ComputeMixin module +------------------------------------------------ + +.. automodule:: graphistry.compute.ComputeMixin + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: graphistry.compute + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index e399710768..bfefab4a32 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -48,13 +48,6 @@ Semantic Search :undoc-members: :show-inheritance: -Compute -================== -.. automodule:: graphistry.compute - :members: - :undoc-members: - :show-inheritance: - DBScan ================== .. automodule:: graphistry.compute.cluster diff --git a/docs/source/index.rst b/docs/source/index.rst index b45393c266..7c9b0eeeac 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,6 +11,7 @@ Here in our docstrings you can find useful packages, modules, and commands to ma :maxdepth: 3 graphistry + graphistry.compute modules Indices and tables From d6806cfb39b1d40768bd5ae9ac1ddda44c88131b Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 20:13:32 +0530 Subject: [PATCH 220/432] resolve short underline error --- docs/source/graphistry.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index bfefab4a32..d548996154 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -18,7 +18,7 @@ Plotter Module :show-inheritance: Pygraphistry Module -================== +=================== .. automodule:: graphistry.pygraphistry :members: @@ -56,7 +56,7 @@ DBScan :show-inheritance: Arrow uploader Module -================== +===================== .. automodule:: graphistry.arrow_uploader :members: @@ -64,7 +64,7 @@ Arrow uploader Module :show-inheritance: Arrow File Uploader Module -================== +========================== .. automodule:: graphistry.ArrowFileUploader :members: From 0b301caf7955f170b964be21a064f32101192d01 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 20:28:16 +0530 Subject: [PATCH 221/432] test1: resolve blank line error --- graphistry/umap_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 633f941c55..1b61b9a9d3 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -479,7 +479,9 @@ def umap( default True. :verbose: whether to print out extra information, default False. :return: self, with attributes set with new data + """ + if engine == UMAP_LEARN: assert_imported() elif engine == CUML: From 24638cf64cf3d9a8f1a376d9f17c68c3ffb517fb Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 20:32:14 +0530 Subject: [PATCH 222/432] test2: resolve blank line error --- graphistry/umap_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 1b61b9a9d3..cc60c209fe 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -479,9 +479,7 @@ def umap( default True. :verbose: whether to print out extra information, default False. :return: self, with attributes set with new data - """ - if engine == UMAP_LEARN: assert_imported() elif engine == CUML: @@ -622,6 +620,7 @@ def umap( if not inplace: return res + def _bind_xy_from_umap( self, res: Any, From 9e0c3d2056b764f6981ca664b86bb815f5973e4b Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 20:46:07 +0530 Subject: [PATCH 223/432] test3: resolve blank line error --- graphistry/feature_utils.py | 1 + graphistry/umap_utils.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index a068ddeb7b..0abcfb6c52 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1776,6 +1776,7 @@ def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): :y: pd.DataFrame of target features :kind: str, one of 'nodes' or 'edges' *args, **kwargs: passed to smart_scaler pipeline + returns: scaled X, y """ diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index cc60c209fe..633f941c55 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -620,7 +620,6 @@ def umap( if not inplace: return res - def _bind_xy_from_umap( self, res: Any, From ac150701f0fb4da74830c583a84bf44cfb6da489 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 20:56:06 +0530 Subject: [PATCH 224/432] doc fix --- graphistry/feature_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 0abcfb6c52..01404eb7df 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2410,8 +2410,8 @@ def featurize( memoize: bool = True, verbose: bool = False, ): - r""" - Featurize Nodes or Edges of the underlying nodes/edges DataFrames. + """ + Featurize Nodes or Edges of the underlying nodes/edges DataFrames. ______________________________________________________________________ :param kind: specify whether to featurize `nodes` or `edges`. From f53710c18f9dffed22d43dd8c69df281d19fe626 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 20:58:58 +0530 Subject: [PATCH 225/432] doc fix --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 01404eb7df..c77103773c 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2410,7 +2410,7 @@ def featurize( memoize: bool = True, verbose: bool = False, ): - """ + r""" Featurize Nodes or Edges of the underlying nodes/edges DataFrames. ______________________________________________________________________ From a8a9dc112b52467bde9220bfdb3054dbf42bfce3 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 21:08:11 +0530 Subject: [PATCH 226/432] doc fix --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index c77103773c..75b733d32a 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2412,7 +2412,7 @@ def featurize( ): r""" Featurize Nodes or Edges of the underlying nodes/edges DataFrames. - ______________________________________________________________________ + __________________________________________________________________ :param kind: specify whether to featurize `nodes` or `edges`. Edge featurization includes a pairwise From 8618b1a5c09817ca8d1e850ac1b2be5172c831f7 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 21:14:03 +0530 Subject: [PATCH 227/432] doc fix --- graphistry/feature_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 75b733d32a..6a12ff37a8 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2818,8 +2818,10 @@ def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'no => ['basket_price_total', 'conversion_percent', 'CTR_percent', 'CVR_percent'] # not as useful for sbert features. + Caveats: - if you have a column name that is a substring of another column name, you may get unexpected results. + Args: :columns (Union[List, str]): list of column names or a single column name that may exist in columns of the feature matrix. If None, returns original feature matrix From 3a6611d455c079dd9de8b7cd162e44af452cc867 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 21:20:54 +0530 Subject: [PATCH 228/432] doc fix --- graphistry/feature_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 6a12ff37a8..90ef1e6850 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -430,7 +430,8 @@ def check_if_textual_column( min_words: float = 2.5, ) -> bool: """ - Checks if `col` column of df is textual or not using basic heuristics + Checks if `col` column of df is textual or not using basic heuristics + __________________________________________________________________________ :param df: DataFrame @@ -2223,6 +2224,7 @@ def transform(self, df: pd.DataFrame, :n_neighbors: int, if return_graph is True, will use this value for n_neighbors in Nearest Neighbors search :scaled: bool, if True, will use scaled transformation of data set during featurization, default True :verbose: bool, if True, will print metadata about the graph construction, default False + **Returns:** X, y: pd.DataFrame, transformed data if return_graph is False From 82d1d3b59dea7c4c4bc2074971535f1523a29c79 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 21:39:25 +0530 Subject: [PATCH 229/432] doc fix --- graphistry/compute/cluster.py | 2 +- graphistry/feature_utils.py | 27 +++++++++++++-------------- graphistry/text_utils.py | 1 + graphistry/umap_utils.py | 1 + 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index f19fbfbe38..eb18cc275e 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -243,7 +243,7 @@ def dbscan( Useful: Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, as well as assessing metrics per cluster, e.g. - https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb + https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb Args: :min_dist float: The maximum distance between two samples for them to be considered as in the same neighborhood. diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 90ef1e6850..fc29a0e720 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -224,9 +224,8 @@ def features_without_target( df: pd.DataFrame, y: Optional[Union[List, str, pd.DataFrame]] = None ) -> pd.DataFrame: """ - Checks if y DataFrame column name is in df, and removes it - from df if so - ___________________________________________________________________ + Checks if y DataFrame column name is in df, and removes it from df if so + ________________________________________________________________________ :param df: model DataFrame :param y: target DataFrame @@ -398,7 +397,7 @@ def is_dataframe_all_numeric(df: pd.DataFrame) -> bool: def find_bad_set_columns(df: pd.DataFrame, bad_set: List = ["[]"]): """ - Finds columns that if not coerced to strings, will break processors. + Finds columns that if not coerced to strings, will break processors. ------------------------------------------------------------------------- :param df: DataFrame :param bad_set: List of strings to look for. @@ -431,7 +430,6 @@ def check_if_textual_column( ) -> bool: """ Checks if `col` column of df is textual or not using basic heuristics - __________________________________________________________________________ :param df: DataFrame @@ -541,6 +539,7 @@ def get_preprocessing_pipeline( Helper function for imputing and scaling np.ndarray data using different scaling transformers. ----------------------------------------------------------------- + :param X: np.ndarray :param impute: whether to run imputing or not :param use_scaler: string in None or @@ -623,11 +622,12 @@ def fit_pipeline( X: pd.DataFrame, transformer, keep_n_decimals: int = 5 ) -> pd.DataFrame: """ - Helper to fit DataFrame over transformer pipeline. - Rounds resulting matrix X by keep_n_digits if not 0, - which helps for when transformer pipeline is scaling or imputer - which sometime introduce small negative numbers, - and umap metrics like Hellinger need to be positive + Helper to fit DataFrame over transformer pipeline. + Rounds resulting matrix X by keep_n_digits if not 0, + which helps for when transformer pipeline is scaling or imputer + which sometime introduce small negative numbers, + and umap metrics like Hellinger need to be positive + :param X: DataFrame to transform. :param transformer: Pipeline object to fit and transform :param keep_n_decimals: Int of how many decimal places to keep in rounded transformed data @@ -871,8 +871,8 @@ def process_dirty_dataframes( Union[SuperVectorizer, FunctionTransformer], ]: """ - Dirty_Cat encoder for record level data. Will automatically turn - inhomogeneous dataframe into matrix using smart conversion tricks. + Dirty_Cat encoder for record level data. Will automatically turn + inhomogeneous dataframe into matrix using smart conversion tricks. ______________________________________________________________________ :param ndf: node DataFrame @@ -1248,8 +1248,7 @@ def encode_edges(edf, src, dst, mlb, fit=False): src (string): source column dst (string): destination column mlb (sklearn): multilabelBinarizer - fit (bool, optional): If true, fits multilabelBinarizer. - Defaults to False. + fit (bool, optional): If true, fits multilabelBinarizer. Defaults to False. Returns: tuple: pd.DataFrame, multilabelBinarizer """ diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index d9dc42060f..9a1abb77f5 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -137,6 +137,7 @@ def search( If an index is not yet built, it is generated `g2.build_index()` on the fly at search time. Otherwise, can set `g2.build_index()` to build it ahead of time. + Args: :query (str): natural language query. :cols (list or str, optional): if fuzzy=False, select which column to query. diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 633f941c55..6c53b53279 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -478,6 +478,7 @@ def umap( :memoize: whether to memoize the results of this method, default True. :verbose: whether to print out extra information, default False. + :return: self, with attributes set with new data """ if engine == UMAP_LEARN: From 3fe4ceee0b36c709495769375ef5d00306a05c77 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 21:45:52 +0530 Subject: [PATCH 230/432] add versioneer --- docs/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 7c9b0eeeac..82dbdda61d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,6 +13,7 @@ Here in our docstrings you can find useful packages, modules, and commands to ma graphistry graphistry.compute modules + versioneer Indices and tables ================== From 81994714a99afaca819d1bae5ec30eb6059beefb Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 10 Mar 2023 21:49:46 +0530 Subject: [PATCH 231/432] add versioneer --- docs/source/versioneer.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/versioneer.rst b/docs/source/versioneer.rst index a34edfc48d..804c171da3 100644 --- a/docs/source/versioneer.rst +++ b/docs/source/versioneer.rst @@ -1,2 +1,2 @@ -.. versioneer module -.. ================= +versioneer module +================= From d01e2bf85f0b939e6cade35ebec4ddafd6dc390c Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:39:39 -0500 Subject: [PATCH 232/432] fix(docstr): removed unneccesary indents/sections --- graphistry/feature_utils.py | 27 +++++++++------------------ graphistry/umap_utils.py | 7 ++----- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index fc29a0e720..39c3ff855b 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -223,9 +223,7 @@ def safe_divide(a, b): def features_without_target( df: pd.DataFrame, y: Optional[Union[List, str, pd.DataFrame]] = None ) -> pd.DataFrame: - """ - Checks if y DataFrame column name is in df, and removes it from df if so - ________________________________________________________________________ + """Checks if y DataFrame column name is in df, and removes it from df if so :param df: model DataFrame :param y: target DataFrame @@ -396,9 +394,8 @@ def is_dataframe_all_numeric(df: pd.DataFrame) -> bool: def find_bad_set_columns(df: pd.DataFrame, bad_set: List = ["[]"]): - """ - Finds columns that if not coerced to strings, will break processors. - ------------------------------------------------------------------------- + """Finds columns that if not coerced to strings, will break processors. + :param df: DataFrame :param bad_set: List of strings to look for. :return: list @@ -428,9 +425,7 @@ def check_if_textual_column( confidence: float = 0.35, min_words: float = 2.5, ) -> bool: - """ - Checks if `col` column of df is textual or not using basic heuristics - __________________________________________________________________________ + """Checks if `col` column of df is textual or not using basic heuristics :param df: DataFrame :param col: column name @@ -469,9 +464,8 @@ def check_if_textual_column( def get_textual_columns( df: pd.DataFrame, min_words: float = 2.5 ) -> List: - """ - Collects columns from df that it deems are textual. - _____________________________________________________________________ + """Collects columns from df that it deems are textual. + :param df: DataFrame :return: list of columns names @@ -795,7 +789,7 @@ def encoder(X, use_scaler): # noqa: E301 def get_cardinality_ratio(df: pd.DataFrame): """Calculates ratio of unique values to total number of rows of DataFrame - ------------------------------------------------------------------------- + :param df: DataFrame """ ratios = {} @@ -1038,7 +1032,6 @@ def process_nodes_dataframes( """ Automatic Deep Learning Embedding/ngrams of Textual Features, with the rest of the columns taken care of by dirty_cat - _________________________________________________________________________ :param df: pandas DataFrame of data :param y: pandas DataFrame of targets @@ -2411,10 +2404,8 @@ def featurize( memoize: bool = True, verbose: bool = False, ): - r""" - Featurize Nodes or Edges of the underlying nodes/edges DataFrames. - __________________________________________________________________ - + r"""Featurize Nodes or Edges of the underlying nodes/edges DataFrames. + :param kind: specify whether to featurize `nodes` or `edges`. Edge featurization includes a pairwise src-to-dst feature block using a MultiLabelBinarizer, diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 6c53b53279..46d5cbd8b6 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -135,7 +135,6 @@ def umap_graph_to_weighted_edges(umap_graph, engine, is_legacy, cfg=config): class UMAPMixin(MIXIN_BASE): """ UMAP Mixin for automagic UMAPing - """ # FIXME where is this used? _umap_memoize: WeakValueDictionary = WeakValueDictionary() @@ -424,10 +423,8 @@ def umap( verbose: bool = False, **featurize_kwargs, ): - """ - UMAP the featurized nodes or edges data, - or pass in your own X, y (optional) dataframes of values - + """UMAP the featurized nodes or edges data, or pass in your own X, y (optional) dataframes of values + Example ------- >>> import graphistry From 812f5c7a76b65be29d39fb7f2abe5b0d3812ca0c Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 10 Mar 2023 12:50:23 -0500 Subject: [PATCH 233/432] fix(docstr): removed unneccessary spacing --- graphistry/feature_utils.py | 17 ++++------------- graphistry/umap_utils.py | 4 ++-- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 39c3ff855b..a728e001ca 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -466,7 +466,6 @@ def get_textual_columns( ) -> List: """Collects columns from df that it deems are textual. - :param df: DataFrame :return: list of columns names """ @@ -529,10 +528,7 @@ def get_preprocessing_pipeline( encode: str = "ordinal", strategy: str = "quantile", ) -> Pipeline: # noqa - """ - Helper function for imputing and scaling np.ndarray data - using different scaling transformers. - ----------------------------------------------------------------- + """Helper function for imputing and scaling np.ndarray data using different scaling transformers. :param X: np.ndarray :param impute: whether to run imputing or not @@ -864,10 +860,7 @@ def process_dirty_dataframes( Union[SuperVectorizer, FunctionTransformer], Union[SuperVectorizer, FunctionTransformer], ]: - """ - Dirty_Cat encoder for record level data. Will automatically turn - inhomogeneous dataframe into matrix using smart conversion tricks. - ______________________________________________________________________ + """Dirty_Cat encoder for record level data. Will automatically turn inhomogeneous dataframe into matrix using smart conversion tricks. :param ndf: node DataFrame :param y: target DataFrame or series @@ -2719,10 +2712,8 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( memoize: bool = True, verbose: bool = False, ) -> Tuple[pd.DataFrame, Optional[pd.DataFrame], MIXIN_BASE]: - """ - helper method gets edge feature and target matrix if X, y - are not specified - ----------------------------------------------------------- + """helper method gets edge feature and target matrix if X, y are not specified + :param X: Data Matrix :param y: target, default None :return: data `X` and `y` diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 46d5cbd8b6..46d961d780 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -426,14 +426,14 @@ def umap( """UMAP the featurized nodes or edges data, or pass in your own X, y (optional) dataframes of values Example - ------- + >>> import graphistry >>> g = graphistry.nodes(pd.DataFrame({'node': [0,1,2], 'data': [1,2,3], 'meta': ['a', 'b', 'c']})) >>> g2 = g.umap(n_components=3, spread=1.0, min_dist=0.1, n_neighbors=12, negative_sample_rate=5, local_connectivity=1, repulsion_strength=1.0, metric='euclidean', suffix='', play=0, encode_position=True, encode_weight=True, dbscan=False, engine='auto', feature_engine='auto', inplace=False, memoize=True, verbose=False) >>> g2.plot() Parameters - ---------- + :X: either a dataframe ndarray of features, or column names to featurize :y: either an dataframe ndarray of targets, or column names to featurize targets From b88ce77193f3846103b4063fc3b1876349a5a77b Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 10 Mar 2023 13:02:46 -0500 Subject: [PATCH 234/432] fix(docstr): fixed unindent --- graphistry/feature_utils.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index a728e001ca..6cc9708507 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2194,8 +2194,7 @@ def transform(self, df: pd.DataFrame, return_graph: bool = True, scaled: bool = True, verbose: bool = False): - """ - Transform new data and append to existing graph, or return dataframes + """Transform new data and append to existing graph, or return dataframes **args:** @@ -2212,8 +2211,8 @@ def transform(self, df: pd.DataFrame, **Returns:** - X, y: pd.DataFrame, transformed data if return_graph is False - or a graphistry Plottable with inferred edges if return_graph is True + X, y: pd.DataFrame, transformed data if return_graph is False + or a graphistry Plottable with inferred edges if return_graph is True """ if kind == "nodes": X, y_ = self._transform("_node_encoder", df, y, scaled=scaled) @@ -2268,7 +2267,6 @@ def scale( # fit some other pipeline clf.fit(X_scaled, y_scaled) - **Args:** :df: pd.DataFrame, raw data to transform, if None, will use data from featurization fit @@ -2288,9 +2286,7 @@ def scale( **Returns:** - (X, y) transformed data if return_graph is False - or a graph with inferred edges if return_graph is True, - or (X, y, scaler, scaler_target) if return_scalers is True + (X, y) transformed data if return_graph is False or a graph with inferred edges if return_graph is True, or (X, y, scaler, scaler_target) if return_scalers is True """ if df is None: # use the original data @@ -2623,7 +2619,6 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( are not specified. if X, y are specified will set them as `_node_target` and `_node_target` attributes - ----------------------------------------------------------- """ res = self.bind() @@ -2805,15 +2800,15 @@ def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'no Caveats: - if you have a column name that is a substring of another column name, you may get unexpected results. - Args: - :columns (Union[List, str]): list of column names or a single column name that may exist in columns - of the feature matrix. If None, returns original feature matrix - :kind (str, optional): Node or Edge features. Defaults to 'nodes'. - :target (bool, optional): If True, returns the target matrix. Defaults to False. + Args: + :columns (Union[List, str]): list of column names or a single column name that may exist in columns + of the feature matrix. If None, returns original feature matrix + :kind (str, optional): Node or Edge features. Defaults to 'nodes'. + :target (bool, optional): If True, returns the target matrix. Defaults to False. - Returns: - pd.DataFrame: feature matrix with only the columns that contain the string `column_part` in their name. - """ + Returns: + pd.DataFrame: feature matrix with only the columns that contain the string `column_part` in their name. + """ if target: X = self._get_target(kind) else: From 940be4eb12bb424a01b3237ddd3298b566b3e4ac Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Fri, 10 Mar 2023 13:15:02 -0500 Subject: [PATCH 235/432] fix(docstr): removed line/spacing --- graphistry/feature_utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 6cc9708507..8c0166c01f 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -270,11 +270,7 @@ def remove_node_column_from_symbolic(X_symbolic, node): def remove_internal_namespace_if_present(df: pd.DataFrame): - """ - Some tranformations below add columns to the DataFrame, - this method removes them before featurization - Will not drop if suffix is added during UMAP-ing - ______________________________________________________________ + """Some tranformations below add columns to the DataFrame, this method removes them before featurization will not drop if suffix is added during UMAP-ing :param df: DataFrame :return: DataFrame with dropped columns in reserved namespace From fdb7d2b81dd34829e724b047f08061056cfc265b Mon Sep 17 00:00:00 2001 From: dc Date: Sun, 12 Mar 2023 07:24:17 +0900 Subject: [PATCH 236/432] Revert "umap cudf" This reverts commit 0a4209068fd30c895d4929ec5e59811038a40835. --- graphistry/umap_utils.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 99697d8922..1bf18a4e72 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -596,11 +596,11 @@ def umap( logger.debug("umap X_: %s", X_) logger.debug("umap y_: %s", y_) - logger.debug("data is type :: %s", (type(X_))) - if isinstance(X_, pd.DataFrame): + + if isinstance(X_,pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) elif 'cudf.core.dataframe' in str(getmodule(X_)): - index_to_nodes_dict = nodes # {}? + index_to_nodes_dict = nodes res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs @@ -699,12 +699,8 @@ def _bind_xy_from_umap( else: emb = res._edge_embedding - if type(df) == type(emb): - df[x_name] = emb.values.T[0] - df[y_name] = emb.values.T[1] - elif isinstance(df, pd.DataFrame) and 'cudf.core.dataframe' in str(getmodule(emb)): - df[x_name] = emb.to_numpy().T[0] - df[y_name] = emb.to_numpy().T[1] + df[x_name] = emb.values.T[0] + df[y_name] = emb.values.T[1] res = res.nodes(df) if kind == "nodes" else res.edges(df) From 17fd3169fe5a0475f6df71833981f2f3284e4b36 Mon Sep 17 00:00:00 2001 From: dc Date: Sun, 12 Mar 2023 07:24:31 +0900 Subject: [PATCH 237/432] Revert "umap cudf" This reverts commit 0a4209068fd30c895d4929ec5e59811038a40835. --- graphistry/umap_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 1bf18a4e72..29e448027f 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -600,7 +600,8 @@ def umap( if isinstance(X_,pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) elif 'cudf.core.dataframe' in str(getmodule(X_)): - index_to_nodes_dict = nodes + import cudf + index_to_nodes_dict = cudf.DataFrame(nodes).reset_index() res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs From af399f687270c379b1bccb52aef223456bd8b7d3 Mon Sep 17 00:00:00 2001 From: dc Date: Sun, 12 Mar 2023 07:26:00 +0900 Subject: [PATCH 238/432] revert b4 alex borq --- graphistry/umap_utils.py | 120 ++++----------------------------------- 1 file changed, 10 insertions(+), 110 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 29e448027f..5c92661ae1 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -12,8 +12,6 @@ from .PlotterBase import Plottable, WeakValueDictionary from .util import check_set_memoize, setup_logger -#logger = logging.getLogger(__name__) - logger = setup_logger(name=__name__, verbose=config.VERBOSE) if TYPE_CHECKING: @@ -100,98 +98,6 @@ def resolve_umap_engine( ) -logger = logging.getLogger(__name__) - -def convert_pandas_to_cudf(func): - def wrapper(*args, **kwargs): - new_args = [] - new_kwargs = {} - for arg in args: - if isinstance(arg, pd.DataFrame): - new_args.append(cudf.DataFrame.from_pandas(arg)) - else: - new_args.append(arg) - for key, value in kwargs.items(): - if isinstance(value, pd.DataFrame): - new_kwargs[key] = cudf.DataFrame.from_pandas(value) - else: - new_kwargs[key] = value - return func(*new_args, **new_kwargs) - return wrapper - -def convert_cudf_to_pandas(func): - def wrapper(*args, **kwargs): - new_args = [] - new_kwargs = {} - for arg in args: - if isinstance(arg, cudf.DataFrame): - new_args.append(arg.to_pandas()) - else: - new_args.append(arg) - for key, value in kwargs.items(): - if isinstance(value, cudf.DataFrame): - new_kwargs[key] = value.to_pandas() - else: - new_kwargs[key] = value - try: - result = func(*new_args, **new_kwargs) - if isinstance(result, cudf.DataFrame): - result = result.to_pandas() - except Exception as e: - logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") - raise e - return result - return wrapper - - -def safe_gpu_dataframes(func, engine_in=None, engine_out=None): - """Decorator function that safely wraps methods given the engine, - specifically flexibility in what part of pipeline to convert to or from pd or cudf - engine_in asserts the dtype of the input (converting if necessary) - while engine_out asserts the output dtype - """ - def wrapper(*args, **kwargs): - new_args = [] - new_kwargs = {} - for arg in args: - if isinstance(arg, cudf.DataFrame) and engine_in == "cuml": - new_args.append(arg) - elif isinstance(arg, pd.DataFrame) and engine_in == "pandas": - new_args.append(arg) - elif isinstance(arg, cudf.DataFrame) and engine_in == "pandas": - new_args.append(arg.to_pandas()) - elif isinstance(arg, pd.DataFrame) and engine_in == "cuml": - new_args.append(cudf.from_pandas(arg)) - else: - new_args.append(arg) - for key, value in kwargs.items(): - if isinstance(value, cudf.DataFrame) and engine_in == "cuml": - new_kwargs[key] = value - elif isinstance(value, pd.DataFrame) and engine_in == "pandas": - new_kwargs[key] = value.to_pandas() - elif isinstance(value, cudf.DataFrame) and engine_in == "pandas": - new_kwargs[key] = value.to_pandas() - elif isinstance(value, pd.DataFrame) and engine_in == "cuml": - new_kwargs[key] = cudf.from_pandas(value) - else: - new_kwargs[key] = value - try: - result = func(*new_args, **new_kwargs) - if isinstance(result, cudf.DataFrame) and engine_out == "cuml": - result = result - elif isinstance(result, pd.DataFrame) and engine_out == "pandas": - result = result - elif isinstance(result, cudf.DataFrame) and engine_out == "pandas": - result = result.to_pandas() - elif isinstance(result, pd.DataFrame) and engine_out == "cuml": - result = cudf.from_pandas(result) - else: - raise ValueError("Unknown engine specified.") - except Exception as e: - logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") - raise e - return result - return wrapper ############################################################################### @@ -259,7 +165,6 @@ class UMAPMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): self.umap_initialized = False - self.engine = 'umap_learn' def umap_lazy_init( self, @@ -312,7 +217,6 @@ def umap_lazy_init( self.engine = engine_resolved self.suffix = suffix - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): if y is None: return None @@ -327,7 +231,6 @@ def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): ) return None - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: raise ValueError("UMAP is not initialized") @@ -358,7 +261,6 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info(f" - or {X.shape[0]/mins:.2f} rows per minute") return self - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: raise ValueError("UMAP is not initialized") @@ -367,7 +269,6 @@ def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = Non emb = self._bundle_embedding(emb, index=X.index) return emb - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def transform_umap( # noqa: E303 self, df: pd.DataFrame, ydf: pd.DataFrame, kind: str = "nodes" ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: @@ -380,7 +281,6 @@ def transform_umap( # noqa: E303 emb = self._bundle_embedding(emb, index=df.index) return emb, x, y - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): @@ -397,7 +297,6 @@ def _bundle_embedding(self, emb, index): emb.columns=columns return emb - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _process_umap( self, res, @@ -596,12 +495,11 @@ def umap( logger.debug("umap X_: %s", X_) logger.debug("umap y_: %s", y_) - - if isinstance(X_,pd.DataFrame): + logger.debug("data is type :: %s", (type(X_))) + if isinstance(X_, pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) elif 'cudf.core.dataframe' in str(getmodule(X_)): - import cudf - index_to_nodes_dict = cudf.DataFrame(nodes).reset_index() + index_to_nodes_dict = nodes res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs @@ -651,7 +549,7 @@ def umap( "kind should be one of `nodes` or `edges` unless" "you are passing explicit matrices" ) - if X is not None and isinstance(X, pd.DataFrame) or '': + if X is not None and isinstance(X, pd.DataFrame): logger.info("New Matrix `X` passed in for UMAP-ing") xy = res.umap_fit_transform(X, y) res._xy = xy @@ -681,7 +579,6 @@ def umap( if not inplace: return res - #@safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _bind_xy_from_umap( self, res: Any, @@ -700,8 +597,12 @@ def _bind_xy_from_umap( else: emb = res._edge_embedding - df[x_name] = emb.values.T[0] - df[y_name] = emb.values.T[1] + if type(df) == type(emb): + df[x_name] = emb.values.T[0] + df[y_name] = emb.values.T[1] + elif isinstance(df, pd.DataFrame) and 'cudf.core.dataframe' in str(getmodule(emb)): + df[x_name] = emb.to_numpy().T[0] + df[y_name] = emb.to_numpy().T[1] res = res.nodes(df) if kind == "nodes" else res.edges(df) @@ -737,7 +638,6 @@ def filter_weighted_edges( ): """ Filter edges based on _weighted_edges_df (ex: from .umap()) - """ if inplace: res = self From 3f3a1f4d1f1ecc4a8480d19def043a09b85bf333 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 11 Mar 2023 16:41:19 -0800 Subject: [PATCH 239/432] adds cudf support and wraps dataframe if engine=cuml --- graphistry/umap_utils.py | 238 ++++++++++++++++++++++++--------------- 1 file changed, 147 insertions(+), 91 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 99697d8922..96398cb62b 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -48,6 +48,16 @@ def lazy_cuml_import_has_dependancy(): except ModuleNotFoundError as e: return False, e, None +def lazy_cudf_import_has_dependancy(): + try: + import warnings + + warnings.filterwarnings("ignore") + import cudf # type: ignore + + return True, "ok", cudf + except ModuleNotFoundError as e: + return False, e, None def assert_imported(): has_dependancy_, import_exn, umap_learn = lazy_umap_import_has_dependancy() @@ -100,98 +110,141 @@ def resolve_umap_engine( ) -logger = logging.getLogger(__name__) -def convert_pandas_to_cudf(func): - def wrapper(*args, **kwargs): - new_args = [] +# def convert_pandas_to_cudf(func): +# def wrapper(*args, **kwargs): +# new_args = [] +# new_kwargs = {} +# for arg in args: +# if isinstance(arg, pd.DataFrame): +# new_args.append(cudf.DataFrame.from_pandas(arg)) +# else: +# new_args.append(arg) +# for key, value in kwargs.items(): +# if isinstance(value, pd.DataFrame): +# new_kwargs[key] = cudf.DataFrame.from_pandas(value) +# else: +# new_kwargs[key] = value +# return func(*new_args, **new_kwargs) +# return wrapper + +# def convert_cudf_to_pandas(func): +# def wrapper(*args, **kwargs): +# new_args = [] +# new_kwargs = {} +# for arg in args: +# if isinstance(arg, cudf.DataFrame): +# new_args.append(arg.to_pandas()) +# else: +# new_args.append(arg) +# for key, value in kwargs.items(): +# if isinstance(value, cudf.DataFrame): +# new_kwargs[key] = value.to_pandas() +# else: +# new_kwargs[key] = value +# try: +# result = func(*new_args, **new_kwargs) +# if isinstance(result, cudf.DataFrame): +# result = result.to_pandas() +# except Exception as e: +# logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") +# raise e +# return result +# return wrapper + + +# def safe_gpu_dataframes(func): +# """Decorator function that safely wraps methods given the engine, +# specifically flexibility in what part of pipeline to convert to or from pd or cudf +# engine_in asserts the dtype of the input (converting if necessary) +# while engine_out asserts the output dtype +# """ + +# from functools import wraps # https://stackoverflow.com/questions/11731136/class-method-decorator-with-self-arguments + +# @wraps(func) +# def dummy(self, *args, **kwargs): +# try: +# result = func(*args, **kwargs) +# except Exception as e: +# logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") +# raise e +# return result + +# @wraps(func) +# def wrapper(self, *args, **kwargs): +# engine_in = self.engine_in +# engine_out = self.engine_out +# new_args = [] +# new_kwargs = {} +# for arg in args: +# if isinstance(arg, cudf.DataFrame) and engine_in == "cuml": +# new_args.append(arg) +# elif isinstance(arg, pd.DataFrame) and engine_in == "pandas": +# new_args.append(arg) +# elif isinstance(arg, cudf.DataFrame) and engine_in == "pandas": +# new_args.append(arg.to_pandas()) +# elif isinstance(arg, pd.DataFrame) and engine_in == "cuml": +# new_args.append(cudf.from_pandas(arg)) +# else: +# new_args.append(arg) +# for key, value in kwargs.items(): +# if isinstance(value, cudf.DataFrame) and engine_in == "cuml": +# new_kwargs[key] = value +# elif isinstance(value, pd.DataFrame) and engine_in == "pandas": +# new_kwargs[key] = value.to_pandas() +# elif isinstance(value, cudf.DataFrame) and engine_in == "pandas": +# new_kwargs[key] = value.to_pandas() +# elif isinstance(value, pd.DataFrame) and engine_in == "cuml": +# new_kwargs[key] = cudf.from_pandas(value) +# else: +# new_kwargs[key] = value +# try: +# result = func(*new_args, **new_kwargs) +# if isinstance(result, cudf.DataFrame) and engine_out == "cuml": +# result = result +# elif isinstance(result, pd.DataFrame) and engine_out == "pandas": +# result = result +# elif isinstance(result, cudf.DataFrame) and engine_out == "pandas": +# result = result.to_pandas() +# elif isinstance(result, pd.DataFrame) and engine_out == "cuml": +# result = cudf.from_pandas(result) +# else: +# raise ValueError("Unknown engine specified.") +# except Exception as e: +# logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") +# raise e +# return result + +# has_cuml_dependancy_, _, cuml = lazy_cuml_import_has_dependancy() +# if has_cuml_dependancy_: +# return wrapper +# else: +# return dummy + + + + +def make_safe_gpu_dataframes(X, y, engine_in): + + def safe_cudf(X, y): new_kwargs = {} - for arg in args: - if isinstance(arg, pd.DataFrame): - new_args.append(cudf.DataFrame.from_pandas(arg)) - else: - new_args.append(arg) + kwargs = {'X': X, 'y': y} for key, value in kwargs.items(): - if isinstance(value, pd.DataFrame): - new_kwargs[key] = cudf.DataFrame.from_pandas(value) - else: - new_kwargs[key] = value - return func(*new_args, **new_kwargs) - return wrapper - -def convert_cudf_to_pandas(func): - def wrapper(*args, **kwargs): - new_args = [] - new_kwargs = {} - for arg in args: - if isinstance(arg, cudf.DataFrame): - new_args.append(arg.to_pandas()) - else: - new_args.append(arg) - for key, value in kwargs.items(): - if isinstance(value, cudf.DataFrame): - new_kwargs[key] = value.to_pandas() - else: - new_kwargs[key] = value - try: - result = func(*new_args, **new_kwargs) - if isinstance(result, cudf.DataFrame): - result = result.to_pandas() - except Exception as e: - logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") - raise e - return result - return wrapper - - -def safe_gpu_dataframes(func, engine_in=None, engine_out=None): - """Decorator function that safely wraps methods given the engine, - specifically flexibility in what part of pipeline to convert to or from pd or cudf - engine_in asserts the dtype of the input (converting if necessary) - while engine_out asserts the output dtype - """ - def wrapper(*args, **kwargs): - new_args = [] - new_kwargs = {} - for arg in args: - if isinstance(arg, cudf.DataFrame) and engine_in == "cuml": - new_args.append(arg) - elif isinstance(arg, pd.DataFrame) and engine_in == "pandas": - new_args.append(arg) - elif isinstance(arg, cudf.DataFrame) and engine_in == "pandas": - new_args.append(arg.to_pandas()) - elif isinstance(arg, pd.DataFrame) and engine_in == "cuml": - new_args.append(cudf.from_pandas(arg)) - else: - new_args.append(arg) - for key, value in kwargs.items(): - if isinstance(value, cudf.DataFrame) and engine_in == "cuml": - new_kwargs[key] = value - elif isinstance(value, pd.DataFrame) and engine_in == "pandas": - new_kwargs[key] = value.to_pandas() - elif isinstance(value, cudf.DataFrame) and engine_in == "pandas": + if isinstance(value, cudf.DataFrame) and engine_in == "pandas": new_kwargs[key] = value.to_pandas() elif isinstance(value, pd.DataFrame) and engine_in == "cuml": new_kwargs[key] = cudf.from_pandas(value) else: new_kwargs[key] = value - try: - result = func(*new_args, **new_kwargs) - if isinstance(result, cudf.DataFrame) and engine_out == "cuml": - result = result - elif isinstance(result, pd.DataFrame) and engine_out == "pandas": - result = result - elif isinstance(result, cudf.DataFrame) and engine_out == "pandas": - result = result.to_pandas() - elif isinstance(result, pd.DataFrame) and engine_out == "cuml": - result = cudf.from_pandas(result) - else: - raise ValueError("Unknown engine specified.") - except Exception as e: - logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") - raise e - return result - return wrapper + return new_kwargs['X'], new_kwargs['y'] + + has_cudf_dependancy_, _, cudf = lazy_cudf_import_has_dependancy() + if has_cudf_dependancy_: + return safe_cudf(X, y) + else: + return X, y + ############################################################################### @@ -310,9 +363,10 @@ def umap_lazy_init( self._umap = umap_engine.UMAP(**umap_kwargs) self.umap_initialized = True self.engine = engine_resolved + self.engine_in = self.engine_out = engine_resolved self.suffix = suffix - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) + #@safe_gpu_dataframes def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): if y is None: return None @@ -327,7 +381,7 @@ def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): ) return None - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) + #@safe_gpu_dataframes def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: raise ValueError("UMAP is not initialized") @@ -358,7 +412,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info(f" - or {X.shape[0]/mins:.2f} rows per minute") return self - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) + #@safe_gpu_dataframes def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): if self._umap is None: raise ValueError("UMAP is not initialized") @@ -367,7 +421,7 @@ def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = Non emb = self._bundle_embedding(emb, index=X.index) return emb - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) + #@safe_gpu_dataframes def transform_umap( # noqa: E303 self, df: pd.DataFrame, ydf: pd.DataFrame, kind: str = "nodes" ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: @@ -380,7 +434,7 @@ def transform_umap( # noqa: E303 emb = self._bundle_embedding(emb, index=df.index) return emb, x, y - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) + #@safe_gpu_dataframes def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): @@ -397,7 +451,7 @@ def _bundle_embedding(self, emb, index): emb.columns=columns return emb - @safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) + #@safe_gpu_dataframes def _process_umap( self, res, @@ -602,6 +656,9 @@ def umap( elif 'cudf.core.dataframe' in str(getmodule(X_)): index_to_nodes_dict = nodes # {}? + ## add the safe coercion here + X_, y_ = make_safe_gpu_dataframes(X_, y_, self.engine) + res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs ) @@ -680,7 +737,6 @@ def umap( if not inplace: return res - #@safe_gpu_dataframes(engine_in=self.engine, engine_out=self.engine) def _bind_xy_from_umap( self, res: Any, From 446bdc8185f0a4046dc69eda205ec11915e22125 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 11 Mar 2023 18:01:33 -0800 Subject: [PATCH 240/432] adds safe gpu wrapper to edges as well --- graphistry/umap_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 96398cb62b..579e2faae3 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -688,6 +688,9 @@ def umap( **featurize_kwargs ) + ## add the safe coercion here + X_, y_ = make_safe_gpu_dataframes(X_, y_, self.engine) + res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs ) From 96363f552f372f6a85e615e63d430d3c22952a17 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Mar 2023 16:26:14 -0700 Subject: [PATCH 241/432] adds safer handling of cupy/cudf arrays --- graphistry/compute/cluster.py | 41 ++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 15b7cf0ed3..8f3fbe852c 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -41,6 +41,17 @@ def lazy_dbscan_import_has_dependency(): return has_min_dependency, DBSCAN, has_cuml_dependency, cuDBSCAN +def lazy_cudf_import_has_dependancy(): + try: + import warnings + + warnings.filterwarnings("ignore") + import cudf # type: ignore + + return True, "ok", cudf + except ModuleNotFoundError as e: + return False, e, None + def resolve_cpu_gpu_engine( engine: DBSCANEngine, @@ -65,6 +76,27 @@ def resolve_cpu_gpu_engine( f"but received: {engine} :: {type(engine)}" ) +def make_safe_gpu_dataframes(X, y, engine): + """helper method to coerce a dataframe to the correct type (pd vs cudf)""" + def safe_cudf(X, y): + new_kwargs = {} + kwargs = {'X': X, 'y': y} + for key, value in kwargs.items(): + if isinstance(value, cudf.DataFrame) and engine == "pandas": + new_kwargs[key] = value.to_pandas() + elif isinstance(value, pd.DataFrame) and engine == "cuml": + new_kwargs[key] = cudf.from_pandas(value) + else: + new_kwargs[key] = value + return new_kwargs['X'], new_kwargs['y'] + + has_cudf_dependancy_, _, cudf = lazy_cudf_import_has_dependancy() + if has_cudf_dependancy_: + print('DBSCAN CUML Matrices') + return safe_cudf(X, y) + else: + return X, y + def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, target): """ @@ -89,7 +121,9 @@ def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, targe if umap and cols is None and g._umap is not None: df = g._get_embedding(kind) - + + if g.engine in [CUML]: + df = make_safe_gpu_dataframes(df, None) #print('\n df:', df.shape, df.columns) return df @@ -112,11 +146,12 @@ def dbscan_fit(g: Any, dbscan: Any, kind: str = "nodes", cols: Optional[Union[Li dbscan.fit(X) labels = dbscan.labels_ + print(labels, type(labels)) if kind == "nodes": - g._nodes = g._nodes.assign(_dbscan=labels) + g._nodes = g._nodes.assign(_dbscan=np.array(labels)) elif kind == "edges": - g._edges = g._edges.assign(_dbscan=labels) + g._edges = g._edges.assign(_dbscan=np.array(labels)) else: raise ValueError("kind must be one of `nodes` or `edges`") From 6dc826ade076c610d29f53f6749317a29247cf61 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Mar 2023 16:26:38 -0700 Subject: [PATCH 242/432] removes unused code --- graphistry/umap_utils.py | 116 --------------------------------------- 1 file changed, 116 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index e85c1ebc1d..8da9c0169a 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -113,121 +113,6 @@ def resolve_umap_engine( ) - -# def convert_pandas_to_cudf(func): -# def wrapper(*args, **kwargs): -# new_args = [] -# new_kwargs = {} -# for arg in args: -# if isinstance(arg, pd.DataFrame): -# new_args.append(cudf.DataFrame.from_pandas(arg)) -# else: -# new_args.append(arg) -# for key, value in kwargs.items(): -# if isinstance(value, pd.DataFrame): -# new_kwargs[key] = cudf.DataFrame.from_pandas(value) -# else: -# new_kwargs[key] = value -# return func(*new_args, **new_kwargs) -# return wrapper - -# def convert_cudf_to_pandas(func): -# def wrapper(*args, **kwargs): -# new_args = [] -# new_kwargs = {} -# for arg in args: -# if isinstance(arg, cudf.DataFrame): -# new_args.append(arg.to_pandas()) -# else: -# new_args.append(arg) -# for key, value in kwargs.items(): -# if isinstance(value, cudf.DataFrame): -# new_kwargs[key] = value.to_pandas() -# else: -# new_kwargs[key] = value -# try: -# result = func(*new_args, **new_kwargs) -# if isinstance(result, cudf.DataFrame): -# result = result.to_pandas() -# except Exception as e: -# logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") -# raise e -# return result -# return wrapper - - -# def safe_gpu_dataframes(func): -# """Decorator function that safely wraps methods given the engine, -# specifically flexibility in what part of pipeline to convert to or from pd or cudf -# engine_in asserts the dtype of the input (converting if necessary) -# while engine_out asserts the output dtype -# """ - -# from functools import wraps # https://stackoverflow.com/questions/11731136/class-method-decorator-with-self-arguments - -# @wraps(func) -# def dummy(self, *args, **kwargs): -# try: -# result = func(*args, **kwargs) -# except Exception as e: -# logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") -# raise e -# return result - -# @wraps(func) -# def wrapper(self, *args, **kwargs): -# engine_in = self.engine_in -# engine_out = self.engine_out -# new_args = [] -# new_kwargs = {} -# for arg in args: -# if isinstance(arg, cudf.DataFrame) and engine_in == "cuml": -# new_args.append(arg) -# elif isinstance(arg, pd.DataFrame) and engine_in == "pandas": -# new_args.append(arg) -# elif isinstance(arg, cudf.DataFrame) and engine_in == "pandas": -# new_args.append(arg.to_pandas()) -# elif isinstance(arg, pd.DataFrame) and engine_in == "cuml": -# new_args.append(cudf.from_pandas(arg)) -# else: -# new_args.append(arg) -# for key, value in kwargs.items(): -# if isinstance(value, cudf.DataFrame) and engine_in == "cuml": -# new_kwargs[key] = value -# elif isinstance(value, pd.DataFrame) and engine_in == "pandas": -# new_kwargs[key] = value.to_pandas() -# elif isinstance(value, cudf.DataFrame) and engine_in == "pandas": -# new_kwargs[key] = value.to_pandas() -# elif isinstance(value, pd.DataFrame) and engine_in == "cuml": -# new_kwargs[key] = cudf.from_pandas(value) -# else: -# new_kwargs[key] = value -# try: -# result = func(*new_args, **new_kwargs) -# if isinstance(result, cudf.DataFrame) and engine_out == "cuml": -# result = result -# elif isinstance(result, pd.DataFrame) and engine_out == "pandas": -# result = result -# elif isinstance(result, cudf.DataFrame) and engine_out == "pandas": -# result = result.to_pandas() -# elif isinstance(result, pd.DataFrame) and engine_out == "cuml": -# result = cudf.from_pandas(result) -# else: -# raise ValueError("Unknown engine specified.") -# except Exception as e: -# logger.exception(f"An error occurred while running {func.__name__}. Exception: {e}") -# raise e -# return result - -# has_cuml_dependancy_, _, cuml = lazy_cuml_import_has_dependancy() -# if has_cuml_dependancy_: -# return wrapper -# else: -# return dummy - - - - def make_safe_gpu_dataframes(X, y, engine_in): def safe_cudf(X, y): @@ -351,7 +236,6 @@ def umap_lazy_init( res._negative_sample_rate = negative_sample_rate res._umap = umap_engine.UMAP(**umap_kwargs) res.engine = engine_resolved - #self.engine = engine_resolved res._suffix = suffix return res From 27c879b15ce5a9e5c53fbffca0a06a325a7afbf8 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Mar 2023 16:27:41 -0700 Subject: [PATCH 243/432] handles pandas via if statement --- graphistry/compute/cluster.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 8f3fbe852c..20dcbfacae 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -145,13 +145,15 @@ def dbscan_fit(g: Any, dbscan: Any, kind: str = "nodes", cols: Optional[Union[Li raise ValueError("No features found for clustering") dbscan.fit(X) - labels = dbscan.labels_ - print(labels, type(labels)) - + if g.engine == 'cuml': + labels = dbscan.labels_.to_numpy() + else: + labels = dbscan.labels_ + if kind == "nodes": - g._nodes = g._nodes.assign(_dbscan=np.array(labels)) + g._nodes = g._nodes.assign(_dbscan=labels) elif kind == "edges": - g._edges = g._edges.assign(_dbscan=np.array(labels)) + g._edges = g._edges.assign(_dbscan=labels) else: raise ValueError("kind must be one of `nodes` or `edges`") From 4f01bdec57b84ce41c1e5281679495c712565352 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Mar 2023 16:29:58 -0700 Subject: [PATCH 244/432] adds missing engine flag --- graphistry/compute/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 20dcbfacae..56310a0679 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -123,7 +123,7 @@ def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, targe df = g._get_embedding(kind) if g.engine in [CUML]: - df = make_safe_gpu_dataframes(df, None) + df = make_safe_gpu_dataframes(df, None, g.engine) #print('\n df:', df.shape, df.columns) return df From 93cadb11c8a6caef9f2fa7f6028790220133583d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 12 Mar 2023 16:32:30 -0700 Subject: [PATCH 245/432] adds missing _ to output --- graphistry/compute/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 56310a0679..c2d5c5f1f1 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -123,7 +123,7 @@ def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, targe df = g._get_embedding(kind) if g.engine in [CUML]: - df = make_safe_gpu_dataframes(df, None, g.engine) + df, _ = make_safe_gpu_dataframes(df, None, g.engine) #print('\n df:', df.shape, df.columns) return df From f479857367bef834181038ab06f310aa03ef6b60 Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 13 Mar 2023 16:25:00 +0900 Subject: [PATCH 246/432] begin testing cudf-cu_cat --- graphistry/feature_utils.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 618a516374..d5df8549bb 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -3,6 +3,7 @@ import os import pandas as pd from time import time +from inspect import getmodule import warnings from functools import partial @@ -130,7 +131,6 @@ def assert_imported_text(): ) raise import_text_exn - def assert_imported(): has_min_dependancy_, import_min_exn = lazy_import_has_min_dependancy() if not has_min_dependancy_: @@ -139,7 +139,7 @@ def assert_imported(): "`pip install graphistry[ai]`" # noqa ) raise import_min_exn - + def assert_cuml_cucat(): has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cu_cat_dependancy() if not has_cuml_dependancy_: @@ -149,7 +149,7 @@ def assert_cuml_cucat(): ) raise import_cuml_exn - + # ############################################################################ # # Rough calltree @@ -208,7 +208,7 @@ def resolve_feature_engine( def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: - if isinstance(y, pd.DataFrame): + if isinstance(y, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(y)): return y if df is None: @@ -229,7 +229,7 @@ def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: def resolve_X(df: Optional[pd.DataFrame], X: XSymbolic) -> pd.DataFrame: - if isinstance(X, pd.DataFrame): + if isinstance(X, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(X)): return X if df is None: @@ -905,7 +905,6 @@ def process_dirty_dataframes( similarity: Optional[str] = None, # "ngram", categories: Optional[str] = "auto", multilabel: bool = False, - feature_engine: Optional[str] = "dirty_cat", ) -> Tuple[ pd.DataFrame, Optional[pd.DataFrame], @@ -936,7 +935,6 @@ def process_dirty_dataframes( from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder elif feature_engine == 'cu_cat': from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - from sklearn.preprocessing import FunctionTransformer t = time() @@ -1178,8 +1176,7 @@ def process_nodes_dataframes( n_topics_target=n_topics_target, similarity=similarity, categories=categories, - multilabel=multilabel, - feature_engine=feature_engine, + multilabel=multilabel ) if embedding: From 923ec9c88530a5940059d77fffd3b57c710de88b Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 13 Mar 2023 16:33:35 +0900 Subject: [PATCH 247/432] begin testing cudf-cu_cat --- graphistry/feature_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index d5df8549bb..feb0aa3bc0 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -905,6 +905,7 @@ def process_dirty_dataframes( similarity: Optional[str] = None, # "ngram", categories: Optional[str] = "auto", multilabel: bool = False, + feature_engine: Optional[str] = "dirty_cat", ) -> Tuple[ pd.DataFrame, Optional[pd.DataFrame], From b4e110d16d7b8f98bb5b11733c028010c9300bed Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 13 Mar 2023 16:53:41 +0900 Subject: [PATCH 248/432] begin cudf_cat for 3x to 10x --- graphistry/feature_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index feb0aa3bc0..f336166a60 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1177,7 +1177,8 @@ def process_nodes_dataframes( n_topics_target=n_topics_target, similarity=similarity, categories=categories, - multilabel=multilabel + multilabel=multilabel, + feature_engine=feature_engine, ) if embedding: From b6a60eaa37d403ab2766fece06b34d90e7ddeeae Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Mar 2023 14:04:23 -0700 Subject: [PATCH 249/432] adds handling of g.transform_umap when engine=cuml --- graphistry/umap_utils.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 8da9c0169a..0eb99ab521 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -113,15 +113,15 @@ def resolve_umap_engine( ) -def make_safe_gpu_dataframes(X, y, engine_in): +def make_safe_gpu_dataframes(X, y, engine): def safe_cudf(X, y): new_kwargs = {} kwargs = {'X': X, 'y': y} for key, value in kwargs.items(): - if isinstance(value, cudf.DataFrame) and engine_in == "pandas": + if isinstance(value, cudf.DataFrame) and engine == "pandas": new_kwargs[key] = value.to_pandas() - elif isinstance(value, pd.DataFrame) and engine_in == "cuml": + elif isinstance(value, pd.DataFrame) and engine == "cuml": new_kwargs[key] = cudf.from_pandas(value) else: new_kwargs[key] = value @@ -325,18 +325,20 @@ def transform_umap(self, df: pd.DataFrame, useful to contextualize new data against existing graph. If False, `sample` is irrelevant. sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs return_graph: Whether to return a graph or just the embeddings - fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data + fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data, default True verbose: Whether to print information about the graph inference """ X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) + X, y_ = make_safe_gpu_dataframes(X, y_, self.engine) emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) if return_graph and kind not in ["edges"]: + emb, _ = make_safe_gpu_dataframes(emb, None, 'pandas') # for now so we don't have to touch infer_edges, force to pandas g = self._infer_edges(emb, X, y_, df, infer_on_umap_embedding=fit_umap_embedding, merge_policy=merge_policy, eps=min_dist, sample=sample, n_neighbors=n_neighbors, verbose=verbose) - return g + return g return emb, X, y_ def _bundle_embedding(self, emb, index): @@ -344,7 +346,7 @@ def _bundle_embedding(self, emb, index): if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): - emb.rename(columns={0:config.X,1: config.Y},inplace=True) + emb.rename(columns={0: config.X, 1: config.Y}, inplace=True) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1]) @@ -352,7 +354,7 @@ def _bundle_embedding(self, emb, index): if 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=columns, index=index) elif 'cudf.core.dataframe' in str(getmodule(emb)): - emb.columns=columns + emb.columns = columns return emb def _process_umap( From 84741c8374493a16f66ebda918ec085c586d5743 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Mar 2023 14:33:42 -0700 Subject: [PATCH 250/432] safe converts X, y before infer_graph method --- graphistry/umap_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 0eb99ab521..555180e74f 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -334,6 +334,7 @@ def transform_umap(self, df: pd.DataFrame, emb = self._bundle_embedding(emb, index=df.index) if return_graph and kind not in ["edges"]: emb, _ = make_safe_gpu_dataframes(emb, None, 'pandas') # for now so we don't have to touch infer_edges, force to pandas + X, y_ = make_safe_gpu_dataframes(X, y_, 'pandas') g = self._infer_edges(emb, X, y_, df, infer_on_umap_embedding=fit_umap_embedding, merge_policy=merge_policy, eps=min_dist, sample=sample, n_neighbors=n_neighbors, From 6aefc496eccbdf45d6d86ac443bb3f750c81572b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Mar 2023 15:24:55 -0700 Subject: [PATCH 251/432] debugging why node not in df --- graphistry/ai_utils.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index e29c334785..40f4e554f3 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -284,6 +284,7 @@ def infer_graph( df["_n"] = numeric_indices df[BATCH] = 1 # 1 for minibatch, 0 for existing graph node = res._node + print('Node', node) NDF = res._nodes NDF[BATCH] = 0 EDF = res._edges @@ -297,8 +298,6 @@ def infer_graph( old_nodes = [] mdists = [] - # vsearch = build_search_index(X_previously_fit, angular=False) - for i in range(X_new.shape[0]): diff = X_previously_fit - X_new.iloc[i, :] dist = np.linalg.norm(diff, axis=1) # Euclidean distance @@ -427,12 +426,16 @@ def infer_self_graph(res, # if umap, need to add '_n' as node id to df, adding new indices to existing graph numeric_indices = np.arange( - X_previously_fit.shape[0], # X_previously_fit.shape[0] + X_new.shape[0] + X_previously_fit.shape[0], dtype=np.float64 # this seems off but works ) df["_n"] = numeric_indices - df[BATCH] = 1 # 1 for minibatch, 0 for existing graph, should all be `1` + df[BATCH] = 1 # 1 for minibatch, 0 for existing graph, here should all be `1` node = res._node + print('node self', node) + + assert node in df.columns + src = res._source dst = res._destination @@ -440,8 +443,6 @@ def infer_self_graph(res, new_edges = [] mdists = [] - # vsearch = build_search_index(X_previously_fit, angular=False) - for i in range(X_new.shape[0]): diff = X_previously_fit - X_new.iloc[i, :] dist = np.linalg.norm(diff, axis=1) # Euclidean distance @@ -462,6 +463,8 @@ def infer_self_graph(res, for i, dist in enumerate(mdists): record_df = df.iloc[i, :] nearest = np.where(dist < eps)[0] + if i < 2: + print('type dist': type(dist)) nn.append(len(nearest)) for j in nearest[:n_neighbors]: # add n_neighbors nearest neighbors, if any, super speedup hack if i != j: From caf367c8767f51fe5f52ba0376326f6c8fb92ac7 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Mar 2023 15:26:19 -0700 Subject: [PATCH 252/432] fix typo --- graphistry/ai_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 40f4e554f3..3e08090cfe 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -464,7 +464,7 @@ def infer_self_graph(res, record_df = df.iloc[i, :] nearest = np.where(dist < eps)[0] if i < 2: - print('type dist': type(dist)) + print('type dist', type(dist)) nn.append(len(nearest)) for j in nearest[:n_neighbors]: # add n_neighbors nearest neighbors, if any, super speedup hack if i != j: From 40914dcd9f5464fdcd821be1fa7cba6088f0e7bd Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Mar 2023 15:41:03 -0700 Subject: [PATCH 253/432] adds test if node not in df, adds numeric index --- graphistry/ai_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 3e08090cfe..c5a457a6d8 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -434,7 +434,8 @@ def infer_self_graph(res, node = res._node print('node self', node) - assert node in df.columns + if node not in df.columns: + df[node] = numeric_indices src = res._source dst = res._destination From b8a7eb5c09b63655636988021cd315706b3594ec Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Mar 2023 15:47:18 -0700 Subject: [PATCH 254/432] adds solution to both infer_graph and infer_self_graph functions --- graphistry/ai_utils.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index c5a457a6d8..f5f8805358 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -284,7 +284,10 @@ def infer_graph( df["_n"] = numeric_indices df[BATCH] = 1 # 1 for minibatch, 0 for existing graph node = res._node - print('Node', node) + + if node not in df.columns: + df[node] = numeric_indices + NDF = res._nodes NDF[BATCH] = 0 EDF = res._edges @@ -432,8 +435,6 @@ def infer_self_graph(res, df["_n"] = numeric_indices df[BATCH] = 1 # 1 for minibatch, 0 for existing graph, here should all be `1` node = res._node - print('node self', node) - if node not in df.columns: df[node] = numeric_indices @@ -464,8 +465,6 @@ def infer_self_graph(res, for i, dist in enumerate(mdists): record_df = df.iloc[i, :] nearest = np.where(dist < eps)[0] - if i < 2: - print('type dist', type(dist)) nn.append(len(nearest)) for j in nearest[:n_neighbors]: # add n_neighbors nearest neighbors, if any, super speedup hack if i != j: From b71d8adff9ba83b252c4ffacc3add9ff0abc1b76 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Mar 2023 15:59:54 -0700 Subject: [PATCH 255/432] fixes concat between cudf and pd --- graphistry/ai_utils.py | 2 ++ graphistry/umap_utils.py | 1 + 2 files changed, 3 insertions(+) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index f5f8805358..a0611eb455 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -366,6 +366,8 @@ def infer_graph( new_emb = None if emb is not None: + if 'cudf.core.dataframe.DataFrame' in type(old_emb): # convert to pd + old_emb = old_emb.to_pandas() new_emb = pd.concat([emb, old_emb], axis=0) new_features = pd.concat([X, FEATS.loc[old_nodes.index]], axis=0) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 555180e74f..a8cba2ed62 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -328,6 +328,7 @@ def transform_umap(self, df: pd.DataFrame, fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data, default True verbose: Whether to print information about the graph inference """ + df, y = make_safe_gpu_dataframes(df, y, 'pandas') X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) X, y_ = make_safe_gpu_dataframes(X, y_, self.engine) emb = self._umap.transform(X) # type: ignore From 8be6aa0dab3768ae111141730be4316db4e69338 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 13 Mar 2023 16:19:08 -0700 Subject: [PATCH 256/432] fixes bug --- graphistry/ai_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index a0611eb455..1f1586e5e4 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -366,7 +366,7 @@ def infer_graph( new_emb = None if emb is not None: - if 'cudf.core.dataframe.DataFrame' in type(old_emb): # convert to pd + if 'cudf.core.dataframe.DataFrame' in str(type(old_emb)): # convert to pd old_emb = old_emb.to_pandas() new_emb = pd.concat([emb, old_emb], axis=0) From 7482eb6d978081b3466d76540a034f1c11263cf5 Mon Sep 17 00:00:00 2001 From: dc Date: Tue, 14 Mar 2023 18:57:40 +0900 Subject: [PATCH 257/432] bring enc_X to cudf --- graphistry/feature_utils.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index f336166a60..44b8664a76 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -974,10 +974,16 @@ def process_dirty_dataframes( # now just set the feature names, since dirty cat changes them in # a weird way... data_encoder.get_feature_names_out = callThrough(features_transformed) - - X_enc = pd.DataFrame( - X_enc, columns=features_transformed, index=ndf.index - ) + print([type(X_enc),type(ndf),type(features_transformed)]) + if 'cudf.core.dataframe' not in str(getmodule(ndf)): + X_enc = pd.DataFrame( + X_enc, columns=features_transformed, index=ndf.index + ) + elif 'cudf.core.dataframe' in str(getmodule(ndf)): + import cudf + X_enc = cudf.DataFrame( + X_enc, columns=features_transformed, index=ndf.index + ) X_enc = X_enc.fillna(0.0) else: logger.info("-*-*- DataFrame is completely numeric") From e1ceb596257f475755b2ecd573a66d7c3f9d0af1 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 14 Mar 2023 15:07:24 -0700 Subject: [PATCH 258/432] coerces previously fit X to pandas if cudf --- graphistry/ai_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index 0ce1bc69c9..a3b4c1888d 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -294,6 +294,11 @@ def infer_graph( old_nodes = [] mdists = [] + ## check if pandas or cudf + if 'cudf.core.dataframe' in str(type(X_previously_fit)): + # move it out of memory... + X_previously_fit = X_previously_fit.to_pandas() + for i in range(X_new.shape[0]): diff = X_previously_fit - X_new.iloc[i, :] dist = np.linalg.norm(diff, axis=1) # Euclidean distance From 0dd3ced4709d3112b9f95177e9b879c3175ff67b Mon Sep 17 00:00:00 2001 From: dc Date: Wed, 15 Mar 2023 09:14:06 +0900 Subject: [PATCH 259/432] seemingly unavoidable cudf edge transforms to pandas --- graphistry/feature_utils.py | 42 +++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 44b8664a76..fe126a6abd 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -675,12 +675,19 @@ def fit_pipeline( """ columns = X.columns index = X.index - - X = transformer.fit_transform(X) - if keep_n_decimals: - X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa - - return pd.DataFrame(X, columns=columns, index=index) + X_type= str(getmodule(X)) + if 'cudf.core.dataframe' not in X_type: + X = transformer.fit_transform(X) + if keep_n_decimals: + X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa + X=pd.DataFrame(X, columns=columns, index=index) + elif 'cudf.core.dataframe' in X_type: + import cudf + X = transformer.fit_transform(X.to_numpy()) + if keep_n_decimals: + X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa + X=cudf.DataFrame(X, columns=columns, index=index) + return X def impute_and_scale_df( @@ -974,7 +981,6 @@ def process_dirty_dataframes( # now just set the feature names, since dirty cat changes them in # a weird way... data_encoder.get_feature_names_out = callThrough(features_transformed) - print([type(X_enc),type(ndf),type(features_transformed)]) if 'cudf.core.dataframe' not in str(getmodule(ndf)): X_enc = pd.DataFrame( X_enc, columns=features_transformed, index=ndf.index @@ -1301,9 +1307,15 @@ def encode_edges(edf, src, dst, mlb, fit=False): """ # uses mlb with fit=T/F so we can use it in transform mode # to recreate edge feature concat definition - source = edf[src] - destination = edf[dst] + logger.debug("Encoding Edges using MultiLabelBinarizer") + edf_type = str(getmodule(edf)) + if 'cudf.core.dataframe' in edf_type: + source = edf.to_pandas()[src] + destination = edf.to_pandas()[dst] + else: + source = edf[src] + destination = edf[dst] if fit: T = mlb.fit_transform(zip(source, destination)) else: @@ -1314,7 +1326,11 @@ def encode_edges(edf, src, dst, mlb, fit=False): ] # stringify the column names or scikits.base throws error mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] - T = pd.DataFrame(T, columns=columns, index=edf.index) + if 'cudf.core.dataframe' in edf_type: + import cudf + T = cudf.DataFrame(T, columns=columns, index=edf.index) + else: + T = pd.DataFrame(T, columns=columns, index=edf.index) logger.info(f"Shape of Edge Encoding: {T.shape}") return T, mlb @@ -1467,7 +1483,11 @@ def process_edge_dataframes( if not X_enc.empty and not T.empty: logger.debug("-" * 60) logger.debug("<= Found Edges and Dirty_cat encoding =>") - X_enc = pd.concat([T, X_enc], axis=1) + T_type= str(getmodule(T)) + if 'cudf.core.dataframe' not in T_type: + X_enc = pd.concat([T, X_enc], axis=1) + elif 'cudf.core.dataframe' not in T_type: + X_enc = cudf.concat([T, X_enc], axis=1) elif not T.empty and X_enc.empty: logger.debug("-" * 60) logger.debug("<= Found only Edges =>") From 73f8c9546f2524d98f7cbcc536f46f8a4112cf47 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 16 Mar 2023 10:39:51 +0900 Subject: [PATCH 260/432] incorp cudf-alex3 for cudf-cu_cat --- graphistry/feature_utils.py | 9 +++++---- graphistry/umap_utils.py | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 10cb6b9420..aa5b830ba3 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -668,7 +668,7 @@ def fit_pipeline( X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa X=pd.DataFrame(X, columns=columns, index=index) elif 'cudf.core.dataframe' in X_type: - import cudf + import cudf ## need proper import routine still X = transformer.fit_transform(X.to_numpy()) if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa @@ -1476,10 +1476,11 @@ def process_edge_dataframes( logger.debug("-" * 60) logger.debug("<= Found Edges and Dirty_cat encoding =>") T_type= str(getmodule(T)) - if 'cudf.core.dataframe' not in T_type: - X_enc = pd.concat([T, X_enc], axis=1) - elif 'cudf.core.dataframe' not in T_type: + if 'cudf.core.dataframe' in T_type: + import cudf X_enc = cudf.concat([T, X_enc], axis=1) + else: + X_enc = pd.concat([T, X_enc], axis=1) elif not T.empty and X_enc.empty: logger.debug("-" * 60) logger.debug("<= Found only Edges =>") diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 4821abf3dd..eb296025ee 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -1,6 +1,7 @@ import copy from time import time from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union +from inspect import getmodule import pandas as pd @@ -279,18 +280,25 @@ def transform_umap( # noqa: E303 emb = self._umap.transform(x) # type: ignore emb = self._bundle_embedding(emb, index=df.index) return emb, x, y - + def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 - if emb.shape[1] == 2: + + if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) + elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): + emb.rename(columns={0:config.X,1: config.Y},inplace=True) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1] - 2) ] - emb = pd.DataFrame(emb, columns=columns, index=index) - return emb + if 'cudf.core.dataframe' not in str(getmodule(emb)): + emb = pd.DataFrame(emb, columns=columns, index=index) + elif 'cudf.core.dataframe' in str(getmodule(emb)): + emb.columns=columns + return emb + def _process_umap( self, res, From c2ce1df0ce6e7f43c11b10e34cd4fbd7365a9458 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 16 Mar 2023 10:41:01 +0900 Subject: [PATCH 261/432] incorp cudf-alex3 for cudf-cu_cat --- graphistry/feature_utils.py | 608 +++++++++++++++++++++++------------- graphistry/umap_utils.py | 21 +- 2 files changed, 409 insertions(+), 220 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index fe126a6abd..406cba183d 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -24,6 +24,7 @@ from . import constants as config from .PlotterBase import WeakValueDictionary, Plottable from .util import setup_logger, check_set_memoize +from .ai_utils import infer_graph, infer_self_graph # add this inside classes and have a method that can set log level logger = setup_logger(name=__name__, verbose=config.VERBOSE) @@ -263,10 +264,7 @@ def safe_divide(a, b): def features_without_target( df: pd.DataFrame, y: Optional[Union[List, str, pd.DataFrame]] = None ) -> pd.DataFrame: - """ - Checks if y DataFrame column name is in df, and removes it - from df if so - ___________________________________________________________________ + """Checks if y DataFrame column name is in df, and removes it from df if so :param df: model DataFrame :param y: target DataFrame @@ -313,11 +311,7 @@ def remove_node_column_from_symbolic(X_symbolic, node): def remove_internal_namespace_if_present(df: pd.DataFrame): - """ - Some tranformations below add columns to the DataFrame, - this method removes them before featurization - Will not drop if suffix is added during UMAP-ing - ______________________________________________________________ + """Some tranformations below add columns to the DataFrame, this method removes them before featurization will not drop if suffix is added during UMAP-ing :param df: DataFrame :return: DataFrame with dropped columns in reserved namespace @@ -437,9 +431,8 @@ def is_dataframe_all_numeric(df: pd.DataFrame) -> bool: def find_bad_set_columns(df: pd.DataFrame, bad_set: List = ["[]"]): - """ - Finds columns that if not coerced to strings, will break processors. - ------------------------------------------------------------------------- + """Finds columns that if not coerced to strings, will break processors. + :param df: DataFrame :param bad_set: List of strings to look for. :return: list @@ -469,9 +462,7 @@ def check_if_textual_column( confidence: float = 0.35, min_words: float = 2.5, ) -> bool: - """ - Checks if `col` column of df is textual or not using basic heuristics - __________________________________________________________________________ + """Checks if `col` column of df is textual or not using basic heuristics :param df: DataFrame :param col: column name @@ -510,10 +501,8 @@ def check_if_textual_column( def get_textual_columns( df: pd.DataFrame, min_words: float = 2.5 ) -> List: - """ - Collects columns from df that it deems are textual. - _____________________________________________________________________ - + """Collects columns from df that it deems are textual. + :param df: DataFrame :return: list of columns names """ @@ -539,7 +528,6 @@ class Embedding: """ Generates random embeddings of a given dimension that aligns with the index of the dataframe - _____________________________________________________________________ """ def __init__(self, df: pd.DataFrame): @@ -577,14 +565,12 @@ def get_preprocessing_pipeline( encode: str = "ordinal", strategy: str = "quantile", ) -> Pipeline: # noqa - """ - Helper function for imputing and scaling np.ndarray data - using different scaling transformers. - ----------------------------------------------------------------- + """Helper function for imputing and scaling np.ndarray data using different scaling transformers. + :param X: np.ndarray :param impute: whether to run imputing or not :param use_scaler: string in None or - ["minmax", "quantile", "zscale", "robust", "kbins"], + ["minmax", "quantile", "standard", "robust", "kbins"], selects scaling transformer, default None :param n_quantiles: if use_scaler = 'quantile', sets the quantile bin size. @@ -613,7 +599,7 @@ def get_preprocessing_pipeline( available_preprocessors = [ "minmax", "quantile", - "zscale", + "standard", "robust", "kbins", ] @@ -640,7 +626,7 @@ def get_preprocessing_pipeline( scaler = QuantileTransformer( n_quantiles=n_quantiles, output_distribution=output_distribution ) - elif use_scaler == "zscale": + elif use_scaler == "standard": scaler = StandardScaler() elif use_scaler == "robust": scaler = RobustScaler(quantile_range=quantile_range) @@ -663,15 +649,15 @@ def fit_pipeline( X: pd.DataFrame, transformer, keep_n_decimals: int = 5 ) -> pd.DataFrame: """ - Helper to fit DataFrame over transformer pipeline. - Rounds resulting matrix X by keep_n_digits if not 0, - which helps for when transformer pipeline is scaling or imputer - which sometime introduce small negative numbers, - and umap metrics like Hellinger need to be positive - :param X, DataFrame to transform. + Helper to fit DataFrame over transformer pipeline. + Rounds resulting matrix X by keep_n_digits if not 0, + which helps for when transformer pipeline is scaling or imputer + which sometime introduce small negative numbers, + and umap metrics like Hellinger need to be positive + + :param X: DataFrame to transform. :param transformer: Pipeline object to fit and transform - :param keep_n_decimals: Int of how many decimal places to keep in - rounded transformed data + :param keep_n_decimals: Int of how many decimal places to keep in rounded transformed data """ columns = X.columns index = X.index @@ -843,7 +829,7 @@ def encoder(X, use_scaler): # noqa: E301 def get_cardinality_ratio(df: pd.DataFrame): """Calculates ratio of unique values to total number of rows of DataFrame - ------------------------------------------------------------------------- + :param df: DataFrame """ ratios = {} @@ -919,10 +905,7 @@ def process_dirty_dataframes( Union[SuperVectorizer, FunctionTransformer], Union[SuperVectorizer, FunctionTransformer], ]: - """ - Dirty_Cat encoder for record level data. Will automatically turn - inhomogeneous dataframe into matrix using smart conversion tricks. - ______________________________________________________________________ + """Dirty_Cat encoder for record level data. Will automatically turn inhomogeneous dataframe into matrix using smart conversion tricks. :param ndf: node DataFrame :param y: target DataFrame or series @@ -932,7 +915,7 @@ def process_dirty_dataframes( threshold, encoder is OneHot, above, it is GapEncoder :param n_topics: number of topics for GapEncoder, default 42 :param use_scaler: None or string in - ['minmax', 'zscale', 'robust', 'quantile'] + ['minmax', 'standard', 'robust', 'quantile'] :param similarity: one of 'ngram', 'levenshtein-ratio', 'jaro', or'jaro-winkler'}) – The type of pairwise string similarity to use. If None or False, uses a SuperVectorizer @@ -1081,6 +1064,8 @@ def process_nodes_dataframes( feature_engine: FeatureEngineConcrete = "pandas" # test_size: Optional[bool] = None, ) -> Tuple[ + pd.DataFrame, + Any, pd.DataFrame, Any, SuperVectorizer, @@ -1093,12 +1078,11 @@ def process_nodes_dataframes( """ Automatic Deep Learning Embedding/ngrams of Textual Features, with the rest of the columns taken care of by dirty_cat - _________________________________________________________________________ :param df: pandas DataFrame of data :param y: pandas DataFrame of targets :param use_scaler: None or string in - ['minmax', 'zscale', 'robust', 'quantile'] + ['minmax', 'standard', 'robust', 'quantile'] :param n_topics: number of topics in Gap Encoder :param use_scaler: :param confidence: Number between 0 and 1, will pass @@ -1124,7 +1108,7 @@ def process_nodes_dataframes( X_enc, y_enc, data_encoder, label_encoder = get_numeric_transformers( df, y ) - X_enc, y_enc, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa + X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa X_enc, y_enc, use_scaler, @@ -1146,6 +1130,8 @@ def process_nodes_dataframes( return ( X_enc, y_enc, + X_encs, + y_encs, data_encoder, label_encoder, scaling_pipeline, @@ -1212,7 +1198,7 @@ def process_nodes_dataframes( f"--The entire Encoding process took {(time()-t)/60:.2f} minutes" ) - X_enc, y_enc, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa + X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa X_enc, y_enc, use_scaler, @@ -1230,6 +1216,8 @@ def process_nodes_dataframes( return ( X_enc, y_enc, + X_encs, + y_encs, data_encoder, label_encoder, scaling_pipeline, @@ -1300,8 +1288,7 @@ def encode_edges(edf, src, dst, mlb, fit=False): src (string): source column dst (string): destination column mlb (sklearn): multilabelBinarizer - fit (bool, optional): If true, fits multilabelBinarizer. - Defaults to False. + fit (bool, optional): If true, fits multilabelBinarizer. Defaults to False. Returns: tuple: pd.DataFrame, multilabelBinarizer """ @@ -1341,7 +1328,7 @@ def process_edge_dataframes( src: str, dst: str, cardinality_threshold: int = 40, - cardinality_threshold_target: int = 100, + cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, use_scaler: Optional[str] = None, @@ -1351,7 +1338,6 @@ def process_edge_dataframes( ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, model_name: str = "paraphrase-MiniLM-L6-v2", similarity: Optional[str] = None, @@ -1366,6 +1352,8 @@ def process_edge_dataframes( keep_n_decimals: int = 5, feature_engine: FeatureEngineConcrete = "pandas", ) -> Tuple[ + pd.DataFrame, + pd.DataFrame, pd.DataFrame, pd.DataFrame, List[Any], @@ -1387,7 +1375,7 @@ def process_edge_dataframes( :param src: source column to select in edf :param dst: destination column to select in edf :param use_scaler: None or string in - ['minmax', 'zscale', 'robust', 'quantile'] + ['minmax', 'standard', 'robust', 'quantile'] :return: Encoded data matrix and target (if not None), the data encoders, and the label encoder. """ @@ -1422,7 +1410,7 @@ def process_edge_dataframes( # add the two datasets together X_enc = pd.concat([T, X_enc], axis=1) # then scale them - X_enc, y_enc, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa + X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa X_enc, y_enc, use_scaler, @@ -1442,6 +1430,8 @@ def process_edge_dataframes( return ( X_enc, y_enc, + X_encs, + y_encs, [mlb_pairwise_edge_encoder, data_encoder], label_encoder, scaling_pipeline, @@ -1453,6 +1443,8 @@ def process_edge_dataframes( ( X_enc, y_enc, + _, + _, data_encoder, label_encoder, _, @@ -1484,10 +1476,11 @@ def process_edge_dataframes( logger.debug("-" * 60) logger.debug("<= Found Edges and Dirty_cat encoding =>") T_type= str(getmodule(T)) - if 'cudf.core.dataframe' not in T_type: - X_enc = pd.concat([T, X_enc], axis=1) - elif 'cudf.core.dataframe' not in T_type: + if 'cudf.core.dataframe' in T_type: + import cudf X_enc = cudf.concat([T, X_enc], axis=1) + else: + X_enc = pd.concat([T, X_enc], axis=1) elif not T.empty and X_enc.empty: logger.debug("-" * 60) logger.debug("<= Found only Edges =>") @@ -1498,7 +1491,7 @@ def process_edge_dataframes( f" {(time()-t)/60:.2f} minutes" ) - X_enc, y_enc, scaling_pipeline, scaling_pipeline_target = smart_scaler( + X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( X_enc, y_enc, use_scaler, @@ -1516,6 +1509,8 @@ def process_edge_dataframes( res = ( X_enc, y_enc, + X_encs, + y_encs, [mlb_pairwise_edge_encoder, data_encoder], label_encoder, scaling_pipeline, @@ -1573,22 +1568,23 @@ def transform_dirty( data_encoder: Union[SuperVectorizer, FunctionTransformer], # type: ignore name: str = "", ) -> pd.DataFrame: - from sklearn.preprocessing import MultiLabelBinarizer + # from sklearn.preprocessing import MultiLabelBinarizer logger.debug(f"-{name} Encoder:") logger.debug(f"\t{data_encoder}\n") # print(f"-{name} Encoder:") # print(f"\t{data_encoder}\n") - try: - logger.debug(f"{data_encoder.get_feature_names_in}") - except Exception as e: - logger.warning(e) - pass + # try: + # logger.debug(f"{data_encoder.get_feature_names_in}") + # except Exception as e: + # logger.warning(e) + # pass logger.debug(f"TRANSFORM pre as df -- \t{df.shape}") # ##################################### for dirty_cat 0.3.0 use_columns = getattr(data_encoder, 'columns_', []) if len(use_columns): - X = data_encoder.transform(df[use_columns]) + #print(f"Using columns: {use_columns}") + X = data_encoder.transform(df[df.columns.intersection(use_columns)]) # ##################################### with dirty_cat 0.2.0 else: X = data_encoder.transform(df) @@ -1616,20 +1612,21 @@ def transform( # this function aligns with what is computed during # processing nodes or edges. ( - X_enc, - y_enc, + _, + _, + _, + _, data_encoder, label_encoder, - scaling_pipeline, - scaling_pipeline_target, + _, + _, text_model, text_cols, ) = res - # feature_columns = X_enc.columns - # feature_columns_target = y_enc.columns logger.info("-" * 90) - + + # index = df.index y = pd.DataFrame([]) T = pd.DataFrame([]) # encode nodes @@ -1685,14 +1682,14 @@ def transform( logger.info(f"--Features matrix shape: {X.shape}") logger.info(f"--Target matrix shape: {y.shape}") - if scaling_pipeline and not X.empty: - logger.info("--Scaling Features") - X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns) - if scaling_pipeline_target and not y.empty: - logger.info(f"--Scaling Target {scaling_pipeline_target}") - y = pd.DataFrame( - scaling_pipeline_target.transform(y), columns=y.columns - ) + # if scaling_pipeline and not X.empty: + # logger.info("--Scaling Features") + # X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns, index=index) + # if scaling_pipeline_target and not y.empty: + # logger.info(f"--Scaling Target {scaling_pipeline_target}") + # y = pd.DataFrame( + # scaling_pipeline_target.transform(y), columns=y.columns, index=index + # ) return X, y @@ -1748,6 +1745,8 @@ def _set_result(self, res): [ X_enc, y_enc, + X_encs, + y_encs, data_encoder, label_encoder, scaling_pipeline, @@ -1761,8 +1760,10 @@ def _set_result(self, res): # label_encoder.target_names_in = self.target_names_in self.feature_columns = X_enc.columns self.feature_columns_target = y_enc.columns - self.X = X_enc - self.y = y_enc + self.X = X_encs + self.y = y_encs + self.X_orignal = X_enc + self.y_orignal = y_enc self.data_encoder = data_encoder # is list for edges self.label_encoder = label_encoder self.scaling_pipeline = scaling_pipeline @@ -1779,40 +1780,68 @@ def fit(self, src=None, dst=None, *args, **kwargs): self._set_result(res) def transform(self, df, ydf=None): + "Raw transform, no scaling." X, y = transform(df, ydf, self.res, self.kind, self.src, self.dst) return X, y + + def _transform_scaled(self, df, ydf, scaling_pipeline, scaling_pipeline_target): + """Transform with scaling fit durning fit.""" + X, y = transform(df, ydf, self.res, self.kind, self.src, self.dst) + if scaling_pipeline is not None and not X.empty: + X = pd.DataFrame(scaling_pipeline.transform(X), columns=X.columns, index=X.index) + if scaling_pipeline_target is not None and y is not None and not y.empty: + y = pd.DataFrame(scaling_pipeline_target.transform(y), columns=y.columns, index=y.index) + return X, y + + def transform_scaled(self, df, ydf=None, scaling_pipeline=None, scaling_pipeline_target=None): + if scaling_pipeline is None: + scaling_pipeline = self.scaling_pipeline + if scaling_pipeline_target is None: + scaling_pipeline_target = self.scaling_pipeline_target + return self._transform_scaled(df, ydf, scaling_pipeline, scaling_pipeline_target) def fit_transform(self, src=None, dst=None, *args, **kwargs): self.fit(src=src, dst=dst, *args, **kwargs) return self.X, self.y - def scale(self, df, ydf=None, set_scaler=False, *args, **kwargs): - # pretty hacky but gets job done -- - """Fits new scaling functions on df, ydf via args-kwargs - (ie use downstream as X_train, X_test ,... or batch - when different scaling on the outputs is required) + def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): + """Fits new scaling functions on df, y via args-kwargs + + **Example:** + :: + + from graphisty.features import SCALERS, SCALER_OPTIONS + print(SCALERS) + g = graphistry.nodes(df) + # set a scaling strategy for features and targets -- umap uses those and produces different results depending. + g2 = g.umap(use_scaler='standard', use_scaler_target=None) + + # later if you want to scale new data, you can do so + X, y = g2.transform(df, df, scaled=False) # unscaled transformer output + # now scale with new settings + X_scaled, y_scaled = g2.scale(X, y, use_scaler='minmax', use_scaler_target='kbins', n_bins=5) + # fit some other pipeline + clf.fit(X_scaled, y_scaled) + + args: + :: + + + ;X: pd.DataFrame of features + :y: pd.DataFrame of target features + :kind: str, one of 'nodes' or 'edges' + *args, **kwargs: passed to smart_scaler pipeline + + returns: + scaled X, y """ - # pop off the previous scaler so that .transform won't use it - self.res[4] = None - self.res[5] = None - - X, y = self.transform(df, ydf) # these are the raw transforms, logger.info("-Fitting new scaler on raw features") X, y, scaling_pipeline, scaling_pipeline_target = smart_scaler( X_enc=X, y_enc=y, *args, **kwargs ) - - if set_scaler: - logger.info("--Setting fit scaler to self") - self.res[4] = scaling_pipeline - self.res[5] = scaling_pipeline_target - self.scaling_pipeline = scaling_pipeline - self.scaling_pipeline_target = scaling_pipeline_target - else: # add the original back - self.res[4] = self.scaling_pipeline - self.res[5] = self.scaling_pipeline_target - - return X, y, scaling_pipeline, scaling_pipeline_target + if return_pipeline: + return X, y, scaling_pipeline, scaling_pipeline_target + return X, y # ###################################################################################################################### @@ -1887,6 +1916,21 @@ def reuse_featurization( memoize=memoize, ) +def get_matrix_by_column_part(X: pd.DataFrame, column_part: str) -> pd.DataFrame: + """Get the feature matrix by column part existing in column names.""" + transformed_columns = X.columns[X.columns.map(lambda x: True if column_part in x else False)] # type: ignore + return X[transformed_columns] + +def get_matrix_by_column_parts(X: pd.DataFrame, column_parts: Optional[Union[list, str]]) -> pd.DataFrame: + """Get the feature matrix by column parts list existing in column names.""" + if column_parts is None: + return X + if isinstance(column_parts, str): + column_parts = [column_parts] + res = pd.concat([get_matrix_by_column_part(X, column_part) for column_part in column_parts], axis=1) # type: ignore + res = res.loc[:, ~res.columns.duplicated()] # type: ignore + return res + class FeatureMixin(MIXIN_BASE): """ @@ -1894,14 +1938,20 @@ class FeatureMixin(MIXIN_BASE): Subclasses UMAPMixin for umap-ing of automatic features. Usage: + :: + g = graphistry.nodes(df, 'node_column') g2 = g.featurize() or for edges, + :: + g = graphistry.edges(df, 'src', 'dst') g2 = g.featurize(kind='edges') - or chain them, + or chain them for both nodes and edges, + :: + g = graphistry.edges(edf, 'src', 'dst').nodes(ndf, 'node_column') g2 = g.featurize().featurize(kind='edges') @@ -1914,25 +1964,25 @@ def __init__(self, *args, **kwargs): pass def _get_feature(self, kind): - kind = kind.replace('s', '') - assert kind in ['node', 'edge'], f'kind needs to be in `nodes` or `edges`, found {kind}' - x = getattr(self, f'_{kind}_features') + kind2 = kind.replace('s', '') + assert kind2 in ['node', 'edge'], f'kind needs to be in `nodes` or `edges`, found {kind}' + x = getattr(self, f'_{kind2}_features') return x def _get_target(self, kind): - kind = kind.replace('s', '') - assert kind in ['node', 'edge'], f'kind needs to be in `nodes` or `edges`, found {kind}' - x = getattr(self, f'_{kind}_target') + kind2 = kind.replace('s', '') + assert kind2 in ['node', 'edge'], f'kind needs to be in `nodes` or `edges`, found {kind}' + x = getattr(self, f'_{kind2}_target') return x def _featurize_nodes( self, X: XSymbolic = None, y: YSymbolic = None, - use_scaler: Optional[str] = "zscale", - use_scaler_target: Optional[str] = "kbins", + use_scaler: Optional[str] = None, + use_scaler_target: Optional[str] = None, cardinality_threshold: int = 40, - cardinality_threshold_target: int = 120, + cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, multilabel: bool = False, @@ -1941,7 +1991,6 @@ def _featurize_nodes( ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, model_name: str = "paraphrase-MiniLM-L6-v2", similarity: Optional[str] = None, @@ -1957,8 +2006,9 @@ def _featurize_nodes( remove_node_column: bool = True, feature_engine: FeatureEngineConcrete = "pandas", memoize: bool = True, + verbose: bool = False, ): - res = self.copy() + res = self.copy() ndf = res._nodes node = res._node @@ -1986,8 +2036,10 @@ def _featurize_nodes( y_resolved = resolve_y(ndf, y) feature_engine = resolve_feature_engine(feature_engine) + + from .features import ModelDict - fkwargs = dict( + fkwargs = ModelDict("Featurize Params", X=X_resolved, y=y_resolved, use_scaler=use_scaler, @@ -2002,7 +2054,6 @@ def _featurize_nodes( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, min_words=min_words, model_name=model_name, similarity=similarity, @@ -2026,6 +2077,7 @@ def _featurize_nodes( old_res = reuse_featurization(res, memoize, fkwargs) if old_res: + print("--- [[ RE-USING NODE FEATURIZATION ]]") if verbose else None logger.info("--- [[ RE-USING NODE FEATURIZATION ]]") fresh_res = copy.copy(res) for attr in ["_node_features", "_node_target", "_node_encoder"]: @@ -2037,21 +2089,24 @@ def _featurize_nodes( X_resolved = remove_internal_namespace_if_present(X_resolved) keys_to_remove = ["X", "y", "remove_node_column"] - nfkwargs = {} + nfkwargs = dict() for key, value in fkwargs.items(): if key not in keys_to_remove: nfkwargs[key] = value - ############################################################# + print('-' * 80) if verbose else None + print("** Featuring nodes") if verbose else None + # ############################################################ encoder = FastEncoder(X_resolved, y_resolved, kind="nodes") encoder.fit(**nfkwargs) - ############################################################ + # ########################################################### # if changing, also update fresh_res res._node_features = encoder.X + res._node_features_raw = encoder.X_orignal # .copy() res._node_target = encoder.y + res._node_target_raw = encoder.y_orignal # .copy() res._node_encoder = encoder # now this does - # all the work `._node_encoder.transform(df, y)` etc return res @@ -2060,17 +2115,16 @@ def _featurize_edges( self, X: XSymbolic = None, y: YSymbolic = None, - use_scaler: Optional[str] = "zscale", - use_scaler_target: Optional[str] = "kbins", + use_scaler: Optional[str] = None, + use_scaler_target: Optional[str] = None, cardinality_threshold: int = 40, - cardinality_threshold_target: int = 20, + cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, use_ngrams: bool = False, ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, multilabel: bool = False, model_name: str = "paraphrase-MiniLM-L6-v2", @@ -2086,6 +2140,7 @@ def _featurize_edges( keep_n_decimals: int = 5, feature_engine: FeatureEngineConcrete = "pandas", memoize: bool = True, + verbose: bool = False, ): res = self.copy() @@ -2118,7 +2173,6 @@ def _featurize_edges( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, min_words=min_words, model_name=model_name, similarity=similarity, @@ -2160,6 +2214,7 @@ def _featurize_edges( if key not in keys_to_remove: nfkwargs[key] = value + print("** Featuring edges") if verbose else None ############################################################### encoder = FastEncoder(X_resolved, y_resolved, kind="edges") encoder.fit(src=res._source, dst=res._destination, **nfkwargs) @@ -2167,13 +2222,30 @@ def _featurize_edges( # if editing, should also update fresh_res res._edge_features = encoder.X + res._edge_features_raw = encoder.X_orignal # .copy() res._edge_target = encoder.y + res._edge_target_raw = encoder.y_orignal # .copy() res._edge_encoder = encoder return res + + def _infer_edges(self, emb, X, y, df, eps='auto', n_neighbors=4, sample=None, infer_on_umap_embedding=False, + verbose=False, merge_policy=False, **kwargs): + res = self.bind() + if merge_policy: + # useful to cluster onto existing graph + g = infer_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, + n_neighbors=n_neighbors, eps=eps, sample=sample, verbose=verbose, **kwargs) + else: + # useful to cluster onto self + g = infer_self_graph(res, emb, X, y, df, infer_on_umap_embedding=infer_on_umap_embedding, + n_neighbors=n_neighbors, eps=eps, verbose=verbose, **kwargs) + return g - def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): + def _transform(self, encoder: str, df: pd.DataFrame, ydf: Optional[pd.DataFrame], scaled): if getattr(self, encoder) is not None: + if scaled: + return getattr(self, encoder).transform_scaled(df, ydf) return getattr(self, encoder).transform(df, ydf) else: logger.debug( @@ -2181,45 +2253,126 @@ def _transform(self, encoder: str, df: pd.DataFrame, ydf: pd.DataFrame): "before being able to transform data" ) - def transform(self, df, ydf, kind): - """Transform new data""" + def transform(self, df: pd.DataFrame, + y: Optional[pd.DataFrame] = None, + kind: str = 'nodes', + min_dist: Union[str, float, int] = 'auto', + n_neighbors: int = 7, + merge_policy: bool = False, + sample: Optional[int] = None, + return_graph: bool = True, + scaled: bool = True, + verbose: bool = False): + """Transform new data and append to existing graph, or return dataframes + + **args:** + + :df: pd.DataFrame, raw data to transform + :ydf: pd.DataFrame, optional + :kind: str # one of `nodes`, `edges` + :return_graph: bool, if True, will return a graph with inferred edges. + :merge_policy: bool, if True, adds batch to existing graph nodes via nearest neighbors. If False, will infer edges only between nodes in the batch, default False + :min_dist: float, if return_graph is True, will use this value in NN search, or 'auto' to infer a good value. min_dist represents the maximum distance between two samples for one to be considered as in the neighborhood of the other. + :sample: int, if return_graph is True, will use sample edges of existing graph to fill out the new graph + :n_neighbors: int, if return_graph is True, will use this value for n_neighbors in Nearest Neighbors search + :scaled: bool, if True, will use scaled transformation of data set during featurization, default True + :verbose: bool, if True, will print metadata about the graph construction, default False + + **Returns:** + + X, y: pd.DataFrame, transformed data if return_graph is False + or a graphistry Plottable with inferred edges if return_graph is True + """ if kind == "nodes": - return self._transform("_node_encoder", df, ydf) + X, y_ = self._transform("_node_encoder", df, y, scaled=scaled) elif kind == "edges": - return self._transform("_edge_encoder", df, ydf) + X, y_ = self._transform("_edge_encoder", df, y, scaled=scaled) else: logger.debug("kind must be one of `nodes`," f"`edges`, found {kind}") + + if return_graph and kind not in ["edges"]: + emb = None # will not be able to infer graph from umap coordinates, + # but will be able to infer graph from features of existing edges + g = self._infer_edges(emb, X, y_, df, eps=min_dist, sample=sample, n_neighbors=n_neighbors, + infer_on_umap_embedding=False, merge_policy=merge_policy, + verbose=verbose) + return g + return X, y_ def scale( self, - df, - ydf, - kind, - use_scaler, - use_scaler_target, - set_scaler=False, + df: Optional[pd.DataFrame] = None, + y: Optional[pd.DataFrame] = None, + kind: str = "nodes", + use_scaler: Union[str, None] = None, + use_scaler_target: Union[str, None] = None, impute: bool = True, n_quantiles: int = 10, output_distribution: str = "normal", quantile_range=(25, 75), - n_bins: int = 2, + n_bins: int = 10, encode: str = "ordinal", strategy: str = "uniform", keep_n_decimals: int = 5, + return_scalers: bool = False, ): + """Scale data using the same scalers as used in the featurization step. + + **Example** + :: + + g = graphistry.nodes(df) + X, y = g.featurize().scale(kind='nodes', use_scaler='robust', use_scaler_target='kbins', n_bins=3) + + # or + g = graphistry.nodes(df) + # set a scaling strategy for features and targets -- umap uses those and produces different results depending. + g2 = g.umap(use_scaler='standard', use_scaler_target=None) + + # later if you want to scale new data, you can do so + X, y = g2.transform(df, df, scale=False) + X_scaled, y_scaled = g2.scale(X, y, use_scaler='minmax', use_scaler_target='kbins', n_bins=5) + # fit some other pipeline + clf.fit(X_scaled, y_scaled) + + **Args:** + + :df: pd.DataFrame, raw data to transform, if None, will use data from featurization fit + :y: pd.DataFrame, optional target data + :kind: str, one of `nodes`, `edges` + :use_scaler: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` + :use_scaler_target: str, optional, one of `minmax`, `robust`, `standard`, `kbins`, `quantile` + :impute: bool, if True, will impute missing values + :n_quantiles: int, number of quantiles to use for quantile scaler + :output_distribution: str, one of `normal`, `uniform`, `lognormal` + :quantile_range: tuple, range of quantiles to use for quantile scaler + :n_bins: int, number of bins to use for KBinsDiscretizer + :encode: str, one of `ordinal`, `onehot`, `onehot-dense`, `binary` + :strategy: str, one of `uniform`, `quantile`, `kmeans` + :keep_n_decimals: int, number of decimals to keep after scaling + :return_scalers: bool, if True, will return the scalers used to scale the data + + **Returns:** + + (X, y) transformed data if return_graph is False or a graph with inferred edges if return_graph is True, or (X, y, scaler, scaler_target) if return_scalers is True + """ + + if df is None: # use the original data + X, y = (self._node_features_raw, self._node_target_raw) if kind == "nodes" else (self._edge_features_raw, self._edge_target_raw) # type: ignore + else: + X, y = self.transform(df, y, kind=kind, return_graph=False, scaled=False) if kind == "nodes" and hasattr(self, "_node_encoder"): # type: ignore if self._node_encoder is not None: # type: ignore ( X, y, - scaling_pipeline, - scaling_pipeline_target, + scaler, + scaler_target ) = self._node_encoder.scale( - df, - ydf, - set_scaler=set_scaler, + X, + y, use_scaler=use_scaler, use_scaler_target=use_scaler_target, impute=impute, @@ -2230,6 +2383,7 @@ def scale( encode=encode, strategy=strategy, keep_n_decimals=keep_n_decimals, + return_pipeline=True ) # type: ignore else: raise AttributeError( @@ -2243,12 +2397,11 @@ def scale( ( X, y, - scaling_pipeline, - scaling_pipeline_target, + scaler, + scaler_target ) = self._edge_encoder.scale( - df, - ydf, - set_scaler=set_scaler, + X, + y, use_scaler=use_scaler, use_scaler_target=use_scaler_target, impute=impute, @@ -2259,14 +2412,17 @@ def scale( encode=encode, strategy=strategy, keep_n_decimals=keep_n_decimals, + return_pipeline=True ) # type: ignore else: raise AttributeError( 'Please run g.featurize(kind="edges", *args, **kwargs) ' 'first before scaling matrices and targets is possible.' ) + if return_scalers: + return X, y, scaler, scaler_target + return X, y - return X, y, scaling_pipeline, scaling_pipeline_target def featurize( self, @@ -2285,29 +2441,29 @@ def featurize( ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - min_words: float = 2.5, + min_words: float = 4.5, model_name: str = "paraphrase-MiniLM-L6-v2", impute: bool = True, n_quantiles: int = 100, output_distribution: str = "normal", - quantile_range=(25, 75), + quantile_range = (25, 75), n_bins: int = 10, encode: str = "ordinal", strategy: str = "uniform", - similarity: Optional[ - str - ] = None, # turn this off in favor of Gap Encoder + similarity: Optional[str] = None, # turn this off in favor of Gap Encoder categories: Optional[str] = "auto", keep_n_decimals: int = 5, remove_node_column: bool = True, inplace: bool = False, feature_engine: FeatureEngine = "auto", + dbscan: bool = False, + min_dist: float = 0.5, # DBSCAN eps + min_samples: int = 1, # DBSCAN min_samples memoize: bool = True, + verbose: bool = False, ): - r""" - Featurize Nodes or Edges of the underlying nodes/edges DataFrames. - ______________________________________________________________________ - + r"""Featurize Nodes or Edges of the underlying nodes/edges DataFrames. + :param kind: specify whether to featurize `nodes` or `edges`. Edge featurization includes a pairwise src-to-dst feature block using a MultiLabelBinarizer, @@ -2320,11 +2476,11 @@ def featurize( :param use_scaler: selects which scaler (and automatically imputes missing values using mean strategy) to scale the data. Options are; - "minmax", "quantile", "zscale", "robust", + "minmax", "quantile", "standard", "robust", "kbins", default None. Please see scikits-learn documentation https://scikit-learn.org/stable/modules/preprocessing.html - Here 'zscale' corresponds to 'StandardScaler' in scikits. + Here 'standard' corresponds to 'StandardScaler' in scikits. :param cardinality_threshold: dirty_cat threshold on cardinality of categorical labels across columns. If value is greater than threshold, will run GapEncoder @@ -2363,20 +2519,21 @@ def featurize( but at cost of encoding time. If faster encoding is needed, `average_word_embeddings_komninos` is useful and produces less semantically relevant vectors. - Please see www.huggingface.co or sentence_transformer + Please see sentence_transformer (https://www.sbert.net/) library for all available models. :param multilabel: if True, will encode a *single* target column composed of lists of lists as multilabel outputs. This only works with y=['a_single_col'], default False :param embedding: If True, produces a random node embedding of size `n_topics` - default, False. + default, False. If no node features are provided, will produce random embeddings + (for GNN models, for example) :param use_ngrams: If True, will encode textual columns as TfIdf Vectors, default, False. :param ngram_range: if use_ngrams=True, can set ngram_range, eg: tuple = (1, 3) :param max_df: if use_ngrams=True, set max word frequency to consider in vocabulary eg: max_df = 0.2, :param min_df: if use_ngrams=True, set min word count to consider in vocabulary - eg: min_df = 3 + eg: min_df = 3 or 0.00001 :param categories: Optional[str] in ["auto", "k-means", "most_frequent"], decides which category to select in Similarity Encoding, default 'auto' :param impute: Whether to impute missing values, default True @@ -2386,7 +2543,7 @@ def featurize( can return distribution as ["normal", "uniform"] :param quantile_range: if use_scaler = 'robust'|'quantile', sets the quantile range. - :param n_bins: number of bins to use in kbins discretizer + :param n_bins: number of bins to use in kbins discretizer, default 10 :param encode: encoding for KBinsDiscretizer, can be one of `onehot`, `onehot-dense`, `ordinal`, default 'ordinal' :param strategy: strategy for KBinsDiscretizer, can be one of @@ -2394,6 +2551,9 @@ def featurize( :param n_quantiles: if use_scaler = "quantile", sets the number of quantiles, default=100 :param output_distribution: if use_scaler="quantile"|"robust", choose from ["normal", "uniform"] + :param dbscan: whether to run DBSCAN, default False. + :param min_dist: DBSCAN eps parameter, default 0.5. + :param min_samples: DBSCAN min_samples parameter, default 5. :param keep_n_decimals: number of decimals to keep :param remove_node_column: whether to remove node column so it is not featurized, default True. @@ -2401,7 +2561,7 @@ def featurize( not, default False. :param memoize: whether to store and reuse results across runs, default True. - :return: self, with new attributes set by the featurization process. + :return: graphistry instance with new attributes set by the featurization process. """ if feature_engine == 'dirty_cat': assert_imported() @@ -2430,11 +2590,10 @@ def featurize( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, # deprecated min_words=min_words, model_name=model_name, - similarity=similarity, # deprecated - categories=categories, # deprecated + similarity=similarity, + categories=categories, impute=impute, n_quantiles=n_quantiles, quantile_range=quantile_range, @@ -2446,6 +2605,7 @@ def featurize( remove_node_column=remove_node_column, feature_engine=feature_engine, memoize=memoize, + verbose=verbose ) elif kind == "edges": res = res._featurize_edges( @@ -2462,11 +2622,10 @@ def featurize( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, # deprecated min_words=min_words, model_name=model_name, - similarity=similarity, # deprecated - categories=categories, # deprecated + similarity=similarity, + categories=categories, impute=impute, n_quantiles=n_quantiles, quantile_range=quantile_range, @@ -2477,12 +2636,17 @@ def featurize( keep_n_decimals=keep_n_decimals, feature_engine=feature_engine, memoize=memoize, + verbose=verbose ) else: logger.warning( f"One may only featurize `nodes` or `edges`, got {kind}" ) return self + + if dbscan: # this adds columns to the dataframe, will break tests of pure featurization & umap, so set to False in those + res = res.dbscan(min_dist=min_dist, min_samples=min_samples, kind=kind, fit_umap_embedding=False, verbose=verbose) # type: ignore + if not inplace: return res @@ -2490,19 +2654,18 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( self, X: XSymbolic = None, y: YSymbolic = None, - use_scaler: Optional[str] = "zscale", - use_scaler_target: Optional[str] = "kbins", + use_scaler: Optional[str] = None, + use_scaler_target: Optional[str] = None, cardinality_threshold: int = 40, cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, multilabel: bool = False, - embedding=False, + embedding: bool = False, use_ngrams: bool = False, ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, model_name: str = "paraphrase-MiniLM-L6-v2", similarity: Optional[ @@ -2521,13 +2684,13 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( feature_engine: FeatureEngineConcrete = "pandas", reuse_if_existing=False, memoize: bool = True, + verbose: bool = False, ) -> Tuple[pd.DataFrame, pd.DataFrame, MIXIN_BASE]: """ helper method gets node feature and target matrix if X, y are not specified. if X, y are specified will set them as `_node_target` and `_node_target` attributes - ----------------------------------------------------------- """ res = self.bind() @@ -2537,7 +2700,7 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( res._node_target = None if reuse_if_existing and res._node_features is not None: - # logger.info('-Reusing Existing Featurization') + logger.info('-Reusing Existing Node Featurization') return res._node_features, res._node_target, res res = res._featurize_nodes( @@ -2555,7 +2718,6 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, min_words=min_words, model_name=model_name, similarity=similarity, @@ -2571,6 +2733,7 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( remove_node_column=remove_node_column, feature_engine=feature_engine, memoize=memoize, + verbose=verbose, ) assert res._node_features is not None # ensure no infinite loop @@ -2586,10 +2749,10 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( self, X: XSymbolic = None, y: YSymbolic = None, - use_scaler: Optional[str] = "robust", - use_scaler_target: Optional[str] = "kbins", + use_scaler: Optional[str] = None, + use_scaler_target: Optional[str] = None, cardinality_threshold: int = 40, - cardinality_threshold_target: int = 20, + cardinality_threshold_target: int = 400, n_topics: int = config.N_TOPICS_DEFAULT, n_topics_target: int = config.N_TOPICS_TARGET_DEFAULT, multilabel: bool = False, @@ -2597,7 +2760,6 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ngram_range: tuple = (1, 3), max_df: float = 0.2, min_df: int = 3, - #confidence: float = 0.35, min_words: float = 2.5, model_name: str = "paraphrase-MiniLM-L6-v2", similarity: Optional[ @@ -2615,11 +2777,10 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( feature_engine: FeatureEngineConcrete = "pandas", reuse_if_existing=False, memoize: bool = True, + verbose: bool = False, ) -> Tuple[pd.DataFrame, Optional[pd.DataFrame], MIXIN_BASE]: - """ - helper method gets edge feature and target matrix if X, y - are not specified - ----------------------------------------------------------- + """helper method gets edge feature and target matrix if X, y are not specified + :param X: Data Matrix :param y: target, default None :return: data `X` and `y` @@ -2632,7 +2793,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( res._edge_target = None if reuse_if_existing and res._edge_features is not None: - # logger.info('-Reusing Existing Featurization') + logger.info('-Reusing Existing Edge Featurization') return res._edge_features, res._edge_target, res res = res._featurize_edges( @@ -2649,7 +2810,6 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( ngram_range=ngram_range, max_df=max_df, min_df=min_df, - #confidence=confidence, min_words=min_words, model_name=model_name, similarity=similarity, @@ -2664,6 +2824,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( keep_n_decimals=keep_n_decimals, feature_engine=feature_engine, memoize=memoize, + verbose=verbose, ) assert res._edge_features is not None # ensure no infinite loop @@ -2675,39 +2836,54 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( memoize=memoize, ) - def _features_by_col(self, column_part: str, kind: str): - if kind == 'nodes' and hasattr(self, '_node_features'): - X = self._node_features - elif kind == 'edges' and hasattr(self, '_edge_features'): - X = self._edge_features - else: - raise ValueError('make sure to call `featurize` or `umap` before calling `get_features_by_cols`') - - transformed_columns = X.columns[X.columns.map(lambda x: True if column_part in x else False)] # type: ignore - return X[transformed_columns] # type: ignore - def get_features_by_cols(self, columns: Union[List, str], kind: str = 'nodes'): - """Returns feature matrix with only the columns that contain the string `column_part` in their name. - - `X = g.get_features_by_cols(['feature1', 'feature2'])` + def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False) -> pd.DataFrame: + """ + Returns feature matrix, and if columns are specified, returns matrix with only the columns that contain + the string `column_part` in their name. + + `X = g.get_matrix(['feature1', 'feature2'])` will retrieve a feature matrix with only the columns that contain the string `feature1` or `feature2` in their name. + + Most useful for topic modeling, where the column names are of the form `topic_0: descriptor`, `topic_1: descriptor`, etc. + Can retrieve unique columns in original dataframe, or actual topic features like [ip_part, shoes, preference_x, etc]. + + Powerful way to retrieve features from a featurized graph by column or (top) features of interest. - example: - res = g2.get_features_by_cols(['172', 'percent']) - res.columns + **Example:** + :: + + # get the full feature matrices + X = g.get_matrix() + y = g.get_matrix(target=True) + + # get subset of features, or topics, given topic model encoding + X = g2.get_matrix(['172', 'percent']) + X.columns => ['ip_172.56.104.67', 'ip_172.58.129.252', 'item_percent'] + # or in targets + y = g2.get_matrix(['total', 'percent'], target=True) + y.columns + => ['basket_price_total', 'conversion_percent', 'CTR_percent', 'CVR_percent'] + + # not as useful for sbert features. + + Caveats: + - if you have a column name that is a substring of another column name, you may get unexpected results. + + Args: + :columns (Union[List, str]): list of column names or a single column name that may exist in columns + of the feature matrix. If None, returns original feature matrix + :kind (str, optional): Node or Edge features. Defaults to 'nodes'. + :target (bool, optional): If True, returns the target matrix. Defaults to False. + + Returns: + pd.DataFrame: feature matrix with only the columns that contain the string `column_part` in their name. + """ + if target: + X = self._get_target(kind) + else: + X = self._get_feature(kind) - Args: - columns (Union[List, str]): list of column names or a single column name that may exist in columns - of the feature matrix. - kind (str, optional): Node or Edge features. Defaults to 'nodes'. - - Returns: - pd.DataFrame: feature matrix with only the columns that contain the string `column_part` in their name. - """ - if isinstance(columns, str): - columns = [columns] - X = pd.concat([self._features_by_col(col, kind=kind) for col in columns], axis=1) # type: ignore - X = X.loc[:, ~X.columns.duplicated()] # type: ignore - return X + return get_matrix_by_column_parts(X, columns) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 5cd092f29d..eb296025ee 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -1,6 +1,7 @@ import copy from time import time from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union +from inspect import getmodule import pandas as pd @@ -279,18 +280,25 @@ def transform_umap( # noqa: E303 emb = self._umap.transform(x) # type: ignore emb = self._bundle_embedding(emb, index=df.index) return emb, x, y - + def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 - if emb.shape[1] == 2: + + if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) + elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): + emb.rename(columns={0:config.X,1: config.Y},inplace=True) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1] - 2) ] - emb = pd.DataFrame(emb, columns=columns, index=index) - return emb + if 'cudf.core.dataframe' not in str(getmodule(emb)): + emb = pd.DataFrame(emb, columns=columns, index=index) + elif 'cudf.core.dataframe' in str(getmodule(emb)): + emb.columns=columns + return emb + def _process_umap( self, res, @@ -491,6 +499,11 @@ def umap( logger.debug("umap X_: %s", X_) logger.debug("umap y_: %s", y_) + logger.debug("data is type :: %s", (type(X_))) + if isinstance(X_, pd.DataFrame): + index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) + elif 'cudf.core.dataframe' in str(getmodule(X_)): + index_to_nodes_dict = nodes res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs From ba5629255d7830d480a2a8c0cbf1bf1b68561663 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 16 Mar 2023 18:23:29 +0900 Subject: [PATCH 262/432] naive cudf tests --- graphistry/tests/test_umap_utils.py | 164 +++++++++++++++++++++++++++- graphistry/umap_utils.py | 11 ++ 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index ca0c3897ba..6ed8f8f00f 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -22,10 +22,11 @@ lazy_import_has_min_dependancy, check_allclose_fit_transform_on_same_data ) -from graphistry.umap_utils import lazy_umap_import_has_dependancy, lazy_cuml_import_has_dependancy +from graphistry.umap_utils import lazy_umap_import_has_dependancy, lazy_cuml_import_has_dependancy, lazy_cudf_import_has_dependancy has_dependancy, _ = lazy_import_has_min_dependancy() has_cuml, _, _ = lazy_cuml_import_has_dependancy() +has_cudf, _, _ = lazy_cudf_import_has_dependancy() has_umap, _, _ = lazy_umap_import_has_dependancy() logger = logging.getLogger(__name__) @@ -560,5 +561,166 @@ def test_filter_edges(self): last_shape = shape[0] +@pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", +) +class TestCUDFMethods(TestUMAPMethods): + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def _test_umap(self, g, use_cols, targets, name, kind, df): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + for scaler in ['kbins', 'robust']: + for cardinality in [2, 200]: + for use_ngram in [True, False]: + for use_col in use_cols: + for target in targets: + logger.debug("*" * 90) + value = [scaler, cardinality, use_ngram, target, use_col] + logger.debug(f"{value}") + logger.debug("-" * 80) + + g = graphistry.nodes(cudf.from_pandas(ndf_reddit)) + g2 = g.umap(kind=kind, + X=use_col, + y=target, + model_name=model_avg_name, + use_scaler=scaler, + use_scaler_target=scaler, + use_ngrams=use_ngram, + engine='cudf', + cardinality_threshold=cardinality, + cardinality_threshold_target=cardinality, + n_neighbors=3) + + self.cases_test_graph(g2, kind=kind, df=df) + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_node_umap(self): + g = graphistry.nodes(cudf.from_pandas(ndf_reddit)) + use_cols = [None, text_cols_reddit, good_cols_reddit, meta_cols_reddit] + targets = [None, single_target_reddit, double_target_reddit] + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + + self._test_umap( + g, + use_cols=use_cols, + targets=targets, + name="Node UMAP with `(target, use_col)=`", + kind="nodes", + df=ndf_reddit, + ) + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_edge_umap(self): + g = graphistry.nodes(cudf.from_pandas(edge_df2), "src", "dst") + targets = [None, 'label'] + use_cols = [None, 'title'] + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + + self._test_umap( + g, + use_cols=use_cols, + targets=targets, + name="Edge UMAP with `(target, use_col)=`", + kind="edges", + df=edge_df2, + ) + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_chaining_nodes(self): + g = graphistry.nodes(cudf.from_pandas(ndf_reddit)) + g2 = g.umap() + + logger.debug('======= g.umap() done ======') + g3a = g2.featurize() + logger.debug('======= g3a.featurize() done ======') + g3 = g3a.umap() + logger.debug('======= g3.umap() done ======') + assert g2._node_features.shape == g3._node_features.shape + # since g3 has feature params with x and y. + g3._feature_params['nodes']['X'].pop('x') + g3._feature_params['nodes']['X'].pop('y') + assert all(g2._feature_params['nodes']['X'] == g3._feature_params['nodes']['X']) + assert g2._feature_params['nodes']['y'].shape == g3._feature_params['nodes']['y'].shape # None + assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_chaining_edges(self): + g = graphistry.nodes(cudf.from_pandas(edge_df), "src", "dst") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + g2 = g.umap(kind='edges') + g3 = g.featurize(kind='edges').umap(kind='edges') + + assert all(g2._feature_params['edges']['X'] == g3._feature_params['edges']['X']) + assert all(g2._feature_params['edges']['y'] == g3._feature_params['edges']['y']) # None + assert all(g2._edge_features == g3._edge_features) + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_feature_kwargs_yield_different_values_using_umap_api(self): + g = graphistry.nodes(cudf.from_pandas(ndf_reddit)) + n_topics_target = 6 + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + + g2 = g.umap(X="type", y="label", cardinality_threshold_target=3, n_topics_target=n_topics_target) # makes a GapEncoded Target + g3 = g.umap(X="type", y="label", cardinality_threshold_target=30000) # makes a one-hot-encoded target + + assert all(g2._feature_params['nodes']['X'] == g3._feature_params['nodes']['X']), "features should be the same" + assert all(g2._feature_params['nodes']['y'] != g3._feature_params['nodes']['y']), "targets in memoize should be different" # None + assert g2._node_target.shape[1] != g3._node_target.shape[1], 'Targets should be different' + assert g2._node_target.shape[1] == n_topics_target, 'Targets ' + + @pytest.mark.skipif( + not has_dependancy or not has_umap, + reason="requires ai+umap feature dependencies", + ) + def test_filter_edges(self): + for kind, g in [("nodes", graphistry.nodes(cudf.from_pandas(ndf_reddit)))]: + g2 = g.umap(kind=kind, model_name=model_avg_name) + last_shape = 0 + for scale in np.linspace(0, 1, 8): # six sigma in 8 steps + g3 = g2.filter_weighted_edges(scale=scale) + shape = g3._edges.shape + logger.debug("*" * 90) + logger.debug( + f"{kind} -- scale: {scale}: resulting edges dataframe shape: {shape}" + ) + logger.debug("-" * 80) + self.assertGreaterEqual(shape[0], last_shape) + last_shape = shape[0] + + if __name__ == "__main__": unittest.main() diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index eb296025ee..46662c4b9b 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -45,6 +45,17 @@ def lazy_cuml_import_has_dependancy(): return True, "ok", cuml except ModuleNotFoundError as e: return False, e, None + +def lazy_cudf_import_has_dependancy(): + try: + import warnings + + warnings.filterwarnings("ignore") + import cudf # type: ignore + + return True, "ok", cudf + except ModuleNotFoundError as e: + return False, e, None def assert_imported(): From f7b7156d983ccd818d000e06252b56f388067678 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 16 Mar 2023 18:40:36 +0900 Subject: [PATCH 263/432] merge clean, add naive tests --- graphistry/tests/test_umap_utils.py | 164 ++++++++++++++++++++++++++++ graphistry/umap_utils.py | 10 ++ 2 files changed, 174 insertions(+) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index acfb39cfd7..ea828f0f84 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -27,14 +27,17 @@ from graphistry.umap_utils import ( lazy_umap_import_has_dependancy, lazy_cuml_import_has_dependancy, + lazy_cudf_import_has_dependancy, ) has_dependancy, _ = lazy_import_has_min_dependancy() has_cuml, _, _ = lazy_cuml_import_has_dependancy() +has_cudf, _, _ = lazy_cudf_import_has_dependancy() has_umap, _, _ = lazy_umap_import_has_dependancy() # print('has_dependancy', has_dependancy) # print('has_cuml', has_cuml) +# print('has_cudf', has_cudf) # print('has_umap', has_umap) logger = logging.getLogger(__name__) @@ -776,5 +779,166 @@ def test_filter_edges(self): last_shape = shape[0] +@pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", +) +class TestCUDFMethods(TestUMAPMethods): + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def _test_umap(self, g, use_cols, targets, name, kind, df): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + for scaler in ['kbins', 'robust']: + for cardinality in [2, 200]: + for use_ngram in [True, False]: + for use_col in use_cols: + for target in targets: + logger.debug("*" * 90) + value = [scaler, cardinality, use_ngram, target, use_col] + logger.debug(f"{value}") + logger.debug("-" * 80) + + g = graphistry.nodes(cudf.from_pandas(ndf_reddit)) + g2 = g.umap(kind=kind, + X=use_col, + y=target, + model_name=model_avg_name, + use_scaler=scaler, + use_scaler_target=scaler, + use_ngrams=use_ngram, + engine='cudf', + cardinality_threshold=cardinality, + cardinality_threshold_target=cardinality, + n_neighbors=3) + + self.cases_test_graph(g2, kind=kind, df=df) + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_node_umap(self): + g = graphistry.nodes(cudf.from_pandas(ndf_reddit)) + use_cols = [None, text_cols_reddit, good_cols_reddit, meta_cols_reddit] + targets = [None, single_target_reddit, double_target_reddit] + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + + self._test_umap( + g, + use_cols=use_cols, + targets=targets, + name="Node UMAP with `(target, use_col)=`", + kind="nodes", + df=ndf_reddit, + ) + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_edge_umap(self): + g = graphistry.nodes(cudf.from_pandas(edge_df2), "src", "dst") + targets = [None, 'label'] + use_cols = [None, 'title'] + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + + self._test_umap( + g, + use_cols=use_cols, + targets=targets, + name="Edge UMAP with `(target, use_col)=`", + kind="edges", + df=edge_df2, + ) + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_chaining_nodes(self): + g = graphistry.nodes(cudf.from_pandas(ndf_reddit)) + g2 = g.umap() + + logger.debug('======= g.umap() done ======') + g3a = g2.featurize() + logger.debug('======= g3a.featurize() done ======') + g3 = g3a.umap() + logger.debug('======= g3.umap() done ======') + assert g2._node_features.shape == g3._node_features.shape + # since g3 has feature params with x and y. + g3._feature_params['nodes']['X'].pop('x') + g3._feature_params['nodes']['X'].pop('y') + assert all(g2._feature_params['nodes']['X'] == g3._feature_params['nodes']['X']) + assert g2._feature_params['nodes']['y'].shape == g3._feature_params['nodes']['y'].shape # None + assert g2._node_embedding.shape == g3._node_embedding.shape # kinda weak sauce + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_chaining_edges(self): + g = graphistry.nodes(cudf.from_pandas(edge_df), "src", "dst") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + g2 = g.umap(kind='edges') + g3 = g.featurize(kind='edges').umap(kind='edges') + + assert all(g2._feature_params['edges']['X'] == g3._feature_params['edges']['X']) + assert all(g2._feature_params['edges']['y'] == g3._feature_params['edges']['y']) # None + assert all(g2._edge_features == g3._edge_features) + + @pytest.mark.skipif( + not has_dependancy or not has_cudf, + reason="requires cudf feature dependencies", + ) + def test_feature_kwargs_yield_different_values_using_umap_api(self): + g = graphistry.nodes(cudf.from_pandas(ndf_reddit)) + n_topics_target = 6 + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + warnings.filterwarnings("ignore", category=DeprecationWarning) + warnings.filterwarnings("ignore", category=FutureWarning) + + g2 = g.umap(X="type", y="label", cardinality_threshold_target=3, n_topics_target=n_topics_target) # makes a GapEncoded Target + g3 = g.umap(X="type", y="label", cardinality_threshold_target=30000) # makes a one-hot-encoded target + + assert all(g2._feature_params['nodes']['X'] == g3._feature_params['nodes']['X']), "features should be the same" + assert all(g2._feature_params['nodes']['y'] != g3._feature_params['nodes']['y']), "targets in memoize should be different" # None + assert g2._node_target.shape[1] != g3._node_target.shape[1], 'Targets should be different' + assert g2._node_target.shape[1] == n_topics_target, 'Targets ' + + @pytest.mark.skipif( + not has_dependancy or not has_umap, + reason="requires ai+umap feature dependencies", + ) + def test_filter_edges(self): + for kind, g in [("nodes", graphistry.nodes(cudf.from_pandas(ndf_reddit)))]: + g2 = g.umap(kind=kind, model_name=model_avg_name) + last_shape = 0 + for scale in np.linspace(0, 1, 8): # six sigma in 8 steps + g3 = g2.filter_weighted_edges(scale=scale) + shape = g3._edges.shape + logger.debug("*" * 90) + logger.debug( + f"{kind} -- scale: {scale}: resulting edges dataframe shape: {shape}" + ) + logger.debug("-" * 80) + self.assertGreaterEqual(shape[0], last_shape) + last_shape = shape[0] + + if __name__ == "__main__": unittest.main() diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index eb296025ee..ff70e17660 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -46,6 +46,16 @@ def lazy_cuml_import_has_dependancy(): except ModuleNotFoundError as e: return False, e, None +def lazy_cudf_import_has_dependancy(): ## rather than adding to cuml import + try: + import warnings + + warnings.filterwarnings("ignore") + import cudf # type: ignore + + return True, "ok", cudf + except ModuleNotFoundError as e: + return False, e, None def assert_imported(): has_dependancy_, import_exn, umap_learn = lazy_umap_import_has_dependancy() From 0b8028a622142fc9d03f217ebd487f985bae138f Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 16 Mar 2023 12:45:31 -0400 Subject: [PATCH 264/432] fix(docstr): added layout & compute to nav --- docs/source/graphistry.compute.rst | 48 ++++++++++++++++++------- docs/source/graphistry.layout.rst | 57 ++++++++++++++++++++++++------ docs/source/graphistry.plugins.rst | 19 ++-------- docs/source/graphistry.rst | 34 +++++++++++------- 4 files changed, 106 insertions(+), 52 deletions(-) diff --git a/docs/source/graphistry.compute.rst b/docs/source/graphistry.compute.rst index 6ea4bdedbd..08b49eedef 100644 --- a/docs/source/graphistry.compute.rst +++ b/docs/source/graphistry.compute.rst @@ -1,29 +1,51 @@ -graphistry.layout package -========================= +ComputeMixin module +------------------------------------------------ -Subpackages ------------ +.. automodule:: graphistry.compute.ComputeMixin + :members: + :undoc-members: + :show-inheritance: -.. toctree:: - :maxdepth: 4 +Chain +--------------- -Submodules ----------- +.. automodule:: graphistry.compute.chain + :members: + :undoc-members: + :show-inheritance: -graphistry.compute.ComputeMixin module ------------------------------------------------- +Cluster +--------------- +.. automodule:: graphistry.compute.cluster + :members: + :undoc-members: + :show-inheritance: -.. automodule:: graphistry.compute.ComputeMixin +Collapse +--------------- +.. automodule:: graphistry.compute.collapse :members: :undoc-members: :show-inheritance: +Conditional +--------------- +.. automodule:: graphistry.compute.conditional + :members: + :undoc-members: + :show-inheritance: -Module contents +Filter by Dictionary --------------- +.. automodule:: graphistry.compute.filter_by_dict + :members: + :undoc-members: + :show-inheritance: -.. automodule:: graphistry.compute +Hop +--------------- +.. automodule:: graphistry.compute.hop :members: :undoc-members: :show-inheritance: diff --git a/docs/source/graphistry.layout.rst b/docs/source/graphistry.layout.rst index 7675db6e35..9216de5252 100644 --- a/docs/source/graphistry.layout.rst +++ b/docs/source/graphistry.layout.rst @@ -1,16 +1,53 @@ -graphistry.layout package -========================= -Subpackages ------------ -.. toctree:: - :maxdepth: 4 +edge Module +----------------------------------- + +.. automodule:: graphistry.layout.graph.edge + :members: + :undoc-members: + :show-inheritance: + +edgeBase Module +--------------------------------------- + +.. automodule:: graphistry.layout.graph.edgeBase + :members: + :undoc-members: + :show-inheritance: + +graph Module +------------------------------------ + +.. automodule:: graphistry.layout.graph.graph + :members: + :undoc-members: + :show-inheritance: + +graphBase Module +---------------------------------------- + +.. automodule:: graphistry.layout.graph.graphBase + :members: + :undoc-members: + :show-inheritance: + +vertex Module +------------------------------------- + +.. automodule:: graphistry.layout.graph.vertex + :members: + :undoc-members: + :show-inheritance: + +vertexBase Module +----------------------------------------- + +.. automodule:: graphistry.layout.graph.vertexBase + :members: + :undoc-members: + :show-inheritance: - graphistry.layout.gib - graphistry.layout.graph - graphistry.layout.sugiyama - graphistry.layout.utils Module contents --------------- diff --git a/docs/source/graphistry.plugins.rst b/docs/source/graphistry.plugins.rst index d3e64fc1ab..492e4ac3fb 100644 --- a/docs/source/graphistry.plugins.rst +++ b/docs/source/graphistry.plugins.rst @@ -1,17 +1,4 @@ -graphistry.plugins package -========================== - -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - -Submodules ----------- - -graphistry.plugins.igraph module +iGraph ------------------------------------------------ .. automodule:: graphistry.plugins.igraph @@ -20,10 +7,10 @@ graphistry.plugins.igraph module :show-inheritance: -Module contents +CuGraph --------------- -.. automodule:: graphistry.plugins +.. automodule:: graphistry.plugins.cugraph :members: :undoc-members: :show-inheritance: diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index a6fdf5cf15..d115114c70 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -1,29 +1,37 @@ -Layout & Plugins +Plotter Module +================== + +.. automodule:: graphistry.PlotterBase + :members: + :undoc-members: + :show-inheritance: + +Plugins ================== .. toctree:: :maxdepth: 3 - graphistry.layout graphistry.plugins - graphistry.plugins_types + -Plotter Module +Compute ================== +.. toctree:: + :maxdepth: 3 -.. automodule:: graphistry.PlotterBase - :members: - :undoc-members: - :show-inheritance: + graphistry.compute -Pygraphistry Module + +Layouts ================== +.. toctree:: + :maxdepth: 3 + + + graphistry.layout -.. automodule:: graphistry.pygraphistry - :members: - :undoc-members: - :show-inheritance: Featurize ================== From 299adf13977a287fe0718b5ce895418f672b849d Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 16 Mar 2023 12:45:58 -0400 Subject: [PATCH 265/432] fix(docstr): removed unneccessary lines --- graphistry/feature_utils.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 6b845576e6..ed5ee15e62 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -223,10 +223,7 @@ def safe_divide(a, b): def features_without_target( df: pd.DataFrame, y: Optional[Union[List, str, pd.DataFrame]] = None ) -> pd.DataFrame: - """ - Checks if y DataFrame column name is in df, and removes it - from df if so - ___________________________________________________________________ + """Checks if y DataFrame column name is in df, and removes it from df if so :param df: model DataFrame :param y: target DataFrame @@ -397,9 +394,8 @@ def is_dataframe_all_numeric(df: pd.DataFrame) -> bool: def find_bad_set_columns(df: pd.DataFrame, bad_set: List = ["[]"]): - """ - Finds columns that if not coerced to strings, will break processors. - ------------------------------------------------------------------------- + """Finds columns that if not coerced to strings, will break processors. + :param df: DataFrame :param bad_set: List of strings to look for. :return: list @@ -429,9 +425,7 @@ def check_if_textual_column( confidence: float = 0.35, min_words: float = 2.5, ) -> bool: - """ - Checks if `col` column of df is textual or not using basic heuristics - __________________________________________________________________________ + """Checks if `col` column of df is textual or not using basic heuristics :param df: DataFrame :param col: column name @@ -470,9 +464,7 @@ def check_if_textual_column( def get_textual_columns( df: pd.DataFrame, min_words: float = 2.5 ) -> List: - """ - Collects columns from df that it deems are textual. - _____________________________________________________________________ + """Collects columns from df that it deems are textual. :param df: DataFrame :return: list of columns names @@ -793,8 +785,8 @@ def encoder(X, use_scaler): # noqa: E301 def get_cardinality_ratio(df: pd.DataFrame): - """Calculates ratio of unique values to total number of rows of DataFrame - ------------------------------------------------------------------------- + """Calculates the ratio of unique values to total number of rows of DataFrame + :param df: DataFrame """ ratios = {} @@ -2409,9 +2401,8 @@ def featurize( memoize: bool = True, verbose: bool = False, ): - r""" - Featurize Nodes or Edges of the underlying nodes/edges DataFrames. - ______________________________________________________________________ + r"""Featurize Nodes or Edges of the underlying nodes/edges DataFrames. + :param kind: specify whether to featurize `nodes` or `edges`. Edge featurization includes a pairwise From eaf69a52ded7206c627f305a87b7c6423d0db0dc Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 16 Mar 2023 11:28:58 -0700 Subject: [PATCH 266/432] forces dbscan to run on cpu only -- once https://github.com/rapidsai/cuml/issues/5277 is resolved we can add gpu support back in --- graphistry/compute/cluster.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 45d287126e..ce93859832 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -147,6 +147,7 @@ def dbscan_fit(g: Any, dbscan: Any, kind: str = "nodes", cols: Optional[Union[Li dbscan.fit(X) if g.engine == 'cuml': labels = dbscan.labels_.to_numpy() + # dbscan.components_ = X[dbscan.core_sample_indices_.to_pandas()] # can't believe len(samples) != unique(labels) ... #cumlfail else: labels = dbscan.labels_ @@ -206,11 +207,11 @@ def _cluster_dbscan( self, res, kind, cols, fit_umap_embedding, target, min_dist, min_samples, verbose, *args, **kwargs ): """ - DBSCAN clustering on cpu or gpu infered by .engine flag + DBSCAN clustering on cpu or *(not yet supported) gpu* infered by .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() - res.engine = resolve_cpu_gpu_engine("auto") + res.engine = resolve_cpu_gpu_engine(UMAP_LEARN) # resolve_cpu_gpu_engine("auto") res._dbscan_params = ModelDict( "latest DBSCAN params", kind=kind, @@ -247,6 +248,7 @@ def dbscan( **kwargs, ): """DBSCAN clustering on cpu or gpu infered automatically. Adds a `_dbscan` column to nodes or edges. + NOTE: g.transform_dbscan(..) currently unsupported on GPU. Examples: :: @@ -307,7 +309,6 @@ def dbscan( *args, **kwargs, ) - #res = res.encode_point_color(column=DBSCAN, as_categorical=True) return res @@ -339,6 +340,11 @@ def _transform_dbscan( X_ = emb else: X_ = XX + + + if self.engine == 'cuml': + print('Transform DBSCAN not supported for engine=`cuml`, use engine=`umap_learn` instead') + return emb, X, y, df labels = dbscan_predict(X_, dbscan) # type: ignore if umap and cols is None: @@ -419,11 +425,13 @@ def transform_dbscan( :verbose: whether to print out progress, default False """ + if self.engine == 'cuml': + print('Transform DBSCAN not supported for `cuml`, use engine=`umap_learn` instead') + return self.transform_umap(df, y, kind=kind, verbose=verbose, return_graph=return_graph) emb, X, y, df = self._transform_dbscan(df, y, kind=kind, verbose=verbose) if return_graph and kind not in ["edges"]: g = self._infer_edges(emb, X, y, df, eps=min_dist, sample=sample, n_neighbors=n_neighbors, # type: ignore infer_on_umap_embedding=infer_umap_embedding ) - #g = g.encode_point_color(column=DBSCAN, as_categorical=True) return g return emb, X, y, df From 8782a6d94bf0b4560cbd7e35f12b77fea20b537a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 16 Mar 2023 15:37:02 -0700 Subject: [PATCH 267/432] adds engine_dbscan flag and if [sklearn ,umap_learn] will allow g.transform_dbscan(..) enrichment --- graphistry/compute/cluster.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index ce93859832..f7d6a3848b 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -56,7 +56,7 @@ def lazy_cudf_import_has_dependancy(): def resolve_cpu_gpu_engine( engine: DBSCANEngine, ) -> DBSCANEngineConcrete: # noqa - if engine in [CUML, UMAP_LEARN]: + if engine in [CUML, UMAP_LEARN, 'sklearn']: return engine # type: ignore if engine in ["auto"]: ( @@ -204,14 +204,17 @@ def __init__(self, *args, **kwargs): pass def _cluster_dbscan( - self, res, kind, cols, fit_umap_embedding, target, min_dist, min_samples, verbose, *args, **kwargs + self, res, kind, cols, fit_umap_embedding, target, min_dist, min_samples, engine, verbose, *args, **kwargs ): """ DBSCAN clustering on cpu or *(not yet supported) gpu* infered by .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() - res.engine = resolve_cpu_gpu_engine(UMAP_LEARN) # resolve_cpu_gpu_engine("auto") + if engine in [CUML]: + print('`g.transform_dbscan(..)` not supported for engine=cuml, will return `g.transform_umap(..)` instead') + + res.engine = resolve_cpu_gpu_engine(engine) # resolve_cpu_gpu_engine("auto") res._dbscan_params = ModelDict( "latest DBSCAN params", kind=kind, @@ -220,6 +223,7 @@ def _cluster_dbscan( fit_umap_embedding=fit_umap_embedding, min_dist=min_dist, min_samples=min_samples, + engine=engine, verbose=verbose, ) @@ -244,6 +248,7 @@ def dbscan( fit_umap_embedding: bool = True, target: bool = False, verbose: bool = False, + engine_dbscan: str = 'sklearn' *args, **kwargs, ): @@ -305,6 +310,7 @@ def dbscan( target=target, min_dist=min_dist, min_samples=min_samples, + engine=engine_dbscan, verbose=verbose, *args, **kwargs, From 44351baa5b6e72d4ff512bd4a4e0007cdfa11ddd Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 13:13:06 -0700 Subject: [PATCH 268/432] bug fix --- graphistry/compute/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index f7d6a3848b..8c4fecbba6 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -248,7 +248,7 @@ def dbscan( fit_umap_embedding: bool = True, target: bool = False, verbose: bool = False, - engine_dbscan: str = 'sklearn' + engine_dbscan: str = 'sklearn', *args, **kwargs, ): From c2d70e2c72bb25b9968783592507c69be38bc575 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 14:32:46 -0700 Subject: [PATCH 269/432] explicit engine_dbscan flags --- graphistry/compute/cluster.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 8c4fecbba6..c067af9ec7 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -82,7 +82,7 @@ def safe_cudf(X, y): new_kwargs = {} kwargs = {'X': X, 'y': y} for key, value in kwargs.items(): - if isinstance(value, cudf.DataFrame) and engine == "pandas": + if isinstance(value, cudf.DataFrame) and engine in ["pandas", 'sklearn', 'umap_learn']: new_kwargs[key] = value.to_pandas() elif isinstance(value, pd.DataFrame) and engine == "cuml": new_kwargs[key] = cudf.from_pandas(value) @@ -122,8 +122,8 @@ def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, targe if umap and cols is None and g._umap is not None: df = g._get_embedding(kind) - if g.engine in [CUML]: - df, _ = make_safe_gpu_dataframes(df, None, g.engine) + if g.engine_dbscan in [CUML]: + df, _ = make_safe_gpu_dataframes(df, None, g.engine_dbscan) #print('\n df:', df.shape, df.columns) return df @@ -204,17 +204,17 @@ def __init__(self, *args, **kwargs): pass def _cluster_dbscan( - self, res, kind, cols, fit_umap_embedding, target, min_dist, min_samples, engine, verbose, *args, **kwargs + self, res, kind, cols, fit_umap_embedding, target, min_dist, min_samples, engine_dbscan, verbose, *args, **kwargs ): """ DBSCAN clustering on cpu or *(not yet supported) gpu* infered by .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() - if engine in [CUML]: + if engine_dbscan in [CUML]: print('`g.transform_dbscan(..)` not supported for engine=cuml, will return `g.transform_umap(..)` instead') - res.engine = resolve_cpu_gpu_engine(engine) # resolve_cpu_gpu_engine("auto") + res.engine_dbscan = resolve_cpu_gpu_engine(engine_dbscan) # resolve_cpu_gpu_engine("auto") res._dbscan_params = ModelDict( "latest DBSCAN params", kind=kind, @@ -223,7 +223,7 @@ def _cluster_dbscan( fit_umap_embedding=fit_umap_embedding, min_dist=min_dist, min_samples=min_samples, - engine=engine, + engine_dbscan=engine_dbscan, verbose=verbose, ) @@ -310,7 +310,7 @@ def dbscan( target=target, min_dist=min_dist, min_samples=min_samples, - engine=engine_dbscan, + engine_dbscan=engine_dbscan, verbose=verbose, *args, **kwargs, @@ -349,7 +349,7 @@ def _transform_dbscan( if self.engine == 'cuml': - print('Transform DBSCAN not supported for engine=`cuml`, use engine=`umap_learn` instead') + print('Transform DBSCAN not supported for engine_dbscan=`cuml`, use engine=`umap_learn`, `pandas` or `sklearn` instead') return emb, X, y, df labels = dbscan_predict(X_, dbscan) # type: ignore From 48fe85f85e1eee190254442c8e885bccf53cfb59 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 14:40:24 -0700 Subject: [PATCH 270/432] bug fix --- graphistry/compute/cluster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index c067af9ec7..9198d00399 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -431,7 +431,7 @@ def transform_dbscan( :verbose: whether to print out progress, default False """ - if self.engine == 'cuml': + if self.engine_dbscan == 'cuml': print('Transform DBSCAN not supported for `cuml`, use engine=`umap_learn` instead') return self.transform_umap(df, y, kind=kind, verbose=verbose, return_graph=return_graph) emb, X, y, df = self._transform_dbscan(df, y, kind=kind, verbose=verbose) From d2611a3a380f31c35d0c533f67ab9b3b8ebbd9f7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 14:50:56 -0700 Subject: [PATCH 271/432] adds pandas coercion before infer_graph --- graphistry/compute/cluster.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 9198d00399..ab4ab7ffcc 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -347,8 +347,7 @@ def _transform_dbscan( else: X_ = XX - - if self.engine == 'cuml': + if res.engine_dbscan == 'cuml': print('Transform DBSCAN not supported for engine_dbscan=`cuml`, use engine=`umap_learn`, `pandas` or `sklearn` instead') return emb, X, y, df @@ -431,11 +430,13 @@ def transform_dbscan( :verbose: whether to print out progress, default False """ - if self.engine_dbscan == 'cuml': - print('Transform DBSCAN not supported for `cuml`, use engine=`umap_learn` instead') - return self.transform_umap(df, y, kind=kind, verbose=verbose, return_graph=return_graph) + # if self.engine_dbscan == 'cuml': + # print('Transform DBSCAN not supported for `cuml`, use engine=`umap_learn` instead') + # return self.transform_umap(df, y, kind=kind, verbose=verbose, return_graph=return_graph) emb, X, y, df = self._transform_dbscan(df, y, kind=kind, verbose=verbose) if return_graph and kind not in ["edges"]: + df, y = make_safe_gpu_dataframes(df, y, 'pandas') + X, emb = make_safe_gpu_dataframes(X, emb, 'pandas') g = self._infer_edges(emb, X, y, df, eps=min_dist, sample=sample, n_neighbors=n_neighbors, # type: ignore infer_on_umap_embedding=infer_umap_embedding ) From d4030bce535af868a46b842fa8fd8fb095f56a1e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 14:58:05 -0700 Subject: [PATCH 272/432] bug fix --- graphistry/compute/cluster.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index ab4ab7ffcc..07bdc7ad72 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -214,7 +214,7 @@ def _cluster_dbscan( if engine_dbscan in [CUML]: print('`g.transform_dbscan(..)` not supported for engine=cuml, will return `g.transform_umap(..)` instead') - res.engine_dbscan = resolve_cpu_gpu_engine(engine_dbscan) # resolve_cpu_gpu_engine("auto") + res.engine_dbscan = engine_dbscan #resolve_cpu_gpu_engine(engine_dbscan) # resolve_cpu_gpu_engine("auto") res._dbscan_params = ModelDict( "latest DBSCAN params", kind=kind, @@ -229,7 +229,7 @@ def _cluster_dbscan( dbscan = ( cuDBSCAN(eps=min_dist, min_samples=min_samples, *args, **kwargs) - if res.engine == CUML + if res.engine_dbscan == CUML else DBSCAN(eps=min_dist, min_samples=min_samples, *args, **kwargs) ) @@ -330,6 +330,7 @@ def _transform_dbscan( target = res._dbscan_params["target"] dbscan = res._node_dbscan if kind == "nodes" else res._edge_dbscan + print('DBSCAN TYPE IN TRANSFORM', type(dbscan)) emb = None if umap and cols is None: From 42e8eb3ce24c362fc43c715ff2e58f2e94055301 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 15:06:07 -0700 Subject: [PATCH 273/432] bug fix --- graphistry/compute/cluster.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 07bdc7ad72..1ebfea04da 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -145,7 +145,8 @@ def dbscan_fit(g: Any, dbscan: Any, kind: str = "nodes", cols: Optional[Union[Li raise ValueError("No features found for clustering") dbscan.fit(X) - if g.engine == 'cuml': + # this is a future feature one cuml supports it + if g.engine_dbscan == 'cuml': labels = dbscan.labels_.to_numpy() # dbscan.components_ = X[dbscan.core_sample_indices_.to_pandas()] # can't believe len(samples) != unique(labels) ... #cumlfail else: @@ -232,6 +233,7 @@ def _cluster_dbscan( if res.engine_dbscan == CUML else DBSCAN(eps=min_dist, min_samples=min_samples, *args, **kwargs) ) + print('dbscan:', dbscan) res = dbscan_fit( res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding, verbose=verbose From 2027c35bbfea2683585cc7802d922eb5b31161d1 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 15:10:30 -0700 Subject: [PATCH 274/432] bug fix --- graphistry/compute/cluster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 1ebfea04da..ae152b52d7 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -122,8 +122,8 @@ def get_model_matrix(g, kind: str, cols: Optional[Union[List, str]], umap, targe if umap and cols is None and g._umap is not None: df = g._get_embedding(kind) - if g.engine_dbscan in [CUML]: - df, _ = make_safe_gpu_dataframes(df, None, g.engine_dbscan) + #if g.engine_dbscan in [CUML]: + df, _ = make_safe_gpu_dataframes(df, None, g.engine_dbscan) #print('\n df:', df.shape, df.columns) return df From 13d91736682ca931203b5adaa6889af204617dbc Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 15:18:47 -0700 Subject: [PATCH 275/432] bug fix --- graphistry/compute/cluster.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index ae152b52d7..051d1d2d94 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -353,7 +353,8 @@ def _transform_dbscan( if res.engine_dbscan == 'cuml': print('Transform DBSCAN not supported for engine_dbscan=`cuml`, use engine=`umap_learn`, `pandas` or `sklearn` instead') return emb, X, y, df - + + X_, _ = make_safe_gpu_dataframes(X_, None, res.engine_dbscan) labels = dbscan_predict(X_, dbscan) # type: ignore if umap and cols is None: df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) # type: ignore From d28859726ba58335788778eab66fcc14cfcb5c4f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 15:22:10 -0700 Subject: [PATCH 276/432] bug fix --- graphistry/compute/cluster.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 051d1d2d94..1632d3ca95 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -354,8 +354,12 @@ def _transform_dbscan( print('Transform DBSCAN not supported for engine_dbscan=`cuml`, use engine=`umap_learn`, `pandas` or `sklearn` instead') return emb, X, y, df - X_, _ = make_safe_gpu_dataframes(X_, None, res.engine_dbscan) + print(type(X_)) + X_, _ = make_safe_gpu_dataframes(X_, None, 'pandas') # fuck all this hacky shit + print('after make safe gpu', type(X_)) + labels = dbscan_predict(X_, dbscan) # type: ignore + print('after dbscan predict', type(labels)) if umap and cols is None: df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) # type: ignore else: From dfba41cdf2dbb66e28341994e4cd435173c153f4 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 15:25:52 -0700 Subject: [PATCH 277/432] bug fix --- graphistry/compute/cluster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 1632d3ca95..8e07161fd8 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -354,8 +354,8 @@ def _transform_dbscan( print('Transform DBSCAN not supported for engine_dbscan=`cuml`, use engine=`umap_learn`, `pandas` or `sklearn` instead') return emb, X, y, df - print(type(X_)) - X_, _ = make_safe_gpu_dataframes(X_, None, 'pandas') # fuck all this hacky shit + print('before', type(X_)) + X_, emb = make_safe_gpu_dataframes(X_, emb, 'pandas') print('after make safe gpu', type(X_)) labels = dbscan_predict(X_, dbscan) # type: ignore From fa2a6bae5abd75670182f837869a15ed555c2937 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 15:47:36 -0700 Subject: [PATCH 278/432] lint --- graphistry/ai_utils.py | 6 +++--- graphistry/compute/cluster.py | 14 +++++++------- graphistry/umap_utils.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/graphistry/ai_utils.py b/graphistry/ai_utils.py index a3b4c1888d..0f58cf3256 100644 --- a/graphistry/ai_utils.py +++ b/graphistry/ai_utils.py @@ -294,9 +294,9 @@ def infer_graph( old_nodes = [] mdists = [] - ## check if pandas or cudf + # check if pandas or cudf if 'cudf.core.dataframe' in str(type(X_previously_fit)): - # move it out of memory... + # move it out of memory... X_previously_fit = X_previously_fit.to_pandas() for i in range(X_new.shape[0]): @@ -364,7 +364,7 @@ def infer_graph( new_emb = None if emb is not None: - if 'cudf.core.dataframe.DataFrame' in str(type(old_emb)): # convert to pd + if 'cudf.core.dataframe.DataFrame' in str(type(old_emb)): # convert to pd old_emb = old_emb.to_pandas() new_emb = pd.concat([emb, old_emb], axis=0) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index 8e07161fd8..e21b74a1b5 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -72,7 +72,7 @@ def resolve_cpu_gpu_engine( raise ValueError( # noqa f'engine expected to be "auto", ' - '"umap_learn", or "cuml" ' + '"umap_learn", "pandas", "sklearn", or "cuml" ' f"but received: {engine} :: {type(engine)}" ) @@ -92,7 +92,7 @@ def safe_cudf(X, y): has_cudf_dependancy_, _, cudf = lazy_cudf_import_has_dependancy() if has_cudf_dependancy_: - print('DBSCAN CUML Matrices') + # print('DBSCAN CUML Matrices') return safe_cudf(X, y) else: return X, y @@ -215,7 +215,7 @@ def _cluster_dbscan( if engine_dbscan in [CUML]: print('`g.transform_dbscan(..)` not supported for engine=cuml, will return `g.transform_umap(..)` instead') - res.engine_dbscan = engine_dbscan #resolve_cpu_gpu_engine(engine_dbscan) # resolve_cpu_gpu_engine("auto") + res.engine_dbscan = engine_dbscan # resolve_cpu_gpu_engine(engine_dbscan) # resolve_cpu_gpu_engine("auto") res._dbscan_params = ModelDict( "latest DBSCAN params", kind=kind, @@ -233,7 +233,7 @@ def _cluster_dbscan( if res.engine_dbscan == CUML else DBSCAN(eps=min_dist, min_samples=min_samples, *args, **kwargs) ) - print('dbscan:', dbscan) + # print('dbscan:', dbscan) res = dbscan_fit( res, dbscan, kind=kind, cols=cols, use_umap_embedding=fit_umap_embedding, verbose=verbose @@ -354,12 +354,12 @@ def _transform_dbscan( print('Transform DBSCAN not supported for engine_dbscan=`cuml`, use engine=`umap_learn`, `pandas` or `sklearn` instead') return emb, X, y, df - print('before', type(X_)) + #print('before', type(X_)) X_, emb = make_safe_gpu_dataframes(X_, emb, 'pandas') - print('after make safe gpu', type(X_)) + #print('after make safe gpu', type(X_)) labels = dbscan_predict(X_, dbscan) # type: ignore - print('after dbscan predict', type(labels)) + #print('after dbscan predict', type(labels)) if umap and cols is None: df = df.assign(_dbscan=labels, x=emb.x, y=emb.y) # type: ignore else: diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index b88fb0815b..05cd80d9ec 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -588,7 +588,7 @@ def umap( elif 'cudf.core.dataframe' in str(getmodule(X_)): index_to_nodes_dict = nodes # {}? - ## add the safe coercion here + # add the safe coercion here X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) res = res._process_umap( @@ -618,7 +618,7 @@ def umap( **featurize_kwargs ) - ## add the safe coercion here + # add the safe coercion here X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) res = res._process_umap( From ea6ce345c78a21e35a45cc0dfa7c1c480ba4bd9c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 15:54:12 -0700 Subject: [PATCH 279/432] lint --- graphistry/feature_utils.py | 4 ++-- graphistry/umap_utils.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 65113ea902..8d302df2b0 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -170,7 +170,7 @@ def resolve_feature_engine( def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: if isinstance(y, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(y)): - return y + return y # type: ignore if df is None: raise ValueError("Missing data for featurization") @@ -191,7 +191,7 @@ def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: def resolve_X(df: Optional[pd.DataFrame], X: XSymbolic) -> pd.DataFrame: if isinstance(X, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(X)): - return X + return X # type: ignore if df is None: raise ValueError("Missing data for featurization") diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 05cd80d9ec..d38cdb145b 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -330,7 +330,7 @@ def transform_umap(self, df: pd.DataFrame, """ df, y = make_safe_gpu_dataframes(df, y, 'pandas') X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) - X, y_ = make_safe_gpu_dataframes(X, y_, self.engine) + X, y_ = make_safe_gpu_dataframes(X, y_, self.engine) # type: ignore emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) if return_graph and kind not in ["edges"]: @@ -589,7 +589,7 @@ def umap( index_to_nodes_dict = nodes # {}? # add the safe coercion here - X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) + X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) # type: ignore res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, verbose, **umap_kwargs @@ -619,7 +619,7 @@ def umap( ) # add the safe coercion here - X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) + X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) # type: ignore res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs From 118e36df0df8fbccb963ecf4ef3779b4c9b28670 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Mar 2023 17:02:34 -0700 Subject: [PATCH 280/432] adds modified test for dbscan params --- graphistry/tests/test_compute_cluster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/tests/test_compute_cluster.py b/graphistry/tests/test_compute_cluster.py index 3c1cfd8dca..c93d0e279d 100644 --- a/graphistry/tests/test_compute_cluster.py +++ b/graphistry/tests/test_compute_cluster.py @@ -45,9 +45,9 @@ def test_featurize_cluster(self): @pytest.mark.skipif(not has_dbscan, reason="requires ai dependencies") def test_dbscan_params(self): dbscan_params = [ModelDict('Testing UMAP', kind='nodes', min_dist=0.2, min_samples=1, cols=None, target=False, - fit_umap_embedding=False, verbose=True), + fit_umap_embedding=False, verbose=True, engine_dbscan='sklearn'), ModelDict('Testing UMAP target', kind='nodes', min_dist=0.1, min_samples=1, cols=None, - fit_umap_embedding=True, target=True, verbose=True) + fit_umap_embedding=True, target=True, verbose=True, engine_dbscan='sklearn'), ] for params in dbscan_params: From 911529c94c9572f85622a1522577f365020ffec4 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 20 Mar 2023 15:13:53 -0400 Subject: [PATCH 281/432] feat(iframe): added a graph homepg, can be removed --- docs/source/index.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index b45393c266..e86502551e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,6 +7,16 @@ PyGraphistry[ai]'s documentation PyGraphistry is a Python visual graph AI library to extract, transform, analyze, model, and visualize big graphs, and especially alongside Graphistry end-to-end GPU server sessions. Installing optional graphistry[ai] dependencies adds graph autoML, including automatic feature engineering, UMAP, and graph neural net support. Combined, PyGraphistry reduces your time to graph for going from raw data to visualizations and AI models down to three lines of code. Here in our docstrings you can find useful packages, modules, and commands to maximize your graph AI experience with PyGraphistry. In the navbar you can find an overview of all the packages and modules we provided and a few useful highlighted ones as well. You can search for them on our Search page. For a full tutorial, refer to our `PyGraphistry `_ repo. + +Click to open interactive version! (For server-backed interactive analytics, use an API key) + + +.. raw:: html + + + +For self-hosting and access to a free API key, refer to our Graphistry `Hub `_. + .. toctree:: :maxdepth: 3 From 2c9dc78404469458da4c538aa3da8600b97a176d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Mar 2023 16:29:26 -0700 Subject: [PATCH 282/432] adds NVIDIA GTC demo that installs from branch --- demos/ai/cyber/redteam-umap-gtc-gpu.ipynb | 1033 +++++++++++++++++++++ 1 file changed, 1033 insertions(+) create mode 100644 demos/ai/cyber/redteam-umap-gtc-gpu.ipynb diff --git a/demos/ai/cyber/redteam-umap-gtc-gpu.ipynb b/demos/ai/cyber/redteam-umap-gtc-gpu.ipynb new file mode 100644 index 0000000000..b6ab00a308 --- /dev/null +++ b/demos/ai/cyber/redteam-umap-gtc-gpu.ipynb @@ -0,0 +1,1033 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "beb5e3e3-f8cd-40ed-bc63-8a862000f192", + "metadata": {}, + "source": [ + "# Analyzing Network Identity Data and Red Team Response with Graphistry AutoML + UMAP\n", + "\n", + "We find a simple model that when clustered in a 2d plane via UMAP allows fast identification of anomalous \n", + "computer to computer connections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9de6fd3-b87b-4dc4-8d1c-b8f3feceb5e6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# ! pip install graphistry[ai] \n", + "! pip install --user --no-deps git+https://github.com/graphistry/pygraphistry.git@cudf-alex3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0215906c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import os\n", + "import pandas as pd\n", + "from joblib import load, dump\n", + "from collections import Counter\n", + "\n", + "import numpy as np\n", + "import matplotlib.pylab as plt\n", + "\n", + "import graphistry\n", + "from graphistry.features import topic_model, search_model, ModelDict" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b34bebd-c91d-49fe-82c9-ec1c83a4a6c1", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "graphistry.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e1747b9-c903-4398-9aa0-b52b69fce021", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "np.random.seed(137)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d2669fd-6164-4376-81bd-79c6c6f4112f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "RENDER = True # set to True to render Graphistry UI inline" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59e1cc0b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "graphistry.register(api=3, protocol=\"https\", server=\"hub.graphistry.com\", username = 'silkspace',\n", + " #os.environ['USERNAME'], \n", + " password='yqQg02&N'\n", + " #os.environ['GRAPHISTRY_PASSWORD']\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "877b4e50-8fa8-4663-bba0-91b661fc735f", + "metadata": {}, + "source": [ + "Alert on & visualize anomalous identity events\n", + "\n", + "Demo dataset: 1.6B windows events over 58 days => logins by 12K user over 14K systems\n", + "adapt to any identity system with logins. Here we subsample down to a small set of 50k events to prove out the pipeline. \n", + "\n", + "* => Can we identify accounts & computers acting anomalously? Resources being oddly accessed?\n", + "* => Can we spot the red team?\n", + "* => Operations: Identity incident alerting + identity data investigations\n", + "\n", + "Community/contact for help handling bigger-than-memory & additional features\n", + "\n", + "Runs on both CPU + multi-GPU\n", + "Tools: PyGraphistry[AI], DGL + PyTorch, and NVIDIA RAPIDS / umap-learn" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe6e61b0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# data source citation\n", + "# \"\"\"A. D. Kent, \"Cybersecurity Data Sources for Dynamic Network Research,\"\n", + "# in Dynamic Networks in Cybersecurity, 2015.\n", + "\n", + "# @InProceedings{akent-2015-enterprise-data,\n", + "# author = {Alexander D. Kent},\n", + "# title = {{Cybersecurity Data Sources for Dynamic Network Research}},\n", + "# year = 2015,\n", + "# booktitle = {Dynamic Networks in Cybersecurity},\n", + "# month = jun,\n", + "# publisher = {Imperial College Press}\n", + "# }\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "554c0d85-1c8a-47f0-87ec-1629d7f7ba3b", + "metadata": {}, + "source": [ + "# Get the Data\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efe68cf8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# small sample (get almost equivalent results without overheating computer over the 1.6B events in the full dataset)\n", + "df = pd.read_csv('https://gist.githubusercontent.com/silkspace/c7b50d0c03dc59f63c48d68d696958ff/raw/31d918267f86f8252d42d2e9597ba6fc03fcdac2/redteam_50k.csv', index_col=0)\n", + "df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "93bab916-a6c1-4a63-95de-2e8d2a72d8a6", + "metadata": { + "execution": { + "iopub.execute_input": "2023-03-20T17:41:26.708147Z", + "iopub.status.busy": "2023-03-20T17:41:26.707740Z", + "iopub.status.idle": "2023-03-20T17:41:26.711459Z", + "shell.execute_reply": "2023-03-20T17:41:26.710695Z", + "shell.execute_reply.started": "2023-03-20T17:41:26.708118Z" + } + }, + "source": [ + "# Graphistry UMAP in a single line of code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29e99375-5b24-4760-b5ed-909f51949f7f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# umap pipeline in one line\n", + "g = graphistry.nodes(df.sample(1000)).umap(engine='umap_learn')\n", + "g.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03610297", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "print(df.shape) # -> 50+k" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66c5126e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# here are the post-facto red team events\n", + "red_team = pd.read_csv('https://gist.githubusercontent.com/silkspace/5cf5a94b9ac4b4ffe38904f20d93edb1/raw/888dabd86f88ea747cf9ff5f6c44725e21536465/redteam_labels.csv', index_col=0)\n", + "red_team['feats2'] = red_team.feats # since red team data didn't include metadata" + ] + }, + { + "cell_type": "markdown", + "id": "3c6615aa", + "metadata": {}, + "source": [ + "# Modeling\n", + "\n", + "Make sure you `mkdir(data)` or change path below\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3641d3b5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "process = True \n", + "# makes a combined feature we can use for topic modeling!\n", + "if process:\n", + " # we create two types of models\n", + " df['feats'] = df.src_computer + ' ' + df.dst_computer + ' ' + df.auth_type + ' ' + df.logontype\n", + " # and one of just computer to computer \n", + " df['feats2'] = df.src_computer + ' ' + df.dst_computer\n", + " ndf = df.drop_duplicates(subset=['feats'])\n", + " ndf.to_parquet('auth-feats-one-column.parquet')\n", + "else:\n", + " ndf = pd.read_parquet('auth-feats-one-column.parquet')\n", + " \n", + "print(ndf.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "32d1755d", + "metadata": {}, + "source": [ + "## Red Team Data \n", + "Add it to the front of the DataFrame so we can keep track of it" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d67c86b8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# make a subsampled dataframe with the anom red-team data at top...so we can keep track.\n", + "# we don't need the full `df`, only the unique entries of 'feats' in `ndf` for \n", + "# fitting a model (in a few cells below)\n", + "\n", + "tdf = pd.concat([red_team.reset_index(), ndf.reset_index()])\n", + "tdf" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f62b7b5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# add a fidicial index used later\n", + "tdf['node'] = range(len(tdf))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ffd6aac", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# total number of red team events\n", + "tdf.RED.sum()" + ] + }, + { + "cell_type": "markdown", + "id": "4264d547-b4a9-49d1-bc68-894f1e839c38", + "metadata": {}, + "source": [ + "## Enrichment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72c53f98", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "def get_confidences_per_cluster(g, col='RED', verbose=False):\n", + " \"\"\"\n", + " From DBSCAN clusters, will assess how many Red Team events exist,\n", + " assessing confidence.\n", + " \n", + " \"\"\"\n", + " resses = []\n", + " df = g._nodes\n", + " labels = df._dbscan\n", + " cnt = Counter(labels)\n", + " for clust, count in cnt.most_common():\n", + " res = df[df._dbscan==clust]\n", + " n = res.shape[0]\n", + " n_reds = res[col].sum()\n", + " resses.append([clust, n_reds/n, n_reds, n])\n", + " if n_reds>0 and verbose:\n", + " print('-'*20)\n", + " print(f'cluster: {clust}\\n red {100*n_reds/n:.2f}% or {n_reds} out of {count}')\n", + " conf_dict = {k[0]: k[1] for k in resses}\n", + " confidence = [conf_dict[k] for k in df._dbscan.values]\n", + " # enrichment\n", + " g._nodes['confidence'] = confidence\n", + " conf_df = pd.DataFrame(resses, columns=['_dbscan', 'confidence', 'n_red', 'total_in_cluster'])\n", + " conf_df = conf_df.sort_values(by='confidence', ascending=False)\n", + " return g, conf_df\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "9a3da6e3-b280-4c69-b0e0-4a92d9aac231", + "metadata": {}, + "source": [ + "# The Full UMAP Pipelines\n", + "Fit a model on 'feats' column" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "504781dc-9fbe-467c-9b4d-2e907133cfb7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# this is a convienence method for setting parameters in `g.featurize()/umap()` -- just a verbose dictionary\n", + "cyber_model = ModelDict('A topic model for computer to computer', **topic_model)\n", + "\n", + "# umap_params_gpu = {'n_components': 2, \n", + "# 'n_neighbors': 20,\n", + "# 'min_dist': 0.1, \n", + "# 'spread': 1, \n", + "# 'local_connectivity': 1, \n", + "# 'repulsion_strength': 2, \n", + "# 'negative_sample_rate': 5}\n", + "#cyber_model.update(umap_params_gpu)\n", + "\n", + "cyber_model.update(dict(n_topics=32, X=['feats2'])) # name the column to featurize, which we lumped into `feats2`\n", + "\n", + "cyber_model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5474ef79-b2dd-4299-bee7-e12d94c79613", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# if you stop processing during execution, sometimes calling this will unblock you on subsequent calls should it give an error.\n", + "#g.reset_caches()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6909cc90", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%time\n", + "process = True # set to false after it's run for ease of speed\n", + "if process:\n", + " # ##################################\n", + " g = graphistry.nodes(tdf, 'node') # two lines does the heavy lifting\n", + " # gpu version, will detect gpu and run\n", + " #g5 = g.umap(engine='auto', **cyber_model, verbose=True).dbscan(min_dist=1, verbose=True)\n", + " \n", + " # cpu version\n", + " g5 = g.umap(engine='umap_learn', **cyber_model, verbose=True).dbscan(min_dist=0.1, verbose=True)\n", + " # #########################\n", + " \n", + " g5, cluster_confidences = get_confidences_per_cluster(g5, verbose=True)\n", + " g5.save_search_instance('auth-feat-topic.search')\n", + "else:\n", + " g = graphistry.bind()\n", + " g5 = g.load_search_instance('auth-feat-topic.search')\n", + " g5, cluster_confidences = get_confidences_per_cluster(g5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01632281-2ace-4917-9932-86b507b3d9e3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# nodes dataframe is now enriched with _dbscan label\n", + "g5._nodes._dbscan" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c1ba011-2aaa-4663-a319-4478502b1b8e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# the UMAP coordinates\n", + "g5._node_embedding" + ] + }, + { + "cell_type": "markdown", + "id": "54c13cba-bc36-4d49-8e7a-7dc05b27610a", + "metadata": {}, + "source": [ + "## Plot Graph\n", + "Color by `confidence` and hover over `red` team histogram to see where events occur. Alternatively, color by `cluster` assignment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "279fef41", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "g5.name('auth test').plot(render=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79ece955", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# see how the model has organized features\n", + "X = g5._node_features\n", + "X" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87b32e09-3ca4-49de-b8c3-2b40ffa2b01d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "x = g5.get_matrix(['interactive', 'c17', 'microsoft'])\n", + "x.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "632d6d0f-8212-4f4a-a920-7600d7456351", + "metadata": {}, + "source": [ + "## Predict | Online Mode\n", + "\n", + "Once a model is fit, predict on new batches as we demonstrate here\n", + "\n", + "There are three main methods\n", + "\n", + "`g.transform` and `g.transform_umap` and if dbscan has been run, `g.transform_dbscan` \n", + "\n", + "see help(*) on each to learn more\n", + "\n", + "One may save the model as above, load it, and wrap in a FastAPI endpoint, etc, to serve in production pipelines." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b44d418", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# first sample a batch from the normal data (auth=df)\n", + "sdf = df.sample(200)\n", + "emb_normal, xp_normal, _ = g5.transform_umap(sdf, None, kind='nodes', return_graph=False)\n", + "# then transform all the red team data\n", + "emb_red, xp_red, _ = g5.transform_umap(red_team, None, kind='nodes', return_graph=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe3e5058-6ac6-4d1a-a368-66ecd5dd703b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "emb_red" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2b6c471-338a-40d6-92a8-03c2505c433f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# transform_dbscan will predict on new data (here just red_team to prove it works)\n", + "g7 = g5.transform_dbscan(red_team, None, kind='nodes', return_graph=True, verbose=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad82c787-c246-440d-9ed6-97ddc2805491", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "_, ccdf = get_confidences_per_cluster(g7)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e0760fe-40c0-45b9-a787-d4f98d557c24", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "print(f'total confidence across clusters {ccdf.confidence.mean()*100}%')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2911840d-ffd7-4815-97fd-53bc43cbc522", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "g7.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "ace3e171-2e33-435e-82d7-7158d7931d14", + "metadata": {}, + "source": [ + "# We can simulate how a batch of new data would behave" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03672813-db4e-4d0c-a5f5-598ab165986c", + "metadata": {}, + "outputs": [], + "source": [ + "# cpu version\n", + "plt.figure(figsize=(10,7))\n", + "plt.scatter(g5._node_embedding.x, g5._node_embedding.y, c='b', s=60, alpha=0.5) # the totality of the fit data\n", + "plt.scatter(emb_normal.x, emb_normal.y, c='g') # batch of new data\n", + "plt.scatter(emb_red.x, emb_red.y, c='r') # red labels to show good cluster seperation\n", + "plt.scatter(emb_normal.x, emb_normal.y, c='g') # batch of new data, to see if they occlude " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a8d5aa9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# # scatter to see how well it does.\n", + "plt.figure(figsize=(10,7))\n", + "plt.scatter(g5._node_embedding.x.to_numpy(), g5._node_embedding.y.to_numpy() , c='b', s=60, alpha=0.5) # the totality of the fit data\n", + "plt.scatter(emb_normal.x.to_numpy(), emb_normal.y.to_numpy(), c='g') # batch of new data\n", + "plt.scatter(emb_red.x.to_numpy(), emb_red.y.to_numpy(), c='r') # red labels to show good cluster seperation\n", + "plt.scatter(emb_normal.x.to_numpy(), emb_normal.y.to_numpy(), c='g') # batch of new data, to see if they occlude " + ] + }, + { + "cell_type": "markdown", + "id": "b53dd8ed-39b2-4000-9ec7-139d1e2a6a85", + "metadata": {}, + "source": [ + "## 96% Reduction in Alerts\n", + "\n", + "This indicates a huge reduction in the search space needed.\n", + "\n", + "Since we have clear cluster assignments along with (post facto) confidences of known anomalous activity, we can reduce the search space on new events (gotten via Kafka, Splunk, etc)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14d207db-9a58-45a3-9876-058632389f17", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# percent of RED team labels we get with 10% confidence or above\n", + "p = cluster_confidences[cluster_confidences.confidence>0.1].n_red.sum()/cluster_confidences[cluster_confidences.confidence>0.1].total_in_cluster.sum()\n", + "print(f'{100*p:.2f}%')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "755a3f27-935d-4ba8-96cb-cbff11fdf00e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# number of data points *not* to consider (and it's more if we look at df proper!)\n", + "cluster_confidences[cluster_confidences.confidence<0.1].total_in_cluster.sum()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5fd1cc50-0900-4694-8400-c426e314ec2e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "p = cluster_confidences[cluster_confidences.confidence<0.1].total_in_cluster.sum()/cluster_confidences.total_in_cluster.sum()\n", + "print(f'Alert Reduction {100*p:.2f}%')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0ee508a5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plt.figure(figsize=(10,7))\n", + "plt.plot(np.cumsum([k[2] for k in cluster_confidences.values]))\n", + "plt.xlabel('Anomolous Cluster Number') # shows that we can ignore first clusters (containing most of the alerts)\n", + "plt.ylabel('Number of Identified Red Team Events')\n", + "print()" + ] + }, + { + "cell_type": "markdown", + "id": "5f168ac8-2324-4f47-b0d7-e4a0b041624f", + "metadata": {}, + "source": [ + "## Supervised UMAP\n", + "Here we use the RED team label to help supervise the UMAP fit. \n", + "This might be useful once teams have actually identified RED team events \n", + "and want to help separate clusters. \n", + "While separation is better, the unsupervised version does well without." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34ad4768-58e5-493e-a5e8-6f4748168e05", + "metadata": {}, + "outputs": [], + "source": [ + "# g.reset_caches()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0c6a16d-a899-43b6-a7ba-75b45f855a78", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "%%time\n", + "process = True\n", + "if process:\n", + " # ################################## # an example of setting features explicitly, could use ModelDict \n", + " g = graphistry.nodes(tdf, 'node')\n", + " g6 = g.umap(X=['feats'], y =['RED'], \n", + " min_words=100000, # set high to bypass sbert encoding\n", + " cardinality_threshold=2, # set low to force topic modeling\n", + " n_topics=32,\n", + " spread=1,\n", + " use_scaler_target=None, # keep labels unscaled\n", + " dbscan=True, engine='umap_learn') # add dbscan here\n", + " # ##################################\n", + " \n", + " g6, cluster_confidences6 = get_confidences_per_cluster(g6, verbose=True)\n", + " g6.save_search_instance('auth-feat-supervised-topic.search')\n", + "else:\n", + " g = graphistry.bind()\n", + " g6 = g.load_search_instance('auth-feat-supervised-topic.search')\n", + " g6, cluster_confidences6 = get_confidences_per_cluster(g6)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a98ef657-5307-41d9-ae31-79c1794b3728", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "g6.get_matrix(target=True).astype(int)" + ] + }, + { + "cell_type": "markdown", + "id": "0cc72ab4-c0da-4541-b32b-aa771d6e510f", + "metadata": { + "tags": [] + }, + "source": [ + "### Plot\n", + "Color by `confidence` and hover over `red` team histogram to see where events occur. Alternatively, color by `_dbscan` assignment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16e09a7d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "g6.name('auth topic with supervised umap').plot(render=RENDER)" + ] + }, + { + "cell_type": "markdown", + "id": "88169a53", + "metadata": {}, + "source": [ + "## A model of Computer-Computer and metadata features\n", + "Here we include `auth_type` and `logontype` " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1731ae44-57e0-4c3e-bad0-ac486bba589c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "tdf['feats']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35b03bc4-915b-431b-ada5-d8281a4ece6d", + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "process = True\n", + "if process:\n", + " # #####################################\n", + " g = graphistry.nodes(tdf, 'node')\n", + " g7 = g.umap(X=['feats'], #y =['RED'], \n", + " min_words=100000, \n", + " cardinality_threshold=2, \n", + " n_topics=32,\n", + " use_scaler=None,\n", + " use_scaler_target=None, \n", + " spread=1,\n", + " dbscan=True, engine='auto') # add dbscan here\n", + " # ###################################\n", + " g7, cluster_confidences7 = get_confidences_per_cluster(g7)\n", + " #g7.save_search_instance('auth-just-ip-topic.search')\n", + "else:\n", + " g7 = graphistry.bind().load_search_instance('auth-just-ip-topic.search')\n", + " g7, cluster_confidences7 = get_confidences_per_cluster(g7)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f291e227-ae14-4205-96dd-3c1de29d12e6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "cluster_confidences7" + ] + }, + { + "cell_type": "markdown", + "id": "836883cb-bc66-4a40-9ca8-f01fd38b6f2a", + "metadata": { + "tags": [] + }, + "source": [ + "### Plot\n", + "Color by `confidence` and hover over `red` team histogram to see where events occur. Alternatively, color by `cluster` assignment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1e586a3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "g7.name('auth topic ips-ips only, no supervision').plot(render=RENDER)\n", + "# very similar to graph with metadata included, showing that ip-ip is strong indicator of phenomenon" + ] + }, + { + "cell_type": "markdown", + "id": "6cf68ed4", + "metadata": {}, + "source": [ + "# Conditional Probability\n", + "Let's see if conditiona probability of computer to computer connections can give us good histograms to tease out red team nodes? This is to baseline the above UMAP models, and we find in retrospect, UMAP wins. \n", + "\n", + "The conditional graph is however useful to see aggregate behavior, and coloring by 'red' team shows topology of Infection" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d6f58dd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "g = graphistry.edges(tdf, \"src_computer\", \"dst_computer\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f3b44db2-b34e-4398-8c5a-7a10bbe5d681", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "x='dst_computer'\n", + "given='src_computer'\n", + "cg = g.conditional_graph(x, given, kind='edges')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b2af6a2-4f10-4707-beb8-4f3447d3e3b8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# the new edge dataframe assess conditiona prob of computer-to-computer connection\n", + "cprob = cg._edges\n", + "cprob" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5258aee1", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# enrich the edges dataframe with the redteam data\n", + "# since cprobs lost those labels during the function call\n", + "indx = cprob.src_computer.isin(red_team.src_computer) & cprob.dst_computer.isin(red_team.dst_computer)\n", + "cprob.loc[indx, 'red'] = 1\n", + "cprob.loc[~indx, 'red'] = 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ff921fc-3ecd-4404-acd7-8db943a4ebcc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "cprob" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4b10152-cac9-4497-b016-dd67b54cdcf2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# add edges back to graphistry instance\n", + "cg._edges = cprob" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b3af1cd-6423-4484-8b99-81fad821f118", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# full condprob graph\n", + "cg.plot(render=RENDER)" + ] + }, + { + "cell_type": "markdown", + "id": "42fb3dff", + "metadata": {}, + "source": [ + "## Learning\n", + "The conditional graph shows that most of the edge probabilities are between 4e-7 and 0.03, whose bucket contains most of the events. Thus the chances of finding the red team edges are ~ 1e-4 -- slim indeed. UMAP wins." + ] + }, + { + "cell_type": "markdown", + "id": "9d2cd536", + "metadata": {}, + "source": [ + "Likewise the transpose conditional is even worse \n", + "with prob_detection ~ 6e-5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0cbef82-421d-489e-8666-84d412cae5a9", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8 (RAPIDS)", + "language": "python", + "name": "rapids" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From e1f64c568979d4be7f3c50c4f975c38c821880c2 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Mar 2023 16:30:54 -0700 Subject: [PATCH 283/432] removes print statements and changes default umap settings --- graphistry/compute/cluster.py | 9 ++------- graphistry/features.py | 4 ++-- graphistry/umap_utils.py | 1 - 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index e21b74a1b5..a5386944dd 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -332,7 +332,7 @@ def _transform_dbscan( target = res._dbscan_params["target"] dbscan = res._node_dbscan if kind == "nodes" else res._edge_dbscan - print('DBSCAN TYPE IN TRANSFORM', type(dbscan)) + # print('DBSCAN TYPE IN TRANSFORM', type(dbscan)) emb = None if umap and cols is None: @@ -351,12 +351,10 @@ def _transform_dbscan( X_ = XX if res.engine_dbscan == 'cuml': - print('Transform DBSCAN not supported for engine_dbscan=`cuml`, use engine=`umap_learn`, `pandas` or `sklearn` instead') + print('Transform DBSCAN not yet supported for engine_dbscan=`cuml`, use engine=`umap_learn`, `pandas` or `sklearn` instead') return emb, X, y, df - #print('before', type(X_)) X_, emb = make_safe_gpu_dataframes(X_, emb, 'pandas') - #print('after make safe gpu', type(X_)) labels = dbscan_predict(X_, dbscan) # type: ignore #print('after dbscan predict', type(labels)) @@ -438,9 +436,6 @@ def transform_dbscan( :verbose: whether to print out progress, default False """ - # if self.engine_dbscan == 'cuml': - # print('Transform DBSCAN not supported for `cuml`, use engine=`umap_learn` instead') - # return self.transform_umap(df, y, kind=kind, verbose=verbose, return_graph=return_graph) emb, X, y, df = self._transform_dbscan(df, y, kind=kind, verbose=verbose) if return_graph and kind not in ["edges"]: df, y = make_safe_gpu_dataframes(df, y, 'pandas') diff --git a/graphistry/features.py b/graphistry/features.py index 3adbc829db..81bf5627fb 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -73,11 +73,11 @@ # ############################################################### # ################# graphistry umap config constants ################# N_COMPONENTS = 2 -N_NEIGHBORS = 15 +N_NEIGHBORS = 20 MIN_DIST = 0.1 SPREAD = 0.5 LOCAL_CONNECTIVITY = 1 -REPULSION_STRENGTH = 1 +REPULSION_STRENGTH = 2 NEGATIVE_SAMPLING_RATE = 5 METRIC = "euclidean" diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index d38cdb145b..c4b74041db 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -326,7 +326,6 @@ def transform_umap(self, df: pd.DataFrame, return_graph: Whether to return a graph or just the embeddings fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data, default True verbose: Whether to print information about the graph inference - """ df, y = make_safe_gpu_dataframes(df, y, 'pandas') X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) From c9249c01c2bdc55dd0d75110b3058b89a25a0971 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Mar 2023 16:50:31 -0700 Subject: [PATCH 284/432] typoo --- demos/ai/cyber/redteam-umap-gtc-gpu.ipynb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/demos/ai/cyber/redteam-umap-gtc-gpu.ipynb b/demos/ai/cyber/redteam-umap-gtc-gpu.ipynb index b6ab00a308..5b8db6ae70 100644 --- a/demos/ai/cyber/redteam-umap-gtc-gpu.ipynb +++ b/demos/ai/cyber/redteam-umap-gtc-gpu.ipynb @@ -90,9 +90,9 @@ }, "outputs": [], "source": [ - "graphistry.register(api=3, protocol=\"https\", server=\"hub.graphistry.com\", username = 'silkspace',\n", + "graphistry.register(api=3, protocol=\"https\", server=\"hub.graphistry.com\", username = '..',\n", " #os.environ['USERNAME'], \n", - " password='yqQg02&N'\n", + " password='..'\n", " #os.environ['GRAPHISTRY_PASSWORD']\n", " )" ] @@ -627,7 +627,8 @@ }, "outputs": [], "source": [ - "# # scatter to see how well it does.\n", + "# gpu version\n", + "# scatter to see how well it does.\n", "plt.figure(figsize=(10,7))\n", "plt.scatter(g5._node_embedding.x.to_numpy(), g5._node_embedding.y.to_numpy() , c='b', s=60, alpha=0.5) # the totality of the fit data\n", "plt.scatter(emb_normal.x.to_numpy(), emb_normal.y.to_numpy(), c='g') # batch of new data\n", @@ -1011,9 +1012,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3.8 (RAPIDS)", + "display_name": "Python 3 (ipykernel)", "language": "python", - "name": "rapids" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -1025,7 +1026,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.16" + "version": "3.8.9" } }, "nbformat": 4, From c5838140564a3a4e0c7fe67eb2e60f5846a1525d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Mar 2023 17:57:31 -0700 Subject: [PATCH 285/432] moves feature_engine resolve to logical order --- graphistry/feature_utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 711a1173b9..4b8484d78c 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2563,16 +2563,21 @@ def featurize( default True. :return: graphistry instance with new attributes set by the featurization process. """ + feature_engine = resolve_feature_engine(feature_engine) + + print('Featurizing nodes with feature_engine=' + feature_engine) + if feature_engine == 'dirty_cat': assert_imported() elif feature_engine == 'cu_cat': assert_cuml_cucat() + if inplace: res = self else: res = self.bind() - feature_engine = resolve_feature_engine(feature_engine) + #feature_engine = resolve_feature_engine(feature_engine) if kind == "nodes": res = res._featurize_nodes( From a5a626ad49591242e83c3f52d1e68d84cfca51e7 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Mar 2023 22:02:48 -0700 Subject: [PATCH 286/432] changes umap spread parameter default to 1 --- graphistry/features.py | 2 +- graphistry/umap_utils.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/graphistry/features.py b/graphistry/features.py index 81bf5627fb..32e83a3a28 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -75,7 +75,7 @@ N_COMPONENTS = 2 N_NEIGHBORS = 20 MIN_DIST = 0.1 -SPREAD = 0.5 +SPREAD = 1 LOCAL_CONNECTIVITY = 1 REPULSION_STRENGTH = 2 NEGATIVE_SAMPLING_RATE = 5 diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index fc3c01cfa6..726aae9c99 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -235,7 +235,6 @@ def umap_lazy_init( print(umap_kwargs) if verbose else None # set new umap kwargs res._umap_params = umap_kwargs - res._n_components = n_components res._metric = metric res._n_neighbors = n_neighbors From c5dc84d7b451209805c1514dd057235f980477a8 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Mar 2023 22:55:47 -0700 Subject: [PATCH 287/432] refactors core featurize and umap engines so that cudf and pd are consistently processed --- graphistry/feature_utils.py | 47 +++++++++++++++++++++++++++++-------- graphistry/umap_utils.py | 30 +++++++++++------------ 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 4b8484d78c..de84c852a0 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -110,18 +110,21 @@ def lazy_import_has_cu_cat_dependancy(): import scipy.sparse # noqa from scipy import __version__ as scipy_version from cu_cat import __version__ as cu_cat_version + import cu_cat from sklearn import __version__ as sklearn_version from cuml import __version__ as cuml_version + import cuml from cudf import __version__ as cudf_version + import cudf logger.debug(f"SCIPY VERSION: {scipy_version}") logger.debug(f"Cuda CAT VERSION: {cu_cat_version}") logger.debug(f"sklearn VERSION: {sklearn_version}") logger.debug(f"cuml VERSION: {cuml_version}") logger.debug(f"cudf VERSION: {cudf_version}") - return True, 'ok' + return True, 'ok', cudf except ModuleNotFoundError as e: - return False, e + return False, e, None def assert_imported_text(): has_dependancy_text_, import_text_exn, _ = lazy_import_has_dependancy_text() @@ -142,14 +145,33 @@ def assert_imported(): raise import_min_exn def assert_cuml_cucat(): - has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cu_cat_dependancy() + has_cuml_dependancy_, import_cuml_exn, cudf = lazy_import_has_cu_cat_dependancy() if not has_cuml_dependancy_: logger.error( # noqa "cuml not found, trying running" # noqa "`pip install rapids`" # noqa ) raise import_cuml_exn - + +def make_safe_gpu_dataframes(X, y, engine): + + def safe_cudf(X, y): + new_kwargs = {} + kwargs = {'X': X, 'y': y} + for key, value in kwargs.items(): + if isinstance(value, cudf.DataFrame) and engine in ["pandas", "dirty_cat", "torch"]: + new_kwargs[key] = value.to_pandas() + elif isinstance(value, pd.DataFrame) and engine in ["cuml", "cu_cat"]: + new_kwargs[key] = cudf.from_pandas(value) + else: + new_kwargs[key] = value + return new_kwargs['X'], new_kwargs['y'] + + has_cudf_dependancy_, _, cudf = lazy_import_has_cu_cat_dependancy() + if has_cudf_dependancy_: + return safe_cudf(X, y) + else: + return X, y # ############################################################################ # @@ -189,7 +211,7 @@ def resolve_feature_engine( has_dependancy_text_, _, _ = lazy_import_has_dependancy_text() if has_dependancy_text_: return "torch" - has_cuml_dependancy_, _ = lazy_import_has_cu_cat_dependancy() + has_cuml_dependancy_, _, cudf = lazy_import_has_cu_cat_dependancy() if has_cuml_dependancy_: return "cu_cat" has_min_dependancy_, _ = lazy_import_has_min_dependancy() @@ -968,12 +990,13 @@ def process_dirty_dataframes( X_enc = pd.DataFrame( X_enc, columns=features_transformed, index=ndf.index ) + X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version elif 'cudf.core.dataframe' in str(getmodule(ndf)): import cudf X_enc = cudf.DataFrame( X_enc, columns=features_transformed, index=ndf.index ) - X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version + #X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version else: logger.info("-*-*- DataFrame is completely numeric") X_enc, _, data_encoder, _ = get_numeric_transformers(ndf, None) @@ -1229,7 +1252,7 @@ class FastMLB: def __init__(self, mlb, in_column, out_columns): if isinstance(in_column, str): in_column = [in_column] - self.columns = in_column # should be singe entry list ['cats'] + self.columns = in_column # should be single entry list ['cats'] self.mlb = mlb self.out_columns = out_columns self.feature_names_in_ = in_column @@ -2035,7 +2058,8 @@ def _featurize_nodes( X_resolved = resolve_X(ndf, X) y_resolved = resolve_y(ndf, y) - feature_engine = resolve_feature_engine(feature_engine) + #feature_engine = resolve_feature_engine(feature_engine) + res.feature_engine = feature_engine from .features import ModelDict @@ -2159,6 +2183,8 @@ def _featurize_edges( **{res._destination: res._edges[res._destination]} ) + res.feature_engine = feature_engine + # now that everything is set fkwargs = dict( X=X_resolved, @@ -2189,6 +2215,7 @@ def _featurize_edges( feature_engine=feature_engine, ) + res._feature_params = { **getattr(res, "_feature_params", {}), "edges": fkwargs, @@ -2571,13 +2598,13 @@ def featurize( assert_imported() elif feature_engine == 'cu_cat': assert_cuml_cucat() - + if inplace: res = self else: res = self.bind() - #feature_engine = resolve_feature_engine(feature_engine) + #res.feature_engine = feature_engine if kind == "nodes": res = res._featurize_nodes( diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 726aae9c99..715659d7ea 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -130,9 +130,9 @@ def safe_cudf(X, y): new_kwargs = {} kwargs = {'X': X, 'y': y} for key, value in kwargs.items(): - if isinstance(value, cudf.DataFrame) and engine == "pandas": + if isinstance(value, cudf.DataFrame) and engine in ["pandas", "umap_learn", "dirty_cat"]: new_kwargs[key] = value.to_pandas() - elif isinstance(value, pd.DataFrame) and engine == "cuml": + elif isinstance(value, pd.DataFrame) and engine in ["cuml", "cu_cat"]: new_kwargs[key] = cudf.from_pandas(value) else: new_kwargs[key] = value @@ -183,7 +183,7 @@ class UMAPMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): #self._umap_initialized = False - #self.engine = self.engine if hasattr(self, "engine") else None + #self.umap_engine = self.umap_engine if hasattr(self, "engine") else None pass @@ -244,7 +244,7 @@ def umap_lazy_init( res._repulsion_strength = repulsion_strength res._negative_sample_rate = negative_sample_rate res._umap = umap_engine.UMAP(**umap_kwargs) - res.engine = engine_resolved + res.umap_engine = engine_resolved res._suffix = suffix return res @@ -280,7 +280,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None, verbose logger.info("-" * 90) logger.info(f"Starting UMAP-ing data of shape {X.shape}") - if self.engine == CUML and is_legacy_cuml(): # type: ignore + if self.umap_engine == CUML and is_legacy_cuml(): # type: ignore from cuml.neighbors import NearestNeighbors knn = NearestNeighbors(n_neighbors=self._n_neighbors) # type: ignore @@ -293,7 +293,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None, verbose self._weighted_adjacency = self._umap.graph_ # if changing, also update fresh_res self._weighted_edges_df = umap_graph_to_weighted_edges( - self._umap.graph_, self.engine, is_legacy_cuml() # type: ignore + self._umap.graph_, self.umap_engine, is_legacy_cuml() # type: ignore ) mins = (time() - t) / 60 @@ -337,9 +337,9 @@ def transform_umap(self, df: pd.DataFrame, fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data, default True verbose: Whether to print information about the graph inference """ - df, y = make_safe_gpu_dataframes(df, y, 'pandas') + df, y = make_safe_gpu_dataframes(df, y, self.feature_engine) X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) - X, y_ = make_safe_gpu_dataframes(X, y_, self.engine) # type: ignore + X, y_ = make_safe_gpu_dataframes(X, y_, self.umap_engine) # type: ignore emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) @@ -471,7 +471,7 @@ def umap( encode_position: bool = True, encode_weight: bool = True, dbscan: bool = False, - engine: UMAPEngine = "auto", + umap_engine: UMAPEngine = "auto", feature_engine: str = "auto", inplace: bool = False, memoize: bool = True, @@ -533,9 +533,9 @@ def umap( :return: self, with attributes set with new data """ - if engine == UMAP_LEARN: + if umap_engine == UMAP_LEARN: assert_imported() - elif engine == CUML: + elif umap_engine == CUML: assert_imported_cuml() umap_kwargs = dict( @@ -547,7 +547,7 @@ def umap( local_connectivity=local_connectivity, repulsion_strength=repulsion_strength, negative_sample_rate=negative_sample_rate, - engine=engine, + umap_engine=umap_engine, suffix=suffix, ) logger.debug("umap_kwargs: %s", umap_kwargs) @@ -599,7 +599,7 @@ def umap( index_to_nodes_dict = nodes # {}? # add the safe coercion here - X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) # type: ignore + X_, y_ = make_safe_gpu_dataframes(X_, y_, res.umap_engine) # type: ignore res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, verbose, **umap_kwargs @@ -629,7 +629,7 @@ def umap( ) # add the safe coercion here - X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) # type: ignore + X_, y_ = make_safe_gpu_dataframes(X_, y_, res.umap_engine) # type: ignore res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs @@ -674,7 +674,7 @@ def umap( res, kind, encode_position, encode_weight, play ) # noqa: E501 - if res.engine == CUML and is_legacy_cuml(): # type: ignore + if res.umap_engine == CUML and is_legacy_cuml(): # type: ignore res = res.prune_self_edges() if dbscan: From 69659f87ee95214d7c65bb004822455759fcc540 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 20 Mar 2023 23:00:19 -0700 Subject: [PATCH 288/432] fixes typo and disambiguates between umap_engine and umap_engine_ --- graphistry/umap_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 715659d7ea..aec17f99e7 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -198,18 +198,18 @@ def umap_lazy_init( negative_sample_rate: int = 5, n_components: int = 2, metric: str = "euclidean", - engine: UMAPEngine = "auto", + umap_engine: UMAPEngine = "auto", suffix: str = "", verbose: bool = False, ): from graphistry.features import ModelDict - engine_resolved = resolve_umap_engine(engine) + engine_resolved = resolve_umap_engine(umap_engine) # FIXME remove as set_new_kwargs will always replace? if engine_resolved == UMAP_LEARN: - _, _, umap_engine = lazy_umap_import_has_dependancy() + _, _, umap_engine_ = lazy_umap_import_has_dependancy() elif engine_resolved == CUML: - _, _, umap_engine = lazy_cuml_import_has_dependancy() + _, _, umap_engine_ = lazy_cuml_import_has_dependancy() else: raise ValueError( "No umap engine, ensure 'auto', 'umap_learn', or 'cuml', and the library is installed" @@ -243,7 +243,7 @@ def umap_lazy_init( res._local_connectivity = local_connectivity res._repulsion_strength = repulsion_strength res._negative_sample_rate = negative_sample_rate - res._umap = umap_engine.UMAP(**umap_kwargs) + res._umap = umap_engine_.UMAP(**umap_kwargs) res.umap_engine = engine_resolved res._suffix = suffix From aedc81e90f9474d94640aaf8724d13524ea2e9ea Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 21 Mar 2023 15:48:28 +0900 Subject: [PATCH 289/432] lazy cudf import (thx alex) --- graphistry/feature_utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index aa5b830ba3..cf7047c0f6 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -113,6 +113,7 @@ def lazy_import_has_cu_cat_dependancy(): from sklearn import __version__ as sklearn_version from cuml import __version__ as cuml_version from cudf import __version__ as cudf_version + import cudf logger.debug(f"SCIPY VERSION: {scipy_version}") logger.debug(f"Cuda CAT VERSION: {cu_cat_version}") logger.debug(f"sklearn VERSION: {sklearn_version}") @@ -668,7 +669,6 @@ def fit_pipeline( X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa X=pd.DataFrame(X, columns=columns, index=index) elif 'cudf.core.dataframe' in X_type: - import cudf ## need proper import routine still X = transformer.fit_transform(X.to_numpy()) if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa @@ -969,7 +969,6 @@ def process_dirty_dataframes( X_enc, columns=features_transformed, index=ndf.index ) elif 'cudf.core.dataframe' in str(getmodule(ndf)): - import cudf X_enc = cudf.DataFrame( X_enc, columns=features_transformed, index=ndf.index ) @@ -1314,7 +1313,6 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf.core.dataframe' in edf_type: - import cudf T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1477,7 +1475,6 @@ def process_edge_dataframes( logger.debug("<= Found Edges and Dirty_cat encoding =>") T_type= str(getmodule(T)) if 'cudf.core.dataframe' in T_type: - import cudf X_enc = cudf.concat([T, X_enc], axis=1) else: X_enc = pd.concat([T, X_enc], axis=1) From 465d486788d475feb1528ea05a899732518d8daa Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 21 Mar 2023 15:49:28 +0900 Subject: [PATCH 290/432] lazy cudf import (thx alex) --- graphistry/feature_utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 406cba183d..cf7047c0f6 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -113,6 +113,7 @@ def lazy_import_has_cu_cat_dependancy(): from sklearn import __version__ as sklearn_version from cuml import __version__ as cuml_version from cudf import __version__ as cudf_version + import cudf logger.debug(f"SCIPY VERSION: {scipy_version}") logger.debug(f"Cuda CAT VERSION: {cu_cat_version}") logger.debug(f"sklearn VERSION: {sklearn_version}") @@ -668,7 +669,6 @@ def fit_pipeline( X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa X=pd.DataFrame(X, columns=columns, index=index) elif 'cudf.core.dataframe' in X_type: - import cudf X = transformer.fit_transform(X.to_numpy()) if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa @@ -969,7 +969,6 @@ def process_dirty_dataframes( X_enc, columns=features_transformed, index=ndf.index ) elif 'cudf.core.dataframe' in str(getmodule(ndf)): - import cudf X_enc = cudf.DataFrame( X_enc, columns=features_transformed, index=ndf.index ) @@ -1314,7 +1313,6 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf.core.dataframe' in edf_type: - import cudf T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1477,7 +1475,6 @@ def process_edge_dataframes( logger.debug("<= Found Edges and Dirty_cat encoding =>") T_type= str(getmodule(T)) if 'cudf.core.dataframe' in T_type: - import cudf X_enc = cudf.concat([T, X_enc], axis=1) else: X_enc = pd.concat([T, X_enc], axis=1) From 87d706c7682fc102d82e8eab76fc9781f65179c0 Mon Sep 17 00:00:00 2001 From: Alex Morrise Date: Tue, 21 Mar 2023 21:32:02 -0700 Subject: [PATCH 291/432] changed umap spread to 1 --- graphistry/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/features.py b/graphistry/features.py index 81bf5627fb..32e83a3a28 100644 --- a/graphistry/features.py +++ b/graphistry/features.py @@ -75,7 +75,7 @@ N_COMPONENTS = 2 N_NEIGHBORS = 20 MIN_DIST = 0.1 -SPREAD = 0.5 +SPREAD = 1 LOCAL_CONNECTIVITY = 1 REPULSION_STRENGTH = 2 NEGATIVE_SAMPLING_RATE = 5 From 08ad02cc5c6b78e0201668d01015c96d66170db2 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 28 Mar 2023 20:02:35 +0800 Subject: [PATCH 292/432] lazy cudf import, pin |torch for now --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index cf7047c0f6..0c24f77d44 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -175,7 +175,7 @@ def assert_cuml_cucat(): # # _featurize_or_get_edges_dataframe_if_X_is_None -FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat", "cu_cat|torch"] +FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat", "cu_cat"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] From 521dc5b66b4f66ef0a8f6e43e77b884df6e40a23 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 28 Mar 2023 20:03:29 +0800 Subject: [PATCH 293/432] lazy cudf import, pin |torch for now --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 0c24f77d44..d1ebe94579 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -175,7 +175,7 @@ def assert_cuml_cucat(): # # _featurize_or_get_edges_dataframe_if_X_is_None -FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat", "cu_cat"] +FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] From ba99c40d4bca051f1da1b9b1ac978e0ca48503b6 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 28 Mar 2023 21:43:16 +0800 Subject: [PATCH 294/432] resolve f_engine --- graphistry/feature_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index d1ebe94579..44827d090d 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2560,6 +2560,9 @@ def featurize( default True. :return: graphistry instance with new attributes set by the featurization process. """ + + feature_engine = resolve_feature_engine(feature_engine) + if feature_engine == 'dirty_cat': assert_imported() elif feature_engine == 'cu_cat': @@ -2569,8 +2572,6 @@ def featurize( else: res = self.bind() - feature_engine = resolve_feature_engine(feature_engine) - if kind == "nodes": res = res._featurize_nodes( X=X, From 5b55f2445665a40a3439e0b14522c385d6d57906 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Mar 2023 16:51:01 -0700 Subject: [PATCH 295/432] adds cudf conversion inside _featurize_* calls --- graphistry/feature_utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index de84c852a0..0879d71f62 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -2058,6 +2058,8 @@ def _featurize_nodes( X_resolved = resolve_X(ndf, X) y_resolved = resolve_y(ndf, y) + X_resolved, y_resolved = make_safe_gpu_dataframes(X_resolved, y_resolved, engine=feature_engine) + #feature_engine = resolve_feature_engine(feature_engine) res.feature_engine = feature_engine @@ -2185,6 +2187,9 @@ def _featurize_edges( res.feature_engine = feature_engine + X_resolved, y_resolved = make_safe_gpu_dataframes(X_resolved, y_resolved, engine=feature_engine) + + # now that everything is set fkwargs = dict( X=X_resolved, @@ -2604,8 +2609,6 @@ def featurize( else: res = self.bind() - #res.feature_engine = feature_engine - if kind == "nodes": res = res._featurize_nodes( X=X, From 5e0a1ce52b8189da291f02512a19162e3ec2764b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Mar 2023 16:58:03 -0700 Subject: [PATCH 296/432] adds print --- graphistry/feature_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 0879d71f62..833b7ecc23 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -169,6 +169,7 @@ def safe_cudf(X, y): has_cudf_dependancy_, _, cudf = lazy_import_has_cu_cat_dependancy() if has_cudf_dependancy_: + print(f"Using GPU: {engine}") return safe_cudf(X, y) else: return X, y From 23da7fc5446102db868579527b22d0f8d97c6e7f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Mar 2023 17:15:46 -0700 Subject: [PATCH 297/432] pulls cudf X into pandas for FAISS indexing --- graphistry/text_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graphistry/text_utils.py b/graphistry/text_utils.py index 9a1abb77f5..63fa5031d0 100644 --- a/graphistry/text_utils.py +++ b/graphistry/text_utils.py @@ -43,6 +43,9 @@ def build_index(self, angular=False, n_trees=None): self.assert_fitted() self.assert_features_line_up_with_nodes() X = self._get_feature("nodes") + if type(X) != pd.DataFrame: + print(f"Converting from {type(X)} to pandas for semantic search index") + X = X.to_pandas() self.search_index = FaissVectorSearch( X.values ) # self._build_search_index(X, angular, n_trees, faiss=False) From 8b1331154a494c3ac93695e43d47abfc88417b8a Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Fri, 31 Mar 2023 10:30:09 +0800 Subject: [PATCH 298/432] placeholder cu_cat setup --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6889be15f5..41347c7d99 100755 --- a/setup.py +++ b/setup.py @@ -41,9 +41,10 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } +# base_extras_heavy['cu_cat'] = ['cu-cat @ git+https://github.com/graphistry/cu_cat.git@cudf-cat'] # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu', 'joblib'] #+ base_extras_heavy['cu_cat'] base_extras = {**base_extras_light, **base_extras_heavy} From 434256c31809938ecfd44009f49d4704591eacc7 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Mon, 3 Apr 2023 15:13:28 +0800 Subject: [PATCH 299/432] tweaks needed for gpu cu_cat --- graphistry/feature_utils.py | 99 +++++++++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index cf7047c0f6..3aecd5a058 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -60,7 +60,7 @@ GapEncoder = Any SimilarityEncoder = Any try: - from sklearn.preprocessing import FunctionTransformer + from cuml.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin except: FunctionTransformer = Any @@ -110,8 +110,10 @@ def lazy_import_has_cu_cat_dependancy(): import scipy.sparse # noqa from scipy import __version__ as scipy_version from cu_cat import __version__ as cu_cat_version + import cu_cat from sklearn import __version__ as sklearn_version from cuml import __version__ as cuml_version + import cuml from cudf import __version__ as cudf_version import cudf logger.debug(f"SCIPY VERSION: {scipy_version}") @@ -120,9 +122,9 @@ def lazy_import_has_cu_cat_dependancy(): logger.debug(f"cuml VERSION: {cuml_version}") logger.debug(f"cudf VERSION: {cudf_version}") - return True, 'ok' + return True, 'ok', cudf except ModuleNotFoundError as e: - return False, e + return False, e, None def assert_imported_text(): has_dependancy_text_, import_text_exn, _ = lazy_import_has_dependancy_text() @@ -143,14 +145,34 @@ def assert_imported(): raise import_min_exn def assert_cuml_cucat(): - has_cuml_dependancy_, import_cuml_exn = lazy_import_has_cu_cat_dependancy() + has_cuml_dependancy_, import_cuml_exn, cudf = lazy_import_has_cu_cat_dependancy() if not has_cuml_dependancy_: logger.error( # noqa "cuml not found, trying running" # noqa "`pip install rapids`" # noqa ) raise import_cuml_exn - + +def make_safe_gpu_dataframes(X, y, engine): + + def safe_cudf(X, y): + new_kwargs = {} + kwargs = {'X': X, 'y': y} + for key, value in kwargs.items(): + if isinstance(value, cudf.DataFrame) and engine in ["pandas", "dirty_cat", "torch"]: + new_kwargs[key] = value.to_pandas() + elif isinstance(value, pd.DataFrame) and engine in ["cuml", "cu_cat"]: + new_kwargs[key] = cudf.from_pandas(value) + else: + new_kwargs[key] = value + return new_kwargs['X'], new_kwargs['y'] + + has_cudf_dependancy_, _, cudf = lazy_import_has_cu_cat_dependancy() + if has_cudf_dependancy_: + print(f"Using GPU: {engine}") + return safe_cudf(X, y) + else: + return X, y # ############################################################################ # @@ -175,7 +197,7 @@ def assert_cuml_cucat(): # # _featurize_or_get_edges_dataframe_if_X_is_None -FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat", "cu_cat|torch"] +FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] @@ -190,7 +212,7 @@ def resolve_feature_engine( has_dependancy_text_, _, _ = lazy_import_has_dependancy_text() if has_dependancy_text_: return "torch" - has_cuml_dependancy_, _ = lazy_import_has_cu_cat_dependancy() + has_cuml_dependancy_, _, cudf = lazy_import_has_cu_cat_dependancy() if has_cuml_dependancy_: return "cu_cat" has_min_dependancy_, _ = lazy_import_has_min_dependancy() @@ -211,7 +233,8 @@ def resolve_feature_engine( def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: if isinstance(y, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(y)): - return y + + return y # type: ignore if df is None: raise ValueError("Missing data for featurization") @@ -232,8 +255,7 @@ def resolve_y(df: Optional[pd.DataFrame], y: YSymbolic) -> pd.DataFrame: def resolve_X(df: Optional[pd.DataFrame], X: XSymbolic) -> pd.DataFrame: if isinstance(X, pd.DataFrame) or 'cudf.core.dataframe' in str(getmodule(X)): - return X - + return X # type: ignore if df is None: raise ValueError("Missing data for featurization") @@ -587,11 +609,19 @@ def get_preprocessing_pipeline( :return: scaled array, imputer instances or None, scaler instance or None """ from sklearn.preprocessing import ( + # FunctionTransformer, + # KBinsDiscretizer, + # MinMaxScaler, + MultiLabelBinarizer, + QuantileTransformer, + # RobustScaler, + # StandardScaler, + ) + from cuml.preprocessing import ( FunctionTransformer, KBinsDiscretizer, MinMaxScaler, - MultiLabelBinarizer, - QuantileTransformer, + # QuantileTransformer, ## cuml 23 only RobustScaler, StandardScaler, ) @@ -864,7 +894,7 @@ def __call__(self, *args, **kwargs): def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. - from sklearn.preprocessing import FunctionTransformer + from cuml.preprocessing import FunctionTransformer label_encoder = False data_encoder = False y_ = y @@ -926,7 +956,7 @@ def process_dirty_dataframes( from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder elif feature_engine == 'cu_cat': from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - from sklearn.preprocessing import FunctionTransformer + from cuml.preprocessing import FunctionTransformer t = time() if not is_dataframe_all_numeric(ndf): @@ -941,7 +971,6 @@ def process_dirty_dataframes( ) logger.info(":: Encoding DataFrame might take a few minutes ------") - X_enc = data_encoder.fit_transform(ndf, y) X_enc = make_array(X_enc) @@ -968,11 +997,16 @@ def process_dirty_dataframes( X_enc = pd.DataFrame( X_enc, columns=features_transformed, index=ndf.index ) + X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version elif 'cudf.core.dataframe' in str(getmodule(ndf)): - X_enc = cudf.DataFrame( - X_enc, columns=features_transformed, index=ndf.index - ) - X_enc = X_enc.fillna(0.0) + import cudf + X_enc = cudf.DataFrame.from_arrow(X_enc) + X_enc.index = ndf.index + # features_transformed=np.array([item.as_py() for item in features_transformed.key()]) + # X_enc.columns = features_transformed #.to_numpy() ##error suggests this -- not working + + + #X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version else: logger.info("-*-*- DataFrame is completely numeric") X_enc, _, data_encoder, _ = get_numeric_transformers(ndf, None) @@ -1196,7 +1230,6 @@ def process_nodes_dataframes( logger.debug( f"--The entire Encoding process took {(time()-t)/60:.2f} minutes" ) - X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa X_enc, y_enc, @@ -1211,7 +1244,6 @@ def process_nodes_dataframes( strategy=strategy, keep_n_decimals=keep_n_decimals, ) - return ( X_enc, y_enc, @@ -1228,7 +1260,7 @@ class FastMLB: def __init__(self, mlb, in_column, out_columns): if isinstance(in_column, str): in_column = [in_column] - self.columns = in_column # should be singe entry list ['cats'] + self.columns = in_column # should be single entry list ['cats'] self.mlb = mlb self.out_columns = out_columns self.feature_names_in_ = in_column @@ -1474,7 +1506,9 @@ def process_edge_dataframes( logger.debug("-" * 60) logger.debug("<= Found Edges and Dirty_cat encoding =>") T_type= str(getmodule(T)) - if 'cudf.core.dataframe' in T_type: + if 'cudf.core.dataframe' not in T_type: + X_enc = pd.concat([T, X_enc], axis=1) + elif 'cudf.core.dataframe' not in T_type: X_enc = cudf.concat([T, X_enc], axis=1) else: X_enc = pd.concat([T, X_enc], axis=1) @@ -1731,7 +1765,6 @@ def _hecho(self, res): logger.info("\n-- Setting Encoder Parts from Fit ::") logger.info(f'Feature Columns In: {self.feature_names_in}') logger.info(f'Target Columns In: {self.target_names_in}') - for name, value in zip(self.res_names, res): if name not in ["X_enc", "y_enc"]: logger.info("-" * 90) @@ -2032,7 +2065,10 @@ def _featurize_nodes( X_resolved = resolve_X(ndf, X) y_resolved = resolve_y(ndf, y) - feature_engine = resolve_feature_engine(feature_engine) + X_resolved, y_resolved = make_safe_gpu_dataframes(X_resolved, y_resolved, engine=feature_engine) + + #feature_engine = resolve_feature_engine(feature_engine) + res.feature_engine = feature_engine from .features import ModelDict @@ -2156,6 +2192,11 @@ def _featurize_edges( **{res._destination: res._edges[res._destination]} ) + res.feature_engine = feature_engine + + X_resolved, y_resolved = make_safe_gpu_dataframes(X_resolved, y_resolved, engine=feature_engine) + + # now that everything is set fkwargs = dict( X=X_resolved, @@ -2186,6 +2227,7 @@ def _featurize_edges( feature_engine=feature_engine, ) + res._feature_params = { **getattr(res, "_feature_params", {}), "edges": fkwargs, @@ -2560,17 +2602,20 @@ def featurize( default True. :return: graphistry instance with new attributes set by the featurization process. """ + feature_engine = resolve_feature_engine(feature_engine) + + print('Featurizing nodes with feature_engine=' + feature_engine) + if feature_engine == 'dirty_cat': assert_imported() elif feature_engine == 'cu_cat': assert_cuml_cucat() + if inplace: res = self else: res = self.bind() - feature_engine = resolve_feature_engine(feature_engine) - if kind == "nodes": res = res._featurize_nodes( X=X, From 0807e766fa69f5e7b941c96d2741376e37a69f28 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Mon, 3 Apr 2023 21:38:59 -0400 Subject: [PATCH 300/432] fix(docs): removed iframe (for now) --- docs/source/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index e86502551e..1704e7f07c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,12 +8,12 @@ PyGraphistry is a Python visual graph AI library to extract, transform, analyze, Here in our docstrings you can find useful packages, modules, and commands to maximize your graph AI experience with PyGraphistry. In the navbar you can find an overview of all the packages and modules we provided and a few useful highlighted ones as well. You can search for them on our Search page. For a full tutorial, refer to our `PyGraphistry `_ repo. -Click to open interactive version! (For server-backed interactive analytics, use an API key) +.. Click to open interactive version! (For server-backed interactive analytics, use an API key) -.. raw:: html +.. .. raw:: html - +.. For self-hosting and access to a free API key, refer to our Graphistry `Hub `_. From d25faf80dc9e760bf534adcb134c4bbc5c54582a Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 4 Apr 2023 21:11:23 +0530 Subject: [PATCH 301/432] fix: cuml umap and tests fix --- .../gpu_rapids/part_iv_gpu_cuml.ipynb | 443 +++++++----------- graphistry/dgl_utils.py | 1 - graphistry/tests/test_umap_utils.py | 37 +- graphistry/umap_utils.py | 7 + 4 files changed, 201 insertions(+), 287 deletions(-) diff --git a/demos/demos_databases_apis/gpu_rapids/part_iv_gpu_cuml.ipynb b/demos/demos_databases_apis/gpu_rapids/part_iv_gpu_cuml.ipynb index 14e67afb7f..849887d4b9 100644 --- a/demos/demos_databases_apis/gpu_rapids/part_iv_gpu_cuml.ipynb +++ b/demos/demos_databases_apis/gpu_rapids/part_iv_gpu_cuml.ipynb @@ -63,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 112, + "execution_count": 2, "metadata": { "vscode": { "languageId": "python" @@ -101,43 +101,43 @@ " \n", " \n", " 0\n", - " 61\n", - " 26\n", - " 937\n", - " 2019-04-05\n", - " 113.47,20.34\n", + " 32\n", + " 185\n", + " 357\n", + " 2017-06-16\n", + " 117.81,22.87\n", " \n", " \n", " 1\n", - " 30\n", - " 19\n", - " 972\n", - " 2019-08-17\n", - " 117.61,20.24\n", + " 66\n", + " 86\n", + " 84\n", + " 2020-03-30\n", + " 110.07,20.52\n", " \n", " \n", " 2\n", - " 27\n", - " 134\n", - " 760\n", - " 2020-05-30\n", - " 115.11,23.5\n", + " 28\n", + " 26\n", + " 862\n", + " 2019-05-12\n", + " 116.16,23.02\n", " \n", " \n", " 3\n", - " 55\n", - " 44\n", - " 864\n", - " 2016-08-17\n", - " 119.14,21.56\n", + " 69\n", + " 193\n", + " 607\n", + " 2019-03-11\n", + " 112.21,23.25\n", " \n", " \n", " 4\n", - " 24\n", - " 184\n", - " 938\n", - " 2017-09-30\n", - " 113.64,23.54\n", + " 34\n", + " 27\n", + " 4\n", + " 2019-08-06\n", + " 114.56,20.99\n", " \n", " \n", " ...\n", @@ -149,43 +149,43 @@ " \n", " \n", " 995\n", - " 69\n", - " 72\n", - " 887\n", - " 2019-10-26\n", - " 115.18,23.8\n", + " 52\n", + " 128\n", + " 435\n", + " 2016-10-19\n", + " 115.3,23.67\n", " \n", " \n", " 996\n", - " 33\n", - " 29\n", - " 651\n", - " 2020-06-15\n", - " 117.05,21.3\n", + " 67\n", + " 116\n", + " 97\n", + " 2016-04-24\n", + " 117.69,23.92\n", " \n", " \n", " 997\n", - " 18\n", - " 101\n", - " 517\n", - " 2019-04-14\n", - " 111.96,23.58\n", + " 32\n", + " 55\n", + " 915\n", + " 2018-11-07\n", + " 113.63,22.74\n", " \n", " \n", " 998\n", - " 65\n", - " 19\n", - " 974\n", - " 2019-05-22\n", - " 112.48,23.63\n", + " 72\n", + " 68\n", + " 148\n", + " 2020-05-23\n", + " 116.39,21.25\n", " \n", " \n", " 999\n", - " 23\n", - " 42\n", - " 156\n", - " 2020-12-10\n", - " 118.72,22.49\n", + " 56\n", + " 19\n", + " 932\n", + " 2016-04-23\n", + " 116.2,23.54\n", " \n", " \n", "\n", @@ -193,23 +193,23 @@ "" ], "text/plain": [ - " age user_id profile date location\n", - "0 61 26 937 2019-04-05 113.47,20.34\n", - "1 30 19 972 2019-08-17 117.61,20.24\n", - "2 27 134 760 2020-05-30 115.11,23.5\n", - "3 55 44 864 2016-08-17 119.14,21.56\n", - "4 24 184 938 2017-09-30 113.64,23.54\n", - ".. ... ... ... ... ...\n", - "995 69 72 887 2019-10-26 115.18,23.8\n", - "996 33 29 651 2020-06-15 117.05,21.3\n", - "997 18 101 517 2019-04-14 111.96,23.58\n", - "998 65 19 974 2019-05-22 112.48,23.63\n", - "999 23 42 156 2020-12-10 118.72,22.49\n", + " age user_id profile date location\n", + "0 32 185 357 2017-06-16 117.81,22.87\n", + "1 66 86 84 2020-03-30 110.07,20.52\n", + "2 28 26 862 2019-05-12 116.16,23.02\n", + "3 69 193 607 2019-03-11 112.21,23.25\n", + "4 34 27 4 2019-08-06 114.56,20.99\n", + ".. .. ... ... ... ...\n", + "995 52 128 435 2016-10-19 115.3,23.67\n", + "996 67 116 97 2016-04-24 117.69,23.92\n", + "997 32 55 915 2018-11-07 113.63,22.74\n", + "998 72 68 148 2020-05-23 116.39,21.25\n", + "999 56 19 932 2016-04-23 116.2,23.54\n", "\n", "[1000 rows x 5 columns]" ] }, - "execution_count": 112, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -230,12 +230,13 @@ "df['lat']=np.round(np.random.uniform(110, 120,size=(samples)), 2)\n", "df['location']=df['lat'].astype(str) +\",\"+ df[\"lon\"].astype(str) \n", "df.drop(columns=['lat','lon'],inplace=True)\n", + "df = df.applymap(str)\n", "df" ] }, { "cell_type": "code", - "execution_count": 113, + "execution_count": 3, "metadata": { "vscode": { "languageId": "python" @@ -243,38 +244,18 @@ }, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "['time: 0.03180466492970784 line/min: 31441.928478420414']\n" + "! Failed umap speedup attempt. Continuing without memoization speedups.* Ignoring target column of shape (1000, 0) in UMAP fit, as it is not one dimensional" ] }, { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 113, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "['time: 0.14064184427261353 line/min: 7110.259433612426']\n" + ] } ], "source": [ @@ -296,7 +277,7 @@ }, { "cell_type": "code", - "execution_count": 114, + "execution_count": 4, "metadata": { "vscode": { "languageId": "python" @@ -304,38 +285,18 @@ }, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "['time: 0.02227895657221476 line/min: 44885.40550625031']\n" + "! Failed umap speedup attempt. Continuing without memoization speedups.* Ignoring target column of shape (1000, 14) in UMAP fit, as it is not one dimensional" ] }, { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 114, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "['time: 0.0287002166112264 line/min: 34842.94260026035']\n" + ] } ], "source": [ @@ -350,7 +311,7 @@ }, { "cell_type": "code", - "execution_count": 115, + "execution_count": 5, "metadata": { "vscode": { "languageId": "python" @@ -358,38 +319,18 @@ }, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "['time: 0.023025786876678465 line/min: 43429.56900260569']\n" + "! Failed umap speedup attempt. Continuing without memoization speedups.* Ignoring target column of shape (1000, 14) in UMAP fit, as it is not one dimensional" ] }, { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 115, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "['time: 0.0024895787239074705 line/min: 401674.38386140653']\n" + ] } ], "source": [ @@ -411,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 117, + "execution_count": 6, "metadata": { "vscode": { "languageId": "python" @@ -419,61 +360,30 @@ }, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "['time: 0.003930246829986573 line/min: 254436.94588602122']\n" + "! Failed umap speedup attempt. Continuing without memoization speedups.* Ignoring target column of shape (1000, 14) in UMAP fit, as it is not one dimensional" ] }, { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 117, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "['time: 0.0022179365158081056 line/min: 450869.5325013168']\n" + ] } ], "source": [ "g = graphistry.nodes(df)\n", "t=time()\n", - "g2 = g.umap(X=['user_id'],y=['date','location'], feature_engine='torch', n_neighbors= 2,min_dist=.5, spread=.1, local_connectivity=2, n_components=5,metric='hellinger')\n", + "g2 = g.umap(X=['user_id'],y=['date','location'], feature_engine='torch', n_neighbors= 2,min_dist=.1, spread=.1, local_connectivity=2, n_components=5,metric='hellinger')\n", "min=(time()-t)/60\n", "lin=df.shape[0]/min\n", "print(['time: '+str(min)+' line/min: '+str(lin)])\n", - "g2.plot()\n" + "g2.plot(render=False)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [] - }, { "cell_type": "markdown", "metadata": {}, @@ -483,18 +393,25 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": 7, "metadata": { "vscode": { "languageId": "python" } }, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "! Failed umap speedup attempt. Continuing without memoization speedups.* Ignoring target column of shape (1000, 0) in UMAP fit, as it is not one dimensional" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "['time: 0.004134837786356608 line/min: 241847.4560960093']\n" + "['time: 0.00446544885635376 line/min: 223941.65338544376']\n" ] } ], @@ -509,18 +426,25 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": 8, "metadata": { "vscode": { "languageId": "python" } }, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "* Ignoring target column of shape (1000, 0) in UMAP fit, as it is not one dimensional" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "['time: 0.06711641947428386 line/min: 14899.483730403068']\n" + "['time: 0.11818180878957113 line/min: 8461.539134001174']\n" ] } ], @@ -542,18 +466,25 @@ }, { "cell_type": "code", - "execution_count": 77, + "execution_count": 12, "metadata": { "vscode": { "languageId": "python" } }, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "! Failed umap speedup attempt. Continuing without memoization speedups.* Ignoring target column of shape (220, 0) in UMAP fit, as it is not one dimensional" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "['time: 0.0008151054382324219 line/min: 269903.7323037323']\n" + "['time: 0.008098324139912924 line/min: 27166.11439590581']\n" ] } ], @@ -570,7 +501,7 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": 13, "metadata": { "vscode": { "languageId": "python" @@ -582,15 +513,15 @@ "output_type": "stream", "text": [ "\n", - "Int64Index: 3728 entries, 0 to 3749\n", + "Int64Index: 2410 entries, 0 to 2821\n", "Data columns (total 3 columns):\n", " # Column Non-Null Count Dtype \n", "--- ------ -------------- ----- \n", - " 0 _src_implicit 3728 non-null int32 \n", - " 1 _dst_implicit 3728 non-null int32 \n", - " 2 _weight 3728 non-null float32\n", + " 0 _src_implicit 2410 non-null int32 \n", + " 1 _dst_implicit 2410 non-null int32 \n", + " 2 _weight 2410 non-null float32\n", "dtypes: float32(1), int32(2)\n", - "memory usage: 72.8 KB\n", + "memory usage: 47.1 KB\n", "None\n" ] }, @@ -622,34 +553,34 @@ " \n", " \n", " \n", - " 1046\n", - " 71\n", - " 144\n", - " 0.205078\n", + " 671\n", + " 51\n", + " 123\n", + " 0.017956\n", " \n", " \n", - " 642\n", - " 41\n", - " 74\n", - " 0.176112\n", + " 2123\n", + " 167\n", + " 194\n", + " 0.663975\n", " \n", " \n", - " 811\n", - " 53\n", - " 152\n", - " 0.079932\n", + " 1761\n", + " 139\n", + " 78\n", + " 0.113361\n", " \n", " \n", - " 2699\n", - " 171\n", - " 70\n", - " 0.140091\n", + " 2444\n", + " 191\n", + " 3\n", + " 0.999991\n", " \n", " \n", - " 1466\n", - " 101\n", - " 144\n", - " 0.050159\n", + " 2441\n", + " 190\n", + " 152\n", + " 0.544303\n", " \n", " \n", "\n", @@ -657,14 +588,14 @@ ], "text/plain": [ " _src_implicit _dst_implicit _weight\n", - "1046 71 144 0.205078\n", - "642 41 74 0.176112\n", - "811 53 152 0.079932\n", - "2699 171 70 0.140091\n", - "1466 101 144 0.050159" + "671 51 123 0.017956\n", + "2123 167 194 0.663975\n", + "1761 139 78 0.113361\n", + "2444 191 3 0.999991\n", + "2441 190 152 0.544303" ] }, - "execution_count": 78, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -676,55 +607,16 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": 16, "metadata": { "vscode": { "languageId": "python" } }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 79, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "g3.plot()" + "#g3.plot()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "python" - } - }, - "outputs": [], - "source": [] } ], "metadata": { @@ -733,7 +625,18 @@ "language": "python", "name": "python3" }, - "orig_nbformat": 4, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.15" + }, "vscode": { "interpreter": { "hash": "21c4dad877b49e935d0a60da22bc51e9bfc4901bc58e488dc71d08b8faef6557" diff --git a/graphistry/dgl_utils.py b/graphistry/dgl_utils.py index 6792073ec8..257c13a701 100644 --- a/graphistry/dgl_utils.py +++ b/graphistry/dgl_utils.py @@ -443,7 +443,6 @@ def build_gnn( :param inplace: default, False, whether to return Graphistry instance in place or not. """ - if inplace: res = self else: diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index acfb39cfd7..836008003e 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -27,11 +27,13 @@ from graphistry.umap_utils import ( lazy_umap_import_has_dependancy, lazy_cuml_import_has_dependancy, + lazy_cudf_import_has_dependancy, ) has_dependancy, _ = lazy_import_has_min_dependancy() has_cuml, _, _ = lazy_cuml_import_has_dependancy() has_umap, _, _ = lazy_umap_import_has_dependancy() +has_cudf, _, cudf = lazy_cudf_import_has_dependancy() # print('has_dependancy', has_dependancy) # print('has_cuml', has_cuml) @@ -137,14 +139,10 @@ def setUp(self): @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_columns_match(self): - d = self.g2._node_features.shape[1] - dt = self.g2._node_target.shape[1] - de = self.g2e._edge_features.shape[1] - det = self.g2e._edge_target.shape[1] - assert (self.X.columns == self.x.columns).sum() == d, "Node Feature Columns do not match" - assert (self.Y.columns == self.y.columns).sum() == dt, "Node Target Columns do not match" - assert (self.Xe.columns == self.xe.columns).sum() == de, "Edge Feature Columns do not match" - assert (self.Ye.columns == self.ye.columns).sum() == det, "Edge Target Columns do not match" + assert set(self.X.columns) == set(self.x.columns), "Node Feature Columns do not match" + assert set(self.Y.columns) == set(self.y.columns), "Node Target Columns do not match" + assert set(self.Xe.columns) == set(self.xe.columns), "Edge Feature Columns do not match" + assert set(self.Ye.columns) == set(self.ye.columns), "Edge Target Columns do not match" @pytest.mark.skipif(not has_umap, reason="requires umap feature dependencies") def test_index_match(self): @@ -208,8 +206,9 @@ def test_umap_kwargs(self): warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=FutureWarning) - g2 = g.umap(**umap_kwargs) - g3 = g.umap(**umap_kwargs2) + g2 = g.umap(**umap_kwargs, engine='umap_learn') + g3 = g.umap(**umap_kwargs2, engine='umap_learn') + assert g2._umap_params==umap_kwargs assert ( g2._umap_params == umap_kwargs ), f"Umap params do not match, found {g2._umap_params} vs {umap_kwargs}" @@ -254,10 +253,13 @@ def test_transform_umap(self): if return_g: assert True else: + objs = (pd.DataFrame,) + if has_cudf: + objs = (pd.DataFrame, cudf.DataFrame) assert len(g4) == 3 - assert isinstance(g4[0], pd.DataFrame) - assert isinstance(g4[1], pd.DataFrame) - assert isinstance(g4[2], pd.DataFrame) + assert isinstance(g4[0], objs) + assert isinstance(g4[1], objs) + assert isinstance(g4[2], objs) assert g4[0].shape[1] == 2 assert g4[1].shape[1] >= 2 assert g4[2].shape[0] == test.shape[0] @@ -277,21 +279,24 @@ class TestUMAPMethods(unittest.TestCase): def _check_attributes(self, g, attributes): msg = "Graphistry instance after umap should have `{}` as attribute" msg2 = "Graphistry instance after umap should not have None values for `{}`" + objs = (pd.DataFrame,) + if has_cudf: + objs = (pd.DataFrame, cudf.DataFrame) for attribute in attributes: self.assertTrue(hasattr(g, attribute), msg.format(attribute)) self.assertTrue(getattr(g, attribute) is not None, msg2.format(attribute)) if "df" in attribute: self.assertIsInstance( - getattr(g, attribute), pd.DataFrame, msg.format(attribute) + getattr(g, attribute), objs, msg.format(attribute) ) if "node_" in attribute: self.assertIsInstance( - getattr(g, attribute), pd.DataFrame, msg.format(attribute) + getattr(g, attribute), objs, msg.format(attribute) ) if "edge_" in attribute: self.assertIsInstance( - getattr(g, attribute), pd.DataFrame, msg.format(attribute) + getattr(g, attribute), objs, msg.format(attribute) ) def cases_check_node_attributes(self, g): diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index c4b74041db..019eefef3c 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -116,6 +116,13 @@ def resolve_umap_engine( def make_safe_gpu_dataframes(X, y, engine): def safe_cudf(X, y): + # remove duplicate columns + if len(X.columns) != len(set(X.columns)): + X = X.loc[:, ~X.columns.duplicated()] + try: + y = y.loc[:, ~y.columns.duplicated()] + except: + pass new_kwargs = {} kwargs = {'X': X, 'y': y} for key, value in kwargs.items(): From d9987de0e6b667fe517b4b098a66d0ee957067b3 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 4 Apr 2023 21:15:05 +0530 Subject: [PATCH 302/432] lint: flake8 typo --- graphistry/tests/test_umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 836008003e..564ffcdbfa 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -208,7 +208,7 @@ def test_umap_kwargs(self): warnings.filterwarnings("ignore", category=FutureWarning) g2 = g.umap(**umap_kwargs, engine='umap_learn') g3 = g.umap(**umap_kwargs2, engine='umap_learn') - assert g2._umap_params==umap_kwargs + assert g2._umap_params == umap_kwargs assert ( g2._umap_params == umap_kwargs ), f"Umap params do not match, found {g2._umap_params} vs {umap_kwargs}" From 168af4bd71427b7922f8731a7ccf7a62d76531b9 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 4 Apr 2023 21:34:42 +0530 Subject: [PATCH 303/432] fix: _dgl_graph fix --- graphistry/.dgl_utils.py.swp | Bin 0 -> 40960 bytes graphistry/tests/test_dgl_utils.py | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 graphistry/.dgl_utils.py.swp diff --git a/graphistry/.dgl_utils.py.swp b/graphistry/.dgl_utils.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..46316aea9ac305deae72075eac60f9f2bebb0cb5 GIT binary patch literal 40960 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 Date: Tue, 4 Apr 2023 21:37:38 +0530 Subject: [PATCH 304/432] typo --- graphistry/tests/.test_dgl_utils.py.swp | Bin 0 -> 16384 bytes graphistry/tests/test_dgl_utils.py | 2 -- 2 files changed, 2 deletions(-) create mode 100644 graphistry/tests/.test_dgl_utils.py.swp diff --git a/graphistry/tests/.test_dgl_utils.py.swp b/graphistry/tests/.test_dgl_utils.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..8339aab95f1ffdf64a432fcf1562a09e6fbc10fc GIT binary patch literal 16384 zcmeI3ON<;x8OJM5csU8L2qXfDE3=m9!IR#Hg|Jp>3CHVLfCCO*n~-5Os+q3oZKkKY z-BrE2V`CntNDzc%#~=h8I0Oj^AtE^-5F!U6K@LbD5iX#h7<_^Q76RoK{$KTLXJ*$g zEMuZ>>9_r=`l{;tzIuGMJ>9wEA9#Ahv2*5Y49XC2?n4IJ_a5Fdq5Ld;CArpU5xz=ybOK^UI5R5 zXTUjd7Muacz%FnXR9@>7TG&=p#=2P>y}pG~CWQxCSw872JG;57FY4D(v@ps$@r^s#8rHYs?l#y|%^_-f+Vkb3LZHbz3uK znh?EdOVcPB^zF=tkIECNgaaQuk&3_y@ZFSk$Tr?%lk-|l_#<63Sz*&@x4jN=6PmB2 zQz;W}#U1{LKwl|q0M83#I9X5bIDBuV0}^zliW$ecaqZkf+!foo$wu3^b4+mF&QcrG z+FDN1N?G)Jyb$xc%)Z4Cg^IKreJOOB$VQ7dbRzt)GZboI?&upjbF(rgWGa^#?yfHX zA!PP!^JSj-X0H@H!<9db1Ig*jJ&egPspPQ#(xJoLq~z;D;i2Ykn)so|UBBO#i3~N* z8sMK4+K)oMho?gkb9@#kwDf$bOcAc^^N#r_uqTvMQ~`^erp3MHBI^a{BbYXnMsZde z`kInrPEh&)k|PC{5Jt|aUKA{H>Gs>WSm8AOE z6kJtN#BngPP#?jw>6tO807*y@ZW6^EH>HZAJY}i03r};}4L@$IH0?y2mC38pQ0XZ<9`@ymLq*+pcM@pxO{j7z0>v4q?cW< zuu`knk_{rrXjnFz{#wavW%pHC<$YF-Jh_GgH1f1dag^xd=IG{|BT1yA;th4E7Tc!( zccP^1?(MQxrFjY&g+cO?OD+75YE2qv47MBd!Q73Ol-}>U2|Q1aVrgNdZDvV}My%us z(Tb^rD(SVjiy_{@bd!4_Y*s0RMCJJ>yJa&Ugd0Cc!LI$E1+4#7q(P zMplzCQb;!OnJ?V!@>_PEmvea$(*o#2_#*G-#<5&pEU@@c7-cXk70)U?7f)j9r@FW+ zW=cl!ze2ou7O^PB|Hc3Je}b6)OJE5c08MZgxP&PgkT7EgMT9~KM$S)Pk{Tt`@jzHC&c4F20s8_0bd6D!5!cN zV)Gw?6Cei1!5r8L82B?{^OwOn@P6=5#NxjK=fJ1HZm<*l6*~9@I1fl4XFz?_8mKi; zYoOM^P169+53@Be8Txq()kZ{hMLEhKTlGXB6Q_|x-BcB;z(QylYj2y%Tfj8$+1=C+ z+>RH9)xIGjAM6qaE{~JQO?#3jn0YbT=W)0)@ei$iv&fZ-obik>3T!P?qBGy*GXp7O zE`oq(aT@JDa`P66=jY~FQCTk!V0xmrir^*-z9Ozyg{D<}BHai}`Jh*oXNoI-U58yF z#DNypWi4JT-!i39RBe_ zR+<|4vER37lek=x6=f&b9`h0CnGFZGSc+l$0D?@tsqVLoFO@ag$}tdI-!#`Serh3x z-qs<^8nR>7_vF8&Z(uc^4#RCdqqdc#Zd@7qzwN}OrLqXQ8uh9znqXu$*eE6Kq}W-O zCc=By{AY!pRRh}9TKsiqVj2nZy*ej9XJIiXDzquupNet(yd1C|JOcBnkx)gY)y^Zp|IdKn>yo( i3yWquTa2U^M{Y+a-!rZ9-F$my<83Fz2JDTu=zjq4sHhGA literal 0 HcmV?d00001 diff --git a/graphistry/tests/test_dgl_utils.py b/graphistry/tests/test_dgl_utils.py index 1fff11155f..baaef46135 100644 --- a/graphistry/tests/test_dgl_utils.py +++ b/graphistry/tests/test_dgl_utils.py @@ -75,8 +75,6 @@ class TestDGL(unittest.TestCase): def _test_cases_dgl(self, g): # simple test to see if DGL graph was set during different featurization + umap strategies G = g._dgl_graph - print('#######################') - print(G) keys = ["feature", "target", "train_mask", "test_mask"] keys_without_target = ["feature", "train_mask", "test_mask"] From 60d3b977b3cc5d193a6f066671bb3785afb8566f Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 4 Apr 2023 21:49:10 +0530 Subject: [PATCH 305/432] test: remove xfail test_dgl_utils --- graphistry/tests/.test_dgl_utils.py.swp | Bin 16384 -> 16384 bytes graphistry/tests/test_dgl_utils.py | 1 - 2 files changed, 1 deletion(-) diff --git a/graphistry/tests/.test_dgl_utils.py.swp b/graphistry/tests/.test_dgl_utils.py.swp index 8339aab95f1ffdf64a432fcf1562a09e6fbc10fc..945f28592b30f79614fa141ec33f1a848fa1eaac 100644 GIT binary patch delta 135 zcmZo@U~Fh$6iqS+^Ym4)&@*HJ0s#hwOTIcOvp0%L3otrtHWaul&-;{>fq{`7BIz($ z(BQsaA_oJ5DG)OQ@ll}6Bp|K;;vgW_0b(T}2I-j029#sl%*ay8I$1`EWAbdXw8{4^ UCT>T8P0*sEE4FxXC^A@u*Fid8HNIFgy zG`O#)&cVR&nVo^*I1o<-;&32V0b(v7egagv4~Vw_F-X@Ww#|$zrL2=>lsG2OHcQ)_ zZ*I;QQ<0XKnWIsZnpm8lXRG9!S)7rWmy(m2m#&bKSdvICJEG{lhE!H=-FjUgq>|iCyILX3i^G{25HUJnt BKPmtK diff --git a/graphistry/tests/test_dgl_utils.py b/graphistry/tests/test_dgl_utils.py index baaef46135..942bb36fef 100644 --- a/graphistry/tests/test_dgl_utils.py +++ b/graphistry/tests/test_dgl_utils.py @@ -166,7 +166,6 @@ def test_build_dgl_graph_from_umap_no_node_column(self): self._test_cases_dgl(g2) @pytest.mark.skipif(not has_dgl, reason="requires DGL dependencies") - @pytest.mark.xfail(reason="Mishandling datetimes: https://github.com/graphistry/pygraphistry/issues/381") def test_build_dgl_with_no_node_features(self): g = graphistry.edges(edf, src, dst) g.reset_caches() # so that we redo calcs From 4d74adefa6b301cf560191ebb1f0c5c2a6b5af72 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 4 Apr 2023 22:11:26 +0530 Subject: [PATCH 306/432] test: remove StartTime test_dgl_utils temp --- graphistry/tests/.test_dgl_utils.py.swp | Bin 16384 -> 16384 bytes graphistry/tests/test_dgl_utils.py | 2 ++ 2 files changed, 2 insertions(+) diff --git a/graphistry/tests/.test_dgl_utils.py.swp b/graphistry/tests/.test_dgl_utils.py.swp index 945f28592b30f79614fa141ec33f1a848fa1eaac..e97036d803aac69ccd682d066bb2a8b906350bcf 100644 GIT binary patch delta 718 zcmYk)NoW&M7{Kvw+H6f4n}96{mJV$-EtE{6*rb6LE2t<|K}$WThpE{HvYDCG77qz{ z5d?!7J&4$ghaLpAjDp~HQV%@{dJqxuQjdbri=nvuiynONd%Wd)yzecIhSF##z4NiT zBQt6;8W&1P!e zle}zl-ued9b~yGW8sX?0obC;7;xvZg$2yTO@CaM0S{_Gn)!g>ZL7#nlJnNJb3FYi! zRyPVG)r@YIOOt~$hMCt(>4H(pscGHHSR`Sl)VyU?%#^0(3@cxokDf0VwOm!N0fTs&cLH@n42}ggi|s}%%LB(@E~2GxlGb*sLa_cts5uwV`#~!Y${axtTkjjgglmC DoTgoM diff --git a/graphistry/tests/test_dgl_utils.py b/graphistry/tests/test_dgl_utils.py index 942bb36fef..55d10badca 100644 --- a/graphistry/tests/test_dgl_utils.py +++ b/graphistry/tests/test_dgl_utils.py @@ -16,6 +16,7 @@ edf = pd.read_csv( "graphistry/tests/data/malware_capture_bot.csv", index_col=0, nrows=50 ) +edf = edf.drop(['StartTime'], axis=1) edf = edf.drop_duplicates() src, dst = "to_node", "from_node" edf["to_node"] = edf.SrcAddr.astype(str) @@ -166,6 +167,7 @@ def test_build_dgl_graph_from_umap_no_node_column(self): self._test_cases_dgl(g2) @pytest.mark.skipif(not has_dgl, reason="requires DGL dependencies") + @pytest.mark.xfail(reason="Mishandling datetimes: https://github.com/graphistry/pygraphistry/issues/381") def test_build_dgl_with_no_node_features(self): g = graphistry.edges(edf, src, dst) g.reset_caches() # so that we redo calcs From aaf275f410384e55dd69262ca4c60c0d84f8868e Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 4 Apr 2023 23:51:43 +0530 Subject: [PATCH 307/432] pinned pandas --- graphistry/tests/.test_dgl_utils.py.swp | Bin 16384 -> 0 bytes graphistry/tests/test_dgl_utils.py | 1 - setup.py | 4 ++-- 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 graphistry/tests/.test_dgl_utils.py.swp diff --git a/graphistry/tests/.test_dgl_utils.py.swp b/graphistry/tests/.test_dgl_utils.py.swp deleted file mode 100644 index e97036d803aac69ccd682d066bb2a8b906350bcf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI3O^h5z6~_z5CW+$!;p>D+(N1uW7hB-7X&$gk{~63L?9PFl>>+y;!BF+BT`T#ln9CNf7M?*GrP89 zEMu&0>9_q=^{VQ$`pzS^Njn~UC$4@nGQnbB%@1_+@xtV&}iU-8p!nE zqla!~2M^57P`Z8AhxmKm{n-mwA3O=p zf=56Mh9CqT@Ww5Sy$)UiYhW*UV>e^x!7Jc*;4Jti_zH-?F>o`u3A}bQV^4!2xEqa~SYajzB!N|z_-9Da5wlj?Ds1875EOowho&CWLG-x1#k7>j87b` z=_hSIu?91tIc(s z_E(%d&r}wnluR{52a(bu>Pvep2^GD{gNU!nk?O7odL_;=bok3-IDx8RR-QFps(x4jH;6P==#PM$O2R@~*s1o{dqy$YTe z%4o8l+;#cES_hQ)Vihxk#g%L47UC}2&P_JjzMW%&i*{DpnAX-}n$^mp*W-nl*H!i{ zhA32I$r?zZvs7AbZs}A6QFkcRs=1@Da+8v;3x$W8ds!MpKKFvbK&CR%Ja0hou+Tvq@sIFqC=!kj z{)Co(AeAY?lLOv0pZoQNl8P!|lhd@h-~PdCX$R zQZdBD@}M!_)bqT^Xp16^wW-Le5TwcrX|d5Z(yaL`wdOdb#Feuq!c1->fJXf3>R9(u zyC8yEWUeQLR84KMeOzuNVbBj?H?(v>Erl*Quj~z-1bu-?xJ|>sAPbFB73G1ILl`mT zC@75kON*{Mo4aBthCNU4jX6F`PGTL2btJV#cG%!FZt)qm2nE+w6iE_}Y}7~ae0s(b z%o`vHDZ)$Rr0Zo=QIw}_mG$6h?vfQG)^f{9CGNV$cs3<agXayr!E&36jX=3bgS(=_ei~;uNWy7g*wiwEq60GCP6Ie;)MLr`AI6%+SRY^2iiQnb9vMOJbbTW2=hw@u(&}5u1`%X5#&Sog z<+V-!-Bea}pI0NlJ)z+MR*`l-iBnzPoY;JGJdJf+zM&4)X4~}tZk$%#eW|KdWu8(- zX^^7iN(=v^T9d{ZgYCwAuyCV!r4M^v3eVG{MA{f>TY1v55z9qFv|=iuO8af@VTgAz z-4tF3o7D;-QAPgAZfT0y^v+(|4|}D0F&N7IWmlSQ6+^W(^|?2VK%Joo*F_3{6bVgx z-CnGnzFLFtnGQ~V>dv%zl*a4Iy=$hZ4*G5?1qOJvW+Ui}eILV;OY>2dbn{!=qDiBR z86^Ll91_=L+KZJe#tY+NnB?G8dCh~h9ngX7Zx!sfIfsT^KNb&E94~tOB98%2y<8Ytk!e+ zB%$7{%e!(uWfcD_#G2<2hf@4s{(t|+i0Qu!=D|In1@?e{Adde9_#XHYcnBN^9pHfT zh|_-vG*IC4;1l3>Kr#Gz#N{u7CjiCm5ulj;4)9OJ=g)z!gNFdc>mL9+z@HJ1{|Nj5 zJPN)B4uhTGWyIyrf>R&?C&3KZ4H)u0>MP1oh2p9&LYcZ&8uv0)t`$q6RYbmR zD*phcdEe<}LFje;D5`fC5g%bMG4yzn#$MK!JjKk5$-aozm5GvQg`CH?ROCM22&3@V zHYK{VExvD6%7lwBlLACt)EIS#)3cW z*X5bws$bV(FA342jn!M5my5hyX%u^#rBU8YTBA@c5P_H=q+2%01muXdsYaj7mk`cS zq^r)xKm?(U^_Pkx*Se2>DHwU7*#y99U1Rm9=J<-%iJI$le2C6^h_r^AfA2T;JHS9G z#Jrt@hi0w3hGV6v)gTE5Hf<$WOR}=;6q{%P!a=hw;gw4Tlof7?o3Q0L9wIf7Uw*erx$b-+kkj!M$Yk<#qOP3 zoF`wJ9C$gAb9y>qrYQWmgu%M7B)aKl<8Lu~Z|bborXDuUu${Wq=>7lNM@e&~0m}`n raXXY2+-B=%JaK8!eE*A)^zta|=oUMxb-t@_&wRY?M%bpk@)rFM6}hQ` diff --git a/graphistry/tests/test_dgl_utils.py b/graphistry/tests/test_dgl_utils.py index 55d10badca..baaef46135 100644 --- a/graphistry/tests/test_dgl_utils.py +++ b/graphistry/tests/test_dgl_utils.py @@ -16,7 +16,6 @@ edf = pd.read_csv( "graphistry/tests/data/malware_capture_bot.csv", index_col=0, nrows=50 ) -edf = edf.drop(['StartTime'], axis=1) edf = edf.drop_duplicates() src, dst = "to_node", "from_node" edf["to_node"] = edf.SrcAddr.astype(str) diff --git a/setup.py b/setup.py index 1c5304d74d..beb9462138 100755 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def unique_flatten_dict(d): core_requires = [ 'numpy', 'palettable >= 3.0', - 'pandas >= 0.17.0', + 'pandas < 2.0.0', 'pyarrow >= 0.15.0', 'requests', 'squarify', @@ -42,7 +42,7 @@ def unique_flatten_dict(d): 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed -base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch', 'sentence-transformers', 'faiss-cpu', 'joblib'] +base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] base_extras = {**base_extras_light, **base_extras_heavy} From fd423a2e53ddfdc1994e03a47f61898de34ba73f Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 5 Apr 2023 17:56:07 +0800 Subject: [PATCH 308/432] alex2/3 cucat req --- graphistry/feature_utils.py | 3 +- graphistry/umap_utils.py | 442 ++++++++++++++++++++++-------------- 2 files changed, 276 insertions(+), 169 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 3aecd5a058..af1bc24ef1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1003,7 +1003,8 @@ def process_dirty_dataframes( X_enc = cudf.DataFrame.from_arrow(X_enc) X_enc.index = ndf.index # features_transformed=np.array([item.as_py() for item in features_transformed.key()]) - # X_enc.columns = features_transformed #.to_numpy() ##error suggests this -- not working + # X_enc.columns = features_transformed.as_py() + # = features_transformed #.to_numpy() ##error suggests this -- not working #X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 46662c4b9b..0897535d09 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -6,13 +6,16 @@ import pandas as pd from . import constants as config +from .constants import CUML, UMAP_LEARN from .feature_utils import (FeatureMixin, Literal, XSymbolic, YSymbolic, prune_weighted_edges_df_and_relabel_nodes, resolve_feature_engine) from .PlotterBase import Plottable, WeakValueDictionary -from .util import check_set_memoize, setup_logger +from .util import check_set_memoize -logger = setup_logger(name=__name__, verbose=config.VERBOSE) +import logging + +logger = logging.getLogger(__name__) if TYPE_CHECKING: MIXIN_BASE = FeatureMixin @@ -40,7 +43,9 @@ def lazy_cuml_import_has_dependancy(): import warnings warnings.filterwarnings("ignore") - import cuml # type: ignore + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + import cuml # type: ignore return True, "ok", cuml except ModuleNotFoundError as e: @@ -57,16 +62,26 @@ def lazy_cudf_import_has_dependancy(): except ModuleNotFoundError as e: return False, e, None +def lazy_cudf_import_has_dependancy(): + try: + import warnings + + warnings.filterwarnings("ignore") + import cudf # type: ignore + + return True, "ok", cudf + except ModuleNotFoundError as e: + return False, e, None def assert_imported(): - has_dependancy_, import_exn, umap_learn = lazy_umap_import_has_dependancy() + has_dependancy_, import_exn, _ = lazy_umap_import_has_dependancy() if not has_dependancy_: logger.error("UMAP not found, trying running " "`pip install graphistry[ai]`") raise import_exn def assert_imported_cuml(): - has_cuml_dependancy_, import_cuml_exn, cuml = lazy_cuml_import_has_dependancy() + has_cuml_dependancy_, import_cuml_exn, _ = lazy_cuml_import_has_dependancy() if not has_cuml_dependancy_: logger.warning("cuML not found, trying running " "`pip install cuml`") raise import_cuml_exn @@ -85,22 +100,22 @@ def is_legacy_cuml(): return False -UMAPEngineConcrete = Literal["cuml", "umap_learn"] +UMAPEngineConcrete = Literal['cuml', 'umap_learn'] UMAPEngine = Literal[UMAPEngineConcrete, "auto"] def resolve_umap_engine( engine: UMAPEngine, ) -> UMAPEngineConcrete: # noqa - if engine in ["cuml", "umap_learn"]: + if engine in [CUML, UMAP_LEARN]: return engine # type: ignore if engine in ["auto"]: - has_cuml_dependancy_, _, cuml = lazy_cuml_import_has_dependancy() + has_cuml_dependancy_, _, _ = lazy_cuml_import_has_dependancy() if has_cuml_dependancy_: - return "cuml" + return 'cuml' has_umap_dependancy_, _, _ = lazy_umap_import_has_dependancy() if has_umap_dependancy_: - return "umap_learn" + return 'umap_learn' raise ValueError( # noqa f'engine expected to be "auto", ' @@ -109,34 +124,27 @@ def resolve_umap_engine( ) -############################################################################### +def make_safe_gpu_dataframes(X, y, engine): + def safe_cudf(X, y): + new_kwargs = {} + kwargs = {'X': X, 'y': y} + for key, value in kwargs.items(): + if isinstance(value, cudf.DataFrame) and engine in ["pandas", "umap_learn", "dirty_cat"]: + new_kwargs[key] = value.to_pandas() + elif isinstance(value, pd.DataFrame) and engine in ["cuml", "cu_cat"]: + new_kwargs[key] = cudf.from_pandas(value) + else: + new_kwargs[key] = value + return new_kwargs['X'], new_kwargs['y'] -umap_kwargs_probs = { - "n_components": 2, - "metric": "hellinger", # info metric, can't use on - # textual encodings since they contain negative values... - # unless scaling min max etc - "n_neighbors": 15, - "min_dist": 0.3, - "verbose": True, - "spread": 0.5, - "local_connectivity": 1, - "repulsion_strength": 1, - "negative_sample_rate": 5, -} - -umap_kwargs_euclidean = { - "n_components": 2, - "metric": "euclidean", - "n_neighbors": 12, - "min_dist": 0.1, - "verbose": True, - "spread": 0.5, - "local_connectivity": 1, - "repulsion_strength": 1, - "negative_sample_rate": 5, -} + has_cudf_dependancy_, _, cudf = lazy_cudf_import_has_dependancy() + if has_cudf_dependancy_: + return safe_cudf(X, y) + else: + return X, y + +############################################################################### # ############################################################################# # @@ -169,43 +177,47 @@ def umap_graph_to_weighted_edges(umap_graph, engine, is_legacy, cfg=config): class UMAPMixin(MIXIN_BASE): """ UMAP Mixin for automagic UMAPing - """ # FIXME where is this used? _umap_memoize: WeakValueDictionary = WeakValueDictionary() def __init__(self, *args, **kwargs): - self.umap_initialized = False + #self._umap_initialized = False + #self.umap_engine = self.umap_engine if hasattr(self, "engine") else None + pass + def umap_lazy_init( self, + res, n_neighbors: int = 12, min_dist: float = 0.1, - spread=0.5, - local_connectivity=1, - repulsion_strength=1, - negative_sample_rate=5, + spread: float = 0.5, + local_connectivity: int = 1, + repulsion_strength: float = 1, + negative_sample_rate: int = 5, n_components: int = 2, metric: str = "euclidean", - engine: UMAPEngine = "auto", + umap_engine: UMAPEngine = "auto", suffix: str = "", + verbose: bool = False, ): - engine_resolved = resolve_umap_engine(engine) + from graphistry.features import ModelDict + + engine_resolved = resolve_umap_engine(umap_engine) # FIXME remove as set_new_kwargs will always replace? - if engine_resolved == "umap_learn": - _, _, umap_engine = lazy_umap_import_has_dependancy() - elif engine_resolved == "cuml": - _, _, umap_engine = lazy_cuml_import_has_dependancy() + if engine_resolved == UMAP_LEARN: + _, _, umap_engine_ = lazy_umap_import_has_dependancy() + elif engine_resolved == CUML: + _, _, umap_engine_ = lazy_cuml_import_has_dependancy() else: raise ValueError( "No umap engine, ensure 'auto', 'umap_learn', or 'cuml', and the library is installed" ) - - if not self.umap_initialized: - umap_kwargs = dict( - { + umap_kwargs = ModelDict("UMAP Parameters", + **{ "n_components": n_components, - **({"metric": metric} if engine_resolved == "umap_learn" else {}), + **({"metric": metric} if engine_resolved == UMAP_LEARN else {}), # type: ignore "n_neighbors": n_neighbors, "min_dist": min_dist, "spread": spread, @@ -214,20 +226,30 @@ def umap_lazy_init( "negative_sample_rate": negative_sample_rate, } ) + + if getattr(res, '_umap_params', None) == umap_kwargs: + print('Same umap params as last time, skipping new init') if verbose else None + return res + + print('lazy init') if verbose else None + print(umap_kwargs) if verbose else None + # set new umap kwargs + res._umap_params = umap_kwargs + res._n_components = n_components + res._metric = metric + res._n_neighbors = n_neighbors + res._min_dist = min_dist + res._spread = spread + res._local_connectivity = local_connectivity + res._repulsion_strength = repulsion_strength + res._negative_sample_rate = negative_sample_rate + res._umap = umap_engine_.UMAP(**umap_kwargs) + res.umap_engine = engine_resolved + res._suffix = suffix + + return res - self.n_components = n_components - self.metric = metric - self.n_neighbors = n_neighbors - self.min_dist = min_dist - self.spread = spread - self.local_connectivity = local_connectivity - self.repulsion_strength = repulsion_strength - self.negative_sample_rate = negative_sample_rate - self._umap = umap_engine.UMAP(**umap_kwargs) - self.umap_initialized = True - self.engine = engine_resolved - self.suffix = suffix - + #@safe_gpu_dataframes def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): if y is None: return None @@ -241,8 +263,16 @@ def _check_target_is_one_dimensional(self, y: Union[pd.DataFrame, None]): "as it is not one dimensional" ) return None + + def _get_embedding(self, kind='nodes'): + if kind == 'nodes': + return self._node_embedding + elif kind == 'edges': + return self._edge_embedding + else: + raise ValueError('kind must be one of `nodes` or `edges`') - def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): + def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None, verbose=False): if self._umap is None: raise ValueError("UMAP is not initialized") t = time() @@ -250,21 +280,20 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info("-" * 90) logger.info(f"Starting UMAP-ing data of shape {X.shape}") - if self.engine == "cuml" and is_legacy_cuml(): + if self.umap_engine == CUML and is_legacy_cuml(): # type: ignore from cuml.neighbors import NearestNeighbors - knn = NearestNeighbors(n_neighbors=self.n_neighbors) + knn = NearestNeighbors(n_neighbors=self._n_neighbors) # type: ignore cc = self._umap.fit(X, y, knn_graph=knn) knn.fit(cc.embedding_) self._umap.graph_ = knn.kneighbors_graph(cc.embedding_) - self._weighted_adjacency = self._umap.graph_ - else: self._umap.fit(X, y) - self._weighted_adjacency = self._umap.graph_ + + self._weighted_adjacency = self._umap.graph_ # if changing, also update fresh_res self._weighted_edges_df = umap_graph_to_weighted_edges( - self._umap.graph_, self.engine, is_legacy_cuml() + self._umap.graph_, self.umap_engine, is_legacy_cuml() # type: ignore ) mins = (time() - t) / 60 @@ -272,42 +301,84 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): logger.info(f" - or {X.shape[0]/mins:.2f} rows per minute") return self - def umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None): + + def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None, verbose=False): if self._umap is None: raise ValueError("UMAP is not initialized") - self.umap_fit(X, y) + self.umap_fit(X, y, verbose=verbose) emb = self._umap.transform(X) emb = self._bundle_embedding(emb, index=X.index) return emb - def transform_umap( # noqa: E303 - self, df: pd.DataFrame, ydf: pd.DataFrame, kind: str = "nodes" - ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: - try: - logger.debug(f"Going into Transform umap {df.shape}, {ydf.shape}") - except: - pass - x, y = self.transform(df, ydf, kind=kind) - emb = self._umap.transform(x) # type: ignore + + def transform_umap(self, df: pd.DataFrame, + y: Optional[pd.DataFrame] = None, + kind: str = 'nodes', + min_dist: Union[str, float, int] = 'auto', + n_neighbors: int = 7, + merge_policy: bool = False, + sample: Optional[int] = None, + return_graph: bool = True, + fit_umap_embedding: bool = True, + verbose: bool = False + ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: + """Transforms data into UMAP embedding + + Args: + :df: Dataframe to transform + :y: Target column + :kind: One of `nodes` or `edges` + :min_dist: Epsilon for including neighbors in infer_graph + :n_neighbors: Number of neighbors to use for contextualization + :merge_policy: if True, use previous graph, adding new batch to existing graph's neighbors + useful to contextualize new data against existing graph. If False, `sample` is irrelevant. + sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs + return_graph: Whether to return a graph or just the embeddings + fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data, default True + verbose: Whether to print information about the graph inference + """ + df, y = make_safe_gpu_dataframes(df, y, self.feature_engine) + X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) + X, y_ = make_safe_gpu_dataframes(X, y_, self.umap_engine) # type: ignore + emb = self._umap.transform(X) # type: ignore emb = self._bundle_embedding(emb, index=df.index) - return emb, x, y - + + if return_graph and kind not in ["edges"]: + emb, _ = make_safe_gpu_dataframes(emb, None, 'pandas') # for now so we don't have to touch infer_edges, force to pandas + X, y_ = make_safe_gpu_dataframes(X, y_, 'pandas') + g = self._infer_edges(emb, X, y_, df, + infer_on_umap_embedding=fit_umap_embedding, merge_policy=merge_policy, + eps=min_dist, sample=sample, n_neighbors=n_neighbors, + verbose=verbose) + return g + return emb, X, y_ + def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 - - if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): + + try: + emb.get() + import cupy as cp + emb_dtype=str(cp.get_array_module(emb)) + except AttributeError: + emb_dtype = str(getmodule(emb)) + + + if emb.shape[1] == 2 and 'cudf' not in emb_dtype and 'cupy' not in emb_dtype: emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) - elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): - emb.rename(columns={0:config.X,1: config.Y},inplace=True) + elif emb.shape[1] == 2 and 'cudf' in emb_dtype: + emb.rename(columns={0: config.X, 1: config.Y}, inplace=True) + elif emb.shape[1] == 2 and 'cupy' in emb_dtype: + import cudf + emb = cudf.DataFrame(emb, columns=[config.X, config.Y], index=index) else: columns = [config.X, config.Y] + [ - f"umap_{k}" for k in range(2, emb.shape[1] - 2) + f"umap_{k}" for k in range(2, emb.shape[1]) ] - - if 'cudf.core.dataframe' not in str(getmodule(emb)): + if 'cudf' not in emb_dtype: emb = pd.DataFrame(emb, columns=columns, index=index) - elif 'cudf.core.dataframe' in str(getmodule(emb)): - emb.columns=columns + elif 'cudf' in emb_dtype: + emb.columns = columns return emb def _process_umap( @@ -318,31 +389,41 @@ def _process_umap( kind, memoize: bool, featurize_kwargs, + verbose = False, **umap_kwargs, ): """ Returns res mutated with new _xy """ - res._umap = self._umap + #from .features import ModelDict + umap_kwargs_pure = umap_kwargs.copy() logger.debug("process_umap before kwargs: %s", umap_kwargs) umap_kwargs.update({"kind": kind, "X": X_, "y": y_}) - umap_kwargs = {**umap_kwargs, "featurize_kwargs": featurize_kwargs or {}} - logger.debug("process_umap after kwargs: %s", umap_kwargs) + umap_kwargs_reuse = {**umap_kwargs, "featurize_kwargs": featurize_kwargs or {}} + logger.debug("process_umap after kwargs: %s", umap_kwargs_reuse) old_res = reuse_umap( - res, memoize, {**umap_kwargs, "featurize_kwargs": featurize_kwargs or {}} + res, memoize, {**umap_kwargs_reuse, "featurize_kwargs": featurize_kwargs or {}} ) if old_res: + print(" --- [[ RE-USING UMAP ]]") if verbose else None logger.info(" --- [[ RE-USING UMAP ]]") + print('umap previous n_components', umap_kwargs['n_components']) if verbose else None fresh_res = copy.copy(res) for attr in ["_xy", "_weighted_edges_df", "_weighted_adjacency"]: setattr(fresh_res, attr, getattr(old_res, attr)) # have to set _raw_data attribute on umap? fresh_res._umap = old_res._umap # this saves the day! + #fresh_res._umap_initialized = True + fresh_res._umap_params = umap_kwargs_pure return fresh_res - emb = res.umap_fit_transform(X_, y_) + print('-' * 60) if verbose else None + print('** Fitting UMAP') if verbose else None + res = res.umap_lazy_init(res, verbose=verbose, **umap_kwargs_pure) + + emb = res._umap_fit_transform(X_, y_, verbose=verbose) res._xy = emb return res @@ -385,9 +466,9 @@ def _set_features( # noqa: E303 def umap( self, - kind: str = "nodes", X: XSymbolic = None, y: YSymbolic = None, + kind: str = "nodes", scale: float = 1.0, n_neighbors: int = 12, min_dist: float = 0.1, @@ -401,58 +482,72 @@ def umap( play: Optional[int] = 0, encode_position: bool = True, encode_weight: bool = True, - engine: UMAPEngine = "auto", - inplace: bool = False, + dbscan: bool = False, + umap_engine: UMAPEngine = "auto", feature_engine: str = "auto", + inplace: bool = False, memoize: bool = True, + verbose: bool = False, **featurize_kwargs, ): - """ - UMAP the featurized node or edges data, - or pass in your own X, y (optional). - - :param kind: `nodes` or `edges` or None. - If None, expects explicit X, y (optional) matrices, - and will Not associate them to nodes or edges. - If X, y (optional) is given, with kind = [nodes, edges], - it will associate new matrices to nodes or edges attributes. - :param feature_engine: How to encode data - ("none", "auto", "pandas", "dirty_cat", "torch") - :param encode_weight: if True, will set new edges_df from - implicit UMAP, default True. - :param encode_position: whether to set default plotting bindings - -- positions x,y from umap for .plot() - :param X: either an ndarray of features, or column names to featurize - :param y: either an ndarray of targets, or column names to featurize - targets - :param scale: multiplicative scale for pruning weighted edge DataFrame - gotten from UMAP, between [0, ..) with high end meaning keep - all edges - :param n_neighbors: UMAP number of nearest neighbors to include for - UMAP connectivity, lower makes more compact layouts. Minimum 2 - :param min_dist: UMAP float between 0 and 1, lower makes more compact - layouts. - :param spread: UMAP spread of values for relaxation - :param local_connectivity: UMAP connectivity parameter - :param repulsion_strength: UMAP repulsion strength - :param negative_sample_rate: UMAP negative sampling rate - :param n_components: number of components in the UMAP projection, - default 2 - :param metric: UMAP metric, default 'euclidean'. - see (UMAP-LEARN)[https://umap-learn.readthedocs.io/ - en/latest/parameters.html] documentation for more. - :param suffix: optional suffix to add to x, y attributes of umap. - :param play: Graphistry play parameter, default 0, how much to evolve - the network during clustering - :param engine: selects which engine to use to calculate UMAP: - NotImplemented yet, default UMAP-LEARN - :param memoize: whether to memoize the results of this method, - default True. + """UMAP the featurized nodes or edges data, or pass in your own X, y (optional) dataframes of values + + Example + + >>> import graphistry + >>> g = graphistry.nodes(pd.DataFrame({'node': [0,1,2], 'data': [1,2,3], 'meta': ['a', 'b', 'c']})) + >>> g2 = g.umap(n_components=3, spread=1.0, min_dist=0.1, n_neighbors=12, negative_sample_rate=5, local_connectivity=1, repulsion_strength=1.0, metric='euclidean', suffix='', play=0, encode_position=True, encode_weight=True, dbscan=False, engine='auto', feature_engine='auto', inplace=False, memoize=True, verbose=False) + >>> g2.plot() + + Parameters + + :X: either a dataframe ndarray of features, or column names to featurize + :y: either an dataframe ndarray of targets, or column names to featurize + targets + :kind: `nodes` or `edges` or None. + If None, expects explicit X, y (optional) matrices, + and will Not associate them to nodes or edges. + If X, y (optional) is given, with kind = [nodes, edges], + it will associate new matrices to nodes or edges attributes. + :scale: multiplicative scale for pruning weighted edge DataFrame + gotten from UMAP, between [0, ..) with high end meaning keep + all edges + :n_neighbors: UMAP number of nearest neighbors to include for + UMAP connectivity, lower makes more compact layouts. Minimum 2 + :min_dist: UMAP float between 0 and 1, lower makes more compact + layouts. + :spread: UMAP spread of values for relaxation + :local_connectivity: UMAP connectivity parameter + :repulsion_strength: UMAP repulsion strength + :negative_sample_rate: UMAP negative sampling rate + :n_components: number of components in the UMAP projection, + default 2 + :metric: UMAP metric, default 'euclidean'. + see (UMAP-LEARN)[https://umap-learn.readthedocs.io/ + en/latest/parameters.html] documentation for more. + :suffix: optional suffix to add to x, y attributes of umap. + :play: Graphistry play parameter, default 0, how much to evolve + the network during clustering. 0 preserves the original UMAP layout. + :encode_weight: if True, will set new edges_df from + implicit UMAP, default True. + :encode_position: whether to set default plotting bindings + -- positions x,y from umap for .plot(), default True + :dbscan: whether to run DBSCAN on the UMAP embedding, default False. + :engine: selects which engine to use to calculate UMAP: + default "auto" will use cuML if available, otherwise UMAP-LEARN. + :feature_engine: How to encode data + ("none", "auto", "pandas", "dirty_cat", "torch") + :inplace: bool = False, whether to modify the current object, default False. + when False, returns a new object, useful for chaining in a functional paradigm. + :memoize: whether to memoize the results of this method, + default True. + :verbose: whether to print out extra information, default False. + :return: self, with attributes set with new data """ - if engine == "umap_learn": + if umap_engine == UMAP_LEARN: assert_imported() - elif engine == "cuml": + elif umap_engine == CUML: assert_imported_cuml() umap_kwargs = dict( @@ -464,7 +559,8 @@ def umap( local_connectivity=local_connectivity, repulsion_strength=repulsion_strength, negative_sample_rate=negative_sample_rate, - engine=engine, + umap_engine=umap_engine, + suffix=suffix, ) logger.debug("umap_kwargs: %s", umap_kwargs) @@ -473,8 +569,7 @@ def umap( else: res = self.bind() - res.umap_lazy_init(engine=engine, suffix=suffix) - # res.suffix = suffix + res = res.umap_lazy_init(res, verbose=verbose, **umap_kwargs) # type: ignore logger.debug("umap input X :: %s", X) logger.debug("umap input y :: %s", y) @@ -482,12 +577,11 @@ def umap( featurize_kwargs = self._set_features( res, X, y, kind, feature_engine, {**featurize_kwargs, "memoize": memoize} ) - # umap_kwargs = {**umap_kwargs, - # 'featurize_kwargs': featurize_kwargs or {}} + if kind == "nodes": + index = res._nodes.index if res._node is None: - logger.debug("-Writing new node name") res = res.nodes( # type: ignore res._nodes.reset_index(drop=True) @@ -495,9 +589,9 @@ def umap( .rename(columns={"index": config.IMPLICIT_NODE_ID}), config.IMPLICIT_NODE_ID, ) + res._nodes.index = index nodes = res._nodes[res._node].values - index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) logger.debug("propagating with featurize_kwargs: %s", featurize_kwargs) ( @@ -513,18 +607,20 @@ def umap( logger.debug("data is type :: %s", (type(X_))) if isinstance(X_, pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) - elif 'cudf.core.dataframe' in str(getmodule(X_)): - index_to_nodes_dict = nodes + elif 'cudf' in str(getmodule(X_)): + index_to_nodes_dict = nodes # {}? + + # add the safe coercion here + X_, y_ = make_safe_gpu_dataframes(X_, y_, res.umap_engine) # type: ignore res = res._process_umap( - res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs + res, X_, y_, kind, memoize, featurize_kwargs, verbose, **umap_kwargs ) res._weighted_adjacency_nodes = res._weighted_adjacency if res._xy is None: raise RuntimeError("This should not happen") res._node_embedding = res._xy - # TODO add edge filter so graph doesn't have double edges # TODO user-guidable edge merge policies like upsert? res._weighted_edges_df_from_nodes = ( prune_weighted_edges_df_and_relabel_nodes( @@ -544,6 +640,9 @@ def umap( **featurize_kwargs ) + # add the safe coercion here + X_, y_ = make_safe_gpu_dataframes(X_, y_, res.umap_engine) # type: ignore + res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs ) @@ -563,9 +662,9 @@ def umap( "kind should be one of `nodes` or `edges` unless" "you are passing explicit matrices" ) - if X is not None and isinstance(X, pd.DataFrame): + if X is not None and isinstance(X, pd.DataFrame) or '': logger.info("New Matrix `X` passed in for UMAP-ing") - xy = res.umap_fit_transform(X, y) + xy = res._umap_fit_transform(X, y, verbose=verbose) res._xy = xy res._weighted_edges_df = prune_weighted_edges_df_and_relabel_nodes( res._weighted_edges_df, scale=scale @@ -577,7 +676,7 @@ def umap( else: logger.error( "If `kind` is `None`, `X` and optionally `y`" - "must be given and be of type pd.DataFrame" + "must be given." ) else: raise ValueError( @@ -587,9 +686,12 @@ def umap( res, kind, encode_position, encode_weight, play ) # noqa: E501 - if res.engine == "cuml" and is_legacy_cuml(): + if res.umap_engine == CUML and is_legacy_cuml(): # type: ignore res = res.prune_self_edges() + if dbscan: + res = res.dbscan(min_dist=min_dist, kind=kind, fit_umap_embedding=True, verbose=verbose) # type: ignore + if not inplace: return res @@ -604,23 +706,26 @@ def _bind_xy_from_umap( df = res._nodes if kind == "nodes" else res._edges df = df.copy(deep=False) - x_name = config.X + res.suffix - y_name = config.Y + res.suffix + x_name = config.X + res._suffix + y_name = config.Y + res._suffix if kind == "nodes": emb = res._node_embedding else: emb = res._edge_embedding + + if type(df) == type(emb): + df[x_name] = emb.values.T[0] + df[y_name] = emb.values.T[1] + elif isinstance(df, pd.DataFrame) and 'cudf' in str(getmodule(emb)): + df[x_name] = emb.to_numpy().T[0] + df[y_name] = emb.to_numpy().T[1] - df[x_name] = emb.values.T[0] # if embedding is greater - # than two dimensions will only take first two coordinates - df[y_name] = emb.values.T[1] - # res = res.nodes(df) if kind == "nodes" else res.edges(df) if encode_weight and kind == "nodes": # adds the implicit edge dataframe and binds it to # graphistry instance - w_name = config.WEIGHT + res.suffix + w_name = config.WEIGHT + res._suffix umap_edges_df = res._weighted_edges_df_from_nodes.copy(deep=False) umap_edges_df = umap_edges_df.rename(columns={config.WEIGHT: w_name}) res = res.edges(umap_edges_df, config.SRC, config.DST) @@ -649,6 +754,7 @@ def filter_weighted_edges( ): """ Filter edges based on _weighted_edges_df (ex: from .umap()) + """ if inplace: res = self From 5f2cb69d6cad5fe4bef1d000a80f677ff072d074 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Wed, 5 Apr 2023 17:12:41 +0530 Subject: [PATCH 309/432] more tests --- graphistry/PlotterBase.py | 10 ++ graphistry/embed_utils.py | 15 ++ graphistry/tests/.test_embed_utils.py.swp | Bin 0 -> 40960 bytes graphistry/tests/test_embed_utils.py | 183 +++++++++++++++++----- 4 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 graphistry/tests/.test_embed_utils.py.swp diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index badb060b19..3da7f9ee5d 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -1033,6 +1033,12 @@ def sample_nodes(g, n): res = base.nodes(nodes2) else: res = copy.copy(base) + # this is temporary + # TODO: for cudf support need to clean the entire codebase + try: + nodes = nodes.to_pandas() + except: + pass res._nodes = nodes # for use in text_utils.py search index if hasattr(res, 'search_index'): @@ -1141,6 +1147,10 @@ def sample_edges(g, n): res = base.edges(edges2) else: res = copy.copy(base) + try: + edges = edges.to_pandas() + except: + pass res._edges = edges return res diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index 713f7b67ea..9ab4d07d00 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -412,18 +412,33 @@ def predict_links( if source is None: src = pd.Series(all_nodes) else: + # this is temporary + try: + source = source.to_pandas() # type: ignore + except: + pass src = pd.Series(source) src = src.map(self._node2id) if relation is None: rel = pd.Series(all_relations) else: + # this is temporary + try: + relation = relation.to_pandas() # type: ignore + except: + pass rel = pd.Series(relation) rel = rel.map(self._relation2id) if destination is None: dst = pd.Series(all_nodes) else: + # this is temporary + try: + destination = destination.to_pandas() # type: ignore + except: + pass dst = pd.Series(destination) dst = dst.map(self._node2id) diff --git a/graphistry/tests/.test_embed_utils.py.swp b/graphistry/tests/.test_embed_utils.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..764bc178db5ec513d75fcb7f821f1d712ce2b5d0 GIT binary patch literal 40960 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 OYU#c~#y0 0) + self.assertIn("score", g_new._edges.columns) + + g_new = g.predict_links(source, relation, destination, threshold=1, anomalous=True) + self.assertTrue( g_new._edges.shape[0] > 0) + self.assertIn("score", g_new._edges.columns) + + @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") + def test_predict_links_all(self): + g = self.graph_no_feat.embed('rel', embedding_dim=self.d, **self.kwargs) + g_new = g.predict_links_all(threshold=0) + self.assertTrue( g_new._edges.shape[0] > 0) + self.assertIn("score", g_new._edges.columns) + + + @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") + def test_chaining(self): + for name, g in self.graphs: + logging.debug('name: %s test changing embedding dim with feats' % name) + g = g.embed('rel', use_feat=True, embedding_dim=self.d, **self.kwargs) + g2 = g.embed('rel', use_feat=True, embedding_dim=2 * self.d, **self.kwargs) + self.assertNotEqual(g._kg_embeddings.shape, g2._kg_embeddings.shape) + + [g.reset_caches() for _, g in self.graphs] + for name, g in self.graphs: + logging.debug('name: %s test changing embedding dim without feats', name) + g = g.embed('rel', use_feat=False, embedding_dim=self.d, **self.kwargs) + g2 = g.embed('rel', use_feat=False, embedding_dim=2 * self.d, **self.kwargs) + self.assertNotEqual(g._kg_embeddings.shape, g2._kg_embeddings.shape) + + [g.reset_caches() for _, g in self.graphs] + for name, g in self.graphs: + logging.debug('name: %s test relationship change', name) + g = g.embed(relation='rel', use_feat=False, embedding_dim=self.d, **self.kwargs) + g2 = g.embed(relation='src', use_feat=False, embedding_dim=self.d, **self.kwargs) + self.assertEqual(g._kg_embeddings.shape, g2._kg_embeddings.shape) + self.assertNotEqual(np.linalg.norm(g._kg_embeddings - g2._kg_embeddings), 0) + + [g.reset_caches() for _, g in self.graphs] + for name, g in self.graphs: + logging.debug('name: %s test relationship change', name) + g = g.embed(relation='rel', use_feat=False, embedding_dim=self.d, **self.kwargs) + g2 = g.embed(relation='rel', use_feat=True, embedding_dim=self.d, **self.kwargs) + self.assertEqual(g._kg_embeddings.shape, g2._kg_embeddings.shape) + self.assertNotEqual(np.linalg.norm(g._kg_embeddings - g2._kg_embeddings), 0) + + +class TestEmbedCUDF(unittest.TestCase): + + @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") + @pytest.mark.skipif(not has_cudf, reason="requires cudf") + def setUp(self): + self.edf = cudf.DataFrame([[0, 1, 0], [1, 2, 0], [2, 0, 1]], + columns=['src', 'dst', 'rel'] + ) + ndf_no_ids = cudf.DataFrame([['a'], ['a'], ['b']], columns=['feat']) + ndf_with_ids = cudf.DataFrame([[0, 'a'], [1, 'a'], [2, 'b']], + columns = ['id', 'feat1'] + ) + + self.graph_no_feat = graphistry.edges(self.edf, 'src', 'dst') + self.graph_with_feat_no_ids = self.graph_no_feat.nodes(ndf_no_ids) + self.graph_with_feat_with_ids = self.graph_no_feat.nodes(ndf_with_ids, 'id') + self.graphs = [ + ('no_feat', self.graph_no_feat), + ('with_feat_no_ids', self.graph_with_feat_no_ids), + ('with_feat_with_ids', self.graph_with_feat_with_ids) + ] + self.d = 4 + + self.kwargs = {'n_topics': 6, 'cardinality_threshold':10, 'epochs': 1, 'sample_size':10, 'num_steps':10} + + + @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") + @pytest.mark.skipif(not has_cudf, reason="requires cudf") def test_embed_out_basic(self): - for name, g in graphs: - g = g.embed('rel', embedding_dim=d, **kwargs) + for name, g in self.graphs: + g = g.embed('rel', embedding_dim=self.d, **self.kwargs) num_nodes = len(set(g._edges['src'] + g._edges['dst'])) logging.debug('name: %s basic tests', name) - self.assertEqual(g._edges.shape, edf.shape) + self.assertEqual(g._edges.shape, self.edf.shape) self.assertEqual(set(g._edges[g._relation]), set(g._edges['rel'])) - self.assertEqual(g._kg_embeddings.shape,(num_nodes, d)) + self.assertEqual(g._kg_embeddings.shape,(num_nodes, self.d)) @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") + @pytest.mark.skipif(not has_cudf, reason="requires cudf") def test_predict_links(self): source = pd.Series([0,2]) relation = None destination = pd.Series([1]) - g = graph_no_feat.embed('rel', embedding_dim=d, **kwargs) + g = self.graph_no_feat.embed('rel', embedding_dim=self.d, **self.kwargs) g_new = g.predict_links(source, relation, destination, threshold=0, anomalous=False) self.assertTrue( g_new._edges.shape[0] > 0) @@ -56,44 +166,47 @@ def test_predict_links(self): self.assertIn("score", g_new._edges.columns) @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") + @pytest.mark.skipif(not has_cudf, reason="requires cudf") def test_predict_links_all(self): - g = graph_no_feat.embed('rel', embedding_dim=d, **kwargs) + g = self.graph_no_feat.embed('rel', embedding_dim=self.d, **self.kwargs) g_new = g.predict_links_all(threshold=0) self.assertTrue( g_new._edges.shape[0] > 0) self.assertIn("score", g_new._edges.columns) @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") + @pytest.mark.skipif(not has_cudf, reason="requires cudf") def test_chaining(self): - for name, g in graphs: + for name, g in self.graphs: logging.debug('name: %s test changing embedding dim with feats' % name) - g = g.embed('rel', use_feat=True, embedding_dim=d, **kwargs) - g2 = g.embed('rel', use_feat=True, embedding_dim=2 * d, **kwargs) + g = g.embed('rel', use_feat=True, embedding_dim=self.d, **self.kwargs) + g2 = g.embed('rel', use_feat=True, embedding_dim=2 * self.d, **self.kwargs) self.assertNotEqual(g._kg_embeddings.shape, g2._kg_embeddings.shape) - [g.reset_caches() for _, g in graphs] - for name, g in graphs: + [g.reset_caches() for _, g in self.graphs] + for name, g in self.graphs: logging.debug('name: %s test changing embedding dim without feats', name) - g = g.embed('rel', use_feat=False, embedding_dim=d, **kwargs) - g2 = g.embed('rel', use_feat=False, embedding_dim=2 * d, **kwargs) + g = g.embed('rel', use_feat=False, embedding_dim=self.d, **self.kwargs) + g2 = g.embed('rel', use_feat=False, embedding_dim=2 * self.d, **self.kwargs) self.assertNotEqual(g._kg_embeddings.shape, g2._kg_embeddings.shape) - [g.reset_caches() for _, g in graphs] - for name, g in graphs: + [g.reset_caches() for _, g in self.graphs] + for name, g in self.graphs: logging.debug('name: %s test relationship change', name) - g = g.embed(relation='rel', use_feat=False, embedding_dim=d, **kwargs) - g2 = g.embed(relation='src', use_feat=False, embedding_dim=d, **kwargs) + g = g.embed(relation='rel', use_feat=False, embedding_dim=self.d, **self.kwargs) + g2 = g.embed(relation='src', use_feat=False, embedding_dim=self.d, **self.kwargs) self.assertEqual(g._kg_embeddings.shape, g2._kg_embeddings.shape) self.assertNotEqual(np.linalg.norm(g._kg_embeddings - g2._kg_embeddings), 0) - [g.reset_caches() for _, g in graphs] - for name, g in graphs: + [g.reset_caches() for _, g in self.graphs] + for name, g in self.graphs: logging.debug('name: %s test relationship change', name) - g = g.embed(relation='rel', use_feat=False, embedding_dim=d, **kwargs) - g2 = g.embed(relation='rel', use_feat=True, embedding_dim=d, **kwargs) + g = g.embed(relation='rel', use_feat=False, embedding_dim=self.d, **self.kwargs) + g2 = g.embed(relation='rel', use_feat=True, embedding_dim=self.d, **self.kwargs) self.assertEqual(g._kg_embeddings.shape, g2._kg_embeddings.shape) self.assertNotEqual(np.linalg.norm(g._kg_embeddings - g2._kg_embeddings), 0) + if __name__ == "__main__": unittest.main() From 408ee529a73cd224dbe4d909cd81455d16b1a205 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Wed, 5 Apr 2023 17:52:18 +0530 Subject: [PATCH 310/432] stable --- graphistry/.feature_utils.py.swp | Bin 0 -> 122880 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 graphistry/.feature_utils.py.swp diff --git a/graphistry/.feature_utils.py.swp b/graphistry/.feature_utils.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..8630c9d6d5aeda58613abec612924aa8158e1cd4 GIT binary patch literal 122880 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 literal 0 HcmV?d00001 From 762b2d22756f59fbb11d0d940f91cc8cea5d7579 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 5 Apr 2023 08:39:39 -0400 Subject: [PATCH 311/432] fix(modules.rst) testing to ci warnings --- docs/source/modules.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 71e0a12335..1f80292530 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -1,9 +1,9 @@ -.. doc -.. === +FIXME +=== -.. .. toctree:: -.. :maxdepth: 4 -.. :caption: Contents: +toctree:: + :maxdepth: 4 + :caption: Contents: -.. versioneer + versioneer From e0abb9591bbe130bf3b70aae50677fcfcb44c312 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 5 Apr 2023 08:47:00 -0400 Subject: [PATCH 312/432] fix(conf.py); added plugins to nitpick --- docs/source/conf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 5797134b6a..f8b7c2190b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -55,6 +55,11 @@ ('py:class', 'graphistry.compute.conditional.ConditionalMixin'), ('py:class', 'graphistry.compute.cluster.ClusterMixin'), ('py:class', 'graphistry.Plottable.Plottable'), + ('py:class', 'graphistry.plugins.cugraph.compute_cugraph'), + ('py:class', 'graphistry.plugins.cugraph.layout_cugraph'), + ('py:class', 'graphistry.plugins.igraph.compute_igraph'), + ('py:class', 'graphistry.plugins.igraph.from_igraph'), + ('py:class', 'graphistry.plugins.igraph.layout_igraph'), ('py:class', 'graphistry.feature_utils.FeatureMixin'), ('py:class', 'graphistry.dgl_utils.DGLGraphMixin'), ('py:class', 'graphistry.umap_utils.UMAPMixin'), From 6ebb74a8f1ee9b2914593a3eff46c575699b3b66 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 5 Apr 2023 08:57:34 -0400 Subject: [PATCH 313/432] fix(modules): added title --- docs/source/modules.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 1f80292530..bec536f2f1 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -1,5 +1,5 @@ -FIXME -=== +# Modules +================================== toctree:: :maxdepth: 4 From 63d665b208c396f87d233e95d01669d6e0ffa7f7 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 5 Apr 2023 08:59:29 -0400 Subject: [PATCH 314/432] fix(modules.rst): added title for ci testing --- docs/source/modules.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/modules.rst b/docs/source/modules.rst index bec536f2f1..2649d02fc2 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -1,5 +1,5 @@ -# Modules -================================== +Modules +##################### toctree:: :maxdepth: 4 From 18e0befc533372791a05b4aee9281a0e90363994 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Wed, 5 Apr 2023 20:57:43 +0530 Subject: [PATCH 315/432] doc fix --- graphistry/umap_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 019eefef3c..79a10dcfa2 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -329,6 +329,7 @@ def transform_umap(self, df: pd.DataFrame, :n_neighbors: Number of neighbors to use for contextualization :merge_policy: if True, use previous graph, adding new batch to existing graph's neighbors useful to contextualize new data against existing graph. If False, `sample` is irrelevant. + sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs return_graph: Whether to return a graph or just the embeddings fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data, default True From 6a5bec9fbb286923122b3f539b91bacb64e59e81 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 5 Apr 2023 11:51:46 -0400 Subject: [PATCH 316/432] fix(rst) docs fixes for CI passing --- docs/source/conf.py | 2 +- docs/source/graphistry.compute.rst | 9 +- docs/source/graphistry.layout.gib.rst | 6 + docs/source/graphistry.layout.graph.rst | 4 + docs/source/graphistry.layout.rst | 7 + docs/source/graphistry.layout.sugiyama.rst | 5 + docs/source/graphistry.layout.utils.rst | 5 + docs/source/graphistry.plugins_types.rst | 6 + docs/source/graphistry.rst | 4 +- docs/source/modules.rst | 2 +- docs/source/versioneer.rst | 4 + graphistry/PlotterBase.py | 33 ++--- graphistry/compute/cluster.py | 19 +-- graphistry/compute/collapse.py | 141 ++++++++------------- graphistry/compute/conditional.py | 2 +- graphistry/feature_utils.py | 78 ++++-------- graphistry/umap_utils.py | 9 +- 17 files changed, 142 insertions(+), 194 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index f8b7c2190b..c7bbd195fc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -56,7 +56,7 @@ ('py:class', 'graphistry.compute.cluster.ClusterMixin'), ('py:class', 'graphistry.Plottable.Plottable'), ('py:class', 'graphistry.plugins.cugraph.compute_cugraph'), - ('py:class', 'graphistry.plugins.cugraph.layout_cugraph'), + ('py:class', 'graphistry.plugins.cugraph.from_cugraph'), ('py:class', 'graphistry.plugins.igraph.compute_igraph'), ('py:class', 'graphistry.plugins.igraph.from_igraph'), ('py:class', 'graphistry.plugins.igraph.layout_igraph'), diff --git a/docs/source/graphistry.compute.rst b/docs/source/graphistry.compute.rst index 08b49eedef..c610034aab 100644 --- a/docs/source/graphistry.compute.rst +++ b/docs/source/graphistry.compute.rst @@ -5,6 +5,7 @@ ComputeMixin module :members: :undoc-members: :show-inheritance: + :noindex: Chain @@ -14,6 +15,7 @@ Chain :members: :undoc-members: :show-inheritance: + :noindex: Cluster --------------- @@ -21,6 +23,7 @@ Cluster :members: :undoc-members: :show-inheritance: + :noindex: Collapse --------------- @@ -28,6 +31,7 @@ Collapse :members: :undoc-members: :show-inheritance: + :noindex: Conditional --------------- @@ -35,13 +39,15 @@ Conditional :members: :undoc-members: :show-inheritance: + :noindex: Filter by Dictionary ---------------- +-------------------- .. automodule:: graphistry.compute.filter_by_dict :members: :undoc-members: :show-inheritance: + :noindex: Hop --------------- @@ -49,3 +55,4 @@ Hop :members: :undoc-members: :show-inheritance: + :noindex: diff --git a/docs/source/graphistry.layout.gib.rst b/docs/source/graphistry.layout.gib.rst index 51352d8212..50b21ec335 100644 --- a/docs/source/graphistry.layout.gib.rst +++ b/docs/source/graphistry.layout.gib.rst @@ -1,3 +1,7 @@ +:orphan: + +.. ^ FIXME + graphistry.layout.gib package ================================== @@ -11,3 +15,5 @@ Module contents :members: :undoc-members: :show-inheritance: + + diff --git a/docs/source/graphistry.layout.graph.rst b/docs/source/graphistry.layout.graph.rst index 72d559ad11..283119ece6 100644 --- a/docs/source/graphistry.layout.graph.rst +++ b/docs/source/graphistry.layout.graph.rst @@ -59,3 +59,7 @@ Module contents :members: :undoc-members: :show-inheritance: + +graphistry.layout.gib + + diff --git a/docs/source/graphistry.layout.rst b/docs/source/graphistry.layout.rst index 9216de5252..b2c1f8c43e 100644 --- a/docs/source/graphistry.layout.rst +++ b/docs/source/graphistry.layout.rst @@ -7,6 +7,7 @@ edge Module :members: :undoc-members: :show-inheritance: + :noindex: edgeBase Module --------------------------------------- @@ -15,6 +16,7 @@ edgeBase Module :members: :undoc-members: :show-inheritance: + :noindex: graph Module ------------------------------------ @@ -23,6 +25,7 @@ graph Module :members: :undoc-members: :show-inheritance: + :noindex: graphBase Module ---------------------------------------- @@ -31,6 +34,7 @@ graphBase Module :members: :undoc-members: :show-inheritance: + :noindex: vertex Module ------------------------------------- @@ -39,6 +43,7 @@ vertex Module :members: :undoc-members: :show-inheritance: + :noindex: vertexBase Module ----------------------------------------- @@ -47,6 +52,7 @@ vertexBase Module :members: :undoc-members: :show-inheritance: + :noindex: Module contents @@ -56,3 +62,4 @@ Module contents :members: :undoc-members: :show-inheritance: + :noindex: diff --git a/docs/source/graphistry.layout.sugiyama.rst b/docs/source/graphistry.layout.sugiyama.rst index 41b83f7cb1..40ffaf7e83 100644 --- a/docs/source/graphistry.layout.sugiyama.rst +++ b/docs/source/graphistry.layout.sugiyama.rst @@ -1,3 +1,5 @@ +:orphan: + graphistry.layout.sugiyama package ================================== @@ -19,3 +21,6 @@ Module contents :members: :undoc-members: :show-inheritance: + + +.. FIXME:orphan \ No newline at end of file diff --git a/docs/source/graphistry.layout.utils.rst b/docs/source/graphistry.layout.utils.rst index de1d80140d..76b71fdc52 100644 --- a/docs/source/graphistry.layout.utils.rst +++ b/docs/source/graphistry.layout.utils.rst @@ -1,6 +1,11 @@ graphistry.layout.utils package =============================== +.. toctree:: + :maxdepth: 2 + + graphistry.layout.graph + Submodules ---------- diff --git a/docs/source/graphistry.plugins_types.rst b/docs/source/graphistry.plugins_types.rst index 2b9b21ee1c..1b07a10b54 100644 --- a/docs/source/graphistry.plugins_types.rst +++ b/docs/source/graphistry.plugins_types.rst @@ -1,6 +1,12 @@ graphistry.plugins\_types package ================================= + +.. toctree:: + :maxdepth: 2 + + graphistry.layout.utils + Submodules ---------- diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index d115114c70..030071b943 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -64,7 +64,7 @@ DBScan :show-inheritance: Arrow uploader Module -================== +============================ .. automodule:: graphistry.arrow_uploader :members: @@ -72,7 +72,7 @@ Arrow uploader Module :show-inheritance: Arrow File Uploader Module -================== +============================ .. automodule:: graphistry.ArrowFileUploader :members: diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 2649d02fc2..ced1d0941f 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -1,7 +1,7 @@ Modules ##################### -toctree:: +.. toctree:: :maxdepth: 4 :caption: Contents: diff --git a/docs/source/versioneer.rst b/docs/source/versioneer.rst index a34edfc48d..ab6065f6a3 100644 --- a/docs/source/versioneer.rst +++ b/docs/source/versioneer.rst @@ -1,2 +1,6 @@ .. versioneer module .. ================= +.. toctree:: + :maxdepth: 2 + + graphistry.plugins_types \ No newline at end of file diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index badb060b19..747f7e74b6 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -342,17 +342,10 @@ def style(self, fg=None, bg=None, page=None, logo=None): def encode_axis(self, rows=[]): """Render radial and linear axes with optional labels - :param rows: List of rows - { - label: Optional[str], - ?r: float, - ?x: float, - ?y: float, - ?internal: true, - ?external: true, - ?space: true - } + :param rows: List of rows - { label: Optional[str],?r: float, ?x: float, ?y: float, ?internal: true, ?external: true, ?space: true } :returns: Plotter + :rtype: Plotter **Example: Several radial axes** @@ -545,9 +538,7 @@ def encode_point_icon(self, column, comparator=None, for_default=True, for_current=False, as_text=False, blend_mode=None, style=None, border=None, shape=None): - """Set node icon with more control than bind(). - Values from Font Awesome 4 such as "laptop": https://fontawesome.com/v4.7.0/icons/ , image URLs (http://...), and data URIs (data:...). - When as_text=True is enabled, values are instead interpreted as raw strings. + """Set node icon with more control than bind(). Values from Font Awesome 4 such as "laptop": https://fontawesome.com/v4.7.0/icons/ , image URLs (http://...), and data URIs (data:...). When as_text=True is enabled, values are instead interpreted as raw strings. :param column: Data column name :type column: str @@ -614,9 +605,7 @@ def encode_edge_icon(self, column, comparator=None, for_default=True, for_current=False, as_text=False, blend_mode=None, style=None, border=None, shape=None): - """Set edge icon with more control than bind() - Values from Font Awesome 4 such as "laptop": https://fontawesome.com/v4.7.0/icons/ , image URLs (http://...), and data URIs (data:...). - When as_text=True is enabled, values are instead interpreted as raw strings. + """Set edge icon with more control than bind() Values from Font Awesome 4 such as "laptop": https://fontawesome.com/v4.7.0/icons/ , image URLs (http://...), and data URIs (data:...). When as_text=True is enabled, values are instead interpreted as raw strings. :param column: Data column name :type column: str @@ -836,10 +825,7 @@ def bind(self, source=None, destination=None, node=None, edge=None, edge_source_color=None, edge_destination_color=None, point_title=None, point_label=None, point_color=None, point_weight=None, point_size=None, point_opacity=None, point_icon=None, point_x=None, point_y=None): - """Relate data attributes to graph structure and visual representation. - - To facilitate reuse and replayable notebooks, the binding call is chainable. Invocation does not effect the old binding: it instead returns a new Plotter instance with the new bindings added to the existing ones. Both the old and new bindings can then be used for different graphs. - + """Relate data attributes to graph structure and visual representation. To facilitate reuse and replayable notebooks, the binding call is chainable. Invocation does not effect the old binding: it instead returns a new Plotter instance with the new bindings added to the existing ones. Both the old and new bindings can then be used for different graphs. :param source: Attribute containing an edge's source ID :type source: str @@ -853,8 +839,7 @@ def bind(self, source=None, destination=None, node=None, edge=None, :param edge: Attribute containing an edge's ID :type edge: str - :param edge_title: Attribute overriding edge's minimized label text. - By default, the edge source and destination is used. + :param edge_title: Attribute overriding edge's minimized label text. By default, the edge source and destination is used. :type edge_title: str :param edge_label: Attribute overriding edge's expanded label text. By default, scrollable list of attribute/value mappings. @@ -894,6 +879,7 @@ def bind(self, source=None, destination=None, node=None, edge=None, :rtype: Plotter **Example: Minimal** + :: import graphistry @@ -901,6 +887,7 @@ def bind(self, source=None, destination=None, node=None, edge=None, g = g.bind(source='src', destination='dst') **Example: Node colors** + :: import graphistry @@ -909,6 +896,7 @@ def bind(self, source=None, destination=None, node=None, edge=None, node='id', point_color='color') **Example: Chaining** + :: import graphistry @@ -925,6 +913,7 @@ def bind(self, source=None, destination=None, node=None, edge=None, g3b = g2b.bind(point_size='size3b') In the above **Chaining** example, all bindings use src/dst/id. Colors and sizes bind to: + :: g: default/default @@ -933,8 +922,6 @@ def bind(self, source=None, destination=None, node=None, edge=None, g2b: color2b/size2b g3a: color2a/size3a g3b: color2b/size3b - - """ res = copy.copy(self) res._source = source or self._source diff --git a/graphistry/compute/cluster.py b/graphistry/compute/cluster.py index f19fbfbe38..9b319bfda0 100644 --- a/graphistry/compute/cluster.py +++ b/graphistry/compute/cluster.py @@ -168,8 +168,7 @@ def __init__(self, *args, **kwargs): def _cluster_dbscan( self, res, kind, cols, fit_umap_embedding, target, min_dist, min_samples, verbose, *args, **kwargs ): - """ - DBSCAN clustering on cpu or gpu infered by .engine flag + """DBSCAN clustering on cpu or gpu infered by .engine flag """ _, DBSCAN, _, cuDBSCAN = lazy_dbscan_import_has_dependency() @@ -241,18 +240,14 @@ def dbscan( g2.plot() # color by `_dbscan` column Useful: - Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, - as well as assessing metrics per cluster, e.g. - https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb + Enriching the graph with cluster labels from UMAP is useful for visualizing clusters in the graph by color, size, etc, as well as assessing metrics per cluster, e.g. https://github.com/graphistry/pygraphistry/blob/master/demos/ai/cyber/cyber-redteam-umap-demo.ipynb Args: :min_dist float: The maximum distance between two samples for them to be considered as in the same neighborhood. :kind str: 'nodes' or 'edges' - :cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features or targets by - fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] + :cols: list of columns to use for clustering given `g.featurize` has been run, nice way to slice features or targets by fragments of interest, e.g. ['ip_172', 'location', 'ssh', 'warnings'] :fit_umap_embedding bool: whether to use UMAP embeddings or features dataframe to cluster DBSCAN - :min_samples: The number of samples in a neighborhood for a point to be considered as a core point. - This includes the point itself. + :min_samples: The number of samples in a neighborhood for a point to be considered as a core point. This includes the point itself. :target: whether to use the target column as the clustering feature """ @@ -328,11 +323,7 @@ def transform_dbscan( return_graph: bool = True, verbose: bool = False, ): # type: ignore - """ - Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster - labels on the minibatch and generates a graph with the minibatch and the original graph, with edges - between the minibatch and the original graph inferred from the umap embedding or features dataframe. - Graph nodes | edges will be colored by '_dbscan' column. + """Transforms a minibatch dataframe to one with a new column '_dbscan' containing the DBSCAN cluster labels on the minibatch and generates a graph with the minibatch and the original graph, with edges between the minibatch and the original graph inferred from the umap embedding or features dataframe. Graph nodes | edges will be colored by '_dbscan' column. Examples: :: diff --git a/graphistry/compute/collapse.py b/graphistry/compute/collapse.py index ddf0885805..e9b06e512c 100644 --- a/graphistry/compute/collapse.py +++ b/graphistry/compute/collapse.py @@ -32,15 +32,15 @@ def unpack(g: Plottable): - """ - Helper method that unpacks graphistry instance + """Helper method that unpacks graphistry instance + ex: - ndf, edf, src, dst, node = unpack(g) - ----------------------------------------------------------------------------------------- + ndf, edf, src, dst, node = unpack(g) :param g: graphistry instance - :returns node DataFrame, edge DataFrame, source column, destination column, node column + + :returns: node DataFrame, edge DataFrame, source column, destination column, node column """ ndf = g._nodes edf = g._edges @@ -51,10 +51,7 @@ def unpack(g: Plottable): def get_children(g: Plottable, node_id: Union[str, int], hops: int = 1): - """ - Helper that gets children at k-hops from node `node_id` - - ------------------------------------------------------------------ + """Helper that gets children at k-hops from node `node_id` :returns graphistry instance of hops """ @@ -65,17 +62,14 @@ def get_children(g: Plottable, node_id: Union[str, int], hops: int = 1): def has_edge( g: Plottable, n1: Union[str, int], n2: Union[str, int], directed: bool = True ) -> bool: - """ - Checks if `n1` and `n2` share an (directed or not) edge - - ------------------------------------------------------------------ + """Checks if `n1` and `n2` share an (directed or not) edge :param g: graphistry instance :param n1: `node` to check if has edge to `n2` :param n2: `node` to check if has edge to `n1` :param directed: bool, if True, checks only outgoing edges from `n1`->`n2`, else finds undirected edges - :returns bool, if edge exists between `n1` and `n2` + :returns: bool, if edge exists between `n1` and `n2` """ ndf, edf, src, dst, node = unpack(g) if directed: @@ -92,16 +86,14 @@ def has_edge( def get_edges_of_node( g: Plottable, node_id: Union[str, int], outgoing_edges: bool = True, hops: int = 1 ): - """ - Gets edges of node at k-hops from node - - ---------------------------------------------------------------------------------- + """Gets edges of node at k-hops from node :param g: graphistry instance :param node_id: `node` to find edges from :param outgoing_edges: bool, if true, finds all outgoing edges of `node`, default True :param hops: the number of hops from `node` to take, default = 1 - :returns DataFrame of edges + + :returns: DataFrame of edges """ _, _, src, dst, _ = unpack(g) g2 = get_children(g, node_id, hops=hops) @@ -119,11 +111,7 @@ def get_edges_in_out_cluster( column: Union[str, int], directed: bool = True, ): - """ - Traverses children of `node_id` and separates them into incluster and outcluster sets depending if they have - `attribute` in node DataFrame `column` - - -------------------------------------------------------------------------------------------------------------------- + """Traverses children of `node_id` and separates them into incluster and outcluster sets depending if they have `attribute` in node DataFrame `column` :param g: graphistry instance :param node_id: `node` with `attribute` in `column` @@ -157,67 +145,57 @@ def get_edges_in_out_cluster( def get_cluster_store_keys(ndf: pd.DataFrame, node: Union[str, int]): - """ - Main innovation in finding and adding to super node. - Checks if node is a segment in any collapse_node in COLLAPSE column of nodes DataFrame - - -------------------------------------------------------------------------------------------- + """Main innovation in finding and adding to super node. Checks if node is a segment in any collapse_node in COLLAPSE column of nodes DataFrame :param ndf: node DataFrame :param node: node to find - :returns DataFrame of bools of where `wrap_key(node)` exists in COLLAPSE column + + :returns: DataFrame of bools of where `wrap_key(node)` exists in COLLAPSE column """ node = wrap_key(node) return ndf[COLLAPSE_NODE].astype(str).str.contains(node, na=False) def in_cluster_store_keys(ndf: pd.DataFrame, node: Union[str, int]) -> bool: - """ - checks if node is in collapse_node in COLLAPSE column of nodes DataFrame - - ------------------------------------------------------------------------------ + """checks if node is in collapse_node in COLLAPSE column of nodes DataFrame :param ndf: nodes DataFrame :param node: node to find - :returns bool + + :returns: bool """ return any(get_cluster_store_keys(ndf, node)) def reduce_key(key: Union[str, int]) -> str: - """ - Takes "1 1 2 1 2 3" -> "1 2 3 - - --------------------------------------------------- + """Takes "1 1 2 1 2 3" -> "1 2 3 :param key: node name - :returns new node name with duplicates removed + + :returns: new node name with duplicates removed """ uniques = " ".join(np.unique(str(key).split())) return uniques def unwrap_key(name: Union[str, int]) -> str: - """ - Unwraps node name: ~name~ -> name - - ---------------------------------------- + """Unwraps node name: ~name~ -> name :param name: node to unwrap - :returns unwrapped node name + + :returns: unwrapped node name """ return str(name).replace(WRAP, "") def wrap_key(name: Union[str, int]) -> str: - """ - Wraps node name -> ~name~ - - ----------------------------------- + """Wraps node name -> ~name~ :param name: node name - :returns wrapped node name + + :returns: wrapped node name """ + name = str(name) if WRAP in name: # idempotency return name @@ -225,17 +203,16 @@ def wrap_key(name: Union[str, int]) -> str: def melt(ndf: pd.DataFrame, node: Union[str, int]) -> str: - """ - Reduces node if in cluster store, otherwise passes it through. + """Reduces node if in cluster store, otherwise passes it through. ex: + node = "4" will take any sequence from get_cluster_store_keys, "1 2 3", "4 3 6" and returns "1 2 3 4 6" when they have a common entry (3). - ------------------------------------------------------------------------------------------------------------- - :param ndf, node DataFrame :param node: node to melt :returns new_parent_name of super node + """ rdf = ndf[get_cluster_store_keys(ndf, node)] topkey = wrap_key(node) @@ -259,14 +236,12 @@ def check_has_set(ndf, parent, child): def get_new_node_name( ndf: pd.DataFrame, parent: Union[str, int], child: Union[str, int] ) -> str: - """ - If child in cluster group, melts name, else makes new parent_name from parent, child - - --------------------------------------------------------------------------------------------------------- + """If child in cluster group, melts name, else makes new parent_name from parent, child :param ndf: node DataFrame :param parent: `node` with `attribute` in `column` :param child: `node` with `attribute` in `column` + :returns new_parent_name """ # THIS IS IMPORTANT FUNCTION -- it is where we wrap the parent/child in WRAP @@ -300,8 +275,6 @@ def collapse_nodes_and_edges( # outside logic controls when that is the case # for example, it assumes parent is already in cluster keys of COLLAPSE node - --------------------------------------------------------------------------------------- - :param g: graphistry instance :param parent: `node` with `attribute` in `column` :param child: `node` with `attribute` in `column` @@ -328,29 +301,24 @@ def collapse_nodes_and_edges( def has_property( g: Plottable, ref_node: Union[str, int], attribute: Union[str, int], column: Union[str, int] ) -> bool: - """ - Checks if ref_node is in node dataframe in column with attribute - - ------------------------------------------------------------------------- - + """Checks if ref_node is in node dataframe in column with attribute :param attribute: :param column: :param g: graphistry instance :param ref_node: `node` to check if it as `attribute` in `column` - :returns bool""" + + :returns: bool + """ ndf, edf, src, dst, node = unpack(g) ref_node = unwrap_key(ref_node) return ref_node in ndf[ndf[column] == attribute][node].values def check_default_columns_present_and_coerce_to_string(g: Plottable): - """ - Helper to set COLLAPSE columns to nodes and edges dataframe, while converting src, dst, node to dtype(str) - - ------------------------------------------------------------------------- - + """Helper to set COLLAPSE columns to nodes and edges dataframe, while converting src, dst, node to dtype(str) :param g: graphistry instance - :returns graphistry instance + + :returns: graphistry instance """ ndf, edf, src, dst, node = unpack(g) if COLLAPSE_NODE not in ndf.columns: @@ -376,32 +344,26 @@ def collapse_algo( column: Union[str, int], seen: dict, ): - """ - Basically candy crush over graph properties in a topology aware manner + """Basically candy crush over graph properties in a topology aware manner - Checks to see if child node has desired property from parent, we will need to check if - (start_node=parent: has_attribute , children nodes: has_attribute) by case - (T, T), (F, T), (T, F) and (F, F), - we start recursive collapse (or not) on the children, reassigning nodes and edges. + Checks to see if child node has desired property from parent, we will need to check if (start_node=parent: has_attribute , children nodes: has_attribute) by case (T, T), (F, T), (T, F) and (F, F),we start recursive collapse (or not) on the children, reassigning nodes and edges. if (T, T), append children nodes to start_node, re-assign the name of the node, and update the edge table with new name, - if (F, T) start k-(potentially new) super nodes, with k the number of children of start_node. - Start node keeps k outgoing edges. + if (F, T) start k-(potentially new) super nodes, with k the number of children of start_node. Start node keeps k outgoing edges. if (T, F) it is the end of the cluster, and we keep new node as is; keep going if (F, F); keep going - - -------------------------------------------------------------------------------------------------------------------- - + :param seen: :param g: graphistry instance :param child: child node to start traversal, for first traversal, set child=parent or vice versa. :param parent: parent node to start traversal, in main call, this is set to child. :param attribute: attribute to collapse by :param column: column in nodes dataframe to collapse over. - :returns graphistry instance with collapsed nodes. + + :returns: graphistry instance with collapsed nodes. """ compute_key = f"{parent} {child}" @@ -456,16 +418,13 @@ def normalize_graph( self_edges: bool = False, unwrap: bool = False ) -> Plottable: - """ - Final step after collapse traversals are done, removes duplicates and moves COLLAPSE columns into respective - (node, src, dst) columns of node, edges dataframe from Graphistry instance g. - - -------------------------------------------------------------------------------------------------------------------- + """Final step after collapse traversals are done, removes duplicates and moves COLLAPSE columns into respective(node, src, dst) columns of node, edges dataframe from Graphistry instance g. :param g: graphistry instance :param self_edges: bool, whether to keep duplicates from ndf, edf, default False :param unwrap: bool, whether to unwrap node text with `~`, default True - :returns final graphistry instance + + :returns: final graphistry instance """ ndf, edf, src, dst, node = unpack(g) @@ -527,7 +486,6 @@ def collapse_by( """ Main call in collapse.py, collapses nodes and edges by attribute, and returns normalized graphistry object. - -------------------------------------------------------------------------------------------------------------------- :param self: graphistry instance :param parent: parent node to start traversal, in main call, this is set to child. :param start_node: @@ -535,6 +493,7 @@ def collapse_by( :param column: column in nodes dataframe to collapse over. :param seen: dict of previously collapsed pairs -- {n1, n2) is seen as different from (n2, n1) :param verbose: bool, default True + :returns graphistry instance with collapsed and normalized nodes. """ from time import time diff --git a/graphistry/compute/conditional.py b/graphistry/compute/conditional.py index 101eee9829..df96c1c31f 100644 --- a/graphistry/compute/conditional.py +++ b/graphistry/compute/conditional.py @@ -66,7 +66,7 @@ def conditional_graph(self, x, given, kind='nodes', *args, **kwargs): Useful for finding the conditional probability of a node or edge attribute returned dataframe sums to 1 on each column - ----------------------------------------------------------- + :param x: target column :param given: the dependent column :param kind: 'nodes' or 'edges' diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index ed5ee15e62..42b6b06476 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -274,7 +274,6 @@ def remove_internal_namespace_if_present(df: pd.DataFrame): Some tranformations below add columns to the DataFrame, this method removes them before featurization Will not drop if suffix is added during UMAP-ing - ______________________________________________________________ :param df: DataFrame :return: DataFrame with dropped columns in reserved namespace @@ -528,10 +527,8 @@ def get_preprocessing_pipeline( encode: str = "ordinal", strategy: str = "quantile", ) -> Pipeline: # noqa - """ - Helper function for imputing and scaling np.ndarray data - using different scaling transformers. - ----------------------------------------------------------------- + """Helper function for imputing and scaling np.ndarray data using different scaling transformers. + :param X: np.ndarray :param impute: whether to run imputing or not :param use_scaler: string in None or @@ -613,12 +610,7 @@ def get_preprocessing_pipeline( def fit_pipeline( X: pd.DataFrame, transformer, keep_n_decimals: int = 5 ) -> pd.DataFrame: - """ - Helper to fit DataFrame over transformer pipeline. - Rounds resulting matrix X by keep_n_digits if not 0, - which helps for when transformer pipeline is scaling or imputer - which sometime introduce small negative numbers, - and umap metrics like Hellinger need to be positive + """Helper to fit DataFrame over transformer pipeline. Rounds resulting matrix X by keep_n_digits if not 0, which helps for when transformer pipeline is scaling or imputer which sometime introduce small negative numbers, and umap metrics like Hellinger need to be positive :param X: DataFrame to transform. :param transformer: Pipeline object to fit and transform :param keep_n_decimals: Int of how many decimal places to keep in rounded transformed data @@ -864,8 +856,7 @@ def process_dirty_dataframes( """ Dirty_Cat encoder for record level data. Will automatically turn inhomogeneous dataframe into matrix using smart conversion tricks. - ______________________________________________________________________ - + :param ndf: node DataFrame :param y: target DataFrame or series :param cardinality_threshold: For ndf columns, below this threshold, @@ -1026,10 +1017,7 @@ def process_nodes_dataframes( Any, List[str], ]: - """ - Automatic Deep Learning Embedding/ngrams of Textual Features, - with the rest of the columns taken care of by dirty_cat - _________________________________________________________________________ + """Automatic Deep Learning Embedding/ngrams of Textual Features, with the rest of the columns taken care of by dirty_cat :param df: pandas DataFrame of data :param y: pandas DataFrame of targets @@ -1048,6 +1036,7 @@ def process_nodes_dataframes( :param model_name: SentenceTransformer model name. See available list at https://www.sbert.net/docs/pretrained_models. html#sentence-embedding-models + :return: X_enc, y_enc, data_encoder, label_encoder, scaling_pipeline, scaling_pipeline_target, @@ -1239,10 +1228,9 @@ def encode_edges(edf, src, dst, mlb, fit=False): src (string): source column dst (string): destination column mlb (sklearn): multilabelBinarizer - fit (bool, optional): If true, fits multilabelBinarizer. - Defaults to False. - Returns: - tuple: pd.DataFrame, multilabelBinarizer + fit (bool, optional): If true, fits multilabelBinarizer. Defaults to False. + + :Returns: tuple: pd.DataFrame, multilabelBinarizer """ # uses mlb with fit=T/F so we can use it in transform mode # to recreate edge feature concat definition @@ -1318,8 +1306,8 @@ def process_edge_dataframes( :param dst: destination column to select in edf :param use_scaler: None or string in ['minmax', 'standard', 'robust', 'quantile'] - :return: Encoded data matrix and target (if not None), - the data encoders, and the label encoder. + + :return: Encoded data matrix and target (if not None), the data encoders, and the label encoder. """ lazy_import_has_min_dependancy() from sklearn.preprocessing import ( @@ -1763,11 +1751,11 @@ def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): args: :: - ;X: pd.DataFrame of features :y: pd.DataFrame of target features :kind: str, one of 'nodes' or 'edges' *args, **kwargs: passed to smart_scaler pipeline + returns: scaled X, y """ @@ -1790,9 +1778,7 @@ def scale(self, X=None, y=None, return_pipeline=False, *args, **kwargs): def prune_weighted_edges_df_and_relabel_nodes( wdf: pd.DataFrame, scale: float = 0.1, index_to_nodes_dict: Optional[Dict] = None ) -> pd.DataFrame: - """ - Prune the weighted edge DataFrame so to return high - fidelity similarity scores. + """Prune the weighted edge DataFrame so to return high fidelity similarity scores. :param wdf: weighted edge DataFrame gotten via UMAP :param scale: lower values means less edges > (max - scale * std) @@ -1869,9 +1855,7 @@ def get_matrix_by_column_parts(X: pd.DataFrame, column_parts: Optional[Union[lis class FeatureMixin(MIXIN_BASE): - """ - FeatureMixin for automatic featurization of nodes and edges DataFrames. - Subclasses UMAPMixin for umap-ing of automatic features. + """FeatureMixin for automatic featurization of nodes and edges DataFrames. Subclasses UMAPMixin for umap-ing of automatic features. Usage: :: @@ -2214,6 +2198,7 @@ def transform(self, df: pd.DataFrame, :n_neighbors: int, if return_graph is True, will use this value for n_neighbors in Nearest Neighbors search :scaled: bool, if True, will use scaled transformation of data set during featurization, default True :verbose: bool, if True, will print metadata about the graph construction, default False + **Returns:** X, y: pd.DataFrame, transformed data if return_graph is False @@ -2403,7 +2388,6 @@ def featurize( ): r"""Featurize Nodes or Edges of the underlying nodes/edges DataFrames. - :param kind: specify whether to featurize `nodes` or `edges`. Edge featurization includes a pairwise src-to-dst feature block using a MultiLabelBinarizer, @@ -2623,12 +2607,7 @@ def _featurize_or_get_nodes_dataframe_if_X_is_None( memoize: bool = True, verbose: bool = False, ) -> Tuple[pd.DataFrame, pd.DataFrame, MIXIN_BASE]: - """ - helper method gets node feature and target matrix if X, y - are not specified. - if X, y are specified will set them as `_node_target` and - `_node_target` attributes - ----------------------------------------------------------- + """helper method gets node feature and target matrix if X, y are not specified. if X, y are specified will set them as `_node_target` and `_node_target` attributes """ res = self.bind() @@ -2717,10 +2696,8 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( memoize: bool = True, verbose: bool = False, ) -> Tuple[pd.DataFrame, Optional[pd.DataFrame], MIXIN_BASE]: - """ - helper method gets edge feature and target matrix if X, y - are not specified - ----------------------------------------------------------- + """ helper method gets edge feature and target matrix if X, y are not specified + :param X: Data Matrix :param y: target, default None :return: data `X` and `y` @@ -2778,18 +2755,7 @@ def _featurize_or_get_edges_dataframe_if_X_is_None( def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'nodes', target: bool = False) -> pd.DataFrame: - """ - Returns feature matrix, and if columns are specified, returns matrix with only the columns that contain - the string `column_part` in their name. - - `X = g.get_matrix(['feature1', 'feature2'])` - will retrieve a feature matrix with only the columns that contain the string - `feature1` or `feature2` in their name. - - Most useful for topic modeling, where the column names are of the form `topic_0: descriptor`, `topic_1: descriptor`, etc. - Can retrieve unique columns in original dataframe, or actual topic features like [ip_part, shoes, preference_x, etc]. - - Powerful way to retrieve features from a featurized graph by column or (top) features of interest. + """Returns feature matrix, and if columns are specified, returns matrix with only the columns that contain the string `column_part` in their name.`X = g.get_matrix(['feature1', 'feature2'])` will retrieve a feature matrix with only the columns that contain the string `feature1` or `feature2` in their name. Most useful for topic modeling, where the column names are of the form `topic_0: descriptor`, `topic_1: descriptor`, etc. Can retrieve unique columns in original dataframe, or actual topic features like [ip_part, shoes, preference_x, etc]. Powerful way to retrieve features from a featurized graph by column or (top) features of interest. **Example:** :: @@ -2808,17 +2774,19 @@ def get_matrix(self, columns: Optional[Union[List, str]] = None, kind: str = 'no => ['basket_price_total', 'conversion_percent', 'CTR_percent', 'CVR_percent'] # not as useful for sbert features. + Caveats: - if you have a column name that is a substring of another column name, you may get unexpected results. + Args: - :columns (Union[List, str]): list of column names or a single column name that may exist in columns - of the feature matrix. If None, returns original feature matrix + :columns (Union[List, str]): list of column names or a single column name that may exist in columns of the feature matrix. If None, returns original feature matrix :kind (str, optional): Node or Edge features. Defaults to 'nodes'. :target (bool, optional): If True, returns the target matrix. Defaults to False. Returns: pd.DataFrame: feature matrix with only the columns that contain the string `column_part` in their name. """ + if target: X = self._get_target(kind) else: diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 633f941c55..235c7ee558 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -133,9 +133,7 @@ def umap_graph_to_weighted_edges(umap_graph, engine, is_legacy, cfg=config): class UMAPMixin(MIXIN_BASE): - """ - UMAP Mixin for automagic UMAPing - + """UMAP Mixin for automagic UMAPing """ # FIXME where is this used? _umap_memoize: WeakValueDictionary = WeakValueDictionary() @@ -429,14 +427,14 @@ def umap( or pass in your own X, y (optional) dataframes of values Example - ------- + >>> import graphistry >>> g = graphistry.nodes(pd.DataFrame({'node': [0,1,2], 'data': [1,2,3], 'meta': ['a', 'b', 'c']})) >>> g2 = g.umap(n_components=3, spread=1.0, min_dist=0.1, n_neighbors=12, negative_sample_rate=5, local_connectivity=1, repulsion_strength=1.0, metric='euclidean', suffix='', play=0, encode_position=True, encode_weight=True, dbscan=False, engine='auto', feature_engine='auto', inplace=False, memoize=True, verbose=False) >>> g2.plot() Parameters - ---------- + :X: either a dataframe ndarray of features, or column names to featurize :y: either an dataframe ndarray of targets, or column names to featurize targets @@ -478,6 +476,7 @@ def umap( :memoize: whether to memoize the results of this method, default True. :verbose: whether to print out extra information, default False. + :return: self, with attributes set with new data """ if engine == UMAP_LEARN: From a58f279963aa8423291c027f15db15e88ec9fa6f Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Thu, 6 Apr 2023 00:11:59 +0530 Subject: [PATCH 317/432] umap trick for cudf dfs --- graphistry/PlotterBase.py | 10 ---------- graphistry/umap_utils.py | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 3da7f9ee5d..badb060b19 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -1033,12 +1033,6 @@ def sample_nodes(g, n): res = base.nodes(nodes2) else: res = copy.copy(base) - # this is temporary - # TODO: for cudf support need to clean the entire codebase - try: - nodes = nodes.to_pandas() - except: - pass res._nodes = nodes # for use in text_utils.py search index if hasattr(res, 'search_index'): @@ -1147,10 +1141,6 @@ def sample_edges(g, n): res = base.edges(edges2) else: res = copy.copy(base) - try: - edges = edges.to_pandas() - except: - pass res._edges = edges return res diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 79a10dcfa2..fb2e2cece5 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -549,6 +549,22 @@ def umap( ) logger.debug("umap_kwargs: %s", umap_kwargs) + # temporary until we have full cudf support in feature_utils.py + has_cudf, _, cudf = lazy_cudf_import_has_dependancy() + + if has_cudf: + flag_nodes_cudf = isinstance(self._nodes, cudf.DataFrame) + flag_edges_cudf = isinstance(self._edges, cudf.DataFrame) + + if flag_nodes_cudf or flag_edges_cudf: + res = self + if flag_nodes_cudf: + res._nodes = res._nodes.to_pandas() + if flag_edges_cudf: + res._edges = res._edges.to_pandas() + res = res.umap(X=self._nodes, y=self._edges, **umap_kwargs) + return res + if inplace: res = self else: @@ -563,7 +579,6 @@ def umap( res, X, y, kind, feature_engine, {**featurize_kwargs, "memoize": memoize} ) - if kind == "nodes": index = res._nodes.index if res._node is None: From 827ae2245151af5c7d495d8c857cd1425f4ab61a Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Thu, 6 Apr 2023 00:15:52 +0530 Subject: [PATCH 318/432] ignore args type --- graphistry/umap_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index fb2e2cece5..b952546950 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -562,7 +562,7 @@ def umap( res._nodes = res._nodes.to_pandas() if flag_edges_cudf: res._edges = res._edges.to_pandas() - res = res.umap(X=self._nodes, y=self._edges, **umap_kwargs) + res = res.umap(X=self._nodes, y=self._edges, **umap_kwargs) # type: ignore return res if inplace: From b95400e481251d31e65045a725d03c010f5e3b94 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 5 Apr 2023 16:12:42 -0400 Subject: [PATCH 319/432] feat(rst) added badges --- docs/source/index.rst | 50 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 1704e7f07c..d7485ec13d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,5 +1,43 @@ -PyGraphistry[ai]'s documentation +PyGraphistry: Explore Relationships ======================================== +.. image:: https://readthedocs.org/projects/pygraphistry/badge/?version=latest + :target: https://pygraphistry.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + + +.. image:: https://github.com/graphistry/pygraphistry/workflows/CI%20Tests/badge.svg + :target: https://github.com/graphistry/pygraphistry/workflows/CI%20Tests/badge.svg + :alt: Build Status + + +.. image:: https://github.com/graphistry/pygraphistry/workflows/CodeQL/badge.svg + :target: https://github.com/graphistry/pygraphistry/actions?query=workflow%3ACodeQL + :alt: CodeQL Status + +.. image:: https://img.shields.io/pypi/v/graphistry.svg + :target: https://pypi.python.org/pypi/graphistry + :alt: PyPi Status + +.. image:: https://img.shields.io/pypi/dm/graphistry + :target: https://img.shields.io/pypi/dm/graphistry + :alt: PyPi Downloads + + +.. image:: https://img.shields.io/pypi/l/graphistry.svg + :target: https://pypi.python.org/pypi/graphistry + :alt: License + +.. image:: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com + :target: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com + :alt: License + +.. image:: https://img.shields.io/badge/slack-Graphistry%20chat-orange.svg?logo=slack + :target: https://join.slack.com/t/graphistry-community/shared_invite/zt-53ik36w2-fpP0Ibjbk7IJuVFIRSnr6g + :alt: Slack + +.. image:: https://img.shields.io/twitter/follow/graphistry + :target: https://twitter.com/graphistry + :alt: Twitter .. Quickstart: .. `Read our tutorial `_ @@ -7,6 +45,10 @@ PyGraphistry[ai]'s documentation PyGraphistry is a Python visual graph AI library to extract, transform, analyze, model, and visualize big graphs, and especially alongside Graphistry end-to-end GPU server sessions. Installing optional graphistry[ai] dependencies adds graph autoML, including automatic feature engineering, UMAP, and graph neural net support. Combined, PyGraphistry reduces your time to graph for going from raw data to visualizations and AI models down to three lines of code. Here in our docstrings you can find useful packages, modules, and commands to maximize your graph AI experience with PyGraphistry. In the navbar you can find an overview of all the packages and modules we provided and a few useful highlighted ones as well. You can search for them on our Search page. For a full tutorial, refer to our `PyGraphistry `_ repo. +.. .. image:: docs/static/docstring.png +.. :width: 600 +.. :alt: PyGraphistry + .. Click to open interactive version! (For server-backed interactive analytics, use an API key) @@ -23,6 +65,12 @@ For self-hosting and access to a free API key, refer to our Graphistry `Hub `_ +* `PyGraphistry + Databricks `_ + + Indices and tables ================== From cb10f3c5d10848c574aae90aaf401aee82e86d10 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 5 Apr 2023 16:14:12 -0400 Subject: [PATCH 320/432] fix(plotter): adding plotter to menu (will update) --- docs/source/graphistry.plotter.rst | 17 +++++++++++++++++ docs/source/graphistry.rst | 7 +++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 docs/source/graphistry.plotter.rst diff --git a/docs/source/graphistry.plotter.rst b/docs/source/graphistry.plotter.rst new file mode 100644 index 0000000000..98079a1bc7 --- /dev/null +++ b/docs/source/graphistry.plotter.rst @@ -0,0 +1,17 @@ +Plotter Base +---------------------- +.. automodule:: graphistry.PlotterBase + :members: + :undoc-members: + :show-inheritance: + :noindex: + + +Plotter Modules +---------------------- +.. automodule:: graphistry.Plottable + :members: + :undoc-members: + :show-inheritance: + :noindex: + diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index 030071b943..b1543d5612 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -1,10 +1,9 @@ Plotter Module ================== +.. toctree:: + :maxdepth: 3 -.. automodule:: graphistry.PlotterBase - :members: - :undoc-members: - :show-inheritance: + graphistry.plotter Plugins ================== From 10907c3fbf48bc60ed4c2c8d9e13fa81aaf10ad7 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Thu, 6 Apr 2023 02:58:07 +0530 Subject: [PATCH 321/432] plotterbase to plotter --- docs/source/graphistry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index 030071b943..9b53605fb0 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -1,7 +1,7 @@ Plotter Module ================== -.. automodule:: graphistry.PlotterBase +.. automodule:: graphistry.plotter :members: :undoc-members: :show-inheritance: From dcf60ac10957ed547fb9d5c78c96b653cbd777da Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Thu, 6 Apr 2023 03:17:32 +0530 Subject: [PATCH 322/432] addStyle to add_style --- graphistry/PlotterBase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 747f7e74b6..628b74991d 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -224,7 +224,7 @@ def __repr__(self): else: return str(rep) - def addStyle(self, fg=None, bg=None, page=None, logo=None): + def add_style(self, fg=None, bg=None, page=None, logo=None): """Set general visual styles See .bind() and .settings(url_params={}) for additional styling options, and style() for another way to set the same attributes. From 75f11b0d1b7be0cba636c1a228feecd3d33b740d Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Wed, 5 Apr 2023 17:57:08 -0400 Subject: [PATCH 323/432] fix(rst): revert plotter changes for ci test --- docs/source/graphistry.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index b1543d5612..32b042a0b8 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -1,9 +1,10 @@ Plotter Module ================== -.. toctree:: - :maxdepth: 3 +.. automodule:: graphistry.PlotterBase + :members: + :undoc-members: + :show-inheritance: - graphistry.plotter Plugins ================== From 953cccc923b84967fe3649a03c87d28e520fab1d Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Thu, 6 Apr 2023 03:27:13 +0530 Subject: [PATCH 324/432] all addStyle to add_style --- graphistry/__init__.py | 2 +- graphistry/pygraphistry.py | 6 +++--- graphistry/tests/test_plotter.py | 22 +++++++++++----------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/graphistry/__init__.py b/graphistry/__init__.py index dd13cb60c5..511b543e57 100644 --- a/graphistry/__init__.py +++ b/graphistry/__init__.py @@ -15,7 +15,7 @@ description, bind, style, - addStyle, + add_style, edges, nodes, graph, diff --git a/graphistry/pygraphistry.py b/graphistry/pygraphistry.py index 2051a32523..e6582928e6 100644 --- a/graphistry/pygraphistry.py +++ b/graphistry/pygraphistry.py @@ -1209,7 +1209,7 @@ def description(description): return Plotter().description(description) @staticmethod - def addStyle(bg=None, fg=None, logo=None, page=None): + def add_style(bg=None, fg=None, logo=None, page=None): """Creates a base plotter with some style settings. For parameters, see ``plotter.addStyle``. @@ -1225,7 +1225,7 @@ def addStyle(bg=None, fg=None, logo=None, page=None): graphistry.addStyle(bg={'color': 'black'}) """ - return Plotter().addStyle(bg=bg, fg=fg, logo=logo, page=page) + return Plotter().add_style(bg=bg, fg=fg, logo=logo, page=page) @staticmethod def style(bg=None, fg=None, logo=None, page=None): @@ -2417,7 +2417,7 @@ def _handle_api_response(response): api_token = PyGraphistry.api_token verify_token = PyGraphistry.verify_token bind = PyGraphistry.bind -addStyle = PyGraphistry.addStyle +add_style = PyGraphistry.add_style style = PyGraphistry.style encode_point_color = PyGraphistry.encode_point_color encode_edge_color = PyGraphistry.encode_edge_color diff --git a/graphistry/tests/test_plotter.py b/graphistry/tests/test_plotter.py index c1518a8160..35142e8026 100644 --- a/graphistry/tests/test_plotter.py +++ b/graphistry/tests/test_plotter.py @@ -720,32 +720,32 @@ def test_addStyle_good(self): logo = {"url": "zzz"} page = {"title": "zzz"} - assert g.addStyle()._style == {} + assert g.add_style()._style == {} - g.addStyle(fg={"blendMode": "screen"}) - assert g.addStyle()._style == {} + g.add_style(fg={"blendMode": "screen"}) + assert g.add_style()._style == {} - assert g.addStyle(bg=copy.deepcopy(bg))._style == {"bg": bg} - assert g.addStyle(bg={"color": "blue"}).addStyle( + assert g.add_style(bg=copy.deepcopy(bg))._style == {"bg": bg} + assert g.add_style(bg={"color": "blue"}).add_style( bg=copy.deepcopy(bg) )._style == {"bg": bg} - assert g.addStyle(bg={"image": {"url": "http://asdf.com/b.png"}}).addStyle( + assert g.add_style(bg={"image": {"url": "http://asdf.com/b.png"}}).add_style( bg=copy.deepcopy(bg) )._style == {"bg": {**bg, "image": {"url": "http://asdf.com/b.png"}}} assert ( - g.addStyle( + g.add_style( bg=copy.deepcopy(bg), fg=copy.deepcopy(fg), logo=copy.deepcopy(logo), page=copy.deepcopy(page), )._style == {"bg": bg, "fg": fg, "logo": logo, "page": page} ) - assert g.addStyle( + assert g.add_style( bg=copy.deepcopy(bg), fg=copy.deepcopy(fg), logo=copy.deepcopy(logo), page=copy.deepcopy(page), - ).addStyle(bg={"color": "green"})._style == { + ).add_style(bg={"color": "green"})._style == { "bg": {"color": "green"}, "fg": fg, "logo": logo, @@ -755,7 +755,7 @@ def test_addStyle_good(self): g2 = graphistry.edges(pd.DataFrame({"s": [0], "d": [0]})).bind( source="s", destination="d" ) - ds = g2.addStyle( + ds = g2.add_style( bg=copy.deepcopy(bg), fg=copy.deepcopy(fg), page=copy.deepcopy(page), @@ -783,7 +783,7 @@ def test_styleApi_reject(self): g2 = graphistry.edges(pd.DataFrame({"s": [0], "d": [0]})).bind( source="s", destination="d" ) - g3 = g2.addStyle( + g3 = g2.add_style( bg=copy.deepcopy(bg), fg=copy.deepcopy(fg), page=copy.deepcopy(page), From 8d644814ada4cc9e6a4e64eaa1215e1a90aeb645 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 16:01:46 -0400 Subject: [PATCH 325/432] fix(plotter): expanding menu --- docs/source/graphistry.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index 32b042a0b8..b1543d5612 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -1,10 +1,9 @@ Plotter Module ================== -.. automodule:: graphistry.PlotterBase - :members: - :undoc-members: - :show-inheritance: +.. toctree:: + :maxdepth: 3 + graphistry.plotter Plugins ================== From 5900e2f68a85756d618a96a18e8db62a1b48dffb Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 16:08:37 -0400 Subject: [PATCH 326/432] fix(docst) added umap to articles --- docs/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index d7485ec13d..ce0734f357 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -69,6 +69,7 @@ Articles ================== * `Graphistry: Visual Graph AI Interactive demo `_ * `PyGraphistry + Databricks `_ +* `PyGraphistry + UMAP `_ Indices and tables From 085077816a13be95b554001870b0b81924e004c6 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 16:09:42 -0400 Subject: [PATCH 327/432] feat(docst) added photo for home pg --- docs/static/docstring.png | Bin 0 -> 927210 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/static/docstring.png diff --git a/docs/static/docstring.png b/docs/static/docstring.png new file mode 100644 index 0000000000000000000000000000000000000000..a61e42c592a04900f0bacb2ca9efc6eef53e1d1f GIT binary patch literal 927210 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
I!|x!_P&z!LaJ_lH%hZP6nu+b8&oIEN7!C@n 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∧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 ` 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ℑ|FbL^&N+j^4uq`80XkENT+jS6Cf}qQ)ru{ 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%0DIfsJ_BFrGvI@3MINvz(zS-s36S~K z99-1}z6V|YzMePn33w9R<5vMAxPMCor`{7O$Iox4_*_zWb;~poCt%?90{B~Nc%Qr{ z@Zi+_-}5}PH&t;V*k#|G|F~^5og=;C{QiaR;@d&cp_X&c2gCML#@T8Hfa-MNyzy+}hR7qhHs_X!fSOj`F4XvH(&^R;cXZz#->Rs-A z_C}#vh)C}vuwz6WVWToR{8?!KM$Y`5%wsb^?{@45Z%G}GC%U-*n#JpBdq)9`RONzp zHOuJf25EeUSz`}EiG6Z?`Q=p&rg(L_X;g_pmfZ%>ZcqvxDH(FyHLddQzEu60TV=m6 zYS>N@2)7|J`4nQ3LSdR> z&GulHkyA$d6U(2lVs{PK7Uu|7q5>RL9_SP!h~B|&pwAMEQ6L3b#S^-XJNno3gKr{CCxkT4%5i<$PEG_y|RQm)Yms zc586%gA6R_Io%~l|Ikp*E~@luGgSl%>(+|K10I;Q@!kq`ThBpHJ2~E3n_T)pseyFk zR`f{#;SlxP3UMxQwvQc2+A$W{PLc|hZM9hmJZ^v9ij7(897E%RQlCM}k}j>(+Wm;0 z&EZG1kV4Rc>R(%3tF3R(bGo@+01qvQh-M=N21Z~BFbduc@DZf?!R%ZZjSuo|w{GZY z_C|jBh3>ol{88%j_P*pJ)U)rjCk7sLBi0fxY$SzajevbKmV(*PMfHmP&(YK-J`grR z-hUZ1sF7y^wGCJXjBLX^h2jDYcgKEuP|xff*Jahs3qk#l;K!+whz56Ljer7!83K5Yt6eD& zn;&!2{Xd1RMH}HuhqdD^c)BKYlX;Wd8~MX?luwgQ;*PK3d)lAf>wV1a4|_akYv;S}><<$J;& z9_IN#OJtz@0lT>VF#r>^%|ai9eM40cdwJis*SQAWbZ-*>UWUo@$Y7rpGIdby!*=2S z90MF`Kv;bkjRwTQNzBmkRek}fcE!DW^f#eN(k-uEBc&jp_X48s+T%8D<5of! z;y=Fps}3wvnc=JgUGhqwe0F}jDg}D=Fkg|2;zP()wWRQ>S`^@S~I-=ZjJR%9D zZy#bE^)b7=xbfy#4hx(wT8Zl`mu(!d;nAaC2guWQ$X;$gsT1)1jS5HBP+B|Z(xq;{ z-LKRf2}q{FzEm#LuV8VSLy)!yfI3cL&9`(QmXot=O5YW5-wSWAWz4AMsBL6UE-~CVDWz%CP zEqCSniTmr(HK6lv*|O;Zd5Z`DiQ8JF5l?4XS57caMDfVOEOXF3?Eb0DzG1957IPI(L0;rRP*0aa@YCbXsMNgb-vDN(j^d78G#)!sKby zsuUFsm>O+$YjU)-zW!*MoSCeU2)6F&HfW|7eEZA=(Tsgcvx0Jg0GgKvoOrk&Fr;Fy z2QUa!5TGFQ0S1*8IcQ-hc}mw%?zWE49UcqmD8HPhed)JV|E4eJ{@&j9+h@Q(13uWb zj-$=f=a>$9Ld9;-D@clzo=iDl0uTx~{0CVN{e?OUq|bm%Hvhv~qx4?XvG=Cxk5VC=V-Akhg>(#ATYoh#w&l^Kuwf7yEzIJvT` z%=5&)W-OVJ`<}{_Qdv^1sw$Oj%61trHk%vMEX|mKY1RP`!@#h^KsQX&@PlR<7%LtENsU zAUm;sgXF1;e5w^q9IR@|7AG3bC*4n0b z%Cel?#W6%okx2K=K#}XCau%DVvb`)(0D83 zh4>n9=Bm=O{v0BN!_u6*EZrx+gMHz?fBBE(WXo}J4BsbhbC;wa=3cV-n7C3G_QrrulBFdPO7=7Zs1sp3ti%NV%p5s#QuAh`9qW*=}Mrrdt#s^`ksLE|+&-Ab90jag?IRlBf7X=1)GxP6RB&K_6ogQ7 zRl^b^T_{>4^?;oJFye&%>a2vQ2BWDd%d?0DMoOd~G(huyLR@G@=*4iL7rwi0#ygpeCVdwwm^lBh2#w7cwrY2vDR074lrdXA`hdOzTcM9D5`Ix6JfTc zupmP;qSstVfDg1M01tk5^7}}QSl}D9F!=Y~pL?6M`+DW4{J$YVz=mKqIl&9FG8WWi z3BZ`$PI0%d%6&d-|LosfTD)u~E2+x>Er=1KlW=@;O1N6arqG56^KxA{Z^}+)cOx>h zeNuK$0vc-|CuYQ0nG9>^B$X=XSVP%YZVp> zOa-pU$v9xcvII%-38FW0O1?p^I2PqhcZ)nVqhnL>F~eLilM)H>i~xWY(0Fjd&|87w z)YDv*SLTSEzOZN(dgA9_l6U@Z_rVH4dj^o9QD2NT$7n;wXlw7G%1C1idK6Ulh|?aH z5sNJ|4$h<3QNgL&MSDT-;3>@Fk5ROtzr#uL4$K2{hGhh1Cr?C^Rhm0)_lLSyDb|$AM`ZW zfef91RrWY|n{u&-nMtQ$JTSJhCBv=P#@>gmQ~%vnK&o}vAn!ZPVpXnn+DJZu6yOppJ+ z=rX`&?e7&EZ1A^PKtqv~30FtAlmHr1lNZ?k)6#S1J(Ai4sOJ?WMMaqn4d^E3NXy$^ z-k^LY8el$r?K@~zk9jM85Jgw#;#1a^bWG~_C5u6zgH{H2rDva#zWF)nd&DDO2fffq z-t4KfT_{BVwai21)Sj?J`6oAmI#q4xg?{;Ha|NUXpcejS+N%B;`6sG6mt<;Jy#hL%-`I5jeL0`M7(tAX!cTx{pC3j1hJhq#E&@bq`o3{-&TjQYl%MA7ubjX*qiiPnj4qA*e zMH@;wSP;6pIv=S>A9FZ=fDNkAfPxL5IhUkSIB{;Aly9prou`UVh4ZTxGEe?<`Ur=Et2I#tPnu?)m7;2jy%MmeY8MK)S z-rCpkdPm+YZvMm(soO;KFFpvP!I3uel=)_x0u5V>(5gErp9q^FPXCfm7Y*28FQex~ z_vJ8KT17t5!~T++KQpla$&^q+_UnCKbZ~U!y$kr~(_t3Q)5lBRwT1q%i?)H0$Y}_x^rI%zY za!QiHUJ1Y?@$=fG3}C=BDjh4I5gB26b5<6D4^mMiB~H>Vd;!X8ZZAod*GW{aCpsUM zc>Ei=)&-(7I1rwB(qM?V;uw0lA4)J6+(dGBxQAloc;|^^t9kuf(sq(O@P&26x3-KR zT{apw^jxz5j!#1mrLzIbXS~m|f6cP-%Br-#y-zO9e@-GRIceBeE=<`C%Q7yL+fPI5 z@1c4`uXG|F?IYd7o!OQ$fP-zFj4ui)04l~2)tMy#2LvfSFgq~wUc=+IXDRcEW9)AC z%S`Ty%nV*mh$GO3(yl)=jY2%gLY z0$`+laQ}3)M#aHUM5D4*^2`&YiaHSV)0kP-nU=t~PX@d1l7AdqCDmg=Qk`0XfG9rc zHxHyv%QM-Ra16E3?zQLSfA0NRL;`biZ$`yPH{|i$XXR|iUE+Q;ZWtK4&$cbD2V@b( zMK%b#g!`o@*dr+zVTDSJiZ4^jxMvy%GuzMOujxYiS)Y0CA{(I<Q{IHV{=+I|+a@MkSFJSHEmlR%HmrOfX)Ofrtjb7>5F~4fQ>O1D7$^ z+Kc$27Z#D4{_^jg`#+2(ObPJsLnGe_xEQCjFk{%t6yF9-s!Q3?B9niSgnNc4BNaBB zo{D@`8PJgc+6KWVeLVp%LnQ>XS7~(`DEV{U@cHTz$1n zfpV!-(NN2he^f2ENf2SkqgakqupOu(4)$#m4IYp5vf8*=wOCysEeeDLQ_YDK)pFDV zq*xEfM*t-85nlLxB93Q79{65Kpw+Mn(4hT{vKbN#q{W@?h`gRvzhvv=sYuk~9N7E) z5FpPM!G@daIjC-fc8i|Yy>$+oD5N&1tooG1`cB*cHt2e7fDHOPXljK{v?9(}Os~t@ z`6s0L?nlvWP&oSbm`SI&J)1ULRR)E!C$!qd5X=NM1^6`8VsUc`03uEG8Or7YKB$EO z9@+pNwl2}{6ao5c%m#IRT~!#4w2eCi;x{hNT0p~lzS*p&e-vm)1308%E=Fov63T8U zAt#VtS|@-3m`sY(ow94a*{%A;JU1Ruj_u?l@-#Jjzhaoy!t*Eti_e>Aqk}SIra}ZI zuc*1gNeIZuL!H#W&ddA+Nc~n`VvRv*Y65GAQu$p!9FrWKYA^!5ga`{aU=HCW zLA1$%NSQ##1yJV7@Ht1PtsSSL3OeG3p%4ea(DxXr5L)`u^tRfuXo4-vklhWH5*-GZ z7$-n8M#<^bREYu|v{tFGO#uT17Id>pGz~&dP=K&p0|9s(rJH=< zvKc7yP#(e5AvL+5wfDoAMhd1GvDhPQ_wYrm_TjK)HkGY`qWu8v-y1On9T=+Z0RsnZ zOP^aqdC;JvLr!-1xr3$G$Is6uL0n}eRXwbnsKLAij&^%d`%16m;;rV6~B&(Xr1- z)om%rkjHu3DS=o!pp_Hx+HbCEC+rJ!emd!wFpJG_?fv@Y=?C~%e?>Rv=>3^DM|sT( zeF&NFQd+fugrceBVQoRD15_HAn<+>r;*uaQ&0J8+ZByj|fJ3*N1+2*ND#MKXhd+K^ zwimK6|CH%Rf5pI_vgJ5`JETPf$*=7<>$^Melih`vWjl5k!ks517Uf(nzr@KuB{|gP z%B5nJ%{6A-8*LC+8+fSLhb|(%(!e7&!fQILYjs3+|Sc*_q1_7 zF#|pUGL+}t04X?U^Dq~VchXh#~{e9M0 zW_M7Mk!5KD06N{@qq}&WT}5D{VSf8|3#!&^sJK*v{iMpt< zI+J_GBi77dI`oAli2{x(*kD^d*bwNhQc)#DMGdr6dZ%zmAFvnW*$D6q@Vr1vFQEBB zxf578RZ3%8z)_6B_ZBSH!_142?ab1wtnaSvE#}y!!b=h0Qpb!ni=qQE6#^Km0S<_5 zz|tWIMat3LoS#W9;kH)hIl!7I4`~Aq8;FEDy~ok;qRId%O`YMWWN1^fI0X|M8R^sJ zvrl7Pv<)BaA8A5S`Z_dONX+nL2fL5GtFNM2exI?F-9hDJ(mHoZZxK}%vP;P6k~ zR5bFYdJd}F01Hm2W^xouA@IL7GX~f&4A^k|rmGlc1MQD89kf(t?((y@9c;K&=@d6R zC=Ta9)07H*wa41b_XDW6j$Lb0nN zl&~F&015@fx5}^_O1xBc-Fm5@6U{UGrI^7i`FC2fNliZ!1cdfQxitoYf#y9zKea`fm zu-|QwR{Q-XtC`vQTl(DJZnJ&?JsH}vEpqKz!g%O!z_w^tRnz5W=S82{1OROu=h*3D zu%IDN{s#~!^O0vL47STqmMdidU!#+tP7@9m`*!WzYg&aeDYSjmHd6svzzNdREPVoX zLto7pcx(C#B_-AIF)yQSp#6ltt~aIoQX3OUqYh!PV2faCp!%GTvWL5gmESoUlJG2N@GW-B=vsrbG@5S8>U}q z>F?!7jQGWi$j_zH5$&gEYr*GKHhd_Q+)~HMPTX zNzi8k@N-lyBK$7LIx&D8uQU!y1w9bJlnm(=I2>V09mKX*C4H=${*?$!?fF%pP$RSn zN(I6@ki~3D(hXDML8G8C>p-m^Actx)_mH~N84JkM1Tapt#ig)JP$0=&)7LCJlB|L+FxZ0&%Z%k8^`~ z8n_mm)s0Mp%qN>=ac4mSyWJ+AH{&5+o@XY4MJlVnq~d)#m6Aa!+sqTe z{p`Zuk|womG)PpWHA*mTYxQ&TA)e=Tmc&+QEiqP=Amh)?`tK~5(+{3#tz4l6FD%b${EB;;{dTWee zFMrUxf(>e|cfU>iQ44rJXgPacn0{qtdlN=eS%yx%jeaI*e|1~llcUd?bP6q7d8;kE zG;K;?+GQ~h+rHQBr659+G@(`GU!5@JY8|wFztSaF0VgO}PCx)NpiYn? zK?BPN>nCgPZf;sLhtJyBYP`zL1+36;RS}^qgQPQ5Vlawm6@8wC2`HOFOLU^$aDW4% z;DR0L*POc?(F@go&`6@bIyA(03oV5sl*&>YT}PjoQwK93-S@YJ%@HDxW1!c0I->##==G-UnG=uoSVBQsUO15&jV*aSMC0P(!r9%0Enp~V^(!*1 z=iV0WwAos=_~E`q5BaFzgo3vgunRxiuqVoZr!EGDH#7TMs^h69g)4rE0eqJXkhbAyzfb(6SxA=^ zISzIY%WiZ-HqSjN@w?w4rEXYpt20FL8^p&wQ>7nvm+7;KhUeP9MtqxJ6h}=M(8;^M z@I|8gEjSzf?;slqfTFwmcodLfKEd>|p4QeZZ-{U(DIa?c7T3{6V5U4X0RfLS- zK{OGR1Wr<+6UzP&x#|c|=C&vUxk$v6aRJIp(wOMGq5t$Z!HlfjB@=5gS!-w z8=RP>IeGYRFXAwJpl$;k{C5vX%Sex`j?T%YFJ6+9kDQb^6(;IURpd3b;p49?Q2`() z|LWmpncUcv0USo#hVN^ar$0T(01w9%2dbkQmr$%qBzjdAIp#K;ab(F6xrBz!ec|Rg zc4_J;bn(r(rz8?>l|Nx6#${tXO{GQwpDwRYUEf~GBo8KIR%a_d|pEQYhK zc6D}I0zGE(hCj+|(6kDDSBsK9G>0~gK2?K3DjxuhSX(n+ANWTrG^A=`mbySYz~-NB z{;GWO37BM$e+4WZO0s_!x+6t&I~E#%GNJXYdg$`x<4lw;19!8 z_-h=o_k8?2#SK8`3O2wxdo|sL)Z7(V z4VNX>OA1BzQ2izkolehQl5Utn^RW6>0UlaNt@x*Euhv%WKdu5mgQicY5VD$zLTc(n zp48BFo(+Hd3Bo8m^SzzYNS~9No0cR%Vt{g4w|lU@K61sm#9E9@^VwnIDp zZbp*R%O=I*cBNU|GKRx*^W+0+WM5Meh_GCkrz!)qteuKm2B~Wu*c=uA$_r8<*HOz+ zIfz_1^#xVWNj-B%kthnKFO{lL$T0W??XL13gf1B&5*G@S^BEGarA+3@rKA{7dt+fL6ya56O(|K504EE5@49;n6^<4*je!jQbcuVs^mEjd^@Z#i{2nrOoAkn2q z`)eaE`wG9o_cQ~WZNMBS`@+$fPPYt8L){85l)Ekl!1LQGrgvvdffz-m-)L0!&g|`5 zzw-ZwGoX=mec7hZo6Ta-0lhxET5?TadT(lUMopJFs=Y{?rb{1v4XfQa>h=#>&gf5+ z-8fh^NBW?JjyNv?dc90L;D$kf>th-%3v8h6q7-m~R5jEtXv;V^+Mv;CRVz4z3``F* zea>IsvyP)>(k76eb8R!NZ7@`Ozz!$Wdne_Ft8}Wi+1FG*>$D2!YV^yRzQ%sogn_oC z6n2hb8|rk~L&TDHJwqOI1~Ioy0A`KqIT*w?P>mo!zI8k|Eq;WA8;Hnllb`Lb61B6V zN%bQGI(Fb_b8ZkKAYBfM_96~B_f#`c(r|OgyjBrx3$p707$XA+D7Fs-4U0ip4a!(Z z!CsiVv?Ch}fDHf*yLIK3>wivDE!f)2VDWt^wo&@s8N;gbZ8q?2?XH3#6lb7uts$|am{BBSJ{JHt5GfGBR9PCWoG zX%=g93|`P}D6_9a&0+H_X2@NiyhsF@>o|L1Q;y%&Nm2}1Wc?riK_Q;cK8**`mC^&7v$v0f-M*$B{U$yf-op7k35w0dcZ-~OL#kFfH6Lg;l__0bt3ifuzK{lT9bg?&x@X12#gYG$$V&`%}|LaR-@e zVeV8oCU$hQ*)qq;_g3YKJO6Cc2yf-$ zrS~p$Fx;}(_LVq9-2lTK+|#WxYpSwP+A>fB(9G7XKgw#Dry@@xV6hwmhp-&9?Jsj4 zeO?gfLyX1b(lV(Pb<7-l@uy}#XYRq{(LNeU`lrUDJI3}l4tiIxLHk!tuQ;mZitA|0 zX==&Q*E#C8kFwlB3FR>d)!3p zV;vMB;JPgXIPiwZ|Ft&xx)t8w2Ti9akdhYdzv~Td>EP9~OVh9rXjeM=5pcX7^sZk+ zfrjkFvq%c;@LUGycYWe(??=xHENpF7BF#P0cl?YbV0QW#<3xB~S1E(J%5d1z-6xwo zf7z*VS(_e{B9%V8F!hx6-~+fR(q9+pZ+&%DG3?(~c7uls7e#zaeJS(7i4T9POu=6c z;nvuhu9*D8_?n68YbluD`?d$nz)+)6DuTF{IWA8wrKOr$mEp{b(nv0?vjMTEmJ!pC zxw|Y{ToutB7tw_>k%p>|MugH*YO0G+MFqQ~QwM;64ov5F!-$x15*fVhaz|F^^p%d_ zgR-d+lyC?jfawwfYJ~7`HptyxM`VX(cL74Qk<#ANiRhKC=cZ%Q$dTUE9==oOL<30* zKrlWha-eijZO$qEM8|xba(qF63Z<;xDce@Cfx)#h8?*z^L)Zmua53O@(NUF{*0NEL zbT!CxD0o|yG;Z(SX=GL-!AX$tMpcA=%icNzxA_dH(^=14(4Vs--!_{_BQpyOlwOZ1 zgF>JGYeUteZRNEtf6!LTFcNN6^_P30mn+kugtMidpbACaMWjc?9380I!Knab(5U}G zdvYu0DafE~1_rDuPKU#0QaEn-K?NbmQAQN6T#ic|)n;2~L$CFIA>BNnwlWkHY>3lI zHiq&<>6f*cH>Pd0-<2@S2zBip#z($?d1D&07M z_@uwa^su)vm+C;zWD)SCQ`#C90ofu@{v$G%>XxqMFG$MSB%8d{9F)3Kfn@*m3*y|` zl0siX+E+ejs#oOv*CHdjY?T1P{9>#0ax9e===J$)PMCvIivpw08yW$8P{xMCh(X38 zL|;cI$U85}1sukQs1l+!K+2>V8IYos`fX_eSITb!Yik3KMj6lA|rqAwlqUt{?|H%t=`_r&70Nd7a@Y?%g z^7s?$^7cntB!WZv^ea=8W*m@4jXc*(0|imGpJMrDQZ4j4OrZVHU&nGt^mt`%iGT`0 z42|-t=9D_k4JiA^5Ze) zKxRO;HR`mM2Q*UWBw^}ZpHnWPsjO>y=5#u-Yyq$V` zw>(byJ2hpB7$HaV>Bc{d1es}_sH#_WSklrQXd#7$%fDuAqX3AL2V_w5Qx@G9YUKQq zCgP+r1T2SRVah-%mjs|wwZS!I!NJ4ia|24ZElT5MYu~e}jKmT>GF6!{+uHwU04xn}-|3@J-G{v z(_)M@{iNssR&njVz462H`K8bBQ`Rw-RBRn90DNr2EEw=LN*KL`JSjh)1@JWA0g|p{ z{E{FgVVP7KHLjF-cQG)$vNdi^12!n&U|VGcaO6E-Wj?AFL#eVOD?2=w-d2FkjCiQJ z(gP#MHf?*)_Fr= zX2X8{k*DT9C(UR|9B;xOnfPzwXnWwW&Ag`X)E|1S=B14*P?pt88~0|kM9UgNtduTo zwm|HI_OXEG$kyCAdPoWBeaBZHw8(+;3aNEjefjgIoMWWtq*=yhHfVZl`_ZK@A31sjTsbKtgd3VvANjItWmFFtvLAf#?aOKYkPw-1~5ZN@N^-janTt38-Q zpmlixMj9WZ026JUL!?hw&u&9aFT$)?`{VWz*l%kD(ots8nmn!aB zovzNans2}fN>M3j;8#F_!9|s_Qx>$qHpz|APKU)M4>LoXZPj2kZ zETuTvZ+*GhM$_wPNZ9z1tzMYNut&(t_L5JFi@-KGHU&}WFx{{k^hk>}>YtO2)J@LQ zBdvaUVmWt5(km2nNRi+D7T|E?%lp=XZ{Q3l*svd9(9J2xpf9`KdA&bj1RQU&GLJuP z&`rL)MYHvJx>-e=qyB_42viL6WIJ@aGCB3t31ztYFf?W0eCth53@xk5+|#bA zS^I$qo!c5Ln1lUk^*YyphH#!--)S>X zw-AHHu$3p5-U--HESdQ#25F~E*kFTxkymhmSH1aRKll}dpfW}P%C?$<4>>1-#rn?6 zjWf{GNTii+DhXZMXaw+%!?4(I-O(la!t4VE za0pZWPikcar@OvcVhBKPZ;=l_$3T|#qg@Pyd+$a2;zcwHI3JPwP4$X}*2l=Jw)*la zj(-&TdhysXaT6I`9()(qXhk~QlcbYS&4QEebe>{m`rc{^C+-#5Tqb9`dav2FHnb5| z=~FK(ruUTE`-$XiT*iq+u#qYT?>K%!j&}^o zm4yj;--BOnwpn1G)D(n>5P2`A-BKcglOlztrL|f9@Y_oAdrwjp)9TVC39DiNfFYcS z%JzC%non}7e?t^(s5b>0)O0YKIHbaarcF`3z&CVgYktY819$K4}co?bu#9&LXHa1s_m^Lu2y_v@wM<^Lju z;cu5F%1(M{JHB)5R}5f~<9c7nP;rB?>JMH05Aqvlf7fg>G0-5(Q{?J%Cv=^NBG5XB zKEr~k7E)C(gz??6(4Y)AoR9%6H^`4_zm|g`Q2=aEi;x1BE^g6gQZYr-05Ij-mSBT; z0X)iu5W#wwM}BlaNLMbCN>M=dJ`X6C1oU*%PB#X*u4v$tsZz0nu8Ny6x-DpFHdyy( zEO}`UY_I{M)q>@ywFU+O9C~1(jLRlfjTC&ylXm0sp%PGi^E#_qn(BXCf4*FxavmEB=NB_O-Lbsp%#Q!Dl`jwAg zcWh;V(&F+Q_a6NTm)_IXWzr}1N?g`ep15wseIND%5c;(NA$XtLoR`_nIn#DHsD?3% z21Jx<7|M`P)5;jO*}#LQS)`Uo!2?jw`f@h_3O1Oa>nWhn!gFMs>h`GBx9{|Bx6c$A z)o7y)JlGM*hiYhPmb}ri6>DHqfsW{)HhX`T4s4wg9z?n{!sG(^RHlQ|K~5YFx2!{3 zW4;4{k}A69-?;2YA69VXe2_J4hlFZM0~Uqe3SHaL?s_n8(I{KLzI*&;yP%aYR2*r)=`-c#Uf(_9bFqK@*Ya5% zpVtFA1PV(yj7{dJG(|$$1u17M9Ssh)RuMSbBYmh&hJ75aBBj)5Pt_c>yL*OH;GmqQ z+VzZvgMM7Etv%n`hJ*HhWoH86s#ac1fG)o37$l7m5GPQS z@x{C(TRWvZdP!n;ldIi)9PNiGUg~;j9{4P=RgE`uA3)pOnt4*1;s9MuebO>ZSx-QR zF6i0XuPIR22@v6ceWZ@(lYe_zf+ykyh_B zyDR`ks6}zq^BjMn3^hN*zK}bfUfC5dT2A+Th%1dCYPiE8{w+ZJqEF6Wy&}CaK%ts_ z^26^iQy+bHZjafZEQcu=m&$Y~s45~nj4FW?WjL6=k=s_&TcV^1q*1l6x|`(c%myNq zn|fzSm_SL0Y7;x8fci-zI^MD@&rtawrPaC$=o2_Pp|Pex38rKoVeC`KPDvrSipGat z8_UL)wUl9#Xl;55KFFh8?=%$sbGfT>fx>|4!tLs?nh(5O|f z8CaUZCed)u?v_0KaY2NkuEbdYw5kT)afWl-pigpI~{0;+T&g&$MoqmKmy*1WS z5im$C+z^tv<(b+x*kjLtnf{5E-$GlVC{6eKrRi$|lnPB5u%X98O2roUFkpj|>oQln zm-jZnggV$k0FP7v@}8crlowW?HJCK&<~W6tu5f4Y>;*23 zO1}5FRJYO+WlZU6ASEi>fG!IfW~7#tb{K;XUGM1WHWg^FMIety+Yq2^kug%L^tvg~ zpw(lxsqB&!&Y^ow&H%po8cq_JL(_u$ay^-or(nUTHPuQ352FL2;Da(721#FdZs9o? z4-cAU>^}-H?2ulqfWxbq-GjW^a{B!rPyP^956behZ~IXLHk^RjU@W^Lj!1_!eT)8T zD#iH)(zH6RIdr#Z(|3L|TB2GjQgF?K^bHC)><1lE^JoujZAkCi-}jpKPr-&fDGlB4 z{KlKEr{yuH0LG>_7NxK0)?Ft}r%=;!mAA{v1#3*A+duaQKP&fq_rEniRa#k;;?g{9 zr<`~@x)6XQMf4_&hS#%iHV84&Zm~sjn`=~sB-J9hfk5pHK)FwLkQFEb>P8qlsp+(u z4Je$+Jt(0q1d>Tl-SN@*VhSkKg9&EltNgjGfP?+pHt?XI_k$1piLe=fY-FIJ(NT`C zkZViZxJl!vjZqZ3S7V536VOWHY7O(qT2Nq71`9nGVhx(4h16v!+!j00At6BDl3xO%Hi3c2uyz z-log`A$!JK?{CU8p!e1Od+p7zqq<+3r>|x?mQK@N*!!x_$bKO1cJ24g#3D7dOP|?0 zVY&~h0do{<+&U*Ye;a85%aV-UV+NUKJs1wQE<#>Q2p?ivl2U~b_5CFxnEL!H@N%Od ztksaXDb?iL-q7)^6j4+z`)a@gK!dXeHqZ#|`$cVx_Rrh5$lk}Q0U4fM{bSl>b@0e- z#K_Cq5=i?pgtElu8+Pqowfjoq&$YOi?Ud>ntZLjk2Q^KTYl|pAw?^X6a~Y0W496 zp7YZaL2MGH0)Umz)gqr6OUd0`u05w^xDoNoJkKiopPC;71wCaL4n^1vTh;S`05ENq zLNc2AJUP4};}os{P^hVRqdNY%ure{)B;UxnBWRPFyhv(Gh4c`_A-mDZS|id+8xU`5 zlTZHe1+%WmdFi+>Oo_~sdk(007rF2;s({7i%c#Kca*FXvfoM9F2@J`Jn_{;%Hqg?S zsf|W+qZDYH3fy?BvoFXQgllKV{?W|$!)$dxCHDg4sRhv+DFB)+0AOS(+jtUDG=wHh z%^4dmvVjI~4N$b5V_jc?0`CgKb91nVNc-9BOG<(i;5Zdoo?JMGm?jrd*&0~7vL!F0 ziLu`l7)fw0)*Tzt(xPof6PZLCGK8a&pgSl>bWb=?vHWf9l22@W6)k zaD7X#z^YVwGnzrFjL!E?bZ=T21E*)^%n!KzK66NGfC{+^={>X)A#(OpfV<x`&g9Spn49-&Ty~p$nq=CNDNWP)>9AL ziyY}aJu_;SYq|HGhb|ukbZdz;A|RRMe%U1bEq>_oNB#0nq*D|~r{jSWU!;i}jr4bb zhTR&_P$s~V2WY6!Kd40qPMegHqwfE!EvMk_b?FXX7*{4*<3Xf$gOcdhm{v<7hbW`D zL;IlSLO&+L_$GyXa|o7$gK`-i*ay|C;R(mKp4X5RbAT6W9i|1N4GS zD|)`m&7ggpf)U|Dmd{@DEYYY?Fv0*7)(Hu5%xOe3Z?~y}{ag0tI0JeQsi`w!-%s|Q z*$%w@`AhX~r$p%UXFE{3FLx4HIOy1wRiIU8G6ygm?iARd`=t)QvK$UGO;_;e*Da8N z1#{%{UT0>+f8CtD4}J#LNZsKOG5AEsLBR&KS4TI_UPTY0DYa&ld1NOIlMtHyfz^-u|~BX>kJ?tU+t7m#05{F7CFcuNNK}~)J18I1{iE_N-n_>t-iPi zLJ&SN+t5Gu{a!93a#>!HD~sqY0BQty7Ig5D#Su{(QP91r&M+*F8X&z075X4ku$f*$ z+E$;E*{=8DSb&f)KNjc2C zdkFDDwhZ{S(Ap~Qi$qy#S1W;o@8z5{w0spJeJkWqYo&zA>vm7CY{bcqY(7q2`QtDd z+N7!VUaCv&%Ei%~oW4IM4apPoO7a;K>C~0`--QEBwF(6s^woX5$p8%sI7}>%j?vkB z?KgN%vd}`?ldCdMg{&^3$0nNiBX5^cm=#{4yZ!l7(sm+XoaD^fw#2D!5iCUk$r7@@ zvdkbWCLw>B>%NT+2G^&!AhRnI(v8y;cBmjJHyi?r^PB799mFXkmKzyjuzeZ5jq0}i z{zssjuK&3-!rbuVV0`%JkHHc^Ff_6Ypu-v98PZBH?uH#nyBfvsXrxNavRs-E%ex2f zx^^sPivBvgLlvs5WVUA|-mpU1!XC5|=J>XI4|GW2lontyK-!TUxp9 z5eA_el&Nrksi`yHQSCFpg>GBT3(H}HN(#?rDLFm|8I@*z*G#0@8f@$KG-#Bou@xMYwOs)4((;OBPg<583o^21 zwHBW$hvohESs2AJx!oD-1Ajy7Ei!Xuo#VqXYC!LbZS3ank}vMOBs~f?U@A`M)skO&QNlG*)(Vt- zXB2=AmTYUVi&OwDb6J1|&_S9O?cVaIfB^_vowoyxBl9o}^)hfJjNjzsB5@)k@p3`|M zJHTkjxpETWSkj2iH2{3*u`8RnWZF{NRYyeP?B`^8UM7pv=BEdoDC3i6MlVH5$Bc|S zd1iN;rLO@7P?0oW+T#7D$MW_8oGnbnC)E^y(6;mm6;>pVV?DEiBY6oy$z$f&br~CA zNaj~Kmn(ap^}GERf#sB@&58uu_kXqbSw$^#C63uoiWRJj@zpIrM!Rjkb}jZ+_!3O0 zKFxN1fnPoxJb5jOj`qJm9sDQps-^h_xvq6S6KnFGZ^O06QWTD%cPVx9pe!ck6Maiwt z$j>!OVZ;eG}HfIUCRA5D{BYK-4)e-`L4*(M#L%pq{ zJWsD?Hyahc+IqL-du>qFY>_v6FmDD{HRoCx3UL}Lb(-24$f<4Z6wPOSO7ub5;ImHE zRx$IuwQcM29jv!Ma;o$1l-YyJgIEkV+lK&odOpAgb5LAO9Y|_fbG@+2a8U3;fdvO0 zu?{qM6!vD|Wq=00)8*8LQAz*os@1q#b8g??8zZ5&@3-FCvULXb0}DD=pC8*fu|Hc& zzj^*D*kC^ocZ#)e)T8&38@*1-)tB0Arq#{%L%{|WV$9W|!8cp(y7>&8HSFleiOoB# zL6#AXtXs%pcWA0X-ApF+^IjW^=8mRoRMCN`1hP`3jdtV^z(euhvqV(|fQMp3vs7x~ zI4zx74=`Q7W$Tj-#AaZ0DF~%iNH%j?Er4jL*%=D}P*5T0)$618POAxe?X#pH?c zEqRo*gZ!m0N)zdT0l@)>MsJLq>tuz99hH`}gkZ6dlB)}i258WVA-+HvumP$7cABW) zF2}k^RJH)+IbH)C%$IwA=mX4k(m&enJ0%q%Z#GR1ZS(}6=|4Beq;&dTvqlbw`SRRd za;E8vXzoN9Ox!p~C=iKX*#S%{ktRY7gs5+ssM4^c(%aG=4j8b(OKM_cU)SDV7~PaS z-dgkwl zt?3BsP3!WkJk7oJf^_8i5uA+5i{tC^wo?O$P^#D7eRq@@GGG5p4{BK@N0#c^jarNNNY7hR{^Yl$f31HYsW6T{{!`zy|4@ zUc>|`33`l_$BD8u^RA{E4bLo%;uKe;575Wc5@9Wz!u*Tm>sMuZZ60AuSQ70$5=tr% z#VPL^hW&DxiVar+;3YdrCT+D-`eLymKDd8JQ#x zNnNBwXFz`Io?ntYf}!XAS0p)38pLrF*1b)zQMX9DXh%B%y@>oQ@D{8rKu^^nsMkAb zK+x0U<^H_%9QV?W%=@7mZwyH(mXc1Ye#jcEiknzHyFsgC%rHPsjJV}e3gD>NAcN@$ zrC9&Ll&~eA2ta}BI0taRv(rK9279sxHt0`uww?rpt5`t8=7LEBxhwo$O0a%fE?3ds z(0d(6TSC0XKZd|#Leg$4`10X4i_w$4FIYb z%$Vz)eie&!zWXcoRxjdoujS?e4o(u-qb+h%-ZvaGfYM4)0S=M3C(fGBn}BL+%5qQu z;y3{HIC0O($^u|S0~IMc56j5cWlOPc64GouUexUcKFLGg?CJ^y>st)Cv>3kQJ}~1vm%{A9^Ua(oJLV8OnxZ+1EAp9@`90YROo z(`xm{b+c{UuK7itB@H^=&gbPQ`))BD_KgtgR#dRzs1_hHWY`Zl4r(_`ub6^{t5$#P zx*uf~+vS`%i1t>AURH6MP5T6ZQ7K2vOM8>ugjpR*HG_L}3A~Yjv~+jkoB@c|*(a%J zWZOgCF|Iu{cTyMB1`wuG&4$UOwJ-NI{wp(I=V>JiC2#FOtycC!g^Ct7(4Y$`gTel6 zvl-m5MBFMMsgBV;Q-C3eZcjQ3?VWa1iE#=ts2v0}==@7i-8U#3)xlu+;a=N4)^7lK zIhF(f_oZnf&XpJgnUFMf_7Uym-mZyIMw}Bw2GG9PuFA1;(nPwGq==}1z8ev`6uIiI zK#ycnI&x=FLUd94lS<=h6Nxr6waw?He>KXe@5G>a=lG+;(l`Hiyq}a^BmOu>S?Y6W z3{VvclFHrhAYXkmhmym~GPeDgshDAH^ZLK(Vhl%bNSevVe+WHe%LdW*jV;P?c^IgYlU`Q!Y|QS0^j-Hk;7v(317Q;YBE&ii6)HY7 z01%m_$-SqNh0Su%Sq60I>czo!$a5FZ$#6@zxmgE#6EZp10i$Gn?>BV$kq)Y~066Gb zN)h#qGy!fPQh0yh0hyegl*Q&L$+u7)WS5efR4F*!b{su4KrXbKTDW(0l%XYLcZk}~ zXT#FKK8Af^qvNzTp{}WR#}^DcQ=f+l{j3Y2Q+2Y00d+ywPkUvJXTu*V0N!{>Td|K0 zfP!CGy;_k*DwE_gd7SJT(yJ6?Yb06!Pq=>8yO<2M6Q}pG->ohxCYGrG~a-F!-=+ml!y6Q)S0_ zqbi8tbFMRXNvt&h^1z~Sbcz2|nko$gM9|%opta7KZX?a~38JBo&wt*eJ%j`M27N5E zn@Y+~ORKbush}lo4OPh!_a`_ri@FJOUCl)8k2L_d+7<;`h7fj)c2%TUnv{Q`O%Shj zZQEhDnDQoUVU!_N5927E^M#nY1(vmk8>N#}F@3&lRzo9IDZZ@bK}japafbc!BtnnQ zrRSt?=n>i%Dk_v3rO~q`^Dgd7AR08^<$rnb$0V4K%l5)0nGbKt1eJQ0qkYoFy%6`E zNvYwtQGJbn@a_365lB<7JXUI$lj(>3zM=nHz7^^ zrzF}rV6+@^IPM{3)?pek->g-BC@Ap3y%C7D$SZ&H3+DR@={?DDSVoV1lllBw|MWI> zP~vn#W|E7DHPiOL0rRt8x5KQoi3J!O==iKna|%jw{Hwo}^ohKAY3f8v|Fv^^*gn|c zf-Yl&sy1`sDB!|D+xGgy5{`EOOf4JGL6-^CfR|pthaJENWj*N2HU%Ga6sgMvqA=!G zywaR+mMEoWv$fLMy6hGeGSsG%Q|6t^|KmcE@L%NDq!vz1hx} z4$5q&N*yYDzHzb`@J2L9ofP{306+jqL_t))jzNkU+2d!R&R{@=!4D1IC^d7faM)kjI zJ1q9{_Iv#@UyAA1s&{g`_EAeLz8>Mo{rhu6szu!bE#*lnSCE_T(!sS3S#C7dXEDAA zBX(_n?B(`@9@@q&POOO+XS99jIJ!6$qP|O%5`?Zz$-_u{JKzJX2sUPb43LenwTuim zhs}Ul+s{cNw>HB;_e6mPWjojagT5;(L%{~il-;IfHxU6Qgku2>w3jstW^r0VR>VrHx^a3cG4pbGX)#g5IgK__sLL=*-*c{0^=^FG4~;EMGC?s)Zy$J zwYM2eYlRDs6Kw_t!dx+tw#oMm}+fkd8XyzMGe z0*`4o)a-{^U@);Q>l%=XXsFG!`Q(V(Rv>RZx;iS3*a`7<-a}ce^OB|vb#G%Q&mqUC z)L5)9x+3|LBjo=($e~}q0eoXft?x|? ze&?>tNua4ge2Ds$aP~K+oD6QD>Z6#igAX*>MMfz|I)w){9^QR)k4udG)#FY4s^_%%4l!l$0+S*Ia(3x$+ zP`~L<7cVVJE=V*LG08&Hwp`wQL7I04jU#7P=8unS=Js|=yhKHmT$ZwzaS6Rkq zF3R6ed{Ta<`B!Bvv1(Wjp_LYK<*B9-0!o2BlX9oz`LgBEHT(M!0~{dD!7o?VM`aL2 zc!!tB7fhbG*Dc?EuSZ^-dI>N|c??1o2T~*$Y|64V-idX<*hu*0mGvE&BS#K!u%B)v#!>&Tq&^C3T4-y~CAwK7cHp(2Jl&9Lp^p|6&ees@6OzK=?+m4=U~w0shNE0B3kbVAjn0SG zF2xjRh``Kh@CNpP2Ax0P?v%S4IyfgmY23afPoz8LBtn2?o@2c#>JU_XR3QQMOp#tv z1#n0pt~ksyH-W$^N*_3b1Ja7HXaiaZK7i0?lYcKwjQIlL2dHjQLPKLr9^YA(UMf>O z(5B*`qkGPU?t`8y1sSyRNTatwzO3U6?Tyt===W+-uZ8-ixR-p+Mg!glFPxL@V<%;v z=TI%^-azDggA^RfSTcrG@l0Fe!>&G|PL1W*SohpW-dcCG1~fd%vBeQz%`9J}z9nLJ z6;kt9w2^cM4{0)L6;dOkO3PCPNVRH=DcC^U zqF{q73O4v!hYkfA4g(i- z8;^Fq`@l9+zk98w>;?rKvb;h}*IN)!q4$Bu%q#^T)K(`iNOp6@Xgz4l&bnkr6eM6Y zZ8}W_8C0u**>noZP*A`@nGLnbmKldwoumQ{y1oJp+UjdrOl_R>+cC-_F)pM-&^foq zti8BB+kP``b$W9L$}>oQ&EC?ju+N0`uJ^r~0nvC@V9-t}()snzh<<*v+M9vlpywus zaOABLs#Rf0wfNR~$p_jb8NJ)E9#&mY$pIarG#LX;5jt~N?uz1L}Tba`P2*3 z5|2?1k(~KtDl1v?tzG0%XS|m4ut~1|HA@p2rXOIZBgQkwIW5kauMKO0W3d1TYmESd z&??bmUu8`e(_vYU1t6}A^dZ3PSFi!wKsA0njliO33WfbE;sk)uiV^ew zBezz#Dl>poL024SsSlCT9ca6VX~AxIah^POL?m6D1GPM2k>C0K?ea%|V6jmG)aw4p z-#;yRN^k0ZmKqw-Cct4JCSPxxD%!`4O_a-Boq%tY_S}k;C?^QOQC=bqBq_!9vi#_$ zX-fg<{-5s}HqKu%yGGw$!O22kGE;>02=Ihszu{{D$STRNyz*PJRHR%f!~QlZFO-Qk zJ3A4)%tG@Y?Up>1B6j;H#lO*OcBkwoZ=d6Az(Lj>3YAIUAQIbIAxc`Ekif2~r5J>P!cO(SO37?ZXPdOA)E6?DU>ifw(|08!ku(uDqi3OK4wpz*Now7?T} zFcTi1%wD)6qg@Vpbm&o;9=$9L;dP0XgMePB!te4l0XW16Bu{b;rU4vmR)aDcE^o}6 z?dqQ%+Q{R!W4~;*qb(hMCSB(^*K7?I!ZV~Uy^Wi6GEdn|L_wQz$h91=whzn2lvrm9 zm$|fo6Mb3wT*u7z1JUCWbt`}x1_)ULKzqnsi@&<^7kj^7KcQg5ae$AzTF!_ARHwdw z)#WS@Kge^-^Fw8fn2*#p(ka4^zDE-D$7)LN54pLA^9%}USJmo)25XM>SAm9Mz^5?; z`)sgbxN*dMnH|3*-qZcu4}gezL)$k>Z38xgL1zcrtuvq?gj$i3_i8tQ5NxM25Ja`c zAsdwMYl>T}gQie7&x=oo)9k3ey zrI~-&pA&6y1nalt?QPdPT!*c5JHDGEol4vJEz%=`=#5<2{GznRt@~u;BI@dsm=geHPsuG@RUM6Z4f_ zso{p@jyS^x88o#*@4dt!X_H6X5M71|DmfucOn+$u4G`q6 z6NJ2z;KI!gPCr8sZM@mmvz^BJ{n9~;+G}AP=yq(z!u|nv{kGri(`{itPWSZ=!k22Fl?Cu>!jT8= z8v}D%2AUPWbgq0#{Dum9ZAtyy!OI37R{a*}fX@R6ptMiGOMY8T!Iwdz)TT%0uMH~M z6y7Bm4Ta)X=htx9KO)ZA}& zdW8)%=n4vSTw^rQR@Q(9ou||KvdxV+pXy6^0X5utCp7X96n&!4ycBd$i!lI*1}x{> zUhmc3KYxD7FdGQaP&p}#GhaCH`KUiX$xAbGBEBgwDrIuPFKvyzvPF5ufxZ^W?h;ib zBJYL;5~Tt{xCS=t-w#VuLx-U--SN(oMB8zJ45*g}N!IHSC|HAcvo7_u9h7`_?)qT3 zPok|sH=jsIDKkqnn&YRO-_E8Jfo1@Y;4vvLJ|~scbF$*n05z@8uTv4SE2jsbz z9aPUi@GwB76}SQEOqQY-{bpJHlOTtBm%dWPsV~E*u{ZAS;k>*uZ?>PkxFo#~^-}b) zO)gXMA`NX`I_KpQIm(li;OJy~t~Kfnv^ZsQZNvC!eaFhOJ%!lx|L}lZd1+BT_&qoT zluLCq1DI&ViCWc(@A|}#n0@(^$37y@%)(#^6vQ8%G0&K*D=&qoW1{am4#*>9Eu>$3 z%b%cs!2u4U>+x-WI3}O`F!~dQ9)8iNDMVqN%r_HpXUjW|Y1#5yAW;DpjM{u*Qhs>& zXJm$Si%_8jRzN_UZ7GR*P#wn+*7eRL-e%GwQl$2%J)8bVd0XPWlFKm`f$`&SCa)hh z(CTE0Qk@9YQpKSAWTz~?7)O9o+XNhFzy=kMj4$Z3k5FP5ao0kIbdd&1Z0=BLtOKTp z7YFw^QS@=x3cWb<0g&LN{Rzbj^8c~- z9zd34*Lmi7-n;U?tGcV&oACp1fWZ)ip-2z}L9-!J6jvrTuvk)BGDL20DK1H^KvC2Z zq=H<96x;~KN~9DUq(BQhfM5tS-t@HZYE!=Im3iK~{m#voRj;b6+D*?4X4t2@UgpcZ zdDFbioA>yPk^tbdDC=o%-M~gs? zlk3N*v7M{_?D})&IDfe7`{c{lzE1cFmF{8vfhpSHBWPxD-84Ta*7*+C_Q%iufsbV- zcTRlh>S#EHW>`|IHA<339Ca2z=S1M7VK=P8a#>klGV5*NK^C3|Xeb$=;WLQh>hh1b zfd(yDnZdE*xi9#6<~S66UVShdY<7dGY?lukgz&ck2rY602*LfSfCJPN+N$e>m|1PL zH)0KF=r(>&OCFjeY>RixIl`vC-VW32C2XM#m^2b|!fH6GMQ>@!a%ir=e92G9!Qd^6 z-&KK?Y1xP z+U`~EtnL1Ha|IMwDDy&$w{;@4^Sl#eumJ|$z;e2FgiEko9k&FzW0Z*yCcfE#45o>u zBgxIErtAb=u3&+7?&)HiseV)a1`VT75J7buEKtJQR?90hLd&bBLyfXp<>o%v=G!Ys zK52Dtpn2#hJw&>xce1H4W~aSf84h}%?zB4r7j|y@PWrAP4C>_XmZjpYI8V){bF=$Q z^%*oBrdojBuYw6>;?y@{4_Uemx|}jyq%zwHJeZC6NX1W@Oc%;@IGqm4A_9O`+=9*k%I_+Pduo8tgLmv<2nXjR;@8E#RihPnq@a4AMO-sLI>u26Hp@~dK5sV3^ILV=s;0-n zatMIB02qqlc8p0!A&rMutN5JKM{cYPVv`={7AO}4EZS+gjf_M(mKO;}AdwP6iUu4&JsP@3#!;uLA(%PUzWce)G<=OIYtT`)<&wJ+CG5>0zA6GNP3mo6B{5BsgF) znrK%i8W|9er#(u@N=FH|hR$(uEyFg@l}VL(_z9~Ab!i@od~*(mtSVJ>8PbgAsQ4oJ ztt8;+7iY3156Zk;TOX2ADZfKkWKNX)b%D|7b$|QLij#Nu9e;lZLk_N2ZR^ z)~x|LAT;PZVCqV8o?WS$xMOa)63%e%mc{wffYH;?0=AH+4K(O7O|_Y*l4;^(je8Sx zts0Q0r(qwlE-V*FX^vM9pwQ*3;E4CiIRJp8bbM5>(OeirB+KX!sBJhn&b8}OtD#wS z;32tmJ_#sQm3>D%a&}`z-aUF;dY^etybnGoUpvoeYKAUt8}!iaCs!geHgZr-yJt-Z zg6bgr&6O7{NnbSEI3_=2 zrhoRzIT`o9C`nMr2XK;Ik)u)x^h?>-2kSwBG(a-PO8^dT=|^+pYS_B3l`%me4`C+r z2+NN0Hzr-A)HLZY+n|Km!Hy3-9t9lKc582T6enD4Rpe{mlE{PKLw1TjLJZDtK3fVn z#0cA(BYaEkMxcSLE`EzCrt3PLcLf{jDTL!-H*BEs((bYXK#uGRIDFyDSLE?WU>?P{ zp7)){y7RjYLex?z-X|&8UDmOD$Nfc9w22Vwldg}6ryYuMsd$x4c5~9>df2RAHJL8} z8v!7W!G7pLn`JL+0dh4~UXjn|r{sq?r+aP6C^-$A>H~-S5GP)f3;=_I3>laV<3q>H z?nVc;gCjW=-NholHy3b7mS8*bry`=?C*dPR8gKpz$AFl!t3SzvlUL{wu3W~k4 zru67X5KVihn|54tIHXbEeS*+C@wfi&1sv$Uw`Hac7)eZXSL&?9Q|I^m8EgiFt>F+r z7;Ff2^}S4i2RB*^-&ne*irrEL+d#uG;S*-~^nLQ}nGHtrobtispNr$MpOWn897G#u_w4@G%Ic?MTtNs4dsr}A1^X)?0 z=DF|Vh|Fo4%u3!Z6@()_(9fgYqy>XwUIScL&dl-ox_A$ui;xJBZNM7wxA&>jzxUVw zRF+|77;d47$Ce%!qnfE*D-fsWUa3#aJ*cu~P@oX{gCsWd>nGw_$kLU_)TEB{ShO zralE5)B-(8*&o{nYty>#>WD~wrF0m^3}IA$Qdx4*(-p7z9nR5#8Gs(Y3;^-Iv8N;+ z+upazo_$Un1N-F)p;$|oY3EQ8AD!%$R~ZdH{*V7xvIzkHK?g1U(yxj8!zW0o`fYOz znpRSsAGnMaf zZ3$CzJS5>_8%W?7oG>9=0M$BO0>G}2C1)-j5(7T4z89v`Zs0>x*-~7SYr#j&Zl^ZW zWOg3K-OpcInwQr|(c9y;LMF!BivMFfK!(meyd{7`by-&UiwCEqw2Y>Y0UO5e2yC#= zp%Zk-%vgPt*uEq7>U%z22V~dKiXNWn?%NEl$Xq*Tj^j>JQSJsbDDy0u&q_ss2CSun zB%UY)tyys%J4wcl-tRha!5ru=9|}6Ce;M2(v|^V{Zn~&W#vRpn$OG=&_n2;SsnT?u zxowQ;D4$v*xm~K`dABKo^-*nA+>0h=x)qpkSM>bJZILbkJ~%7TaEe z2B{e;Om%j_H)u-K1`66$Q0~2vpYW^N_sdp3Oz5qR1+ebTFfjaCqBTSd8z@Yvq@SvcC zvH?o8O&YpUqsOZB>Jc(LGzLe^J{?N?CSG1jMR5?*tLE${JszPFgWV%Cn_m~d(mt^= z+5!qRw2YAC)|GXj`QUI{BbF;g>o?VHV!r+xzy~`_&IU%b-sLs)6=>7Cdfn1Z*h519 z+H5ynztgmiIy8D^HS7j9Xc&bLrS_}WlQM{^bCQsXK7g7|%hfh$v!R${$hpZviBT?! zD6sJ7{eUSmpX)6Cj+*D~W!`r?AC5BbI@`+a3{3X-&Z##BxR1t8Xmwax2n05x8&LSI zcc+#!^Y-i2Y^e8&d7#WU`$ONiX~I0roy4H5?!5svv_aH^zHv$LAOs3Xw3L%z2Q^%` zze2}KYn(x)eHPt|Do(pKJ?t;b!UoI}Mx)n! zi=Yw}0|+#5e7ky%AtX8{SFX=XDxJX@jRC+#BuqG%0!WeW9=VRAi9D+eBJ3IGH;J!> zq>c!s8=*ySI3(4-{BLscDA$(XMwt@|HaxfdH#|SI8<>I($Gg6Vjz1}H>6ct-T{Fv7 z5ORnRiY#o?(&Cy-aJjAxZ!sGbxCr_W$_$JoAN{*!Xzhb5e*@*F4@WjEx!fH4sLH9X zA7sS(g7hNJt;~j0HX*}n)6$9{Vyk_(wuKw$P>@>FGBij48%9DC(hs=1l%I#W@f@qR z+O2j$GfB96Nc|oYZ{L1tEWKcsxdzd1xDDVTIdev85vS3Z&<*>1QCJTJQcqgo(<;|n^qC>H#?S5)|7T>wzVZ<>N zbWoe<+Aqf|h#aF;lIK2FR$Le1I}P3!YPAjPcT_m1P8iDwzq@C&+kg(|(>3|p#Z?15 zs4k0UjZo%;f(iM>xbc#~J{I3`msMYqGTV0<4B{8GrPtyWY358!|jUYnLuc6N-J`1Mj=*x^8Df z8)R5Vf5)D`)pixwTsISp{MO>%$RE7)yQZ2ydhGv}+VUb=J8s%=gt5uKl`LM8=!F;L zK%LOD_Rf6$lj7~&FRK8PKy1G+d=rr0jk~GG z-p0+qY!tmDqv>ETyg%K#0wtVwrS#gPZ}uAPG2o>)_GmXA5#S+RKr052L&TETtZjXc`;~q3N;rw z;&i#LH)VLqX&J+mVA&KD%9Uc>lzpQ=yf_f=bg zuNhmCChr>?bkOV*3zt`Acnaz?qLAwTq(jdr_D<960}bwhgZp4wuyTL5#hln#PQUAUR~f~w zv`4aQ%|ka7Q?Ma~@Z8c$O5!jZ>aM&rBWtouKRIte2FpphVZ*;=3o1AfK6FHMdZ$)Z zV1iNe%*d>Sll|yCj7Tn;mU;lBV261$T!Uq9?mahiuJ*%jo=xt_ul%~@l<8(q{W@u2 z+uM)C`(+>66b(|y*5dY3oLgdnucg~S5!?UOl|OH9u{E)QhtFUAtbBCxV_Pej`|77> z7%=dVm7*@c@-M98{nVfFJP`l?zkjSO>!q!8@N(=2963UKmbmvdM6)3A6J(+wt*9FE z9fVE5Jc`U9I>$0LtpkRQ0$@E|JYdQ^QJ-h(k~Mv{a@XY)X+Qk~h=LaATX0&_jRpMp zJ_9!B1{$^y8%8%`34z1e5E(oAH$xeoH#J%xPZA!XRN=+>BzhE=WwP%(s(vF|L6t(Km9gL!pay*K2 zU&wH6qX0Gz=_2gJU1r3W`qVVbgo;1Dy75^=C~Go>vpb_Ayo*&y4k0qyh#_F9Ayz(# zd!{5pXw`~qMZC2^?wykiOf_YtmO~d~aa$G&Q+db70Bo27jG6$BvjPn6sgJTeg+8UtYB(4fl!H-H z83)HZj>MX+01m17#z&-z7KGmYwvccHZJ52RyDLB{(c)b*9JF2q9lm_!X#n3Y`m|)= zAf2)Huf0JZ%#^se+o2WBb~?@`&x?!SMW>a8B|LuTmsitdA-X%zK|uz!Yh>@37&s;mPQBlN41fIG+--vmy1izqDDROPQ`g^9 z18hjoogqt66(G_lcFGsg?tH9eCiWds5%6Dp8<80S65!?79t>!>#lie*f9u zHoI)qs`3jj|C;>cv7bQL7nT|!6y@Z!9H6a>l3rHL1{!p}$~l(-p8FpCKK=T(v-rRv zvH@KrB=xkmx4YLRm9DCzotpMht?d`xup1kA*o(bj zvmG?d!kzaLH=dN%lix3nK}O!R1G@uwa3XfrkQby8S&)@QL?3bLC*Mc9GJpenEbEyo z>+^x}jTElM8Wojhn_!Cc>w1 z+(~!2v_}4K#Pt~cM~TmD=NPm`x*TEi^`;*Ne835Ddq{6cb_>H=BPE!oIQ#~&KQlF z0v+w)2Ee(4yx}<1j;VcQ8yG>S0O8AEfDvv#3ioAn04j2lbdzeuZ+}$+y%m|wjF7(_ z0H-Y`sOvn4AC4dHm)v|-vg=ti9ilQgrlA33T=KYOBelk8P(=#MJZs$NB?m&H?gq48 zQXax4(JUV`7gnXph`N^%GqpMw&K~VI3-?u&SNrlOVJL!k=+mD-ZzDlj6}}n)Xt1qz1(IWwp&v!?$bCnXj84 z+OO!?-R5$gY3T?!uXgQ}^~^Tq=$KcY{E>##axBS(WH4fZZe}B{(@&oJU!>Xr%KX-Y zKM!ld8gR(142jQ&Lzqa*nKlU$LKHL&Wgk>v5`^W?6#?L=ZE;u+;aYzFda4N+d=;62 zy%1XOBUR|D^8XZnOZu99GCO@;h8+{;5dPW0e<_8oq?zyhP$rH74jQX`ZhcigG&Ka^ zFw6BEK{U0@@-A7#DNs{~-!=`ac=*&&$(_wfVDzBO6ASIOWsG~w@+ua^L}G7JR8 zyy6ZWQJ#(*2_G@#7fWcVfB?OF;63u(wMFr=FBG+8A%E2p7Nx9Zx?k%qnhXyawuT!a z*{i9mgx5?q-+&Z_jbv>@_?Yp~!rF4xn#8;Qk$o~H0 z<{AIqQ$Map@m+7}jBn2h3rp zPD4e32FkU#uUZTwD67G?%=8&ic7rdrHwb5!{cfCkNTm^q57D<;y0@UicjL*v700wj z%VZZKuCIeUW&@!(Ql) z7Q$fa({s~$O<6Oq+o)s1676v*V4&alk0Jw8YrP4P&^3fmD43z;Efxba<~tpSHtnz-?m0pQz8g$|&pRWPHx0AU zPMND8f!XIiC!WWC0LSu&03D#wwP~yN{#*uhAlGsPe;y}?C|Gi&2RBo(y^FY9H3LEb zHrT?W_SCGr$%l%}D$7Bg9=i>>Ko)@?#7W)Fl#x)ZAilN^{gYUC=O`-ubP_o_DA!(M z#FxdrMwztr+d0Ct&td9rgAeWW<~LOBAhU&Lx6rPZ3wrK_nV_7Uj1k*jLI`(gv;2)9 z15UMb-K^h0pwbS7c&ix;A&G;(s9u2OAUE^ck;2LEKO!NV8iIK3$DbL4^EZqin^ zJ4W!3lV4ktL@!wy@{H<3w^!%!Oersqj%XKM9W{}c8Hs-Xq(u!b^_`H`(#!Ht@2tF5 zJR*#tZd$G2lhx~0Qf^L}W%-S~EM8ic48LEb8-;t)LH0+(@;`oIS{QgVpu?|xXcE!A zqNG+_q|PMIJil=xs2l{#3a|(&jzw|cfcKK2K*M@mU0o97{!_qcY2l(|R?z?%^+K~w zOKEIF2D9U`#>jjro`UUQIh>mZ*)dZGSUCG15nv>RR4r$L59#@)eCbm)`ME#b0vqg2 zB8(QF$hzbNj@))@$q!Hc5Axpi(~_pss(>Mqqf8NF!{OV=PLufP#5U_bbH_WV&p+ZgLn@@2*uL7dXulr@Wcu}U}*1e>m4V_lc@+@=N z)1HPDXB-j$`0I+*WbAMc><6!u$}ph_nQ=5jaQ$-E}x@)=oJnh}5==0SILpT`9u&PQ8{hFc=O&SDUBQPsmguzr@S;AGVw%7(6e(*~_ zFTQ$FEKnO>xwMapeL$N23|^goQFV>!1GGQyO}knsXpBOV!k4A8+EdG7=smacrxY`^T|gc!ZIZF-n_ zm<}-y+kWx^Mk6Eq zaep8pStK;b@1bF*&^qByez(bM!MSanzt&{Rpuyq<6mhmeX$AEh0M!G4N@gGJU0g_6 z&s>Uou%w|`WKYtt3)@PtQaa559PI_MNJ6IECnXWGK(rj%7HT#~F~$3~-UJZYVeTEE zM(1iDIsjGJ2g-K1*Op$MGgYP+34^r-(bDnXEMea!q(axWoeVQI>p2o#W*SCa zV1-fW_a>l6!=arAV!as-^+ErcDIyeQ;-l@o-utOJ9CJJzgLY=H`(VSpzs~n-eDkkB z1p1d|hxl&DRH6C?+n+0Km2}v0`&6AR+tCknjmX(nf>_2i8LytdfxVzt)Zs^3j*ejc z)xQ?ksgJ;Pco?qmkd$Yi+m_A3fDW+%iJ)rlp8tX@230KVMnJ&cx7K|p_9#NL3OZ=V zFWwHnu(z*s?(TLcU!DDY(cW*15qGcI#({H?LFlUN=p$`kAIC3H#T+ps!kI-0_nZ)~cA5a7Jvbx*JSSJV3(?KGB%m)R5ZUXrW*vuj z`GR=Hdt@zH2diLUL2?|xB!?4Q4jk@uLD8jl|-Ly^T}toX+C8K`g_ zlDzM5mIM3IZ9pdj+IiIXh%EZgh|}YjmHrD{uR#gV>j*z`k9{Q>9`Q-BocIXjFyShrVrthIy*tjeK7+nOuBkl-Jq|VN&E5r|unt=w zh!#TyT%ZbFLfMH5szq6LYVwBw6cpaFOtXM|;FJ3$z3-~Mh^ckYX3ztO0XX;&`KvI1 z5owNzk7s+Qc@Umdupu>e#Rx=ZDA3Kwb)wjkb#4beMQ2Kh<@}3Vj-M_w;hxq~@vo3d zw%{VOiAO$F_*ukB$k z=t4`L9q0MS$=}en6@gG6&UruSF}CBR$WnNq$cD{R*XFfriPuemZ_ZaQ}P78WM2uHq5 z8}{J#RrKf8YP|2nHEQOMIk;!BHGa^m>vh;YDjwUE<3lm&?Z|R*6*j~4${F*79)QuX zzfap0v`u{5+tQr?!%o1Wt~4sldeW`?uX6#dJ32>%PPO~jHh5vmv_BLCyc0_=>oWnP zXAz*G%O&H-4nWMichq@3mgMPgh%eD=zy|%UK*O62Iw;_eO4Hk&i~hsd`4dV!(T-osv@~_U(!X!e7&SAJ-Xli zx;fQQitO}j5=&(NX!hOJ@^P=48e3CrqYRNp5Q5xg^P=p(@XqNQ= zH0a~6VHE-5xJ%@c)R%I=YjvPDfd*50`-fw6R2&yCaD!WAH+TW5U4b5Y!sN)dbQW}( zrnb~Z#;_Hbv3Ll&hIu+AR{uw*3O?w&%wjg!Q!S?*G(A=qf>I=qGTYhpUeDKQq_or< zXYDp{a|h#HRs+7d;Qq}0EATeH0zsbnBG145vVW^iMS+j)X0IS{4kwWvML1MEB!rr39P-m2|U=KLmeglq2(`; zc5`4;55lhJPNxcxSjX(5<2WSGbIfXwwo~icS%PfwUe(n&4%Ncxg38;{cG^cftxj8n zkwqM%R#NIU92z=}{^kN252K7`7o{3bif3SZV@MQ*u}0p3PL|c(tL11*Y5%)XX!It0 z92?Z*$_DiBvo~lgj1V8Hql9Nui%@#4PO4So>;F_x!5*#slb=7ssH4`|kWc=*kBD#m z3_YU#l40aOs-v&8{VmcgmNdKrzc?`rg0hP_`wo#C+abksm(zAS}>W${G% z#1ppI3c6{qH%g~#Tmr2bln>8I>&V1bkGNfTKcjw66LO+}gIWM8yqcq@Dx=(M(fw1f z0*4LQU{>&P`dhOmwc_YOt5Y7~eW8DA3t0)AO9FsYpsm7MY5VRK00}jX$u7(jkB!)- zzVZ@-T}J55T8p?Kbn5PKT)wmXUu3rM46c7&8nFta-cOqd7tI6iMEj%(_*R;DTBbZt zA|mKv;FmO*V?jA!QuV1DuGs_J68cGxd}eW43dwcp+HZ|+R?uitut9+a+hV1boC)D@ zVfqHPEdcYT#@_@K^Z)etZ<5U*Ex&W^_qZpy{&aqS>01h6zV#kl8~)ho*vtrS^zz`vtCN?#c(nzuE)X5RzbcS$?_v%V-QNnp3jD z2F8f(Q{?_6Oa+w4-4;gt%jrKulj29r`fP<%aA{I$5?<4gWwT80X}2H8n2TJmjLHh> z0FU*ZH0&82QC%)fOLaajE7uZ)Qy?(ux+ebQl3Ow!djkq>4)WW@Bg}v1IbX-A%`11;wjDI z5P*c!hUt*k09ItLGra*)K zsdK)6{i@U#OAEr%Q= zY@<4#m#>rojL`bXmad@ha7-pUfCjs+oot29?^(o;OR$DI858z)_OxL$AgsriTZgpH zZBRnXSb#;Rx4`q%v0ec)HJ!TK;U6|2kzN64;Q3200W|a_WPH+3gLg;~ce9Otl-n9a zn+xt@CNq0MO!~_)>v^)PP|#s*mNd40V)<|A)7Yh6-_(Rfw84e-64|b(ZkhD7-3$(> zJvsV5iRY}r-+9`LzKH|kRj?j5!)~UGhK;k|Fsoj;_^dqfEC2BZMnmUz*=a&4dIou~ z+z1+WmU}zR2jj!{jOAdxE#FROf0xHvs8txPB=(e;8+`%j%hSc%A?)?G84l!xMo?z0 zSOWbD!f^1((SCRw8 z58(dp?q7j>cm?!XrKzwU)O%R(om6Fq8Cb1|c6zln(WwFq>abh%QM+wYCYIXwBJ8-$ zl)a#Tm;Y){wnJj>^Ek0FGG9DE$90T2iVtqfX0Zu6^iARnOv@0Xes>By*dT*_yq(8r z0}Z-N*$kSFxLCoV?kMDNGP35@v2SIf;bFJU3%fx92Nz((zUUN!bSVikGTcMhK&I+6 zPKaGrr|skx2x8LN=vDB70Y5Yx)JkL?$kb=#QqH1InuYBT1sX<4yQQp#?~wQV^;(2D zc|AX0DP*({>~mdb=Go67keN|H#j>m4p~Fb1gt1F$S;MjM@t&!)X3e4?0K@`wv0m%C z#B3HG%Y03M7j6wtlMzw(lngVv7pk2Aa5yV#7hmGm_>ieXKlGL_t}<$TU_xA}X-UpJ zD--)3-(0UVcBfhM%j^tz^!NL z;&Una_&=L8e|cDtIarWtdDstQu|8OlW$|Rol0n>YH!DFa*S{1DNplDBbNtUR0K6R5 zKwuE(ua8|?b*{HwCdz@hdbkJ!@e|y`IqMId0EZ_py->j=N`2*kgrWET>X)gkc?NOG zy8QF^_sL`ZAC?h7!ukc6IAaPnSTKxzTuTgeL}7~rxsB#LIA)Ow%z**`UEQ!P6m#$= zyTzBZ+6!+qC0RfU+cc|&_LVdgME}(Ed%C>AD0>&9+yFT|r=6y>{nN=y;^z8jO*#s1 zm^sY51B(W5SYUt0xD0C98Mn+`fK4zdh}EIZ03$TE&v#Hv#~|^~$eRVCbj- zV$N)*9|OJ#BLb-)Ll8}ZTLBIBhSP;hggZ=1l=PIiB(ed}IGp?-#^n5uStGQAT(eQ0j~36_nu-Z1pHCSstjM`S%0l!5-OcFk;b8y_p} z_HwVHt{veOsv%LVt&>%0aIa0aEpN1}cBS5c^;NTe+XjGyf)5R^16<8r`i`vg@>~gg2dda@Q*2{b5P3QK87py7WHw{++Ad*55UpBDZphKohRl>kO^f zdYiwkFUjr)pQmG>0zpeT=(@&pfcj{d1^UV)1r%U|hE!NDR5~&R)P+MKM$Hs-2*_l2 z#Qf>VTW4o@=iWYhenskrZJ;-KP*R9p#meNPZ`#e6wZ&HvYI`co3tU@io)QHP6ev)M zhs{)&1`N?h$SP#gVVVuBSC*YRQ&|sYdxg}sI?5>f;RS?Wv>T)~yqTts(OArSNxhl- zy}z&fSKy7h0^YEx7r4X98(pmO=}rJ#+?qk0u9&s8}ACrKK^#v zruG2w#DU<-p85{1az=WWzb+9N4B6-*z=u=jZ+PWt!*=Kd9o&Epn?jVm56VL5__p2J zAsz~rI&n%O`nDn5OT7>i-o4|J=C$XJ2ULMsptg0*Er`{L6Y8x16xXc|QIOHvxIb!I zN*5in**uvm&|yg72z2EcErs=<7W4L-_}&$0;A)M!)j@8y0R~G5PGqI@4CDU&rc7B@ z%(U~P6KuHC5DJ^MP+Z**_tm^iJpNv*>Tc^dR>#|eTDqDoLirCqa6tax6MqHRZIyY? zM-R!>#RVCk{=9tC`9aBYZzd99>2l%d5JusNS}cVOAq;A>IBWh^QTo}3b~RT(F^Z+2 zROvj|aTM#_nU0y|_J@X5batSF zupE-BE7E`RF_Xr$HT|?4>Yf+pU^n#nF5^7vwi;rQhV9UOyl+zo@<064!*T&2A*W-g zoUb3@$jARKbZJ6b${S9E_!x!vaKmsZ^s&fhIY+)Gh_TLv=cVXfKm$TM+dFP5`(Vw~ zT32ZhbD#dY9E`n3_B})f6f_{I2s7RAgjs0AverW3MjUlP8`(*GTxDw$5vY1~o4!)_V z{gL%FWf*qEXg11q>yvV1L&|;&wDT2-dna5&EkAx;0yy@+aQffMLWS@I{@GwdyDF}d zi~G?nCwm@{A2{$)`OE1)H>z2S3G%^j5=`nRtU zYtoRPI5{Xk|FL6|d-WN)MqK_X+8Hq)AvBSOJop${U0z+}UPPPXWS6`;f7(72IR$IS zgV5&fFdB3tb;vg|Y3{KSV8cPFrK?iOB~5*r9&^jM!LTxns*%V?EF}#T5GT#rxEHGqUv+p2gx2$Q6-)}vy zPJhcj#MMeoHp*Fr=U?iN=An!L7sB|&fyVJ^7uS@vhQ=5x3j2!E; zqJMgu`f$c>WeHeBDGjmN!|nuV=&V9PhXbU^RP7$itY^Hlj6H9$9994wtTx_Wt|y`F zhTUyRt~UH~sMl*)98G+KKEkGZ2q9tgQWk5Zg7pSrdc|n-Zhjs5qc>l_`~GQ%38}bU zv)K$T7*?ef3vf`ahTi>?WJZGVN46FHYw*6TH~Brdw`^8~3s!`J6RL%Ax3)qkMgPg& z!3M3Q40>47gBOGxyteaG}q%luwR5Dcxud?X>*HHSxkQqjg2UXi&T-9yCQ7 zu+{ve)!u0eG^i=qpf>*fKO@U$t#^W1aFdVK=e{D|{t@v-yKYkI?YSy|fMj;%!gm+o z@OS_Et-a5N{r2>iPcO*#0QmEG`&>YOfyIs1qKvEP)378 zJCdf=<~eG&lcpi3i~$o^Z@Svr!_pOIxmL(>U8G0Oa+~S!A|n7rL?Yh;--vzE$!K?s z?Q_0Ay?+JX?pMG^n1Y5?ycMR;RG1WSO4xQl#n$IuW9fGT8}#_z*B0^9`7*Rvbz(cU z2XwjGt+H8Y83hvb&l5i>+1Mdjfb9?{ugK5*(OCwM>+-oD%S#=mgDcCZQaKMD`(-)6 zUyAYyQh10ZuJad!uB)w>{)eo75&32C=YUf69?|k06 z{znn=bKul5jBC|NDzh9EcT+!GMg9gV>!<>behwilxt7)0u#Sd$5w;tJ*^mLez8zMB3gLzME0^2|560!O1BVC|u$jv-hCHH6^4XCN$D{<8tYl z4e<^%Ik$#X34^KT$r>?GmvfG9OBq_dh6UL7mGygC_n6s{`F*#(x43_iXq36aOJ?voIE=k^zlBG6dVVKLFWiJix zq0nldgx31W{y{j$zH87dNP<*4B?gq$%c!nUZR&#;>**q~+s4f_1p*I$KG(_D*~ zxpppx2e2tkcuSdU0!o4nm-or1zca%~eM?fbQD2x#$V0i>oK&42IrTtX zlG%A8h#WH8`nsG)1l!}S!diNm&2LG$9Lx zYnx^CdyM67)lk6JlCPrQKzerO%ZS#DJ+mrbcq||z!G3vc_`Nu-WkN{Ew3b4E)K6b6 zdqEnW9*K~VBtz4i=XvkmRVebb`1kJ(OoMJBf$~M43vl}@>HrFCC2O8#h>z}O$+LeWv3*Byxb@lG z+is=jaB=N|*;sWKl)<3Z1H-5{*k|V7y9c;nR`Su{ouSrPLF}-9K)|{gd}yG@v=uf{ zhK=A)Z2#SDR)cQq?IqNrXTQ$MlmF^3%=Bg-YtMg`a5bOA_8+}jvA69yHv+oB7^y%J zko3O}mV?z=-NVbEd7yK|6#ie&{g(Xwy)~hdQ z{nu$@32pM}tWdYtNlGjF%K9o+U;Lo}>;_)4+R?U|4R&W-OBwb8b*kxd7s!D&HVQ6S zORT9?qb}5%t@7VqRfTCDGF|i*So^Twf<52g<^3!0#$5qr7rX?+>8$|qZ2V4hjVI{L zBm&lpUcm&lB(!d2HAJA&>*&cMj@+FHH|}IF>~<9s>C>Wr3M72&FKPF@I6u&=f8bAZ z@`WElHv#b^FRIN7I;d&c5I{vPa)h*NI%-6N)NYfsrD`a^K-*ZQ!`^8&$l%{LGED2o zwnszCN84)HOpCYmb+eipj#^1ocO58uLxQN~U8v2EOOi|*h59;_`1Llxu(`?Rj0HA? zZU7r@#cHs@1+B<(f;Ov^1x7jJaU5jVf+XpqD^;}>2}tw(-vbCh%C2_NPJm%+>#aHM zH9MugCZfLu2oXRJ#G2P3B7`trJ5Iy)y>bQ(^LL(gUuUrn_d6l_o%!Lc7C zl%nC=M_>k;2z?d+vvz$JmcunE!5X;m!jM$SwgKcM{^!$@7!S*0Wmtx;F0ky&;@hQ1QqCUs-~I5LymaJiQHgE!Bo3k;_Q`wXaBD>KF~9=@#&zBh zo%3clTOYcL(`(VO&R1{}2<1qXt!s7Q@B8J-FOu1z2HU~(j4Y8yOT|gw;e;0GrQsd! zoCC}QoN}{&1s7bT7xlK;F%`!$PAn;90Yc)WvOPjaEp?d?hC^s`l;>qpih}mH)Ci?e zEA%FLFw87^9T_vDphKtv?HJI`cbFJ|2UHZ81)4!YO2cnXl~dFTqQ41I|$ zCz;h;Hm3}QM|HNt^n$3_Q|lXYanjx0E2md4;i!-}oNMODdrZhj0WnS|ItkZNdDrb; zAsbMV@T6*;EC#Ta{QM?8;efPK2w%_OZ0vK&A7A?{=iHEwA9|8hk{19P4x7vq?k1zL z+SS{`G;BgGnOc_jPCjBZBQ8C28DYc#=hu>HGG~l(_Jh%0*-yy85=@3M=RPuWJSWEy zoV%tZ)sFg=-Jn*X9UFvE^T?@xXuhY=toYr71on*BbE;|Si3SwMpY0NpLn(7E3@V1j}USF$fikAGxWz(LDvXhIgv zkQ-SJR&AFRXjnZ1qv5z5L>pwX=cp+~qmC4?ORftbJ^uj$dWR|tacF{msQ>{Kf zZ>A33+4ll`@FA*NK{54Kp$-OUSY7~R7$)n)2s#bcv6+JbY>>jm8#Ehq-6ku-h%c`Z zW)WoYwU_6^x|j7Ps1T}FH-H395-bQCd{7`l84~5J0s{_-wRKR;K5z1&phG5Ekk=M2 zix+|J?cjnH7NMaL?-_gVO={U%)$9Nnl*yp&nfJs4rhwr?u8~$25y?3Bx>`B4VlqtV z{9ZtVuDcymp~woM6wyNnw7vt`4fZuqpdmH;JYQRY4juQFUEnsR`UbnF=HaCnftvNh zlRpZzZ%w{A{|)NrRmIC%r%Elg!W)<;kFMpYb~^6udEQ@^GjDyS5h7B{&`H<=7IYo; zQi}3UBC<;vE0VRt3X=lUQk9}-<>05HB zPu0?Ohvvx4bOJi|xf-K@4C}?9{Y@g^W)fBK!ZKoyRAzt&u8V`w5^HNdO=yO$W1c?? zaL~27FKcSPv_D#SCHioAn3hrL))M;t%{J@&-|W5K-{1W!@Wx+(5HG|miu7-$c%(l5 z>WCd52+~P=2b>fztP=YnwLd{!AN;iL19OHcCsN@msQmeJo^_nkycxel@ zz4B=%UL6v9g_-hwvmx7@vC2?Z!G*eU!06yHuiw>j22`**e);m=0D~#q{!p;N!^m{D zb+vP`DA*8#sbI4j2GMR%i!utmGo9bI4f(P``oBP+Azj^wu_4H&rpya9W#+dwYK&}m znNDOyohHoZG+iSzZ+~RSb)SKvjrt{O>|GKY!VyDJy_F^{B$-2O>mKNqPy?zo=TXlV zq(G8dQXIM&-8nGT4PD-HeH3V@zV_Koz@cXx!8%_-atr4rOy|+%9VUPJtn?q= zZ^9tTl|jj)G2y)AkbTY>iI48HcWZ#ILb1STZCUyU_DLy7r~EU2A*BM~*W^J{E(Org zgpxeC@(l@8)+BJ?DRXZpuUI3;I<3MCJa)=@$D<&@C?(f}s{DD)zJyjqyanh`HK2n| zi)aC?_Fdx{*gi76d0^XSw(cKu1M$@?0jQ`~89mRRrvvSRlDl6%{wYQ;uLB-T5&xb? z;hcY}vab%aI(ho1+Xl%ipKT3rEy%TwE60&ZL9X!5Mo6)xOqmGjuqSa$XU&mRxDOm_ z^9ZfhN#N?0D8h~9H3SmhMHYk_@J|DJKa7V80E3ze`BmNPgo|XQ+fDWnMEcOPl*t%R z3E3*VxZk1S6-K75r^Y@*Gu7t<{fAmYuO~~IT~eBh$vWXF-2wU}I55>-j}#IB9c{B2 z4-Vg&O!-G8Sx=&eQD84t&Dy&PxkkDK04Tx&;PNqF*A-m4Ci!wLpBojx1Y%3A;6OuAdTGaUW1mqumFgcPJ@(lsa$X%OZVxdbMlb_Bt{Ug$61h*%^2!{7kfW>6N-o|ecU0;H)T zAeHT)aR-ws)3zw!PCec)Q)o44mYmD0u)Y9~gCJ`je%lK5_J?9twh1=MqK}Y+ZDE`C znY(=>^1iWmnMIqOs}O_z6CmTu2=SwdjwDrQ}97e0f#f>ZnOmF70V2xUSjAfe`D{^A;d2ew+UnF}p+^Liv! zNf_OQ+XWX)C497a@3a^MRwjc3;eRIs$DTIGV3)C{UA${k01kT7>U&hJvPL$aWx^%6 zmzB|=X0sa1m)rcvT|Oh8-XXx2$=lrG?OQAWcwWZ`zgxSffPpWygKI{|Ewm@TDJ{`rWVs6njR~4@?|g* zH(MU)+Wz2l9`ADAYUCA^P>{ho9^Fg3qh_WS>p<^Pt9m_V%S?(OX7$|E?AYrBqZWR0 zIWNWfC0r>;SeY5z1~U?!;8PL7%5%2rMbsptciG z@It}YzdJD-V9;$k=k@#Tgjsy?gDdjkr~7e`a`G1+WrW#7>N`Z&qM5UdG}95s0k%O0 ztbv zBs7HMT4UaPzmt!kf(?!kBEMT;!$_#h2tzhFI2DI{WYA&)+-|)JHk3*((k|}p3_H3! z27u5Go;ugI`P*uiF*?So2A{zZmvT(vgi6HXd()R|Yox8q>eVd5Xh8h9Nq7TzM6E;^ zMPEo(3B9;P47F-KsO^g~0+zhI2|9!ZVIBZDxSR9HY~bKCy4B)c9XimINA{uRap5}P z!>Y{BI_1pE7s(nBG{TR4jP!naqb|J%`eAff9qqv6R>w<0vl^)*t3jvCOgtq6D=$c5 z>GSND*xOh{(pM3sOCv(Kj)FT3hWP$E-~ygt+qxL(zPds-0XmbuSYs1(kcZbv@2C5- z?-f0Fd*0r+0PI>DwFCnY^jY%S ziAJ0gCMIPW;Dhnia!=SXvT z%mQ5e40`wp=hXew=0c3V@xDK}BLC+3-{$$Z_O59sfARexGEp25CoF|vWJxwOO(Tx6 zn$=I(x&l`A2$r&W_8Pqh20c>cumKzno+8{sM}bRGS-5u1gjOiqA$jCAIXcrVC-y%f zXD?ktWYlshw0VnH|AK~x(@ zMJIu0&~DIkaBV0n3ZRK};+`XU`i*O{R8)2iI18aLdXbWhvVZ!;-;`f?37M_T$Q+?4?M$== zY|y$CY*2=QrjgtrgrQJaflpEZD6o!C%h(q8TaM|F6X&M8i;xIL);Z5w$OW4=N*diO zq%bFZ3nA2*{AGKy&8e;VqWfAUtJ>GDy=)i_CM*Pi;3y@P&7h{oBmk48GTC`xJ2Y07 z#dT~_hR9xHTZwQmM?EZwut&0aSR{nE)aaAC5svi6C&bZpSo#S+n@4!JvHr4zmv{y} z-GoxWvT0j+XE+7F!~^hTTlr4WB;`%V7_|p>rEs%RvE%RYW42Xg>VT=gt8{lKq4G_v{RS zILl~cIyc*MUem_zwDjt-D5F6ih&ayf`0+<}7O}o7*r1swVh``WewU@Y0pB}|*f|j#9`jt zt~FOz%ssB5p)YoP*++< zGt{hQW>$Ob+q$LJ`mJTw3)vzWy3FWP)T<*!EFD@D)o8zFsg8~t18kV64Xz4WAS4HDU*^1LvV8YC_5B04A8}wKY_ukkj z>hgD@?R3`OiRXC#oVH&98$5h|&H^QGm+f$;V8iykZUG9)XsEzw@X_Av1~lkBsn!Wh z)Cjrow1uj6-m{(MI`?UZS#+geHQ^DR>vUd02V?Y?lnqh!5tk2(L7Q`*KP5h5{oHL` z2dkh}PBU86l<^SOP>UW`S&fzlWp*{ya41zR0HFb>pg@C~9(T};-kGmbE0f0_fT03K zbgsN?ExgHRrFaU`7@-Jcw@~v1U)?MnbSkqU4A^kREbAf^BN&8=43i<8(b!o)W7aFn zO|3>s!dsc@Hl)`P@f+P6Y`9q+HVdMx;%{o&tnya@h|sVMwFvZDHD|Mcg9eosvEwG} z7f){wz%vXwoGJ$(Mi`BUsl%27H`-2@nNF2BzLIF4H&Qz--*}M@Ynvt08zNR4u|N-? zgR^%+ipi8j4}e-=p=J)+_9clu@u)0ay^7-Zyj(n+H()_O;MjU}ReA@k->h~dB<|E2 zOb#3=`k+2jgY~gGJR#AIr1Z{TBgSJwd=FWn5(+x#pN35+gP{|2NQC@|x8@N`g(Vnk zn)R1+D2^jcxMlil={==W+wA!r%iHfueKwsqn$B2Rs)T}ioI(6|>zQi-=9y7_r!{Kn zYLQLES41ZyDC^S&(pu)kJ$h97U`ZCqh*A?qO4;pd^$eeuqpwHNfaZRn6@wDLMrIAD z?nw_qoP>1xQUtZ2uufV=HLIOqyBO4De7u%=CMN~m-~}24fU~Tzm9kBlbIK4 z^8fv0Op?wexjfe=FF9A_!q=}#;oO{j;FJ5MccezxMBWOUP^Es@7N*b0zV)|?`+DS5 z*AJMsz|vvaI;c)(UBz-QGO*PPcw4~9P}2~m(}&3*LZ*uS&(_Q`m)C07GfnP!p0$qx z7=Fvwlp~Cy>JRG}c~m}*^KaX08)s!53OaC(y-0rXk^cig1!Mz@dAgXlK!@Vmf~;id zw>8Ojz;Ezs?L|{20HD=royTo2S7&`C4u@6a)w%StXPu)X`0K145}@g#$22O<4{7&4`i9bzMqIAW9h8%YP_NQeOH7ACe8; zKYE{<{wL+Fd)92a>xa!|u+K}M4+T)(C^Lfa^q2LxbkI*zckX7Y@9@mZpYVKpVa-$! zwmmK%8GCBCqT7}o1atswK$E|?0?XmRZGaB;hP@H4Eq_kHRY~*yD8p<+{MaxbI8F!R z^cr@tieQW>Yj3#AR6z!{)asHXdIt@YA>M1X&$|@op>Kl?p{ZS0-|fQnc390CZQ1~@ z#o-Xxt$&({soy3u=}etN|NN84?ZjOLU&`2;Th#d@1yA z03nnOvC|Y}P%~f}AcF!8&ffl=#cujtfrd1PnQH4oczd>V8g9Db9xFAJLfH-Bk4F{qh3tr->~qCEp~%7GGJeK1srUkp%Yx_oVUyB^vpsUJp*frvL(8L zP}(Cmg0G$1*z?-nD99Pap^s(AD@}OBt;|l3=|MR^+JYwDq3h7Ot(GhGJV+;O5OBm2 zw$$}{9)S*E!QRF$qaedd2?zELOu+^<1srTs;K6}69uFZxNbG%g%j%SllBc^IEQ`NL zCrDt37Q2lr*FmLM!Jcz{BEvh%gG2zV}F(Xw~@oK7Babk;MfB9gj;W>y~`9 zE=8#Nwt48h78@Rk@SCgcM#a_To(&x#bC7`1a*|T848k>74d`7IG+kgg43i-wO*BH( zY%oOULzz|icfUkt2*4qHTY2wKKPG;}RTU`M$x_hrJ551{=z3DJ-F=dUb(p^DlI-;@ zZzwumqaF)*Pmyl9j5zaPf_FS4;w`c?oL>JDKMyy!p&9P1UBBzErf)~^ZRbT zESaevAROgUXy#6NU_bW&ApuPX007o>CQb2LIx3aZIT>|amdq3H#3BsiG0J^}B0bHsBDtHfGYSHU{WhVO8pIR|4EK%Zy}u zNVi#&`5aj*Mj4%hvLAI34nszT$jXAeGO(R8tLrtj<;_e%1~nC;w6!7BA-07Yb!ygA zZhw3q;&L?{!W_(wh`e%kU7r4Jm==Ux*qqG|{p&#kHt6s7ub%yN&XaHx=-X;uv=6k8 z_N(vym*%_up~tD_0Fy} zd`{AXbr}nvlqRf)^3qEt!$pmZ5QXSIsdz0)`bc6JCvp2brSlr;Z=mPb)6O37^p~dP zgCCmg+|@mw|MJ4WH+Aa$(d{}x+wYSHhaTFtpEDaz%R-w~;s&67|G-ZX;$q!9+Y0XT z<@=M>Tci(j#Z`TbgFdXy;@}v1RWX ze9Tms0)^HPiwy-IIFnQ*%~ZP-`VvwmgnqR^0ezuw;JhnS!YtwYCV;>a-uTvXo!2=N zilG(YtZP3~iwv7>RCm>h)aTiFO}ocqjgWIukf7JZntr`ja_WD z8C!)FWKdI}A;xI6nlckQ*$MW1=e=pdKiJg9k_&WlYW2#lfP-mM)JdXl1seRs6;~*) zR)M{c?E7Bvmscg0J3~sfy@3a1J1C1m84n&h%elS>pytzlwZ$ZrIiaSRE&^m*fWJ>H zJKq^Sv%v>D9i$CDXvh9$LXW1xTYOkxg9XxR(N+`TMn zLD$#lz{hP7}VeTQHH;t2c5D55NdTLBLWGE@+m>_x27r=pGf$0g9!GTZ9DZ16!r zheX&f*H^SZ!0(3;bE>>#KnJs6&kyUxu8{nVroXCr(e7BK_Xz_VjQqA79FuFtTHowJ zzB zV48VCO+za*dxp%%#aVSrbb7z!`ZpwuR>4RlCoiKLF{_y%9E+rH1Ps9Ode6X9#v$LR zT{U$m*svGqpg@DCh~@?9HSMqrv(YXeoJ zsN$*_ut5uLU0E75PSy*Pv@bYlw%_@oK*Ip(F1uhUsVS3TbtWV08A1()ls1npgy!a5 zw;ZQL*7}XKji?re#2cD1Ar-;mOMo*enIg=f6gmjll4QSx|3HI};%qoJ=uRm&L!t1 zfgheK%o0Kv-*U{H(1L8V1myjjJ*mLOl~?VVaf9^3sik?_kOLvtN}` zF)O`C9=L<8-Z@2mupBCg+v?w)fDSx%-yN19=1u<&#J7$o4HB!CA~(FIR!WuI^fhiC z3pB9GYCEP%?rQqkzBfDO)d^QdfHt^RM^ek$79|!=x6zbTY7t*R77^_!=^OAa%myFq zsa=JBPeKoTtgS?CGaZsVuo`X=fg)Macu&|I_IcQ#f~JR4w?IWA8^nuxRXmgW^k0n- zk^xTO93wOOV7}1+gMB_aRY&Qek&Yj&bLBgDzSF^b2cPEsQ`_YVXn4gdbQE+zUZ1s{ z&-pH!+}5I<>;{|Bpc~u3!d?~G>l(c4t#tNPAE?NJv;2;6w{(`=@^u zlYxgt3YwZFIDesG^QT}zr?8~571TUZc z`ZdlL3l(tKFkc&l&}(SG2hvfhRf$c#!m(j1)$! z>-;*ZlswuS2{K$L;9y%5%CK`SO{`&o`19R6*nK&oI>ZR8r~$UAfa^x}y4i>R4mY>G z*6+5Q!4239Wmf3`1y8m(8e;}zs1q*Hl00IA*GcIaH9(m@Er!iNNqtuIU98LWeXM|i z-c$Af!K$k&188boI~Oy$4Oucl=vDQl84n-Mi)R$ULDMf`YpT&Y8VjTaU8st?kLT0e zQ)oAt!)7h@2f25fX`HJ>!-!gXmb=8cZ2eZ1$$3IHtiDnm0HU0NJkE%FIhq21a7wK| zg~o?VMoHH=+VIOto3Z0fq4Uv7OCgh!Ao2gR_a0D^Wm$Rd z4wDh-A|k!7tSoQ3s;j3h(_?#jMgxNegr0-|J+TmoC!B(2G}o_qFx z{{3&VT?k}QTF(IB;I!&Chl4LbV(J=B^KJlzwpH6rs@1?IS1#LdvIp>n9{pWeTbx*- zJt1-K&F!uqe&omPmtXn?x7q^Wr%aj_fhiNk2%#a>c0^iR5xr|jiMX(wN7Q!8p1lAg zD0ySs&)WtUmTM~ghA1gL*I~;RqX(_fe=qd}>C$ZoHi+k-WQ&9aFF)s28>HvH6T!g; zdiEovMtW2J1T-w7akE(TuK#I{iMJJ?1-*kxKVszv*%i78fST8r2_qq7;`G9}+n%m7 zFlF9@6X?H8Z5_wGAriy6WuCa$<;8rQQBFe47N#jSZ@z}lldcK>j^l-jRs^+5| z@3N;Rzi2aSlVt5UX5X;)`|Qc-UjtyKuJVP4Z}-^5Y*1LmorYF4ZvXgD(w;c$f%lIe z_P(7br|@w|sfscTdKBw+6jltk*2L^oG++|WxN3-F!`{0dupqR3feGE= zf-PjPkOhhRK*-QM;DW$}z`EN#(4g06Df9v?bi?iwGa+C&9QD&d2J37$%v?W@qe*@J zD#3;-=PZ)|q@h*PM18PE#+UX3o4?y)Kqx$|RDrF)Fyd!kYKnNi$5s%c4m7ZB0S!&v zd)$89&kAXj(TtEFbIKQx+*o+)v6YLb$jY&3iKFl0{I~x;-g+x<{u0r6k`Tc&cLX?m z)j}#>?d4XsiVah^gg33q+%tbnUwl4E6^w;srOp`z;^G- z?Oc%dc%&{edpqTKDx(yh+c7V6e7y*U?e&ER-O+mOY#p^@S81E7UjmSGr+ozT1whDJ z8nhHz2)}5|bWuh~vfA8kvDu5kYBvne`Z_zOn_F*Kb~iI5B-f7MSsP&i=`^f?TLiO#bHDc3SJ$6GbA?Cx)_vvr$ zwgv(kWC9yjk|q8s)^qNx<%Wid&EEhDZfj%P6lz=6w-e#vj`sS}5RK|^n&xj$ht>E? zlg?4;=C;}&|J+sk*1wGt3s`@;u*)qL_z;+YgZyI1fd@MQ4=S5noOjv|^TT6~`4E5( zt(`UN-dC|?w>k?v`{L}M0rKhkZrqn(+2&!;7PwV8t);RET+aV3vUEhOr=tX=nT!rK zu8kJ)4a}ZQSUD4=7Z4?OLReOJDeNiO2cuZPFZ8-My#4l+N&9v>fy29o2p=)r6QtqB z5e*Nn*&<97SBf724H~Z!Vo{w+T6`p7mEH-mAJ9PdHs!Y8zEZQqwXn?$;fTO4K#kh% zyEcdeI+XFoSwa6oQ?OwGA_|F&Wkx*5Rs|ve4?`vNGNAg;H&<)ic$Dk80N^m-K!@|w6L#kM zHLIjN2eiJTKO%<1vC1A>peMZ4MMfIG{>hb=(DYKsFPC=hvR`^*AP``B?nQegbH(1j z>oLMEb~%7yk#z84H3TLAqni$-ANp<5uM!6GpZ~`F7EZRp9GJIE2{uKf2X@3Y!gUC# z4SQ`kt$UPknVx>KYV_V4Y|4K7#pmo>?t8F4-8dq)Ll(ww>(kHJ(hwqT#tU;dP1tw|@YTX=QzCX-cZ z^GCz0FjZE_^iZ;+o#^R=Hb9^5c5l4f7f8I>@#?*lH3D|ql`p`^&RjoheTUz(-Qt_i zBaB3uB?K^3R|$1*YeUC^3`lww2`7=QWvG7u#!`Lq)wVYQ5VEZRM7TicqZkbcs0wJ1 zZ2}vZo_qeYRvV;;o;FuM;>ouoeCjsDk8~Rro_fN49MO+jc01gY0%$4sRHS z!=2u-6#5N~mI9T2Eo^#e#h$&c2H5Lk`iQU6v+yazc|Hc7DGNXc={rn=U=C(x(RL`i ztQG@1Z-q1p!? zwrs{tMoE=fwgFooB}et#MGN^M7OyqPFw*2TE)G@u5wP+a&=uHV_do&=2-6gWc3x*F zY(7%e$q~vf=25Um81O~t%(B7uKI^B!*61K9l%m+>A-Vz|WHS@34s1vYm|!2>3ku7q zBBqJngCD2w3f{$!z=i}sLk^%}W@Np-g#wECF|-_B3&3F&c0>O)&nXK)hjUG5tTlV_}Lz1hwbM;+TCC?t>}&h1cWgmfBq z3b)91d5&c7^{Yb0`a^=tYOsV03Jz$}y<( zWT+Tww>}&UWdTu$MN@Z+wHzY-DJ!Q!tz@JSlcnag0(2}3#fz|SN)|@9sj}pO4WSNt z00lU>(}2(E0}YjdMKVMzStL&ekXome<7hv?N+}~uH4$02G8r<4*JC!vbDdlb+x6ZO zy@xO*ag-~RDTP)X(9o71)2Pa!VYX$yMCFT(N1gc7$G&{6k?O72+8=IDevZo8;6C|g&`dLmCCd&O`Wqe*+RCnAf!Y?g?jzhQ@`qH`+q$7 zg#EpHzqh`MoN(D-dDQaLPuq_@wZ^kRT1>)ozwHw*+mCoJTD8jP?@!! zFj<9ii-gwo+f^KG(i}KoV$%vtbe?^_K5Hp*k}hIcOnjq+Ih-XeX$vVJAL)J%f}-3o zE?2R6)ROedOY1>@5m*l`h)nJTJk&S-I*kZ$nq0YpGp^7mul{Hve0i{K$gVA2wcVX# zZqH4Gniv`ucWQxt!`-kPmKW1*cPk6?b`+OoE*G`FL@OzP5d~gZ03kAakiV0Yv-at1 zKXnXxJ)Lwc5^BU##C@1T^I_EbcDE4r(Cz$yYGu%7=PucNsmnfnHE(^61!0Z_AzZfS zHnp2>_c~;*pR?5Pk=re9wR!J*TJ=RQrNbbQA-a3dw%ce;A;gPol$l%I5p)O`4RJDG z$qLGf0mCEI3DXN8URH$#(G=;VUcNC5L#Z)mtLU1LzV=Q4hupbWtVGCY|FBn=xE;NX ze*`q-CZ17~(qgxv=kTTl9A4{tyqwDuyF>*f ztUPQ6fenEveWsWWd(-1Kl|lu+nU@o4WZaL_j3`c14vW{iK)8#X+8~$`mfTG<=@6kO zuh8Q*O@oF{g?kuWJ0#e_s~qa%^Y#p*cn4&7m5r zmd5EeCeR^*nD0p_m{G#j&iYw+wkdksvj*e)0eJE@8a^gptbTl|!JB;z3T=!iGdoHr z&$%Ve%oE6=A-Zuk8dEr)mW|S|n^~c!oe;bJL@!J!yWG(7_6P0dGc0ThZH{1$5EcNcTqv>l1_%)BggmdcGwNUUT=S>XCc<{v>omrbKykZQ^GIL*ZMW@Ubc@F zEOd>=EHm|b05decCOk{tT8XA4ESnCydS=c_JR=7lT(@VQ^EyS!giFlDL-t$$_8EHU z0H^r-oB!;6R+&y%H>`V!eKT?{ zQ`Oz1>qYNE-~yHXz_nOxo*=X)vth=5wjot{R{Tbzo~$q-fDxbd#rnU z!45{o0U7`q25tlz>KpWHIHi@}YB^HQuM(3UA)G??t$+B}>qY+Edw$VUgji%?$&}G6 zh_%Ko29W&l$mV^mJ^O-1V0py%jiC=A2$2w7fR!X2dkguDwZzd3<6kc!bL;tnP0o4e zAM9u`O9e}+O|vmeLW$p%{WQXt=tk7y{59J$f5-{j#i=W9m9A}6{kwdiIN0Xh36%`&8BSiT}-~F%L=B_PW19Z3s=&+l!!KsC*(PN}JMO5%+wm`3b-Po?|GNDS< z%EtUO0NPe-jkIvz0SyV$TZhRo>zFe&zyWW6#T5V#$TWi;dPe~V{E&yo4()f-mD+$k zGyjy8_-01w2#As*`aliA$>lXPSvW^BT%b&9y$Z45-pJ;2yv*1u*O%N=Gf#%RUO)&5 zQTCy=wi5s$O%6-EZa1xuzFRvEf9%InUyqqkg@wK)`wk z_9OO#tRLaKS=9DruP4~BokJieC}2K#vF17ooa}mKfOA1(ra-%2!*xGBrApmktdnY2 z!_upB;sPeHc>kkTK26um_2(?=GXONd&SZ$O4q7~~#>Pr<%ZErY93C|#mk84GJ_?5SYIh63z`Bl3Ib$Y|$IzOATRnDP34tPlF!2}r?N~qu8GdxSf zy2%E_YVhM*w;pRBvCMNB=%mGIiz5!XjAa>n*a~L_V-`m_(S%+KXbx?4FVi?vUqldu!^0Q-r0PQ*$^V8{4d;!H-;?eVZlM9 zQL2nl7qidW{^UdTohYPYsb{yHNT0K7(Y?&~o{@TihLJ|#p$dbe22&v6hfD};2rQnS zLmvb>{#1gH47@eIZ5)8bhTCa?OZUKQqzC0+-OvlVZ1YpM38t5`A}4E|u#i%w$*~(E zFio1!1*jY!bl==I;^SKp1YD-GVq&gp$3}P4u=g6s0tbqK4W1Fknc)8Ccn+~qdf8o8 zdYsWHz`_&1{jxpu-S;D0Naq9D6^eYb^>-cMau^oW<(FpALh#<7Hb1_9b-kT;06|wo z6jxy+6>@+LY&!x~cp1QvcNtqz}cbbOV2p*pKU-AegkE3c1OOXt(|dAri1HrI8@T6q>KHA0GFQ$*-iw=O`W&RD{l(cw~mv#!g`>Xf}}*9V;zgVvUAg`~m8up^-pCvBAFh%abq_ zvi2R5e_~(y7vE-!T#pR;5v|cyG=@mgiw;^FVxR-DL##!(uJ4F7Yyy^y_gv**+FWs( z5~ti^kFC+=!fOP`j--xJhc#PUZ;S2fym`g5w6bh1A?igK4=u0_S^yj_*dzXKYj%Fh z1B)7u!+{nJGdxg2^$zCUsJcV$&Gg!{3orx9v4YJ}FGqNeWd{ca?dKl^!Mqi%!Ug88d$ZqiMr|_V?(Q_of0Mz@O;`oy|2e>?vcOSLV>bK{tPC z5d;AXE43|ER42fTY;bs&?Qvt9e}3!P2|Li&N0`<2j`i2K|2=yl{YN;*F&^_{wg9-$ z+fB^`JEuB6K~+mllI%lAhL0l8cQ3$#?6Jce;;Lyt2mugt4CfJ1?M;y?nlktJS`a&d z5N<>MPhn+|P>S<}5Co8A=FCaK!YD9v3Z5toAN-1f(x%rI5^#g z+>#~kx!*d5#%}Kf-X^62prJ^3MKFHDsn1?;v(yrL+ggDZ_HqV#B)Q%Bdbrw4J$|ip z2dAe-Z!i7ap@2Cz?Hi~sb_#D8%P~)<#U_*z0%*_A` z0Yk$TR9Wg2)sYGa@O6$qpAir=AupVPn&_ z*l{0Ch>g!zjh^`RXweGL|D{wJYKE4HfL0S~{0Ha_O~eODc}VVZ6#g46jtJpwC2O&A zA3~9H2&=U;F1}%WZmviVAgl)T8VYMO}`OR6v4${)b<*zxzwaZrFb?BR4c;Nx+@g(q0I?jAnuNz3`gy zTemJCUa|cZz5@kB*e=e{z9d)Dn&GxZU?nN9;OGiDSGXbcWg`*pbocwB0k5}E$SyqFPbhe3^F)I$t*?N1Oq%9@}vcAAE$Tnkon41jWT9^hi$u_|IYQo+LNh#@#x z15~XR3)VRVhz0xVfB*OkRwWcF1mob($|LsJKl)GB^jkmAzM>8!kZ|eMpZ*@ESdY6` z`_BK$u!-f979u-PsyXF=lF>_fdpbUDk%g=SWg5jF^MrYXV8F<#XoW=yujpwXt{?AC zBb`25p`8pDvIOZhFU;qxKYzwej}6~%(H*oNTmgO-0WE^}vpx@}FR;_;SKQlM0Z2ZY zxW|sh?gK;u&R??L z4tBt+ur%ud4c*ZCWgY&oq4>6=_mZvR37Amd^3dk?e`Ddj$p;;|I``gjB3Plb>QM(QZB=gu0p2PNbyf*Kg^Bdy|D4av@ zYBuP(@eeB=;d4tJb5C!(YZ%FK*lL=mYCr&l0E7;o#h`L|WwP*XC*2Y?_E*bko6W1| zYKICX*vM@RkJ2dE02#JlL0K}gJzY@H)y0W#M6Yb6+-1?yip}DLBuO)tYKBpu6vy*T z)`DPLDUK30Y>R4!IQcFb--zr6Rwbrg*4@;+syz66&Zf^IZGct^qS~6XnP8! zsJU>L0BjGQieRWxo`p$2Cnx=$&c}jT^n8PkYw!B_w?Og!fS z3LSgPP+q-fF5$!^=h1cOweclVI$k5J;`mcm} zP209&Gs!%e8~QCjL8k^G3KO4Nup=KGwD4}|>;*yzs!a%;u0ah(1%25AD}wFx^tW1i zb`4tmymispFk_x$6N2UNiGO*)BE07X!YSHPfLaW{@!vm=6FKKPQ&#)Pr~oVH_)ssP zBJ3R4k87EVWqrM>-c-@L(J@%-MJr<0{iG-tXi&3>!gX6JAs*^C5_DY>2(q>#%-Y-R zU5|Y20!)Uy0~-V~$foHHZM6M~y@;^RTQ5%hvSS^24ohR1V7w#rE}LGA*+-AJIH!B& z^2@fE^4`Y);0jm_*GaD{z(L^Gomyv>>(G$7LV&|eVbC7!?EfVB}@zXqcTG{z(GKp_^z3JjQ|G{7^{XKb$HU3QJ|im~pT9jd(y zFz{}ByyLo^MbOr&No&K&`G-$_8>x@QXerv?zwi5f_D2ZKff36RidLcrc5)42^p!bl zY4@Jh2%zMj{qSXrUMGDfz|_7U`zU%JYgXMiZ1+6%yWXYX5x(@wXQAO&oqkQ|yZ#%1 z1J67mDj(YaH21V>*Cr0xzQq=6efWNFoh`ot9Ih=A62of3#@Z9;NW>4h-r+4pZ#nsC z0I?6g5AoC)4=^f``J*W@Xf1@2+zNbX-1ynW({=DjER4flM;jN{#{<0(Wvhrs+HL?9 zu1r1aGQ2hOjTmh|QD1MTk);gaGS^@oLBjkx&jjy83#laY47&vWHS2HVd$QAZwwr&- zPnCHO87#K5AcKThc2-`ox%rEB5{(Dhy<$A56Oyb-HP{AX95@sBAcnyf@WETS`Bw~x zwa_`cG6`_NIi`IUgi7@SpzdixA8vZwQk^3%R3ZQv#AbLkEQZZTvGdg06wd3)c)h?@ zMniqUO-7#lk#7VvBSkG87*Pab-~W@p?@DLa7ilvs+Atb0VnE2MFuepo$ks1SQm-#r z|Gke`yglXSz4No>3ZTzbflU?R+M28Td5C<*kM$j>7Y6@+`&owcPx^D|ufzt>8wS{* zyy6iAgzlqV{&_Eq?JIV1`ujm85sQ>y_V|uRV^6KN^>(*riG29BlM@y!e`omSxB?ZW45KQL zxd9D{C>aZ6WoXW_sGsBAO}xQ=#5|Hfq8z)xq#yCna7UfN5X=vTft*2*nPOGSU=mRD z&o?O7IQ*xd`!+W@OPaBV`rmIK7=5&H@m-G#WRTqoXb5(oY#7ZGGgbg_$aIgWlh@+a zPMafkw3kL?Z(Bc35REcAf$}{x#@vcHJmDEz4KGZi?xyQVTJIzB^q=k zvEH!UQjP{DHMb6RO>aErjZ%%vJqJ$3`>(&pqGLs(>-(U>cX1A| zIL7HYqz5cHLZbjHA@akMS~ z>Up-~Nw(qdj$GpM8Ba zLbX@6--BIoc6rM75mpj|z0eg)+J)5#x5-N`1BG`lKECg-Ibh)8g|xlrz=QR4kYz%C zmPxx;Tzt_=9s6x>XR8hKY<+RLYKM4U|KGFUW4}V%;l7>+?EU?J!+!C?&pNhFfexn@ zL>OPd8UGK@{S@_9hyCLd{|pCcl?zOo-tcbft1!A0IEFDCChB+s<;->w+H?8}Djr_l z87)U_cN^(kVXz5M@L(9nZ37hY0&IE{tGtua4uQEbav2?fkMDTN<`9|GyWzKzd4|vh zi=pYz&;z{V%G)duu#njIF}I`)>p`~0IS1fF>rKFi0B8`%a5JF6ZImC0M~dYjFj#ff zRvW~rJXefazPt|Lu(5Q#Q1k!=W~~?2@#8(fsRhiKyqzH4^0d#QNU!Ud!blv)T58LB|?KV%CX25uGz=Jd$^e6BE#)JL@2yDlC zXgppq8pi2={lMXk^O(QvJ@?(YS;DOHfUcwVIl$C3+HmS1)q~LoVAsQYBaPYTTZl9gc0IYTc+);e^+4e#bh?1speY*-4X5a6)FvPAP>eX|79S?1ic7Vhu0 z{=I~V0qB(~YXErEl~^MSqQ27GVFYl&nM6(laDne-gfO~_61$GwM>}ZaIcmHlZ!3mF zc9GOj`~|EASCaxXsA*C|l{s8iNP^i*s5P%r8LN_gy-m)6oPr=sK|+xN69PbCYtOol zN1%jkJ7B^sxWk{OQr<2h&T?j0wm8|aomTm4=?MX1EZi33q3=&mx;-`(LYDjB zeU`n6UPEwfjl;xsTBlfUc>9lgW7%7cpIZ7X4ob!S=_%18a;rNjmV+AdTLmP=+FK#s zxT}40FMq&f*e-2raD2*MAj@1qByxgOjJunxV-KM}g|xLVkVcK3^ohkimhM}&r7%o~ z5FhHBSP15V$J*GYkpBWiiOIJ@Goc_4lZhh<6}sI*KB5{-HHqO`s!UH z&GzrVFoCm6Cr3){zz ze4Z(N6SD4pl`Dv}Ms8&6=vdCoTylq`=Sd+N?Ks<^*u2HV{WzhjC-l%s?}R!`LXC7+ zY1|F@RVvN|PP)oG^~5!M{A+i176m{A;6oJVLmS}3JenApNYpVJZUCJAjyG$l%fO8@Js!etE&`72n(KIiiir z?qqx(rpjl|%-i2;c%Fjw#c)_&ye(D!(&rh{AI7C)J*r|r~l{3OrS zn4S3UpV(A7S7$lwug_n-aEi<|SFQcvaqAcyJ5-wT?UzOl}%({%USo^wyckLWjvNc#UvXkE&2=bRpHE zoIaIIm|neFg^GrSZaEdJV&1+ICaPkiqP?le-R^h^WPP-!h4f|}WVrxATqCFR8px<`DdM||KH!jln}QEs8q5s{O7ZJ~ZFi4? zWpB0N)}C(x5CP@>?Q|W!8uwKp69N&`IV_M;)5l;nI8%d=Doa`n$4If+SbOQCEmNAv zzybh4(<}7g2SQVg^8*&oIfTTrWwW1t`ro+t?F=G|M7XmLqJg0~#%K_4nD!Cuabxb* zvHq3=_CWR;^j@4jb(YBIL;oQ@>wna?u5gR#3YmJsP_nYiP~+3nHatcndvL=6oF^t) zanb#4(4P5ojZs1&L{aq`rn7a*t->if`rB#nqp7eq07ag-Whk?2eh=~>VwGo}yy}if z`RNDtbwOiC0esX0AtH1g)}U_cA9riVBcJ=|aknfMXz>f$1n-FenvNJDu;8&SYiVBQ{nc`)ep+hINRwZ-Y#?{#lXy_dA z8r;ntv@d3OxL;KxKWLT!RwMxgYhJWQm zk6rlUbz9G&mp~fDxg#Dx)u|4V$sv2j$yzF>P$-CyFlRw&03K&f;bB7gdfH52j@IZy9CM8&c1`gVKj z*Z;ZOL?^@*JvQp+Lvh_0zDT(w5kDz%ro& z=rUw&wOX?AB56xW@2QBlMY0t{SH%`tvgSy;sUB&xEW(ds;1*#iUVnFE6Xrf@lq2~5!$uOXFbeYZTy;5Uw8o)Oq127r!7nvd5jQ|8kuwS9S-g0IcV8< zt}|bL+WHT@yS~>(eFw1`4zr&|TOi|~Y#t!u^dyxgwtx>I>v7q3$aVq{(X$1N5YQ0l zGpMY>CZb(?G+MV*AKwJhW#azaG3b!Fe9|8H2mgU^Jr78!Z)>yN)noqy$;J^ z_=ciyX}}>jzlg9G>dpM=BjoklFa$A`MW}%N`qW#luQh3B?xmsd%n~t|!9rL1_5G2W zy}}j*9;^?JSSO(qdW`d1KBmD@2xxF27Jw6tjEG?Qs~L*Xpg%Dj1U_gOupOGHIK+0y zrl`Q)=Js~(-(qs){RgYJhw2n}TcfZ!(hNza~{9e$q`=^?yM?`g~W ztaW8x_Sp@Cr1E>0#St3{lYZ)UYz69G6KTO@C0g65^rRh%U2@b^n|cnRts%SG(rqho za%TfH^tO794T*#`0u4dDZy+8PN4>hGGD7`3iLOtZO_1Zh1KR1mV?%cO^VevId*`Wp z(CY&YI)*hIi8WI1C1SmRLf(fuh`-%Q=RqJticA)F3OW=B0eP)V*(Yi^EjKqkSsVAa ziZf8CLCN>S%Yvs-AcH`$i?g0rd!#u{zZ{K39M%Y-BeiNPJw+|-k3WXth*;up~1 zkW5S6|7x`h@Ii8Bo zFCeVggBHU35)awzrHn26EH~{>Ktso<8tOG$9?#qKD>*mMn7=fM=EP9U675x+Vp$%r zL4boS-rZuI{Uw_@4In|7Lt=nlMjro>6|#bm%CLeK$$JlZz*Um0811utIF=!M=%M|# zt3PXB_(Pmd|9SKWZo4(qH5F>ny(?zAs+X3%>+{@n%|5aRy$3%=xrk6&A~$I@Gz-Z6 zU`;eC3q@M=01u_11TaMti~}Mr&}V2uxKNhD`PoxFMz)7MLSfD5I&_&>5DIxv$V31( zsO-;t`Cr)v14?r|3%~|VX+7CRLL~x=Hq)Rd#NmLSGBB9mI9!5pv;@GHscm$SXnDAA z)jD{91U4)KG%V*lV50}8{NzU;wugS=*I;sbb*0MhYaJoorq`yK!#=o~o`c?|Nt(Hv595$jY!1Bh-nk0cijWn`gb2N&Dpa-yqfJqK&W3y85|5 zJsRe^$`VcO)J24Naq~vnciR*oZh}ybXsL-*ywlXV2$1F{?H4cosQuu({*fI443UUu zxJ1^Dj$Kxxet&P__v}kUeb&l5`5Yb5kN?5X0SxE_6VyXh`{dO>vHQFK3gCv6`MbzW z@i7a>{{TG@g&Gv>>f#l5ERCSU6d@$z>wq#ocp%<_`zLULV->a-B6CT|Pj}=(lpPSQ z?@drvSIK+-v;)mu;~ofbnBbvWM{HO^0SYfRlxb8HSqPa=qT;y#e76_>_x<8G04W%6Z94I>m8IGwjT z0G0=A1`Q8y@i_x9A`KXEX3~3S12Cd9D&|DDb&@G)Xzy{iC+};pd8Yy$T-w=YSdrUk zEp0CCO=V#>G6i-vxWIdH8{onX7lj!D8pLwYb%<^O9X8jX+pYa3oVPNY zw!*3hH1H*+QsgB3N=+A`7NE)0m~L9j02=gHF9@ z3&Jb5mJ>K}xo6Zd9U8M(Y%O=U(*hPUe*SL(4lO?TAVx%BEifU1aEr3ff(Vwqm4|Y; zJ=TNQ5PU12>+PId06=UV%GuStAQaj&h- zC$Sf1Uvc+o9??f{`@vqk^}jsx9KE7lq;ng0Gt>wb%R#0Piw$e6(WK0Nz%2@>*-iBR zC7%6D%k4YD-%(oILzeFvVIyt3cr{@a(r-{`M4nzu@2T3@x9;Y~)W(=QHWs!B74S`&B^tve zcK7D(bEms(_t9=^0jO@Fk(rI3Kp3&;QYMPp5~L-#gNUGI0v2_r1WQ*&&G@LHJk zdNoVoATLdaZ1(w(l?S<(NgQ%Ss@h`*Z9Xy%oPl5}?lLYm>jp=yr<89xEEDy=k~r1v zs4uk3`oqH(QroJFtW=k()-zBg?P8SPtu!GqIBH-yO>Nur%=a#i2I+-U!SEV^;EA-L_V8hlGgNzjyU@d99(WLhv z6Uk-?FklZ^BXpLs%xZ`%7dU5L5eGa(X<+vNPMpQ*k~Kf{h*hp$wKCI5(vJ@VHWZ2J zkCVP}xf}*$@fbQP+tf_A?>4rym?!*(?Ey3>{bRZFURxzw!YU}$RQ`gEPkz>|^~*2) zGWUqE7~T)vn~|1Ln^@B`G(#vuv&}QqlRD5k+ixDV zA=<9fAN~a!`Qm@HLh=W!>B`TL8g<0N*PpVl`SPMY`3*nIJ_&CKp@k9d6 zmo4KH*FIst@`3;6j;Vly96$JRsOm4+tf0!?W4Y_)KV9`Rz1dfn4= z2XQ)`P}~GQMDU3OelX_8qmY8@`Kafeyd(>?Euln1^@Z|RvAog=b5me%A~8q=ITd7r$2(Z4o8}tO%JC zp(N#{vj!G~H{;g70T_`g&DuU*9mi+@RS=tDsf1`QpinxyYG)_Cb6rr19jv<^rqRZD zD`A)+8y{b=(b4W(f)0gDnh?ELtndC0-Shy(Y!Kj(spYKL(`SQ(QQWR=1s^J~9`qO3 zR~3A4;DUcFt1wQ)WDuCp=0CIj`$5V!w~d11zMEkNU8zznY55Gyt_agARv5{v;rIAC1hX)|x<_ zUD4O%M|ur`+H|=;<$aVr9wCuGGvCuZRnm~n;4?bvF zv<^TJ6UwwMQ<)v(O^aBGA+MkVo%3H**_OD)SHA!VtPQBfY;*iKO>8i(n zYp%{ipokqmVevhrB0Kq#rC$1i#Xs~BZzVOVI-v%uEInEUXw5p+rfqZ`0-#}~5V7H_ zr|j~W*D!F*25)OO{t9%+e$tyS@L}*lTEc@4V3^BacJtLJ)p$#F$WER2DD_2R+Y9{d zIxt6XbCTHQHaqhEF?SEO((V&v%0N^rovzZrr_r2%zD`HY@_M0OmwP+aZwpQb=ZQ&+ zK;T0FJc#9xyx#1txKd}%_aLBno1lZ+;H&uA)pn1|P@%i&Jqa5_?+FN|Z`?_dY!qv= z(5(soBlf`mva%(_>$KLpdbuu{B<@=hKxN80i!Z?98>DyHYwTK$MsAE=JWM^nTBjQ2 z8|+)TeFM-{8>w_TVv`q|EIZa`{ewAM&4umJg|eNh3|qSAI)|Vb+W^7vo?tzrky?Vb z!PnRzp@VU(kzQUv zvi}IXp|nu7IT#jw(4uFKkJG!JasWe~b1GEH*g@N*IUIM_W^tTee?;0URnln_X7Twy zI3HZ9`Y-?>N9L1q^MhHPFX_Z9GF&WN5+aHM2 zBfP$92jfH5(s7>^32j*<;imOm4&OeFxF`grQYRc>-mr=(}nCJEUjwQdwx`3EW^}Ujxx=&5;DQ5A|ViDizuTX zu)IEHJ@H+z&kAK%DsvMEr1Yf*IivWyMEpN#Y?}$eQ&bg z8T~g_o4RV@*)ta6uK+tm3?%^?5yBd{vu<0#Pgwv8vp%=@$95!i*qQ;@1_8mo;|~BV z01?hEorVE6U_W%?N7#3fXU+S*2&zL;lm|&*2}6#gv$>OVSZ11cm}B1EG2}|`N^%aj zFbO}(6xt#!jH5#UUu)FI*h8d2y_GfjsWdzGeVr>>TAg*^LxFFGz=!t!``!Gl;Df{? z1u6(&u7eI-tH1(L$rD2-?4_xf-0V969rnO-_%tAnhOdPk?+@8$FRt4+KlISuZ*G3x zddOJuA%qS?!@X9SPuo0o^eYI&hwgi?-8)3486SXCkYsVB+kM~M4gn@I(}==Gy?f%a zUr?q-C|D>BNZe@3gn_w?6>WNdd5&9rP{J+Q_)*wIB~RLH1R-T!*u}RR09)?D1U^I?m=CWj_#iMr)|f(8fWnFZ2CqJG%!Nj9q4n19ncLkD9a&q?B3<3lgWGD{D4CVs$4O*V!3IMNnKwm5%<8U zmjctTyXuA9$L4Qj2w(fm=#J2U5KxC8KD`J{>;@8^*D)8NgLoD#Y>*3Te zirTi8HTxP68h@(-5S}y0hx{FT&98t$j8+I6kYnq=KF+xgdih`T9lnYU2B5&!O>EWu z5bB_Rf93lhxxpC?w$TVa+&E8bdoG}yiU`|#aHO*#qr}e7;;N%MFW_7)HN;bU+iE=S z;9C2&rmD>P^>3p$dz~KqGEPv%rBC$MjNuH)`djw9-p}1=%mk)$*c!@VEqxAXD4%-S z^1t;P7XR=^>D}!MR&hW;+y+tCPeNxu>p%yESOh@BYLVWVg%vxr@DeKHUazUZ24$;= zuWft?H#WFsYzz3%2ly~vnQvaNz=zke_5j*69Ym_h&7bjlLzUgrH9!Q zj-@-CDmOx7u0$g!LN8QQ+w(g=xH zyRF*Vr2@_b9tc!O#8>Ras|h9Dl6!3sIQqg=-!dZ&vv-y%f|T?X+$uOTh~qOWfa9flTbqBpm~`x7chIjx%g z{4u=Rlb;|IDnu@}T-2-pmbV1vplJ4NHrO>nPEr2H%A zQuf*C({9!cKRh0Ej@woLRgn`LO}7(j;ytsy#PknMU$or854u(L^oCEKvD(2g3n3uB zL})|r)Htb7d#$+@7D{uGP*v&|o*!AIxT^jq7!!a*oI8HE10l{6{!;FkwXOdUzudF?zVkn2t+g z|LtV-m?n%u;K5jOud8PVaIzQj3xH1rm=WVP6elyq;x)q3VE_27(mRu9pSW-bT}iczJDQV#1U+^+bIta5>;ZT{&=Yo<(k|DXc%94^JL^L9)UQs6 z2w{2(P5SEx{^mxhjcQ|K-ffNvSZD+vR%V~Cuc_ls06+w;&`#MZv_dnCnSkLY(Bb8~ z03G(ha=5av^y)zep0%UBJPbSw_0Wp?S>0j;kU>^ssMM8WBvb$as${E(Fg?&iSR3IM zGdS=sPkD9ky+hqFw{q4O-do?ep0xsCScnidnVwIvBV^jP>STcr&Ir-a_uEaoc`RJ> z-e+s|07QWi#ijFFjqtaU6M~Ea0^F{31E|R~Jm}y*qeI3Dy8CENA`m7K+aI z+TBl;^=&C5Ai;58f?4&UmMt#>F#2;k02DepHo%xM2Po{a6q-_vRwiY!YQ|_{qpVlk zHI{mljIT~k0W=6~@KBWCVhG_C=-`r@>k_$1De~%3X9AOkLx@IUjfyf<;p-s7xetH_ z#W!iXjRfg=&}Q8Rt$jBZ^hj>EZaeWuw~9;8@fAvNED_ijXQyrsIw-^<02`X2NWNMz z$?cB0ah`yLoX>g?;~}ulG#_+-Lac|C)F_TX+RdrFNdX8s3PEpiaGqNoDX)zGJOzc` z8Q$>~kkh?J14*`(5u|wnw@CM*OFxLW6T(QI=+nW-E8?@Rd|JrA71Qxbzd+TKMo_mS49Ohu4KcH4?gq*R?zQ8FXstz!xNL< zU9j8&X^0VvTcHllufsr#lw6~0R&2x2bAxILS492vQ-?-RFfQfV!v_b z6@C##&!I@j(b@vipWH`TW|CBtJSO``mI?T1wX0wB8teiZ29LpLKmhXmmvzlr{g4VA z8T6piO*pm_I?p00-qdLTBLv-x2!H?hQ8jqmNsmgn$!ghF(g=+b%e)BFLZ(m*u^hkv zEKJe$85~F2j>S}m_1zb<-1T`|ytsh^sOh72sQw~|z_pGLhT`f0w>kGCpurP>#K}c7 zV;PMrJ^f{;pON0gV8{a|CP{}m2oN((Jp4{3pg|zR^x6eiZiEbB4K2bvh-VMjwPt!}dG1b3 zUAI@LW0Ei;y3te#M`Jkaphhr1z64GCaE`R6hz3?+E;J+B)l@;~5YQpooV8{>bG|86 z?xG($b~%f~)n~RBU4eCCZyuul>%?I#0aE7B7|3v|J9++7#BfXDzo})3(2qRfN2GkE zcX@zVaXlp@9zn?01CnBhs^(!o6nW4E=tk%;TO$?W_nr8DJDEOZe{ku428ZH~a{>|1 z<|5%Shko6g|Fw_Vn>t;Y(zisQsb zfwdIslc&kl(W6!)J4G2*Lm1GZ>F^;pUq1@B(4P%Q%qiu0vY7*8%w_`nI zX3krRbhMky524ruLcpMqIS@j}z_dV)T1(PGZD{o;yr@-P59cyZS7*=7+? z&26+xw2e>@s-X#4s~lLpDGlZW7{MHAI1Kcq00YZ*aD->9!31f~z4#Z_yyuwh+CA2= zWMgy@;H3y-BYf!qtaL-<{!2tY|rLRQhBH8%~ubv6kT6E62_q7b>b)|e~TNN*+ zdbrC6GwNlMs_RwL>eb@g6`td(3vkfpfd-K@La}5Yb6|ph-b=jWOLSxewghv~Sz?5p zurDsZzL`r?d|>OQ@6u-QpbiiO$5$`wjda3p+sXEf4qNKj;hh%mI6s;|cjWrhZn?&7 z?qva&{t@iB+8bj*845?y{SjZ|Kn5ftfYl<%cRWG@jU!XTD7bL5%FMstTw*ngimP4?o2*8LyhxaEcx0Mc4Ob7ib>&5F0 zJOsxW3b zF>Fk4H`e%uZY=lK^p-t6^#%L<)RXLoh6VTHV8>zmt-rd_7!Jzb>9CA&m4Ju%M>ew^ zvQS{Ng=re&#OH&~54QK=09Eb$$y06@0oZV-`@32B^%gQyg!a+N-DKr0;pd_kb=b!q}vny8q@@#p|TeFLoK}P5sg`m=a7#sdP$@ZqiguNxelHcoFT#h1RNx5 zi`HCQv*j905kw>_W%fZ+q^53O4W;O(?P$Z44~7zmtWUEHi3O{*kwv6s-C|@27@^mB zz6Q{+>!{YYi7(7rau?17Wm`I8Hv5X+3&5$QcOQEC0J4e*s-J#Pz8$-lU7$leQx(iuZ4GtqC$WaJR2P$u-D6t0qK zqTn)HXyX!*>;zbl^_2$f%!RN$@-7c7&^$}oc60y$KmbWZK~x7wz+l*oGjav6Y7t;| zEV7SqoU4|`+4`v${xvL`n$u`#rRQ2HF#DT#BX~K(y%|QUf%gQkAxkC;3G)gFXl0*A zyYJQJ(R(1IOPypeak~fsT5b#353ca8@ea2?{xPc(zR`vN<4>IZ6&Ma-`<;(}2d@cX zIC?=s>sI`|KcimxQ@1VsJn^yr>c)ThrC)?`kh1rUZa5VG@wrd9%tP<)|A2k-!M|f` z2*V~LJ$C=#accvd*=hqx!$Xa4xSedz19II;Dmuir>6 zd&!phhSW&E975YeY=-sHIcg?TGpgZYMTUksK;M?a1{wzxV$2UEAjsBfJ)u~Khz^RIV2ivhdB zg)(d|(RdDBE=8?>4(%!G4fHVxiA@gp`ypG<6708Z4q(&;t7}^xc+eSu8e2EdbcX{o zwps91o+Xw;2=Ffgv&&}Rz<@&;3P;I_O&}0+b=*~~?l!qblE!$eHBRMPpU<=hEC^)q z#$84V0TJpdabN?b9RQ(CsGNrUs70gy+8#iITc7bMD(zY7>@}!6Y~eMrTRBY%uZYuN z73iQecrx{7zL#Ddfe3jTcG7LQm5Jq`@-l^7e5HYh;P}LD5C{>4z9)0{D_H0c#e@)J zLLfwi@i&@n1srsI`*0EzFCXX=+)5bnR?FXL=lqpEo)WasCVGhV+^ZM=D+M?-f_t7L zA3UMKP}M;&u3_+}E7UMwbxGs2RKn{Plv8~b5X+cH@8PSlG`*MF@`BG|2=)?eQ$1E& zt#QuQDR zFxcK}U155N*Cs7n>b8r^6&p>?+vwV&6NY?MJHE-v0v{Gr_u?RRSk&*~4M&J~MeI=F z6*u%YZL_Unp#<;%T|Zw$KLGlA2lP*}8|2pHD53)~LTbjAjpsn1gKTAn-qCf!6~=a1 zsJqAYZmrV`ri>De07L?|pKLq8VLO1q750B5@_1(Hi)?R|_ie+0YAmyLoc@3JjO=gw zc7!^qs}Y*3PM`OR(I^VFC%n(8fO+To56<%{J>?mT5F0B1rX{pyiNaZ%DvscQ z(cn^9ZKXc+-XF(uU_qVb&;UAU*&Y}KSBD~2o{f5T19HH%?Hq^H{^J}R+e9S1lGai4dRi(_Ek^G76I{OE_(!72r6yeUYbiS z16Vuo5}6RPh$1G#_Vi&KHR_bWI>QM8UY8*5a^JB-LboK%#wta+{~^lF52aVMSJ|f z{+kusyi8z&G)WR0EQH1aXJ9zUx;xUAq(eM5Pdl0yJk*a+YF}StQ5aMkKtuEvKtr(N zR-k3>@-u`OcG?95|N4_Hq}J@YE#RTNbk)j!wi9i8jqt)KVvxz*fKEOWLf?4R@&Xe= zPVWJ?tk?Ht1VY@21z{WhIszSv06uLUarYsaKmWWX`$k}Iv{1r*S*VP?YgwOW$vV|z zb0SqQ6SRMdfRA;+!2uiojy4Va?KaNyL#!HS)yH?-j#on2E-HZWjTS-Oc*n4d#)5CO zaTpB(8A^i>kP+&NMbPeeO&JdJkAK*ma_HH0T1ytFV$Y4{ovgX?^nm9F@X-=-x~vf*bZ>eJl(?p zaQJFm&$r?}m5oBS6W}0f>OK7+**qvAb=AH~d!7b=qq@G>N80zj&=|e9UQeV~UwBVv z@T@ir>xbdZqbBi>YMY@eyD`7!KnHKShDy4#9e@Y{3^Hjk`tGI1Vhub`*3e?BQ$orkfgc@n065lT9pj`-fn3-PUGvhme-0}fv zt#M-Kw_5ewdB<|dy>bCY4_YPw4gwt-O+8bx{QL!LqL*gj=s_Y0$rBDRqR(d7|8~|N zP09w(xt{M5z>Vx;?oXLdT1>P8=-YMkw*T>-5@mykg_EQad>9;`d7Ms_6n8b)9D0i5 z(9%=%;>M5POV0a^M&y5gX2OPY^LG5uK8urvHG#ujrI#^3`6S$H(${5R@zG0Ne)LiI zpsr+BVH+q@4H-H<#aT-Q_HuEBF5zeo|vB<<^2mlCy4qdY^S~@gnYp^>4Ylbbc z&UTBfWNhHC==a{o=*gFf;UE*hunut9#JQtOX4Ry3KWC;_ES92?j7c{k1-o*pw%`~8 zVh(JzPLA)P%Q(V&JP)Z8C4is&r{m~bgb6btKDs3WxL|hwHx1Y~eUEzgU}cj6P4V-A zqu2-zBr_cjaCml*Y${}f*q3<3!nF=NpI&qTu+}fb#u#jV$)*XTNl-WCByQ1Q>d_9I zPmMS3tD)FkkBa3W+nTLH^JtBdh?Lbq%T`FC7fP>18*aF7{@9oQE#DM$Ldf#d2hboVU)nbU8iv|N z?EKPMGOmbn&AkF}PzXbX@){1ofS6vLwvo1BSgxEe-@l36g#ClxnX`RY5h$g{zK4*7 zC;!V|ckBPosn5IN_ni0vHw=Cr|I~Ndwc+1%leyHe&5m8N`=9y+`|k2Jn+50>;HY{b z9zx9Y{~a+&?42Ew6z(FAi5{Fd1 zJsgDv#Wjy6(cPH!fS7f@A!0#@0g?1Uh!|lSvI#(kYy64na4XQ^i3=A=kJ$ywp({B3 zoreM&WQ0)cWVKmXThoh#tf4Q$IXFOMYxn^x3v4L)tcIrB1RJzZ2_*;2i2{Oynb`~0 zxA$RpI10Hq30;4d(1ZSDs~sP_5qxL_8U!-P>Og~=+Tq6sAu98*G_V?gbP!e%2{fJ1 zez+4`L)cbE4P_YwrF&0&VB6KjY*_vRe(L_i*3^RzNZ*aue>Qi)u^ig`^w!&M=yjaA z9rzG+sg-VI8NH5&ap#+!KZytiV1sK7;L8z0R_1SI`4ac#H8Oz>wIZ=#e3%x#{D_y`mzi#Ccw?*RmUFNPd{R~Cod2LZHPp{1^D<8d-yTUFymCeWY; zkPsr|K!8F>EfMHvLXw?EgO07&>Q;!8cKH)w6q}o4_ISArKfLU_oIL z*A_uJ8?2plp8i1_S>;P@4S;e}y%*LyuR z7A*RzFdzghcp`vaLsSRj&C~tcXvDt5VyNGyyB^WIBKqh~*4E5B9@Hy46}pZ2J25E& zu8Sm(x^mR2m-crUwE$*uY?LGrmGUL8{Io z0%QuGD5^0P>VPg!ta%=4GZ}*>vom&${N$}@0CX;dY_1!yZ}|*41Nv+wq+TA}u;B-J zE)>p{wG=uJcctlYlXH$psOj7%U5S)3T@dp*Yo3tg?Jym3#OSXRgNam>^^+~3SjgMl zjMxk`@|zJd?4UvHgAXAZjAA+@jvcV{A3fnj_w@)if)3hej?|D%-9uI&JfT?+smnR9 zyXsjq)b7|26`Z6pe=n=O$PB8gQyX7rqg z(L5l3UkgK&+iW4dY75r^i(r5>EzHoHPPj#b-3ycB>g6?SYpGfWX4vXv){;FPR_%J! zibu(lKEeI}nV&a%;8FKFgh{KHYhOiotk*hduw1yZjIf!<3Mnhr71QT@9UX{{9Md>} z3w=1-X|bgC*{;-#?dv#)Gv7zj%)K{&4O&0b)NXzBiY;^9(|`@@8J*`w9Up)V0u{t? z7;MAI_uAB2-GxjP*V=7$bk+iZp>uqnTh=w)hEs|oycZ!#A5fT`$sut;$OSCQTn2@G zGCml1(+Z&)0l4r$ct33zrnmHTxsCF(BymqtKVFE*IZD~$JT&X%980KYxIP_&al{U* zcKnf)#b(Ft93q4yY4cX+^xEe89P1+3p7cl(q?sE)gJGoegquB+KmY%;_a0!DW@mlp zdvn#TTRC-ASLmFkXL^D((gefA0m+yojp8UwlBQ>Rrl)gvb#+(H>E60&fB$p7bL&=hRj26*pnbdRe&2V}m(F*> z|NP(grOoR_U84-k0iOxUQVnc4T*=v)IDlXTI@E&=$}Adv@Inv(j&QD5vn}NZwgnty zA0f$~dW=frGVb+SY)-Ec%Jv-X=2lsNbuf)bgTt}1O#&D!RIb^x7#*#bcya+d9SIwm zxeP1k07Fw2pfs*TR(4->Th^f*d!ZSXlEKzKYnZui7aNlHNaI190`Ry{xDcgV!C<7X zuTqr|pwI_+aD5rI8|v{C;KN|k5cOCdEM{$YZotN|qmYF)vBb*vbv!Ugk}SoRxsLwS zUes&UZ_HqXGK)(|RGAn@4zXH4N?O?)`riA0(B-NBks3wsWe?maTmVFpd-^qu0iFh| z`q_?S4giV6)k(#s8LyVDj7n7EspqWxwnyyx+;#hg!Ea>3-CAF)+t(!6gRpg8AZ|$Y z#s$a&ktBGl$d{L~Eih}@zIRyXzHh2s*yP;v*gjacML-Av5Rwdar4_@Shg*6uwD`g; zK?fO+yrqcIfcFu@8`Dl;gMfxA>OwIL19t;Ve{q@de?NVrEa%oq1sg1dDe6K~qh*gC zu8X<7U9sg_os~hJ&jhZadk@w!N~PlK4-Gn750iinFOJb((azk5ZJMs+x??nG+##T0 zbqQNAd+zg$hFS?X*@+kpiQ&B)i2xxgupeq5#PS9cLX3zwV}mfxab7Vta=N`N?W{nD z=~qr#*Ad^AZAoIQXHMD5D=#qMIfiRq)akYX81~;tt3~*4vG3bIqsVY>O^%pp? zCi)t1(@4rdqZ=KlBOpwRJc@HvWu?&&SbCdmLWNS9QiD*e!dzJb2Pn^lC<2KA=f~h% zy7C0=t6V1Np3&?JlzS+bU!4XcXcd1f|kO*v`7kAfC4>s&H`Ei-}(aMxx12Ox;1p2C} zG@wNpomf4#7^@K*0K`sg2dQBMHH&--{X~F=H&UcNAz7^f5CR#n@?HlvC_b46e~80)BYPJTMfZ&H7xl?UDtN!6`-{pt7f~jZ)8|lh244$`jmp<#I136 zdMrB;Y6=A8-An`kzC4M&0umkH73!IeB+PS zqoS3Ab`6Wp%>XRTSOjm*9J0yCpw}t@-Bw^STn)J+**JfQ)An%0VCZCJN`Lo40%#D* z+FH-*zl*JLau1or*kGxQwd`UKKAh(n@bY~_Yo2SKgW^18)6nIy;cthVSL`}g&hzMG zi`|f`PC+xj4Cv5}-g6^^?N!S|Tg+#VZ2}&o8X-4yCL@veLMW$If0R|ozHEj5w_2w4 zz>X!4UPDcYNp_aWW+yA%jZ93a%@~MWcX<|I2#k+Ux&4NQG*+NnEqVE}B|1B;VR*<^ zt~33?;9WUhv_x~l=2zA%m0tAcqaGWo43Z{RY9-70G!qfr6jOf#Fq+HwK1e`A9=fQ) ztLWxy!g(nBeRg)`*Ii!G=v}>ZNB&2&dRIN^bH2X!F_AjQ*sW zV&aP$z#{f2N*A$8-q_~~SVe_^-qxC0TkQ+i3)aVTXyR{lR4RWM6Z^vpR{W2@;>sz0 z+xu+&cRp%y-d7t&>cV%EZpz?IpxLr8kR-*G0%|8b0|wlZ>Kp@_J*;5k;X~i%)rDQ0 zF~$T5Cdkr7K!=FB>rzXtBIQ7cOBf7Xkxhjy65{Jw4&OWW&nOyZ29wcmY<#~xGX1#E z#jxSv&>r_pyB~r!k4)0WtOI1|X$o9uz^&6{>4HsT@brDz{DlS9xa}A)O zpOvifEr5eq2_0R100zCbx@W|W!~l_?JjdE5?XUdeL)3RBz++3cypGz(K+YDY*6rl< zl69~4U?6jr@5I0V+0G?7Cgz;3zSs;w1U`szzg6zkZo}66Vm1hH5Yu5hmP38|?LY>_AGqd0m-D;v4UwKP z4+nTQU44G-kCZMuwF>Qz4WGBJiQO;|_+DEv;0bd@Y!!K1xQg4}B~)Hs1{{2kR+Ok2 z&jHrdi|izb(ICb`l5wa!!c47B*q+>=s~d_F*w6?=XAFkdB;SfA{oSw!+NyXOps)#a z$~8V6sb0hIYS|`mG5e7ZJYfIy|2m10S|(wDfJIaVh5#~t{TqMIp8Jy-Ykxalj9NWI z=HyfN*<7UJqXj+L{1DhsVl3T-A->qVzt4UOHK8eNG5q(_Px&!6?M)iLu$gy$yR|Yg zTPsc4$!jNVxMPoPXGpQ$(?k9JYkwXUGTp};CAzkSf?r8@-YNhfT(`UBf-+6CS2P7d ztzsShbIC%)f*1zSnYcJ=dHUnlhy{^}SP-G8Jun+S3Cls-ZAGjGv2L;h+ky=aXc&XB z*M~O}7++GIklaT@G7Yc_(WEBpuo?t5WV$<9sjrKurMUS{V95!*E?_?(4-=|qpKrhF za|(FS-;udx8=o45^^jvCnXzu#kv{B)3=ZzIfrtIVMSZ?I-Uoz`kxMM4Y-EjWJye)m zma$n8Mm1H~L=vg+Nd!hnN>a1b7K5(p3F<{)!)gRJq&j;68+`v%pC;@FpkZZw_6-9a z!nM?c4?yg+B{S|X#J^$SgMbD=hFRvIGiq0uH=mKV0MbeO*o8+jk;&ekZnZM3tk*=P zqVqq01i;}%D@`(hQY$0CL0>mufxhxEiQj&IBTZmI=;D1^AGduz%2(M+8=c3U*R6#5 zrcULF$q>VWco{#j0xASFIPgJvTwWd)$*anzYv!N=Fri681y@;0|2(S;D{it9W%0#B ze-n$rPc1Y$bG8~79Ld`KP9T6U!R*EH1;=g($J=dX*C7RkT$3P)E^klVBj=M)-h$E0 zZU$yOxd2aWR}$-i*me;3@J6y7w!gBCPyr8V*bnQ_1yd0VVx3A{JtiUXVC0j2U*8-Jxzu@ev= z_QS)s;jjC~Qa3_&Qd`@vK2JARr>AX z1vJE2VXrXhEd%1NvkDjkNJznMsKlf$BAveuEUf!)?;t=#7Ty2(uKrC%8WT`t6GaB} zDRiPKbE{}|t5t30*t7QzaM*eh9gFoK!Of=S{6I!J$Aox|4MzmNAOfQ5bzmdXo&3?F!O&>b&gpmBb2&Eo(3r!D!Z zPcqnNC7Ej~eDqIUeEqjGg}nkvHwF~Ll_@@jo3<22)aJ?!HZKI8%P38y0jhj%k2r$7GRANvJYSQFn?bILyl^#NC2!TUk~42G`r z&UoTc`&wJUBKg?LIcMkKK*|rO&lfi)>-AUlP#ljB3@WRUZvU>vL#$8ux*c4JUR%N) z9PfXnkYGZTu>lNj&d!aainMCWojz^*3cDFu0^%s7Hb}nJlfD96;Q3zy^=~V#~MZkts%#YXlnDCYW0>p5TsNd;D1*0t= zv*FZkdoh8!1^=#Tbv%*`1}cB0_dl>Qlb7{rv^f9e}pbWAa3E!$_m{igwVlc*dtTCzNZLDymT?K_nz`kod5heJ%xWY6FV zE;)xghPMSBc2cdld|qt|yPX>s4tKW97)5(%dE(M%cg741YGVhhjQDm zjw^A2es^TovHYwu%#5D&WY#tOdAx&d?$*dW$T7N!YmXF4FGs5X+&b`CtfolWjftP=8Uc2l#Mq=4BA|leCu} zXwdl&1A1x%(A9lE!1(d{OLF@a3`pKx>v}^`KA_%qL&JN%-k$_Dio#1TS;;X66yzu5 z!8wM81a6q(CigyGzY?b!A$?87@uz{eOB+qO)gx zylhjEw>SuO`4-7;J37?2kUHlf|L8)}(Qe$z>Zp=dW7}uH{yt@|CumPHqBJRmfU{5T5%z+PUiEip~8v{%9oB>y`jm zx~|$dZCZwZ!G?s&Dz%kuxbjx4n0K4hBxX?~=!{)~Ap;Ns?E zV1v>vt*_bAPB&avP{Rza7l(_mNJ!6Lko~-WiM%xoB?M}1)LW@y=&z0Mn|4UJN}+u=l#(yj}cb2hdNj62aY7{$@3>TJ{sFrb*3t0I<;2WsRt5=*uf!I2&#gm%|?XacUbm zzTfEcZ32MNyIci$$i6|U6}5}pb_bhHvE5P3X6A#gJAMz;E@rWvHOqWUkL2vki|$;V zv?GEJyO@(c#+VJ)LG*3>0XE!B?kU6JW+zfyS@fUNrvV=#8x9I|?1yOhz~IJ-wwnA$ zTgBG)>%(jNOueIhsY!%=r)DAB45=hQ1FIvhyoha~^om2hg15q76wU(_&>=vYBJC5Nhw=TY197Py4sC+WclPDyeI|9TCGpBDIPD zG=zLnTj37#UH21@s>Z7CXo{^Ky^Rr8y#rMYfe4a%hJ%i`da6*^pB2$P2{d`N6U^<@ zfi$T((SP?UJ2K>0(#lNT!@QL%*}9}R-Mtxu;$|7O+BDpe2FX`l2v=6OjX(!|hhD-q zl&UWmiGV>@Mo8kCz9>G*?>%FL1oc}b4#z4``?On>>-2A7JH=!2Bv6Gp5C9s*E3I!Q z{2t#=0(X13GgICq`L>gKb%=WRk0I?=Kw+zzQAnq)cXK)JfQI!F6F0u!Za~2Vs%kR~ z)+L>Q4IK?VcJ|7=I~Lf0fdJ>@yUM_~h`L^!No?wY2V8vp&-&_hR%x%{J`fNv(jioQ z{fgC@3uCshs7W?P8=GcqpnAdL%l=$0@LvC1WVQan?*c;X#VBChzVOXobZ2;MIKyB+ z@4mlvbcz$`p>LHq_5m7j@3qPb+VUzdu(WQwpuScUDO-U3kmXOJchvKx-?mF9z7vY| zDyyV`pt6G+MrRsZ6u2%#A7Aq0pZjZAZGV}Gqn}u|0_y$e zcYFe7!3s=-OD>JPQAq7UAVbqM%!958YwT`>9K6>q12*6U)TIkQZ_5zQqK8Bb21#*g z-ZE_{Y~XrTBJGxi(L`;6t8yg=ikIy|Zxy$TSqE;U*D|=W^MmY47X~>28U-*&3Rj@( zSDNsyk+rr%Nn1;!QZe(Ai|fzqw}&Fu$N9!_n_w^=rykCt^Ln6yVamwBY1q0<{D{f0 zFnxjkciFDR@-_xjNntV|_}S6VxMX5Di0SZZ)hWU`wH3>u1A4AJR_vrYQJ+5SKl<|e zj_ihz=61Hamqy-?q#kg11Q&o8QGJ`E-`gy{(f!afKW_)y4?Ft}iWkE{_8gL1K#tB# zkIfV&H+hXXn!vyzAT8eVY&MS<6YMHl*%w zyM$l;$7%bgKk^Fd#xNr06ZV1U-o-vZM`hLi>){#u!k0em(!~HBGjID|i=9Z@$V;1L$)n1MJ5b4neTHMM>2DrBdeia)_%MOd-`f~pfp~!SAYJQO{JpUC8YT^Ow-~nr-e`xF86{6fWFeG$PK~19y`$1g{|J|{| zOBmu!V^hTg6O6N(HUJ96zxgCjU*-FdaBa5>KFk3y%m6S5qqJNhtfeC zQWIRKI=KbV5Yp3xy?!Epu-SLL)19!4>zMp2XWWQEo++db;6MdU?v+9y0AX$vqd0}B z?GEYmp)$CUu}C+P=Gf);BO;}COa;ZocvzJT4ITRxC}a+Z&_m-~Dg)(VSFS6EL8{91 z00-xlJ_`AC9SvE0Y~vcq>EqUeoyi==C z`f*m*cI6n@gwb6n-XFUkFV z+cmYs`Pto;^7K`2l)3p_Dot@wyz-01gps3SMYK2TXxDqvC%HuXVZOD(o zQ>s7|4*(IRZ@`#UBIfgo#q(-KK!XN)T4icT_S-d9rTcl{Y$z6o?FeBu)hbP)IuB+W|vbcu-`ZF(~-8G#OBJ7}<`0i**PmNx2o z*Pa%b$lt&u3%s6{DW`U!!U9_%6nwYa&-`5)Cx9F#jy8RX6-(&-U;IHk`L++)S8#WB zq}Kzd>SiRllphSq$fIm9$Vfy5fWCPd#vK#r5xM8^?p zxr#m|TRJtqowQBOuDUpvBC0YQ0dBAMqp6a*B}WQ$b;@TbVt6uzQAz<0QCC>YGYinr zAHVL7pM^@E=US9YebFkbrtk&P?`&t1guz8(O1ON4e{4OxpR1^w^tc=M=UHarQtl{O z8ql8Up(U2ffGoU|EwTY0)JJ$g7(|OeR7slh=mOWrZ|v{g$3&w%!bG9Trm6$jKtR6~ z#!y4LVlCHjN!T`MbBU`i!^Op?a349#T@-9z(|#MponeEupvus0*DDuotn0FMtnOxo zvuHi1{oqSL!{pHyvCz>5*l-_=n?(SCE*Ld_5G=b10t8YR(-NGqlEIzZ;+Qpdu<}M# znZKyYfTtW5ADes1*51%(S-c&z@`ouBn`*`$#3dFSFp!pOZ?WFwKGa$|04tyrH;OHk zw=)Z$!Id{F;Y%0-%;Eu|30o4$S$hs!7C{6y>`ouRQ0AyzTsmV*L+v)7^!2f@gm(|T ze;mz>Gr)uu;DCp!olBJM_~5%;5hwFk?L>A6!>%+ayu#oPQHCM~HY9nz_Y!Of*FCw| zY>f$M=}2eZ18Fm;TJ0oKr;sXzWIJ}lPWiSAFX4fN&v28pQ#mSb6MBqa|HXVYDU;4+?IRVHqJ96MZ+9);~N>IcFP$U8ImSMHY z%Z2il03ANM_S^QI``%$k9(l&aFR|eDPyUZ9wmhe=dgGhS00Qdkl!`|K3N?O-a8voQx6*$v(||Kh6nl{U{(7CXGn3leb@%u{q?JzReKxS z+`4RU=x&XDfv1Ohx5BUHqIAz!A)D34vY z)ENNr77WzJC#(SD#+cN#qoyrsP`(``!S|fPPC?r-RMhA@UQz$yJZbHa;SPfH7(~vo zFuQ*7(=Jz{eJ}QLu1AI-j{(3^2ZnAAG=$R$Sor#b!|pJE4mt&cXEBDuE%P@C1Ma@bA_nsL#L~rw?n4aQo7P@0ThV z@|46Q;9uX1YYg3%@CcZQ4(m3Uj^31~E{ffFlb^e64!x5j`rbO`gTRMMb=nmW!v;}| z{#C$&2NYs8buS%vumIG_Jg94V!rmT@kt?Vxy2gMK)RLB}bW}a`ccti>#(aNSik^oL zWx;E4GsynBG902yyI~VtW3HV&=O+3k0T9k;Bg0|_iyGxP`vgp~-BzO>0TTiw^b}k; zVH}_O?VkwuYWMxk54pU))g)f6sLB$m?6xXT`S@etvvDpTmJz^!0WY1a1vW#F`AI4p zdFH0Z-L`>B)edTg8Liq1xMwhE!Vi+kd3B{ZD*hUcyzD$Uyn{02;%^Ogh^_STaIUZw z;2;|jvfl42wU5!NYsjm8Q64m{P*;IM36I9dYjt?5Y&U@u%Rz28rE;OaKdT~uA&3VT z_)uUph~cmmaHxNt#jX=BTXOnjjR8>wxM*+fa4V5CIiXm$6}Yg~j)NjJk$<(!flViR zjSueD#i#1|3UykamV_E;O;KIju70^hJNyLJkD^1~5EhPi$a+V?^$Z91$<$YXTa?6S z=m3!8yXxzKo2J>RaaN-F-lJwF`o>-JWUV0N2fH;Wjwb!C+1AOzgVQ!#Nd4CD(^Bzp zwO=2{VFXyXcn*EM-a<$x*J=Qy4HzE;p)bl>I?<)bYI<4JTqP&H+py=D70OP4XFUL% zU=3dqk93XQ!EOEoupxz?`y}oJSNS&cSm{>N-qGL(SYk8;IeUc>9CB#b8?UNf9l=f? zv&SuNAuE%tT5Q91(5j6FlmZ>}7uXKTl_9$}HfPx;)KHkHAKs7QujU&KWVw?Ie30CM z-G^cmdbM?JX5;_(ZcEGpMypefUWW)L%NS={Y<3Gi8cm}IXDpsL0 zOJoSH3gV{vbf7S0*H|4}V>MG{HL~=*n~Cr&`DS1aNIGU-u`HDA(cK=@cY*G_{^a2s zP2h1LgExaF3H?WQmkrC5wYVza#tMKny_IEJ+N{LNRbc}6jmHnTHcsGO zJ5D|tWyAVsZPi{PCU8f83j4qW#T%q1ND@{ zfP~eRY5Nj&D4y-IE`X^jfM7FYpVrQO{Oz&dJ@-3~?eM~mX4X~b@rVS?AWShc5*Z4U~Bw0E-=;2@QXXL(LHO4UAg{`c+r%BYJfma)eI zGhsUsfFN~>1mn|7=YE85Gzt5)1p5Z{r~dy`Q=`!6PU;u}8|0nhFP>bpuitZ@^?l%- zwilP5DFG7ect&~hMXU0i#`o^G1+MQqzWf$T16*e4Gk)jLqRsEyXU|@`WL1EQD(V+A zS61!HhyGY^-`4i^+Bbeb-z)#J|L8y1FaFe9>^Hypw*bmE#=+qpw90>lRq@uwHYStD zU9bln#%s*7Hv_{#7pA@;NC}s`{q2v2!`imqu0bvE7CT~moVtF>j`tt;po7#WFbsKF z>Q9B3?O~x)jD~oBzhg9n;_`qHYp7!6ncS_>=cuJ_gw58N+l49y;Cuu?EM6I9(X?Xg z7%i2(1&II$$=!ku<@qaiEeX(YU1~g4>)v(HmA8s3;xKtv!0iGJF7r)(1U^ilPBgiM zody1So5gTVa06x*mAEb_I){d%{w<*PI7L5VIkjYJ;tjx3=EgCT3kN z!%cRiQn89{qimW*bHL)QBKr~BNjWlXxizk`;@_RZeeYbq!(3%a@wFHwt4TcS18M=< zsY@EvLpH^8FpCWtzHIC4ZU-QNeY3h`GgEWc4ErfaM^g_A8{Srw+JPVmsnHP^B+(4p zDGVq=AlpR5s0={?R0+p#3cw*;O#QYr2QtEdx5&0Q8|^bl3Csfr7-+WO{NOwrv-K42}gfeEc#LMCicF7z~n%!vp;cjPX;giBfQ{7w$`w|14=_Zy^lI zx^c600R7(ctoCSyH;AZK8CHqc8E^`02x=E0pDT-%T1maWyTWzvSym+0WR;p@w1%F-={GLsl6lcePy@fP4i02;(Fsl=eZ_y@o2(kAgCoWdAM0B8r(ZMqK44}gz6OZ=xc58j= zBR2NHU$hn3D01$4Nr7To#wVxt(epV(h zw$*dkmT`>+>P=fmAJ*>gU*nEf#%EC%*{FrJ%+XGk^A6*lu~fjgaU5G67>ktr`gs5u zF37}h7xyW=_Le;L#% zy2iS!fNIF{;2O39x}EO-{Q9Id<5@vQB^OaO%FgYwful{%ZKaqm7sP5P=EB z9&Fn5eAhee_9N-YXmBUo=Et)iCIKH%U2FCr?frdtj<`eH z59^E5HhuE5)^*}>OSg2HSPiSEU$oc|u8_NXy&6TA?_WyvUGo#mcLh4E(Y8xuAM1^j zisqw{%ta~>VLQWdtJnyUbYO75eFxUIY$e43r_0Q9 zDE6+m9tywf?5|zMWIG57l5E51)!xnLDPfJ<0S^Qh*_Svl0k@oSkVHmJmjC+0wwA%C zE6DB3T~FKn%MV+A5oB4c2#uiYwxwmfP7o&0qJatI8q}p~iNTH^X)~y3;R9!IHH^-C z38cqK%sz36ftX`3a7NM=7+}0KzG`p5ihfXKh!Qi%7TDlNob{l=BC9*P_I;gA?(g#K z3P#|dxbg`$lUB|-k3?w#-8>lDQ@FqiG1}FsXfP-d_@VrY69rzB^?Hv2 zTMhilg{H)-Suhx@Te)))f5v`-I)yH~%m=7HiNFOh9^z0GrBbmW8-s<7uxL+t^hVgu zGidPZv6Q7~2S(5+@w~7f>@!5F9Z~K#?M5KOH3ny%?EsAym5bYv1eGnugfe)ixKpq} z=a6laz-G9ebH181cMCS?oLUL+YTRl%dE@Q_+ZfQ^^12*V-PIJ5QisAW`>w`q{J3}* z?N|(oaFKOevNL&?I)aK-om7fik?i|CK3|@J<2Wvh8?Z#)mg(9AJOuR#xn5*V!{ziK zgzP|g5Tb(7yfS|(a}BkP06wG=ZL}>Jmq$PKKtnd!hZ@-g@z{u9JT9Q2ox$>Uas${v zqUtnYLzka?ZWUiu;lf+tfsJT+7SOS<8=+MH-GJw7vJvao#+;_^M zjjb=c9&D3E^91?UFpL!MJL=qSQd{ejqqDzgxoN+GSz)5ug4KT~!f?XN&^CHGA;>4% z_-%|l77WgP(gg+pVyfx5_C zw+h`}U_-SH5DhSanUghL-fhWY79e_$VO(<2l6ckF&k9+5*D<>m$FSl;$;MIu4uk8K zLxm!@K42X!?UtK;hBlzhI`-x^dq-^XpSEeX{%k67kN~P#MXrwApmyw0&;o(|Fq0Zv&YFcB=eLt=s@M zR2j6E*4tT(dASxBJWvP}kx|IA)To}m!&ru&!Z0Q542&q2XGZPq08eKC8yQ?nRd9&uqq`#*$n zz789L9aBo~uGxHm8|y#^*?IUk@A)-5`-$&kF)DAD0nXx|J8e(2H$WYK#18+>Ut>kbaxU8RgVYJeE=k+x(H7oaihYb;sw?6U3Iz1u!R+uR{Ac)Ltn!UChAf8Tp;Wr|hxH9XPm9e^aK z#ut#PGS0vslSUCBu`h)>;`EnoH17cjr4chC8vznJPU9w5B9K9nXy4=hQcTYCbEhrF z9Ax_0C!9KhR1)qLqrsi*CO=;FVvkGV!G{S{bS3~GmLv9quQ%QF=QoojR4JN=QKg8$ zh7yYpm7^zcmCgJGWt<@2g2S zkLtr*1VpKvSr%>rz@Xzc<}w{QKc+g|bHMe&?X=Igl&P2K6qr}8T((Mg9|npU6A9#> zCQu@v;U0htzA*p0(PP(#zRCBKt^VWFWu{s-CW(*!#J{nV|L;F#V8_tvv3I)0t_<5j z7J>_SY`_#0IWUmpD5!o=;%`;^Fcy-2f|xI@IqS0u%R7J}W(yd;$`nwspt99O3<5Wd zV^Ge-LlgQ04R#)ic>n^!Yc& zzzDGwRIVhX)4BB*R5aGyfD(2D*C$3ti5W=%CB$+_MMy*dA_PM0^$SXMXY)XVK!<(P zBX%9@uWv?g7Mm{k4i6k#0YIQq=c8R=<<z-z7r@WfI$375uL9k_ z%8F76aG?a1yj~Uo0_GMcth0r-9S;bz`EI7}P%Qy)B?Z_5@&+K0A;(0VQJsW)X1<5} zINducDMi1!QKmeB3%ahx#5IfsUNspZ3$n)QZ)149?%78%>N0Lq8jDk;tlH50Cs`V0 z7q^=gX!G&C_gfOMVR`PFE$5c36-(O5Ro)4<4*rCep)W)ICe)!M%$P2!P6@!HUXoFNSYrt*&p*6!e~t+^<`+B9*$k>Aqd3?S zP~bt1s_mLjqNks>67~>gSRhE1{EAt8BE)7`Vq-AmDl= znr!Cy>&rA-JUwbf>f&&0KVDH*?eMOnE=~Q97!6kz#_YlDefD(&-|hfjwYQfRPrGCj zYvc<^~zI~w^19An_a4-~2oshH@_Tv6@-LWJNG##=GHasSZcrW2y_GkCn z8kFJ-BdEtLjoH2q;`#PZuRi1Qwlg}%rPPBB`T+msv%laX#B4}nA19Z_ehEOs z^7=9$#1A{>LmAdW5V0F%UqLQ{Q>hudz8 zD_CGdab=bH-a(fvNsHuU8Jj9SC(yt@QOiDq!O~~Xp0US|_(|s~X(q3Hx)#5NS0hP7 zlD?w>&lc8vd!(Mk*fe-3?0{E~C_Ct^RYqD-^ zSk%kX)mL0U*F1E@F&h+DzV89l)36!DLPZ0>*;Wzg&^6R*3zM*F7hsv)0q7u=iX!8k zdkr`&I8b4-cBxW-n0G1=0!?~dczfh=Z$F0cC2h9@D!7z&KhrOBK$|gjDs=BIGJb{m zO19am!?@1HwXf7CW6r}xUEX`J`=(W?xCi%UpB?-6wPGIc`RkZr^xl1!HSd(=D4PqiF2fNnJAO|+DV;V>&cO@DyOQj;mirZ45 zLTB#FSAMeAK#CL{*g~)>PI+=omS$B(5?BoZa1e+PKm(Q5mQ7+g7#&^GyEJq%TK!Nu zF*xe;>-0fYLm-A!EP@vece1)4*>(tQhroWQi~g$E9mtlvam9!VDGq_6| zG@D~L$Ej)(!h5L)Ae1In=d-y!PG^q4hG0btlwM8lie(1w(d2CFWN-7`zqyaZzpvOR%q5J{Z%Pwo4Pi|Fy@Yaf#H5djN2Tl;Wl@4V-Xs#U&a!@)&uh~$gIcQ$jW23fYD0m?2Z5ZFS(-soRHmp^5x$21^y6~k?&xu+KFc%%iAF_;eQg{egW zqX6cxAj$ykl49hiUg6;@8NhqOB&$VvXXH^?LQK`pF9L3WDAd;~S5V!Ls&OYP_MHn0 zmP%fW(#GuXdKtwwqm*Hr1OC~KiU<}!>?)HfiBq41 zm3SEI?Mi+UM#H|LA*V9&!skaUjp0Q}3KyX(fCiH8hj;PJFqSv600;xKVc#KE%rT!(#6?MQT$wF7fFQ%#%Ttq2xNR_uEe_wdlVz!=c#YA5pY&t%CCOq zmuwo91i7$G;QBHPXe1jAy%@G#<6Aq@e$*~6T%`lA*!5-V9Dq=U4rUDiA`iG%f=VZ& zkt`WGH4CXzh~Xf{K?{ApSP89}R<`oOGh~1Cm3@{j?&myB)`E)5N51m`R=SsLOz<2G zq%3uJ_o-1nRX!4cpvt@NtG`@JUiqJlVX^#G!8mPhKdMQvVHyNt_1Ur8PzR_lOez)v z9mI0T6dN#S&~6XDeV{(c8@3~mBWX#eJT zGcX!r*lCcA&W*b5K>L7w`ocN;+WogTsb8kAQp?@cy}3WUnQaN#bC{w}`kL-lE6-nc z=S={JO5$z9wgxb`c!d>hWMygG65XA?@5=+wOP!MMvI%fVl8x*}G~v$E9Y+&gckl-M zOkwxo)cAxuvsb@pw&PO00DQRq zQ%4&YC@&MT(SQnu1SLa$m@6Y5c!&Tm7jpMMf35+CyMHV@=hR?91BKI*Q1BUag$aDf zA@HC?xIhBgU^v#EvD0(-O|M&_Q(So0(t8)g>Nlcy<&ebA=N7OTdg!ded^(k)++c=Km zqIrVyY{dDa&Br&!-vCBHO%8by3{t|rN=rog;N_xUO;p%+Qes8B2YV%|_YK^u(uX>s z!O+bl#Uw8o^v-XV@O6x<;*;)UhfyfCeV~TE;GSP!jiy_kx>A0@}J{3b0{mv26PedctWq zR}x>}_12@6Hq<%oZxwxmm8gR(3h6~^j0FrH#l=_%g-4kMEHsp%(^E%di5}W(|6X@; z>p6>Rfxrf#$j+#7m~@B78I|icgzfon$L)y+f0F?`vn|qUX7JuGe;ZpycaQ&4ed>F% zU&XFQxnUU2$6#R+if}7z zfQzWP_1V%D+>nkhL%+>91CAB?>)i{ue_U;Y(Exos0`_;MuG_8*EBv`Gdy-Y)2UhR5 zXFJZ>o(W&K)(><$MuVJN1a>Hos7D=F)`q@SO@6QyrEa z2H9y)omE{E(_v^WWq-W-yuZzDe(`%{dthBGqGBS^4{srPR1iEsF6szFxd9!X~t^+4zGo=$eUg7$IkyA&*lYKGrn&XNDxFU z28mb-^>!Q-rZzl5+9ImJ0ccQo7FB@x^*Qajec9NN^Z_)a`NoxAEZb*Z_$~kk?4(f6 z7ys6`+WFX*Y3UazQ+Mf{dn)a-M(ynKWZk+FfbPKd23o3 z!T4%B-_tw!7W{WYgfq>v#zOa7fMdzUcsma0}bnYPi|tKIAjNhRQVn903gj z12v$b7SE1AhXtNP4u+p(xP3E--~#J@1U5)=Tfhc|ZFfzTg%Q0n2Qr*p>wv+~YKPlV z#{y&sB7=&HS6+69EyEis6C*RvS_8(DdxjpiRRD<~E2u%O)KU0XRy^~~0}`lpM}R~~ zAApK*ymi}j4*`p*l@(X~sj&&`n>qu4P5-@8w#lwuOQUMn4$#n6x6rWlTDCi0#mp*d_%*cnAybHDjkWxweYJ4G+s1EXs5NS8-h7Rzi;Q-|IX9QJ(IAGs?c+B>H}-tN zq1Ohzs;y*pb0h2L;cjkO=($y-f$&70~I#fI=aom!9(rjND(P;hp64ZS^e0A^2L z$LLR#AO~7M*y1NT6~6rdJjCLx$j9=`b=A3sd*8a%xzo7GOW_WyVtrUMZfj>HjU4H) zvK?KJ>@-MqVvUttu^QBrwWs%PQqNdfU*U2$0J=g6Fay0{u>~~w8W~-%Rp_9@J1`rf zQvsfO0Iw0R3USoKbp0s?)q$N5wjup?Td@>kfDQr~ucAoz`*h4l|A;h zA&>5d{BwNk61^)2B!5RvEg#|{3(m$r@fkBGE0*8K6E7V zd=V{CZxmEtsLpWLQehBTK~*Bz-)Prc9&_caHE7WD>LgLXK&+O=5itXnBejRo-56r# zzJ)jKB)a#b04*+0_ll+0uqOcMpr@uOPPL=Y_V4bs{GWc@718@`-{Cxv=s-3)>g9=( zWA>gCeKs|M4!S%l<#$`#;eD2Q=1VSF`6#X^VPuVdW!!Ru824mA8EaUx4D6Qje5b8M z*Dd#zl7|60u1xKZZbbps{zfVLt(;LjfJ^^~mUE?BpCOK1((|*kohNesZ8P z4y2D`Cj_q(=@Oo;@HoP^o631Z!j%HL^Oeia*28dXp92MyCjbMQz!w%gd*MO}6k!KB zRy|^q@EoSsFzyRpbKm&9U2bBfvSHnhyzPFN2VI-W6yP8kDPO`ed>-!wP4w?&yS{qK zxhV~&dE`>^rhr4cJ;pbHU99wzHFkrpIT*MM?l7;MpRu!7uUO{bk{yY)@I3JZ0_gC> z%roIsZtK7YzW;Xmfj^A5{M`0k`1-qg1+JGgC z8{qH6o{Q0y`ER`g)vbfL<$OxI&s*e!PyMW8Gl<<#FLG5Zm5IKl-PC(uP5>QbI5LZR zTL}t$29Fg|?Cs0#Ho4R6ZyI#lP*w)Rs9gYv>i`WwHRA^s9}_9nFRvWV`w)s$)vsSm5;l&N@xC^{a+i3qitN~ny%ZS4B)~kz6ixek~J zVPUH}wiMFRH~88fV*PP*#G=>;-Mp9E2CPsy8B{__ubc*4YqL*kyhHiTFvA=1W_5g5 zU#*bE2qdTlu(QZDZ@m_&FVu_rBo7p9gtkOLOPaZ!{+3n?q=`Vd=>A?a0Ab@YcHXOj z45y+rTkn!!F&P9f+!SQk?i3z)V2Pb4gIi6fZgJ_7d&=iHrs*FSpK zj(5CiB1x(k=x}X-$+27cWE`x?x)=^pQ=tAiupod90v;sTm}G$ND}u@p5Kxp(DHWJP z-B+mCXxHTtW1<)teU#dU&f`niIH%4l5vxIie~GTkYi-ala{Ay+Kr}tAt~h{#Z7@RV zbKUw^b{xi-eE)fIp?T{&_9-?^47X&G@j1E23^Z98kCZfKF+v(ypVzTh!w7FL91~H; zGN_6n5xqm@@e+n7;n+9W8%Mlv4#L#8KHRUdZ=AC}?$zz(v&c`&%sf#xSrR z-Ut9fpGpAY?&SUj5FX#vL>4_|-M9Yq4E)PFH;utMm_}>GDu$16S>`@Ofe`vGEB5=R zrucRfc;ILz54jYft^19Be2_bE;qI6+!bi>4t7>GNtr#U-7M(wm20GmYZGn!&pJ9MtI! z+x^H}>{Az}UDmJNkC86z?R}q9`N*R~D_!EuS%~ytH+eBl&yjnjat5`ChKX# z)5SbS4%?K1bhBfY=*8;+gWvKvbqm8^33Si%fAXhRK6c#I38fQIwP&E!G0oOXgEqLQ z&zh#6vbpO}i2)h1iFIqnnB(Mi-`|FFt~>h^fL&ed)-~8_(^nVB=Rugt;R#Fku1fL8 z8at{s;9OuY;rg=CCg@MQR$14%ehI^vIa>#mSYK+h*``%%;{E0#i{(uzCKB}Vf~d*> zdvRQT7NI!zuo`v-+Yl4DwA3X1NXJnJ3g}*fXyU#HdEvt zGXA@N@WdmQYixl@@EK>IF`P%`KXKs++uh!8j}Jd&pFKB?8_ZXzgM5R%w6v z_Hml+m*&5bvsdlI>)&PH(e@iKW7GCKf8lQc5S_EnU&7rkebO=uQN?ddTN>{t>UtW_ z)6OuKl*D;13-Jq<`5cBG$(x)71k5I^8QUdGjRV&H?5|?Pg@qHoW7}f$#}%-{kL)_k z!B%T*AGf)6Kvt^Jn+0^Zl^qA&k|yTf=9|C4>NO8G=qNt8J*&Z|-1yb~%An?#ua7&s zInBJUtuigJp=oEZK}?1!UL`7pIVoaC9;d)0{J<{y@}~iFm*coD6z0v$^KI>hB4E4pxrUUhzAUK5XPqCiyaQnc+ zS2f`9^WXh@?unL}EdJKdea6212Of3DEgk*=Y7(izuO^X2R^l{C&p^8@(0Cu|$k~}0 zPwx>MVE~j(bbGy!W>!hmps&`k6jX2(a3MvwR3$>)5DiYNhijna4RpN9PqJFP@@c|I zALW1sfdnfIf~1O3&qh$XAUgMk>P5tGU?QT@wF27;Dvm`fTgH%B02@@kz>mO^*ooZ` z&Y`VIHhIX;!^nYX*b^?08r*za9rNQ>i0g`0m_OA7!}P`qoZhBD?puii8=^$(9o^P??IpZi`Z``+0E`e?gUGFC zzR^!`XImd|VK-wyh|Pe_^$0MyQT?hNmBCf?_EaB3y>o@~6Qi#y^X)vaHfxz9UblJh zP^Wie`rK6;k9F{@S6#ldr>SEM;;p&e<{3~sfcJgxw={$CoQz;1YJV@GuiQGFcWM+B z-t7x79<=@YMlFj+iPCjn|L5t8g9Ii+VKPTK*oR;>w?&2_0T(iD7?j3-d;$u2c@+1e zg;iT-B9<8Gu*TN7UFpASU$=O_oorjNJ&8_B=BKO-OX$e1ce&!%rdF&0_nbvsT6X|0 zGy*zTGZkB`Oyfe-?Bgq6wq4Z$d%X2M*45FziGdKVPg^k?-rM-F+lKJ{=^-nizpRE~ z2#*CIXg1ymYlu%bof@^3WG4)QR?D?-fDMlie63?Ts0;xR-B5M|)1jvmcEpTdoh+yD z8#3Ronng+`Crxj)FWA%J)V1wVK$4dxE}%vOC=+XAywHN`RSRIlv|~F2c7wo%wvMdL zW;G`A-*nIJ2krWmr)@XGjc0KcD!@T<5wJn>3T_^cF&Lf!aFKMng_zq19RxH8aF9XE zc*KHo1=amXokHw})_6N}UD{4mavnwXXqu0Git*M+`C?t_+u!Xdl~4~U+fx`+^|HC< z3rG-~L240OxrZ^{M>^i#tlAu~q<^W|tn!g*zLAy0Gky$U7?0;PRn0q__{HYI; zO7Dp#8y3X(-T#C3mC0va*f{P^fAP70Y5&~bt)`uEB4bPpNG9ZUf;{#ogSfr;7Pyl#C6;yA=>=TK7)&P*1y#H72 zn`XX-yyR!`pgeol63u+i(ZY(ZZ6|TxB6I9k>^Y1~j-X;N@7zWTjCiA@9&8ZEy`9t> zj;zjLcV-4pH$i;@TMc(20vYzS7wzQri>NDi0RsB`1&sYpT^P02-VST-*`jVySindy zfAg2!sjAI@eU0swYJX(Ysh37xamRtp@F-3D6H|k>pFUtGSy;kTO4IEz8+Iz=UW9MQ zV5l#5YkVGemkW$JnudvGvMvO&mPx{uQgvuNRf$$NXH8)_A>9-Lhv&3pAc z)b5A&aQkz9RRa!sBmx_>YB7sm=LntVqI7fteVw|4YnS!I7)Uf~kgo!IaFPa39bnJf zUpPChfi#a?;hJ1fMott`%GW?P&#Ggb4zmV8d0<Y4{A{D7!b2T=I@K74CgulI2g078GOe0~BPQh*Ol z*`e3uG6bgGi3?7Bd-?{wDuw?B1>CuY!tqLE%=OmKQDp=sl25HN__fE|iTlXI0q>RT+QT4!8MTHW#fV9z<4|sh(>AW_=6ogECCLhag>O9=8pjx^$o>SN zLoA6=MF510OP?S;c|a_t0V#VJfJ$9Jt|Cngf(T>?5Q9_})F@Q}47`vRRSFL#_=$Ij zYe*Jgb`>z-$jUH(ZCI8r(nhsLZZMPbP7#T7U&+LD6?bD^aD9m@#~4fuJr*}PWvl%d z?OL`;OfSIPB(@+3izjGMBhj&BbO9HdJoLa;(`%@(Vpsy`Xh6>#M~*%rzdUM;Lx!BS zV7F7@IwT>sTn5mgq;do}Clhi-%2XolyIZxrqGWo>*cDtJCQ}$wL`Pquwj|$m0^-LQ zDPz{Ya0+9FTD8Hdh6emj{QHXzZrD`vOm?tvpNtF=h6Dj<5a=N3kG9$maRMQJ=<`1a zGldm3`n#WO{a1?KR;02Z*$wMqqBo0gcissdo~)lsq(-=x7OSH#L)@yZGyv)iCl0p?LNP&K@i~&jk7f*idjJ3(O z&cy*cbRTLH{bQK07$Ip7E<+bBwtLC>rrOVHpsdqN%D8P58)9u1pX?XY){Hy5xy5!= zDaN_JN$c%8hyehc8m|xAiPrboEc1Zwe2<-}Ua@ZKOD8O$;@F>9w&PJ7S>t}W=e4O- zyMoGz?xCd(Iy%O7tz+JHuMV&RhX)MK@!4}vIF^HqECzK7cR|t5(}lCOL$MTOC!v9m zp>?fh`}ck^dDS{G6nfYW;Zb1P78LV&ovFh#A2|&G7ZS*>zpgB?nkaG1qz(qVtBEh@o*RqHq#9Wn{kYSjm-6K zJ2yg6ak=K7myAV97AkY7%lH&e!|~V?-*4@>w!0bNFgAcOQ2`lxso~@HMOYVL!|WzC zvyi~o`v52eHxqu*`6eeOS4a@~Wk zNHlEFbs9O6FjQdC2lY5$~VBBn&Zoxp~0TfwWv*X^fG zk7Q~EkRJ&k#Kh7fAcXJ7x^vh%D&cKwq~zRZ%A>_jlE-*t1EBBgBAY;lZO-Vx1ptPw zW7|Z&RzXtf9iZ?4Jka<{-NJznVf?rG++Wo;90J2ZkK`YJ_%GQ{{@#oBo}<0CuiGPD z3KvL_h$aL&R(In5eCy7N2u8E!7zi_f2TO1&3`#D8k(#JTr6P1iX~2b6I?Xw}NJ!E` zA|eSMD|9GeK?{(1S&WRtg9jc+5O5GAfDl@J4eW%iahmAlGUQ^!V2_<_y20zpi#yFDw3IvXA$nNQW3{% zL*}7GpPhw@ujj(^QhO~gLMM8B``;Lw1_+*lo}6Ps%&I(k)itSm*@+0u2xZ5UezKE{ z!UaI+;aA*NAh!TtzZGcYBq5{?x@>W`1Co5;=HCVfI=~EBco_gflQ*UpxP9j8Hz$ma zijxqod)Bv&3xRWO4_N#B3*oS~efZSBrR@$@IUV?HD!JKpINyTjU zZYD>a7%JpFuKw14wCw--zqmZ7K7SQAi!poXzFxcfyq~~ovKzpKed`%3w*VSZ2WFm4 zy9HSmh3(+ib=ajBmH^V4Y$blpicKq)cnNob(S2B731*@nJT76FafKE69(3O?+Dn$Y z&}wbvoU`qa#pcHWR`|0F==10gPc}SY9WWx>L7Sg?5sw4{@KPAXEVN)86LpLOB`eNn zELO-_oO9-x_@7!h?e1l}AF&*sGwzXRHIVNFZ1C4KlAgBFhFJ$9bXL1; zGhk?oou2;;W%3iy=c-j7R{QsnV%-V(T z*=cYra^L8BA(U7LZ2uWG;{yaf%6|H7|)-g}E+=Kafm?LoKN zD6m1&iXpZoF&m=6p}LC|=}psL#TZOoTBN~nDgBRdga*)hEjBK(8Y2dRR?!G{gIW!4 zoe=)k!8Q=jlD#su?BYXQSXK22{jVDkyNQoS6f(@_5W$776f+~rgM-Y8y3J(*_5dKDO!}k-uxvkfs?@c1G;W(^_ zo#e}-^E_WaY27JJ2oK-z@g9{IpoS<`&4NFr3-GN3ZATOq7z?Mymt4GpPXusO2ccPU zu{w?d*sUwp??+MExZB)F8gjesE5+a8{Iq>|l{nP;7%SHl%L+gPG<5kXkEJx>rHquH zWDA6lWTXyJOxWpVR=qL4P@%o-@5$Q3y^XA%d1HFqnF9}0Cv<%R65_Z#+|}|4SH{@F zH@RDKY!VW9R*44~!n06A&F@)x z8XWL2jXo$g82EE1td+5Grf?k_A$~6a5{YEV!oFg3TmgItHy|KFe-gQ&m6X-6BQ;>c z9r>Szf>;j`0I@c~o){2%{{CH5Rw#?>j7Mq}N_#UA!(nQo!T!$Zzp`tU%T}PxkhH|w z?L+M!ar?u~moZ9u8KAw@9WNA6e^|$-Ia`U?2J#ruvrEQDmn_%Qh5r8r__K8Gq-8q$ zEZxCC30XaL+UCCN1D3}NPLX+5slobM2PAe3AE>v9U})6n%`8Vn=1q zLG+Zd!w}RWRMrxcyv|A7tfFSoy0U0({#TFE}-dMDGzhQOfU!z38!nc{>>Y~--TNEK z%s8;&;lvT!v+uYq&n?=SFTH>cH|(DEi?+l{aSwSr7~hH6P!Bff3@==N!KMH(HYUdY zyz)?StnpE+KKG=JVPN!X^K|WkbS?5e5ttZNR>Wm+wk`IMQ(>4`^q|ta+R`>TzG8=t zHo3eP@>jT)9^2iur&g8#ho_nV?rw{rulu!(bvvB` zt&kbg+_%`^)FCEwt1e6~J2TB2AWmRRgsn?-!_PJ!uuHS?75}?u{vAv^+#AE<`hgRF z*Ln01;LwS>!$APvwf4tdj+4b_Yo#CR@OsW?W=CC^KnF>+oVJ1NaqDR*+Fv~MerIqr z01{HEXuz$jfVsc%_P=L8{l%Z+JNDP{w(PqAS`+qi@hi5s?SP$}dJc7qf@3js15$j~ z@V~JC=ltIWa9|9|x4$oU(0YIC8?c4KK%XKs?tJ{`d2iMI^kZqqTu8#al@ZA#NdWBdVTu^op%;(P`YO!xswej(czL(CG)7Mu`1;*jtqAtWRQ zjAI+yc#N0XJo{K0Nu$x)-Rgbcs=D_2{?EB}yGtc?Pitm8GfwhKU3Kf;bN6%4J@0d# z_jwV*F0B%7;J^p3DHHGztYJKS?Xxe~+unVfD_@&#QeuiqpV@@I!O`9${uW=gZ;3)G zHD-xSQMG`>2G9VbVHvHW)}gy!v11xtT(E2!CKm6VLLBfeRIr1v(6#~@{2i}eJ8vuV zSFPNGkTZZoYWE?3(YD{dh~#a(n@p0Kv1cvYShC%1uiSZUdk`;iVeZ^>=rx?S_|cQL zj@afJ**03+(9Id}>e5Tx?2cCx2(iQ<$(76;T2B#lxvESEo~v%&S1B~8n)qvOIJo0& zzGy*#49bu(ch0r_Uiq4~N@9<%JCNaKXXOJAV*b--s)k$a95TMyHowK@mH%wBp$xv~ zz1L+GP-@D&oO8Y}gA68UWYy%09{K{|5>0}N>6yEE%x?>O9c%se`h*_sT9-tnGQ@|= zvDav(KZ>eq<2yHwO~57=82ueV3qqamD!!pjY?O09svka!!FQDWW!is;7+W!rt<>UsheLeR`>O~32s)h>S}r*+;w6U1XRG{~v#Q_zIqRoCeK_(yw^lKE#ez7M zl@NlH#DC|AQLm70v7Bw7V~XR$0KF{6NL+;uZ6H##Z_IIRiiqrqRzdSQ4Wr~isGLJ> z39hsDqHqR(PqcZI>uvcm^Sl%6Y5Jm#FMgNXu0H(3-Mnh7%YV&Zn_GbH&4@nYP@ZeD zvD!^1ylVvApJTZKLq&EBam<^oyJnq`u#?sZ+o3sl-qtIf$nNPSdc9Q%7}w9_mqHVq zAtXrty3G?|W~j=B&~i;j8$3orBViam6Cy|2L!G3AE@ef9;bGYroeylX>uuJxLhkl5 zIvROmb74X_VPnpw!aJ>6U0m7iJ_I(5tPZ*&F-9f|s2dgZ_LV!kYTLT?Fs8Fxem0X6CK~9U2GrTW0LCDQq z2dw?jUbo)lg<-yF7hqNdabn-H>#+ULr=GOZ`p6AngBTA>jKJ4}4cd^L=dt`H>t*`| zLQ0V7*swiKgKk>jhpbY9(LxA97##=c&>ruMSSnnx*{f^THxRew`Jzq7rfsx1YP&jy z+&cb82?~8LnL{Lwxx#3FztYcg8w-Sj%@InmOi0rGZHMfWh^TI50vddqf+_NrY#tT} zDMblW8iR47j1{%wk^9C9c6q>iUi|g0|E{^YZh=}6nfLwaOP@k}1{RAN->P}+2GVCcy9OnFpzd-Z4Bo^TdYUgUSShWJCej{kG0`+2^MIjCZ_J0~hT0KP}nl zzoGzb{@Xt)P@9*K9sBALuuY?A6C#F5eQjpa^jU0!WWMe=x}!Oob86~ z@M?!xgm^y*aBw;fD`-=UqSLUlY(cRaNOifoHjn7-^}><0=W;94l@){^8Gu}5fbm%4 zg2f35m?Q-867Rc1T{i*<0vTk!HiH-q*?5Wjbp7+S-7yGgxWE8~{tot3nTND3ap`Zo zPOUUMW5wBNcXTc9|LeB6v`hrgtR?VKUM;XeI~Sv&wZFl-mQw)UVQbtMLla_+C1^rS zkE3}-h9lTzW6Q&Kaoz*71w?ehhVZSE-!E#lAj8cH3t$k?@Va6$-0VPW=WT{tfS#~G ze`spe7Wv;{TEGGU!a@qgdI<&WKCup9G;~nGb768CSKm#dC`Um11;DzIT`FDQdWy{UYHaEeA=%sM`q)|W{+UzP-T$QFY(PG`8t zKlQY9nN-e;IZtn}97G?N?S%CpkYOEatAL099YiJNY}bzHy9-tQ(6U0ONaW22UvwU|z=PI#t-%L@398dxJ@+1TpV_b-*syWF zG!7bB+Nt>rhwaMy?TyMf4usz~=35T(8GsIdmMg>aD)7U%S|CO9j=As+Zw-m~)mrU# z{P_YdHcM4Y6xv)kMkUur%q|q-@FI=}$H5yUK&U{;dRYhdLUkk0k*ZSwt`Br-w&?T; z+AC)vZ{&DGXp)YXw0 z-QVIl3Tj+Kj53yLoEv`~fhuV<79!BWW1)FQ{YRnWb^s<+*JWqdg|O7`T))*A*O|{~ z&}>cTU9JSLP#6Fr!su75fp-pGiMs6t31R~BsAiN%4d&ZwJYgLO8_%@f=iW_m1o>K5 z&RgUPqmMzQ=;W$a7?s}8fT*s?4ZOBIumLn^?)wuzZhxHqJphh%7g7<$@jBjds=5M< z2fzj~9DH`ep1z#z3JiP8S0<|8pLTc8!OZem1w@dFR6=u&hjA7lO9ZF3 zhmeYLii|1G^jKR5tO$;Gj=b-!2xP8NG0Lm!mdPgUp_u_Y*|b1N$ReVWSv%EA28`J* z9N=F28YuCgjqZvCvAcd0-NZiYRu&>cTP(VV-aW1{pHkSw@#zdos2(wZ}T@=+k06Y-i*7%RISk2M{r~ zG(gsnF-E%^VP>Z79HVRs?GW36N88ewcI!b9vpeQ$lTDAU+kCc6Mv#J$ag^tx!`c#F z$J^g$X`YTI#g&~yfWrmeQwISp`h!V;nSix%J`cCW?7f{Q?MNtkgU&=d;ivNroAq4V zU0v?8|JgR}L>mv$PiqNXwR1)29X4)aHMGLA+8%81kFNIHf6_PSRnT$KT%E0NVmC;? z;ar4kg4F^!eMuby03bk_LMjA0yf8oJm^V?{Xu0qI?c&GKYf0M!qkjpzAmRXoskKR) zWJkI5Zu{_8KTb9Rk3Aw(Cg`#ZWjTf$k5BvhgW;bdoNkO~jecOcV8iJvHoh`q@BYY- zAS%l9U&>f#>5#?Zt-OnyZDo1f7H7U-O-)_4@5tM&KXpQzp?DE=dBmX!b1E;S z9qYkQKPjez4>~A=!aJ~NSm>Axv@z|6UD5`3(2KDe~{<%>FMbdz0MQk`@1U-o|dW+l_hutEA9WNQE5et-=E2RKL<;724)@8VF@ZLd0y4 zMd~yfHg3SHFd=|JR)1}3fd~Kk-0W~(Yw+P_$F$wNC$Ayyw8itb-FkW!w_;6fx1N^O z0}Fon+WDHNXH?IptiK6yrJB$dH**IcB#YcA6$;LiQf>)>$9?u3G(%cYJGpxS&4D(` z&@MX1Tc~!N-xWJGE-@~#wytlFbnZ8Xr>I@@+FYfw>}WXW_PYS3T~+~%@V?o0Rg6-? zlSM{3(}YpPn-Vt-qwx2<(N$;L0j)2=OIbA)9gugkQ{ z&0}XQIDDqZ=ge_w~`*YmmfL-{=Z`fVC`z-s#2Q2yQ2M9gz?)&+7 zK49U7q(#+vZy0pi5!$7+7Fr4^L`F$>N!ueZQyLE~t0S;ca(0dpW#0ylUm$d_#a=kw z<%vU9Lnp-ENX&K{ns`p5L8&tHjZkU#TqU!{?KU+G^N$f)=|m`0q@^)u$?@|xlfbd! z!bdtXHjKk9lbGa@o-sE)QM}Do*Bk9>bi@iRQ*NIBk%}Y$M-tY6U}FpiL@a_>4n_tA za-?|^^Wl+?O4yG94%jgt`dK=iu!bshROiNFV+hRcMF@~kmGZ&^Zjc~CHiB#Yf5+|O z;DF~0D@%n;putvlxZxffcvhJz*uXb@#HI4(XP+nb-{;-~dnwtEHbXf(62u)?hfu zQqe~1OSRdbo&Ox6E{{{kL9DPsVBTgBYmJBEw2@g_Oy2LVZSbD<65%T$>MuF8dCb)h z^XgYB0?#I<_Li_A={^vOF_vaTz6v60XiF@IwrHFE{K=2mKl==tC1^T)oK%=G2F}8y zCyfwq-q_fJ^YntP%#K>m?&H?E>xkRtgO`TgcOxQ|{jKC~X(-_cCv9Jc(mKMjx>ZAG z;?z@101FGYtKS1rf`nVFF!;3$e7ie&a$}3Dgeob-V!5j8;Ae5zDDX{~5a^Mg9kzA) zbh0KgCbdW~w#I5hKx$^+d{O`lStTkk2m~<5B>L!wOI&$j#`^XR*i6BzAG=$;d#Q30 z7aq530C3H)EcyYe9N18&%^<*G8Ni|I78nlNwr`()X598C$oSJ-vF@(puPgnA+*w2+ zpF3so6DO>)?>1XphJ_J}TWd3ERnI-*G#e7HQYeLgz3RUOIyj~S+C{atlDz-}{$TB_ z>!;V}ZFcsO6VBCpdzQ4b(=f(-o8hNDuvQwot$}g&d zfLt#^BZuC2fw(@64T{O2BZ)wLl}ICfKa9 zE#RQeRfHa{@+q@hU4O|?;op< zN9T6u7Wc1i%kHNyD*3G%W+8`J=8xLDA9WvFiASoKI>z-5dEM$a{rOVYyXnSWqnT4s z>cx!F`ELh2=K!VwK8AmQG zpn>V20OzV2eGsS4KW=XW92=wmB(`_&m<*v70v>A34|fT2?7`3i071XohOSu%&;rki za{vUve34LpegFKgJ0E{x^;^sb0T13E=gFwP-g5#VWNQngY`*vzd%FD|EA=hfAym@a zR;KORSOBo0x4M-ZzpeulbYG|!qiY9jbh+QUyBBPC)9{Oe3Goc#iKI*QEi8%vmRP#t z7!M`rsk!(b7=z0=+@tmHVCQ2P&rl(r%>aL5fj6O~( z=xhbh2=pAVuQonw2Ua{VW;iryeU-ho zevK?oJ)^{>Gy2a6`MHT*02!bem{=3C4l-99BRP^W)#iIH z+0#De&`<^wfyc@n|pfHl5H(EbaKcl=l{@3j98Z<^lPY_(fj@%yEx{7 z3$5)iCUEd&^?-vI3qEt+@-Yw}pvC2Xt7iHpK?F6%?ZGUQaxiytI>ilkNmt1 z#Nn>++-h6D{q_uiW0%EYorYjMtxm-W^B_D9-4wO$$6G(=!hoOY`Wvpf(K?A(iTj!% zl;#}2ymtDBKKqAEn_XBsZwFfr*^eFn3A?NNPWnT0uCJ*!UhC=|q3?zB4G7oH+pf-5 zD-)8D0-)773Umk%p5ZednCVQT{R*)V>)~+kVSn~3|IPKzjE`A#wq$er8Z4h=BsDJxr>njF}W9)H2T2VQHi!M`GzW$*qdg{E(LSPrtNFCIC6-diM~m*--` zYWf>)o>EpSTte1>Ir_m<;7G1We-@J%r{_S}7 zOPB4AckZJzS+J?+8L@LqP**>g1OSNHO1#Ae>8E`I0Y`tuARSNTtq&}vNiXRkP~~-A z3Q!4YVXcwQPScRrNjVAH&!B`&p$t1p8mAnNl}s$(l9&PfjHBADBMNd(axm1%m*||7 z@D=XiAFI}5mflAj*F^K;fEf4oMeQ5Y9?h=7u?{>(!V8(;b;EjMBgp9xfyLium<1(9 z9UBz)&83tx;yWg~4|0TLCjd{h@6nlMnK$v=ObR`1l-xecwvvJJEhP&jim!3O$r4b?^}E7y%Cg z&+1L{r1v0uy@C&_|F;S@_$h7OiPe1g3^qE)6OwgCIL3;jP20a3Iqt@ z#IG!)NWWUL0&I#9&Q_UawHgg-RMIt%M2OM1kRuvb*dq%#wjv#DZC&7lj-v?EN`h-_ z5YRyFdlRBt8eK&nryG|HA+!KAXXzKXqayX1e|Qyu3|ZT5LKJwr*fLJDEEe|Oea(OiwmM;5IHET*FXblJB!+GSnK5MRL*MDvS&!fD ze&3_YP;yLag>fC^HzEqd4f)JmHTzzEtK%i3FeCIB6X1!weB zAcm~JbAWRS+w@rmG(v>GWywMi$=Xbpmp$Y5#C`T`;USxu<@u!Qv?8>MWT{PGMKlei ziT1vBRYQBU^RT^nZp4j-7a0|o9e~~N*{`4BDIv>7V>bZ68Rl-*d>0|R?9SYq4N$Nk>y+UZo2?Q}8zBF&A$ClAh zSt5)CN0ktnj^G3xj8m2wjaVj}vXSf^HqhUOa^`0@_9c+P2OVxhwD8)g2OT@&Em?pf zC?^+MJ5FnaWV>2`?X>>96~Eb@89##$6pTcu+HF6A7Qit9NJ3-KqOLgY5)se zj9OE>n*-SNu64j=sL*l&1j;VqTO;8@$}~}?Qw2+gY9faPj&R?%i?=Kd)fN0MW?^bCL`;f~Ab zv*a?g+YDL}8hjw~2`gmKP+`40Xa3Nh7U;k_^XqB5FgtDs6oN#s*RMVP1ycA9vt92Q zzl`R_Z$9-a_A@7chWCs5o$4vmR5PuCE_ZbE2Q8eMx3zf>lxblf-C_0YVL71X(6mNq zo)ee^?ZSL!gFuJp2{RaI?SExMEMQ$2VIHOPBNn9}ze>7ZFNCGh({XS|vH2mV+|Oj8 z&?PY#1Ta*p07F0k1K>i4w3Q6QZ?0G~HGX9Ru%QvKq1DZ8Tb)7cBxsWlf6}G1Y(H`T zMy81lN=z(HTo+4bX7nq&59DbQ%B}#;;8PGNf#1 zm-lWF*pOc#H1P17U)FiQk}C^n5a=MbgF-Ew@&3s%kP@AG-i1hPxj#QlLO?@qCAp|JQ&|kcn&EWj&?*`j^9k7T8z0PjBj`}@y<=nc=>`Zv| zT8q0#XM>AqE;tn;U)<`-m?JwD|=8mC2U}~Nh zPZPAXB{~%{HN-M?mNRr_{n5zAXvtBe#SoBVrjCD_bM`y`eh7m)9qV+Iw1sF$d(%tC za~iZQIVaL_kfO5Cfeal5mC}CYPzZp~NUXN2F1r{IF0xrR8voySt&d9Q5tj6*sK#8qBD5o>ez>UuuCbrG-`6*vCf} z>_lI)1824Z9(4GxSMWiPj-IV*6Sku(z0S2zW}15Q+XVmF(sHE}t*u9E%9K;J?v&RV z?@C5N#&fO>&@=7>jea=+B4k{r16p;wtIxZ)JOGg8JQjl7Oc4yVW(ujU70Uioa_}6q`ltAmP0#}huZ6`8kfFN2C8zH5jm&;_%>^F zN%J_0o>N#99hveAhyoS?p&J<~QYe=LM_ezr+Sy~5fgB+pZiJkUI#k_C3_!WL*XTI9Gd#kVRzxXL%Jy2JD7%0aY^M zu|P^k_|Mj5^VVNE@8$<^dIF3}=i;K^0|o#hFb>Lly=5?6$ElxQY1G6gbZb3q0`~-YNJOT1%M@2i9yLFl*OfkmxK!II6Af(>V2~jC_tZ zi_;un^gKCx&~_i{u$7CKIU3$+fh_C-1QB5tBnhXOVda^I6}x)rfVB;cQ6dKD!26B) zi`ijx5cXQ(GI8B*FZd;}MIzjv{vqZe?6y9$;=(bEDm1rPwgb(8f|Iw~@KWB6>__8+ zL5QWm3gH-6050&$0n2Vf-{eT&7}*uXb{KHl6BiiJh|MlpQ-ONvo)MfgvT1aU@3BM+ z!j`P7MkOS1P}OaYudv!|?Y!fI5wdek&GF8=O12F8BLG81&bCB9W@>KEdOAAnAjdmZ zn1gn&Yku7WZ;h}MMoU+mE<*>M-UsKuW`mUS?QO)+kI)ynx=bCWKb;|@;tJQMQ#uY< zYN-+C18kw5IQ!`5;2^YlVbdVljc$qB>~c}JgrX2Md*9$Rgl z@^|N$4ZS#Z{Qy_t?q-Ykka6P4|7r6>gPixKqplU|Ln$^kQk^*8K4^ylU=~wByG)jb z4uHr*3=(yg*6low_48yp*@-D5iO)t+`GqP|;t*ny=c~`*R@*6+S~tK3mHEvlf7M#z zZ7u{tzngIOGFkLT095w|d+bVO3derY252`GT9YSruPS{J5Hb109@$I ztXp~c5$19hsXY#02yO!~_^a!C46vaQWYULVB!}wb?eitsfed=A4+jE3>zaak_^Ys@5 zG{7u;rL3bGrda=7zx^W&54_&&H3J-ulB4_V^r{Oxc-KMiWwu;1(%C@_VDRquZCtPD z#~*bLvOMH{q%KQ>qWgA{m!4WSHEuCwm(T6uEsXAW^*;+$t!(3@-;zxZ7i{c5JcVogbfgl;svnKiw$;)19W?!xAeBsU*B55i-1IsM#tAwXdcoh` z9kU!Ac9*J-rE=)fY$F9bLCy;OqK!0uPhMMe4w3qzKIotg`hdp9bWI0Osp;4ZAVA!% zbnHg#ahg$$oXa**M9}7m0xu@FJujR>+t?|)L1pW$HhYD19%MTOA1)(A2CETO_*tYt;6??%iFF-(!;wudfh~1E&+P%`-^^g0N`u4#s)yLY=c7OYN zCbD#%Wvcu8+LAj8vrze)fe)JhQkf%K_vM*d74KYLY&!y394L5`f~v2z*Gla|E!dz{ zMi!kb;?|xfw4xL6N&7xVTe<|Td4Z6w`C1AAOJg`KTLEzDBubw}pl@xP;C4{`sQ9Dl z+7qW)hK_rCSIM58Qb!e|?UH;5hpn>HH4}2(+-miVLtlLS=hZq?Z-Vudtt2^K5{H*2Mpu1Kb`d}E=gc?(we2`;g zGN=#@@32WV$7mp_vSeZ6lw~EAuiC~{X;WJVvyIPSnv+0`vP?#X;F{MM#}E{bHv7Po z_kQ#{n=XGm*IAhw))zLn-5F0?g-(1KW(98sQbXo#g|Gs>MHVIi8(@YE-AP>e0cLo2 z&n7OGT-Zf_mudr|au;DlaJ*qyNFUh*g0RwTaPG z-tsfE)|NZ%$_Pks=4Y+|6tE?hRdpV`h2Cdln)#!D=KXLLYjbIc_fIOHwOvtx4qe>x zij7vfp|mIM=;{?atfILq_VE0G?Yi(;_r7gx&RRG_y1zC#WSy>_r5y}_6VNtkV@o0H zZ|40#oP9aU4rdyz<3yVkdlqc*GUr@1ad6g&v0X-zJhTVoWtjIo=^R?tDz>!NV1vCd z36cDtB}+qRTf~N&u2k0ykeQ;v9tG^U4KdJzh<%Ffa1nik%kvj3!}FfJ2EY&V(KpEi zGz_d;JRjvz;BK@=ZDAHbeKu|X^d4%*H< zSt07JK7^unxmX4nKmqA3h?Nj$biXOqg81JW{T#7%dL0KO zRTC)EpA_4eYeXGVGuK8P)3m;q=ee)2klM*WqFU(vp*Mz(hswm_=sBQOv6f!8Hc}EGsTQ9aW zPkmV5kkhN*n8YLR9l8gm16mvW9qPTUzQ`^AUS1rw=C(mZ#YwA*hQLKKR~)EjuDIom z-Qo)Q=bp3T%(R6O=@qLXy?ZaLhBXt3Nvwv}x>VMS=g9hV^ewlzmv2IG^cPH7BxEjB z!h1P+UYa*spD}6Z30MHsv-X#NuQs>*zt&XfBiL(kN=h@SxMKbJ*14y}{J+L#QuzTu z_>xhn8La>v`y>t>@k!w63pw-rC2mSlb999)xJb5XOno zkd5c*JQ2!}9$teE8?+z($lI;C9YPy9u{wL`C^2Fpx?fLw(0=vnSFE*T&USUtw^(FU zj8;vp{(YdeC2@dmu=ptp;9^!lF z*U^!HUK}PacnGz4jYg{DuJ8$w7Q9Zs_Y(F-89|%r+>SzbE>Tdi0u?nZldoD@HCc}SJJfK zHmLPp{kienU&(#l%C`LbJ|dbvE9M*{=f$_6`?P>Ift=+OnFbU-A?*`||Gb!~Oo}!@ ze*$4NdamoD_4WPDHq~uCi#yqq*hV@drQ6m0t2h6;`STTeQUC1dnW{AjyZPNXY0b1Y zL~WcDd;&&wOxkCh=Ts~RmGk1kX>+*dnr`7(=DF^oz-ondjn3l$>8}J!rwqcW$EyN3 zq;<*~VMQ@I6kg!Gy>_8MS@EL6ENAe`3TY(J;>(N}!(>1U$2XmOBTjTTnFb75 z{7nb!!Z0{LonuAxVn&+FsI-@o-GsIg_C~k7B;|DJCjgS@yOeJu&o;*(#FlMzaBXzX zrG`d}GIHF0;gn?u_i~uDbpkrvG1D+JQmJjMaUGK}a@G@Wk;WOEVbptmaoPrn%?$w# zB?z@BWi?9OW^qQ38#2(EVYo$Bpkhw4nK>K=00(zIm*^1Uz(&)!5A(3)QoYt#YP7~g zJ00XC;B%|Z;Mm7d9M2*;+)AwW!6So&%B27#n=Cjy1quX1gtjLX!_k{4+A@w}9t!dF zl{~2&x#rxPg~=k}gdrO|5wU0&*C3f>1dsPI_dLKe)H~3GbCj_}VUo}ZQcRM%v6zPP zonAEwa_3iaOs_GoU`f>R1JI+3)6~&)%92NpAyz=gOB80AvR4pt;S8pQ10DD-wx6J0 zQP)C%=+{1HL4-ivgW{DWHiT>3dY8opjvz9K1H-}<7$~wV-V^yCtO0SgEZPD&-ocHn zlci3? z8Gu-XkJyE?#5I_?x@1dvg#IeimTiH?%Kgk|dkB%F9$-EFfEKXymN>FSKoS20%CH<| zrE7zvn?`If7`HA)w_75BJQIXchzUb#!vJ+oAXfJv4nBZ>v?)VBgOTqkt~c+vcEx$J zsrgB^@ithB-4$ykEM{UDN7hc^5W@_Lan6VO>3f{FNr0AgnNG`f7t*MUdmSbIKx)7B z60bfsJ8YAMY3qkbp!TBF(rWFYU3NKlmgi-ey1AP=nzKLnTG@W!jfd>aL(>*HxB?4< zBoqhN(p zvg_<-laHjqXCQB~8`_{e@`jBD5O``cIvJq#wNZr|qffC)_fvEl?suTUG*q%@Ypx(D+wvmKboc zOtik7eNVH2K_C9L#oB0Z8Z3p=oGoWy0)=VwX%E=;`%-VPegsu}3L_4*72Dua^mccU ztJLNESAX8tM*qSx-+h;xH=O7-{RN+V+)_|vWArobi^6v4qaR*L*#bg_8cj1EFxU3(XulMipI)@= z)J5y)X?Lr%lVL$R4ZbxqvVU=I%68v`;oz6icSov&fyTTGv4~Rlb?$lxycC2wb%6DU&?m3TSq{{`d@y@HAUero`u1ERscv}TusAjAnhc_(SUDM zYfI-Jv3Tbmi+6aSQ0=mB`m_LsRqBmEhOtL@Z;g7>KIq`LsRC4+_ka+ihf_Ij4b454 zV-O$=02iQ7@PBOc3ZQ_)4*`{EO>NG8m8U-Qx2S}?Jl_3#ZpY6*IJ|KHhwmTUnAXJf zGheagE2r%Qo!t{(|Hqd5_CKgD=jHrg@1s3}z3>~IR@(h<=={+*U<=t6Mte}D#vxdP zUC@75qYd2IZ+8<4(e;TFu1uCz_{-n^Fw=26$%FSFPS4p>3t4;W^Bwl5177E-Kq{?3 zC2ezm{tTUpHl|74wSLu_CjZ=3J;niSGWFALdtqJ_Q7*3ftWi>s#ZYyR60Wga;3dV# z`z0a1SQN*<$cO}fD=~TW&sZiWL8eb-u#oZY^}cC{pP+L{|JP%AFjvlmfCk?*YOGC& z?*HVqG!7dh{4~_sgwtkl8w=Q7yJ&2g>B>W4_Sp%+01YzhtFtcisof(>8M{g~xf2l) zfdK*@-bJ5O%!HjX5!?oM@ZpDAi0PnxiOaj)e*9zHYSUUy$f7;Z2(0XtWIODXWw2E_ zTm?{mIgGb%aw?TAW1NoG>%n~RM|G-NWv@Fd=K!;8D^tCeNfc2!1^#(y)Ku5+W*QIP z8rOf;vLCd>*M89SH;Vz`gMBYn7gbJx$X-$c)|;+ht?AhoOJvhFubt*=d2?f3)2-`U z(vr;DO zLJ((PnSp}bf0@s+2tkux2XtizZhoEsu>ycH9-8~@%2h8_?X(s;utgUkO5=r=f-{#H zqdL09vABj?3wSSupmZ{W+IUzI~fJ{MovT3<4U8bu1)Ug?Iwbv38!_PaJzL> z#Lc=J+pH4hA%kCpz z^%NN>1lsZL%3d6H$D*A@T3T#ss$l6N*%^jP);uVH4hNaC>x|YrJA8!9&t=OjF4-a@ zdTW#3acfy4dK(G>B|5;_$};aXE@=c7Nk@WnMzdfQq&mAsO!ER@JRR=VyE zEPrEeJyE37I3m1>7&Qk-FZa7^k%p}#OHO*7$Yk={ES2IOf!!wv%WyXNl=sbN7{_eG zs^ZSNrp|P$VT;+@=qprhATaF~UxVQwr*XNGw9`ug+g;(lVn2QU_w9w;v>l9f^NtJH zf4}Yj#^LO=hSDq!P{7_QacPCg?3y7ao_YX2P{eWW?U*6eXU-zIy<7{JWH3Hj2)&%X zvPA4V_YIIEI+}Dk427&9540Z8i+`9LeG=emslZvr*EZ(Is4U5aXj=L?)&90*&qMl9rlzxXQAA2p6zw|6&iRD zsIrD2WUuwMt=Qy3)FxqIbnpz6=4LHKh)1n;5`J;}V`2OHz>S^q$&v>s^#jh$%sgj@ zP|}a48|)j|&H7TiEq{8UIb{3B3f#vfn~v^AM`GMx#G|a^^K7OJ4&+^6=2LEN4UM>%Khwe#rGpN7^ykFKYI5k>o>FUd-(huSs!kE-)$7A znTW$8(BFmuR7P<8ngI(F41`7KpH9J4UgmG0RUsCA8%1xJsJ2m>Uz!8Vqzw!W4qC8> z?aP_uLRD5rz12t$ngwhK!d_9vIg`+1wA))&{fxsRQFe$IGlfjZW~b(1GaRwOd)~b5 zDtiDMuEBKJ^&+5yLMjwuvV`WvOSK%KA#sE_N$0e&39%c#UQL(zN;4huq_)hTK5bFh z3{g^CMvou2#Wg@i0S$oKLw#O-+Blf%M0)&!MN$Bc{Rit8eRF=pwjoC|9Yos88gWDa zS>D(wqK?i4G}MJhv?R$|L!Z0?P*n5j}LhKMCC7mjb;4f4= zeu>EkXjpYhyhWfDx)CoLWLYBpJk1Itd|4E`p}v*~uaNjonaW;f8kHR9T!(nc6<{Ph z!iZ8ws?mP%?!ES>PYv5Y|I9J_hws{Dh2g)n!Pay32}Uu3HfC$hC+rLV`m6TCe^rQ$ zS1rNyEze6nLCj&47jYJyh7}s?_$u@<#9_S4jza~-RnCI%oCpKZ?PELMG%0Z)bj3g}Vn+7G03n!^s1gA$9V)SL5tO+;kHpkHK zk$G^YrW$E~H%rdu)Sjvn%^w5z&fGUns*LvzQSt6tWOOJg0E@iWPOlGoV4_B|bPmgm z6#ACf1Eavdhl{kiESP}eu4g|^jPLLK#AkS~(D5BvcA5t7c;6vg&7kiA3nWuz2MBm* zWMpe;Y{gpJx@}>O_v+c8?HPb+#IZDWv%h$j5%!2JBQh5Z?q=q0Mz>cT_+V>uI07D! zkzi!MGcZmC@al< zgk1~}d}i^CeQ)bACmbkBylD1HYbZyvol$(xSOWki79s0QY1w)Z@4CF!ZyA^zjm;wf z4x7%6m&IbmUDsJy84@=fCWP`pxy|-e`fY-gcyEdyw-M6Mb!Qm$%6Ro5-hn_JqqHw1 zNT1D!=|M)hPwpPE;W;|6jQ+Ou2{iL`m@1@F4Dj5{O!Hh&Pc`B#TWU|+{>(lA2ukvN zHKZ@%{9j`5m2IF`v6f@x9^_CAf`NuX8_SNk_4H9Bq(FA0`yR_6+#63OY-wf5&GSCU zW)j(NV}(l_^At|e7;U%bse2Ge?DqfoxZTqdwU51j)t+0;*-$H^)Qzx<03F7c7onUl z0XXzJJ%{7&d^R?9-9+=nNo%7ICZOPYtJox>s28EbzYPu8PXDm%>im*jnFIW6Y_x+X-{a=% zETW};K;>Q-3xI~$pM2k^-Sb8;}6Sb3TKhv~CB zz~Q=$ZT`4H-(ho+r_C^mHUl9OGk^`D_Py58-C9#(>&X>B>YZ2;rf~^Y-vn7areH4# zaM8S-S{86Ti?0*seN&sA;*EqLeHLvMu^ieQ6LP0pys=yXfI$^kKtlv3N%F7%4a>ry zUPrfRjO)@$n?2OKS?B7vkeRpuA#T=M4s8$A)UUUr-#mvK86(725R;+$jNWwpZ2*S) z@?qGh5|Av=VaXF-7AUsRCfB!Z^=o}={Vvf*E0#PUp~$p=1zDJPpe&>iS(>~^sDpQw z*DrDl>ONi@z`;KV3E`B9>3!z+>2scRbAxmUL)i>8D4}^U0f>Lv^RB;bi@*IL_gf?J z2?V*cEY$NiEqLZPs_Q|+{=xUVX?=LjquCpo{_)TKBR(tko`Jt>(Py5p0Hfz6WP2+h zFL@ez34nOr6s;)>*5WoWl#zF}X|! zbdgkXE)0W}RMrPV+$E^_5CcP&rm_DcM5kIf-X7j|cgR`5e9%D-kPkFyG*?d11jnbp zt>Vbt2DbRv4tUV^1#avlOMrh~It}Nf;UvCXQ|I1Y%xyMw4dFVKE7*)H#oeX0KwEqn zKtt{BeBeXhzRA&gwLk|s+VU~{>+2uGOZnC|F3hglK{8j|%zP)f7Jv|&$2nOw+AlX- zSMz-KgAY7tp1(~^-->znesg^>XTC9xN;@0TlcICc^W@vjL?ZoV`qs0d=Tmjsw^~+* z>a4bz#R(BB>ZZp2d{AKzAmn!D(nq=i-o|!%9i%}c=73{Iv1)|a_p_6m?)GuCBs7vH zrh^>63VIA8VyMG>dp{Zi#a2eZcOHQb5~uO5GHk6z!U{sJPUs%Fy1_D8xf(m3XOzB_ z2V^EjKEvo?F@o+v`Nr!d|F|*?JAt;YiN92N&VmVErsY=4Km3dZhlo|)#~+|Uh>(W> zF~`0YR>omAj6naFX`Lbceh(l58+fLuG1(Y!%NE?4;PFpmhWREG|^xm|Jidk zKfcB}0>1-XEdtJe;&-03@A}cZIN!}<)Uo!XFHl?)56UuH&7aevoLdryU3~^g0VLz$%fpoXr424E@lbTf^_&3xaahtrN(t*vkF?5!~adi=Xch zAf;Qy4w?b^j|FG#x$*&5cCIkP=)Ttu!Y6U_F6CzIzs)}7{{HCT2i-h@4Knx5cGx{m zNG}e_q=k7OhRNPBmpE*3-fv~VD1|s5-t(v}6Ap82d4Lp<9w0P-?yAiUku6})6}P=_ z(8j;5aGxzTuGrTCUk7|ZAQJt6#@)QzNk3RT-v!K;atw(T*c4B(*1fGwHYo69P%OcNibgOiwaZ0_5}A4Hr2{N+@c$F$x65qmQM&qU}j;>9{gv&YpHks zZL|FpMuXvG!##GY_@rBR2_QulC#7Q<7DaukOt+9d)-oK#ikKvP#PyBnQ|L)r#)&<$ z_ij6L`Vm5q$QDEAI)%1EJW73G(51O)!IsI=@f-srw=K^S(tQ1Bad~Ejep`?A4E5VW zHEpN2Z+)uf2y~$C(6+>Kgb0DjFV65VZea?ikkx`0h4p^NauC>13pxbDYt~L!*M(e& z5D!2u&P9LEM;p+1ShxEL5AuMA_;tX;_rX#+o7}^l0+sUairc3?YExcr%w(buJqfWJ ze8EVCOU$MxZIXLkfE^}wf=t>AGnq*@{b2diZn_ijV1clQ6hOx++6}%%#OA{8?YzVO z>d^Pv$4>oQGC7<^Lu8eZ2f~?>JvOp-*&ch~4p;~be9_OSP>*hVYXn+8A;h#518)Lc z`SPUA5>Mat#_xa%UUdMeYEUG?K|wWg@_G05oN#R*56__ zJVN;Qtymu*ONf_~FDTiI-pNiZhrn@j>mcBC70s<#=}iYa9Mj=@?&zzTyZz+sJHBJ1 zK+DH|%Yh7<5j}ar5�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�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%0ALSHDcVj6LIVe5-#Osr4u?(dVn(UJC>F!rl ziZBxhT@5@ps(I)hP`^m6Ubdc>h|kHq-lQz;AQUWOWIOC~vplktDv{A!NG zo^boD4SE*PZZU6NV8JgbD;%}#s3#XgSB)#1X=aZvfS6aO-&mdG36Z7@yb;L{3W_4& z6l&NYp~knKTuIB>e7|M-Y~GxY@UonjVyTzU<0Xl%?s><#&O0aZu=m?UKpU#J=W`x+ z7m}{C9cRuRCR)x*Cb{cu!K`C}?k9W3kLSOO1;&HYc7S*$Sc^(igYTsdzuJu|Pw>CM zH|81G>(6i;o#1&-jRH^Rk7!->400a2#7y13k?{VWAvWoNO^)Ems-vw@b_3{oz_~^m1!)=x!k;~h) zZu^_<|0^h-$Iu4pj5ua_&ergcpR9%@?6A6IpKyLewDsrm%R z5>Ptmig$5p&l~^>-+Iqe9VDzB8U-Cyi|;v?%G1{4S{+ofJJ&Y0V5nGSHGc9$p)<@# zit=}AD;ivQSH9^DogtVJ-H2E4l7N@9J_s@2NAoHW9UCtaUKBe2aiI7Bkx)PFBNn2E zHQ&4^U_61pQ)-s_}t%17VYp|4FRNt7?QFrEM_+k9#( z!F(pqX_WRh11wikT12f$U1MDj)sOg+N5)mxUo0}aGuuR%p`92l$w9t)=O&5gp(3mP zlJ@XHjvF*-BG4C|Q_4U*1F?n-By??4+`XxJ!pE(y6L0M5;P7jY@w2E7%g@g(fBfBCH-jKbyCLIy*S= zq%KPgiqqm|j&_B{a(}fxW6rQ{hHHSOOILph3008`0mjofhx4qq@m;jJAMy@6!tA4u+gJt%&&i0LG% zYHAv70G!4gk>wWRuU2Y7Apl$xwNTYFk0d(~kjQ2yZBs1F%jlW)`(78fyq$b3o#Ps; zZfAKgB{L6Gtv^;L*UPlgmyvHwar_mzUS>FicDOY6w=S|HlORAXU90Ea2*sxwG_alO z=TK#FA>PdR%5AI#mz;RBLw9Sb%4;l?dj-s!*Kxd?_4B5P=8$XOcXtnn*%#I#L4KjM z`jyEjCv&!Z2zrme8krlG30W$Jv7o)25H^tLxj{I9=&f%3BQj#Jwr#TnKG3opDxZhb zyRAp9pRsSBP!|PNssB1DI@KqiI<1gYtUD{DSjm-$Kcf9VVP z=6`FL=O?e#-AZqtlbs~0rW1OT~%xb9i6YpH0Km;@vO35e-F=wTFog7UHR zG#7Toyz1L)5sy&w`*etV&nmACE$1HFhXS<%+QkA*z)nnb>!~ zw!S~We15$6u=umm`*NCheGsVECcOcVWn?!2+vj{cgi>)(XcWP;&>Mv@j(+2}V=9^ zqR78DmzPg{`Yc@hlci;Nlj9pPAD{A4hkVJdfg{TGh>nAO6&|!mx4&paruO^_=ic&z zwW1rY0mCi|BL<95sHb%5E}zAv12EVYCNx|H+Fw(UL{hdPS6acRP#K;6BFgabcw1j|zDOEH0FoASiu58pD zWM~Gs!vHLoi6nx2L!PZ1`lTbH-7Q6juj)kyD?)}LI)a~}M&sh3D~qkAyb)65V;B^k zP@L_84XS(dT2$xWOoL09*O+2}gX{{uI4Baod!3rtJy9Fyu7o|>F1W^budr0O*JY!8 zs-T1?NDjux8hfAI^Kxqi5K$GRR!XERUuI3-jd5ho7l608Fpop9;K;-J2C2>|Bw~^* z=)`c2FBTc%Jte>;s01byc;`p%Ga3dG*KW z^k$QOF}~oOS#^6Efhn%ilvbPwr0z)A+)Q@Ct*JzD$TdiPfI~%$rfT}BkBgL(z2Opn zCk>v#SN8iTRAwjOacqj=#balEYYL<5dL>T}DWI36unqWLLvlMQV*=TecCHOv$)+li zAO&soh(RVQlT=kqAjW71Gr5)}+?JacNO1Ph^0?1*|E^Tib{YO#r*8Bk%LM=YRSEvc zx_LL`_jwGtXI^qqBikNc@#(LM?k=~0)z`m_=A8YoKJ@xH^7u>aBjat_dC40W5y2M& z1GpTfvpD}l1b_}Ss*4HIbo+I&#+*#33ObinaFqejtBHw0W$(E>wNgF~QsNpShcb`i zGzt_U&W}1+D*ttgealJJB_+L+!?59BFchyqlao$M_pM3s&#iNgx;mGRuZG@;)QTaEG8FFVC za#Vz+uQntHfXYH*%6T&5?byj_>lQ;Y^{gdg{hF_)j=_mk3Pw#dxq+7iGYu}(79Umv z#+-WtGla`TMQ=dPDXWCzZGo zKHKT8;AZ-=zI}|2Fak|{!){|U?a3Q1L3hy(2{utWwvzlob00y|d0L}du95aKK4$m{ zllrnWZB}sM91lHZtm1z6`vx>S8O`4Utg46Z(X)aFF~;}!QR#G-{P*JmnM zbs8aIyUqQUiU!iM$8i$y9vfe9wP54pvzWCYeppHg*3z8L8dTDY@TpZm2};gKWl-VA z_%b;MgJ6^8sqo{?xJ$O25JPj9IQ+W7?(m$GCX>LSXQm^k!I@SVsCJpx2*Iq=i%z+q z(|b49^QPtF!L&C|uxw`*d+BI2KVX;}4tXMZd4Ppm(*}X7Ol^zQ)-bbxXQv1xli}F| zjFV6AT+Che?rEOKlmDjivcHGNK;xMd1yTrBmd)aOr65I%q>1655`?+tMIl~KOSO@z zD0$B8L}bs|8cr3E%{}<0{*3uke{R7&n{HNY2ffMlH}!kE$WDkasme1FJBfwo6f&j8 zHGUr%mR@wp$`>s0gjmY3uW)4~e6OME_5Yyi_BHCIKZ7D8XoXiAWVHc2>~0=5om6 zgr4IuhikI&`O>$ zGkRajMgyGhD;fzJ%p-62A3~R9{0NWJTT_Rg{$}XMyd@f)*v8_HWP{Jg{v)ZQq6ctU zo;_RKSQt&W5r~9~c+GgwRO|@}cJ*MzG15unSw7*~NpA!wCgJ2yu-v=vIe&GDA92J6 zEW=++Y%`Bt&vEhD@n!hsvMuMt?($1^{I|eprU-?D;~Dymw3*NziBu~Vy3HC=ul=T) zr-@BEN+U6c9&pCyaC+=^M-U=dOXb&}^qKtrh|T+!(1mW59*}TU%9S&NLfV7em&}SbsHN& z^7P&j@)txv0r*2L3@1G@k4_GsC92JYWC5@Tc%~McuTJ@4)=QS{GG0W17Ec6o4!?wM zIGwz%=ZuM_nOb1v6)p&VmY|Ezi+z(*=g0qiTn1tMHPGC_P65JK-S3qL%2)+E*ZjuWTWSoS(V@+4kp>K9dsA7XCX|t#{@Ms^8Z%$H(Nt9D@c_em zr+8L1T(`K9PUK**IOCA`B~MbyqRsb>-+T?S4hqHQ6602{2>7j1Vp%=fqI^gr6YPAO zv>q_ZjrH)$*-+f-Rizs3mkWVuy^daJx2}=fK!PPZokkq=Mpyrvdj6gh*xFNY0!VL# zz<&?awUH7_Z=#3X&HT50gFgctg5P+zPWbmAB@3y2Br&Z#zcuvV15(-(s=rO||8-^; zM^4G2@)_kiO(U7%zYcb>J;8t5eLL3iKq?vPaVcoG9{RT}I_~rK|C;4LGv+NL9h*YS zfyMk^H+6lddGqZAQ_ZL5pBYM z-PDaaL1?mQ|6ZfpF$)`#jV`N-Gv4fz|1t^#D**FmivODT|2yvgSg-#lj+?(iGk{TM UxI Date: Fri, 7 Apr 2023 01:40:59 +0530 Subject: [PATCH 328/432] test add chain in __init__.py --- graphistry/compute/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphistry/compute/__init__.py b/graphistry/compute/__init__.py index 3c7c7f45e3..0b35410e21 100644 --- a/graphistry/compute/__init__.py +++ b/graphistry/compute/__init__.py @@ -2,3 +2,4 @@ from .ast import ( n, e_forward, e_reverse, e_undirected ) +from .chain import chain From c7bc46d22ec53cda81c50ba3a4aac60618c088c2 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 02:01:29 +0530 Subject: [PATCH 329/432] test nitpick --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 279ba999ff..8773a713e2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,6 +69,7 @@ ('py:class', 'graphistry.text_utils.SearchToGraphMixin'), ('py:class', 'graphistry.embed_utils.HeterographEmbedModuleMixin'), ('py:class', 'graphistry.PlotterBase.PlotterBase'), + ('py:function', 'graphistry.compute.chain.chain') ('py:class', 'IGraph graph'), ('py:class', 'igraph'), ('py:class', 'dgl'), From a88961bf5cf403979eca699e61c26973d634d697 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 02:05:20 +0530 Subject: [PATCH 330/432] test nitpick 2 --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8773a713e2..474986caa3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,7 +69,7 @@ ('py:class', 'graphistry.text_utils.SearchToGraphMixin'), ('py:class', 'graphistry.embed_utils.HeterographEmbedModuleMixin'), ('py:class', 'graphistry.PlotterBase.PlotterBase'), - ('py:function', 'graphistry.compute.chain.chain') + ('py:function', 'graphistry.compute.chain.chain'), ('py:class', 'IGraph graph'), ('py:class', 'igraph'), ('py:class', 'dgl'), From f8d6ee10400963411028ea2fd4d39e93d5bff102 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 02:11:50 +0530 Subject: [PATCH 331/432] test --- docs/source/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 474986caa3..7a9f1657d8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -41,7 +41,7 @@ ] # mock imports -autodoc_mock_imports = ["graphistry.compute.chain"] +autodoc_mock_imports = ["graphistry.compute.chain.chain"] #FIXME Why is sphinx/autodoc failing here? nitpick_ignore = [ @@ -69,7 +69,6 @@ ('py:class', 'graphistry.text_utils.SearchToGraphMixin'), ('py:class', 'graphistry.embed_utils.HeterographEmbedModuleMixin'), ('py:class', 'graphistry.PlotterBase.PlotterBase'), - ('py:function', 'graphistry.compute.chain.chain'), ('py:class', 'IGraph graph'), ('py:class', 'igraph'), ('py:class', 'dgl'), From 8115020313cb00d2fed3fa4d9df57022bf13a2e0 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 02:32:47 +0530 Subject: [PATCH 332/432] test 3 --- docs/source/conf.py | 3 --- graphistry/plugins/cugraph.py | 19 ------------------- 2 files changed, 22 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7a9f1657d8..c7bbd195fc 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,9 +40,6 @@ "sphinx_autodoc_typehints", ] -# mock imports -autodoc_mock_imports = ["graphistry.compute.chain.chain"] - #FIXME Why is sphinx/autodoc failing here? nitpick_ignore = [ ('py:class', '1'), # Ex: api : Optional[Literal[1, 3]] diff --git a/graphistry/plugins/cugraph.py b/graphistry/plugins/cugraph.py index 5e7a656b25..c02b6c48ca 100644 --- a/graphistry/plugins/cugraph.py +++ b/graphistry/plugins/cugraph.py @@ -332,34 +332,15 @@ def layout_cugraph( """Layout the grpah using a cuGraph algorithm. For a list of layouts, see cugraph documentation (currently just force_atlas2). :param layout: Name of an cugraph layout method like `force_atlas2` - :type layout: str - :param params: Any named parameters to pass to the underlying cugraph method - :type params: dict - :param kind: The kind of cugraph Graph - :type kind: CuGraphKind - :param directed: During the to_cugraph conversion, whether to be directed. (default True) - :type directed: bool - :param G: The cugraph graph (G) to layout. If None, the current graph is used. - :type G: Optional[Any] - :param bind_position: Whether to call bind(point_x=, point_y=) (default True) - :type bind_position: bool - :param x_out_col: Attribute to write x position to. (default 'x') - :type x_out_col: str - :param y_out_col: Attribute to write x position to. (default 'y') - :type y_out_col: str - :param play: If defined, set settings(url_params={'play': play}). (default 0) - :type play: Optional[str] - :returns: Plotter - :rtype: Plotter **Example: ForceAtlas2 layout** :: From e581f97dd1596b195da024a91bf5ea8b4284dca8 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 02:38:27 +0530 Subject: [PATCH 333/432] test 4 --- graphistry/compute/chain.py | 2 -- graphistry/plugins/cugraph.py | 7 ------- graphistry/plugins/igraph.py | 26 -------------------------- 3 files changed, 35 deletions(-) diff --git a/graphistry/compute/chain.py b/graphistry/compute/chain.py index 057d56d328..818e2b3dff 100644 --- a/graphistry/compute/chain.py +++ b/graphistry/compute/chain.py @@ -98,10 +98,8 @@ def chain(self: Plottable, ops: List[ASTObject]) -> Plottable: If any matchers are named, add a correspondingly named boolean-valued column to the output :param ops: List[ASTobject] Various node and edge matchers - :type fg: dict :returns: Plotter - :rtype: Plotter **Example: Find nodes of some type** :: diff --git a/graphistry/plugins/cugraph.py b/graphistry/plugins/cugraph.py index c02b6c48ca..0930efed42 100644 --- a/graphistry/plugins/cugraph.py +++ b/graphistry/plugins/cugraph.py @@ -222,20 +222,13 @@ def compute_cugraph( """Run cugraph algorithm on graph. For algorithm parameters, see cuGraph docs. :param alg: algorithm name - :type alg: str :param out_col: node table output column name, defaults to alg param - :type out_col: Optional[str] :param params: algorithm parameters passed to cuGraph as kwargs - :type params: dict :param kind: kind of cugraph to use - :type kind: CuGraphKind :param directed: whether graph is directed - :type directed: bool :param G: cugraph graph to use; if None, use self - :type G: Optional[cugraph.Graph] :return: Plottable - :rtype: Plottable **Example: Pagerank** :: diff --git a/graphistry/plugins/igraph.py b/graphistry/plugins/igraph.py index 5fe5c2f8a6..0dbcb80dc3 100644 --- a/graphistry/plugins/igraph.py +++ b/graphistry/plugins/igraph.py @@ -294,22 +294,12 @@ def compute_igraph( """Enrich or replace graph using igraph methods :param alg: Name of an igraph.Graph method like `pagerank` - :type alg: str - :param out_col: For algorithms that generate a node attribute column, `out_col` is the desired output column name. When `None`, use the algorithm's name. (default None) - :type out_col: Optional[str] - :param directed: During the to_igraph conversion, whether to be directed. If None, try directed and then undirected. (default None) - :type directed: Optional[bool] - :param use_vids: During the to_igraph conversion, whether to interpret IDs as igraph vertex IDs (non-negative integers) or arbitrary values (False, default) - :type use_vids: bool - :param params: Any named parameters to pass to the underlying igraph method - :type params: dict :returns: Plotter - :rtype: Plotter **Example: Pagerank** :: @@ -425,31 +415,15 @@ def layout_igraph( """Compute graph layout using igraph algorithm. For a list of layouts, see layout_algs or igraph documentation. :param layout: Name of an igraph.Graph.layout method like `sugiyama` - :type layout: str - :param directed: During the to_igraph conversion, whether to be directed. If None, try directed and then undirected. (default None) - :type directed: Optional[bool] - :param use_vids: Whether to use igraph vertex ids (non-negative integers) or arbitary node ids (False, default) - :type use_vids: bool - :param bind_position: Whether to call bind(point_x=, point_y=) (default True) - :type bind_position: bool - :param x_out_col: Attribute to write x position to. (default 'x') - :type x_out_col: str - :param y_out_col: Attribute to write x position to. (default 'y') - :type y_out_col: str - :param play: If defined, set settings(url_params={'play': play}). (default 0) - :type play: Optional[str] - :param params: Any named parameters to pass to the underlying igraph method - :type params: dict :returns: Plotter - :rtype: Plotter **Example: Sugiyama layout** :: From bde96b18e2f670b387add903ea96365cbfc4971d Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 02:44:48 +0530 Subject: [PATCH 334/432] test 5 --- graphistry/compute/__init__.py | 1 - graphistry/compute/chain.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/graphistry/compute/__init__.py b/graphistry/compute/__init__.py index 0b35410e21..3c7c7f45e3 100644 --- a/graphistry/compute/__init__.py +++ b/graphistry/compute/__init__.py @@ -2,4 +2,3 @@ from .ast import ( n, e_forward, e_reverse, e_undirected ) -from .chain import chain diff --git a/graphistry/compute/chain.py b/graphistry/compute/chain.py index 818e2b3dff..a06c913018 100644 --- a/graphistry/compute/chain.py +++ b/graphistry/compute/chain.py @@ -90,26 +90,26 @@ def combine_steps(g: Plottable, kind: str, steps: List[Tuple[ASTObject,Plottable def chain(self: Plottable, ops: List[ASTObject]) -> Plottable: """ - Experimental: Chain a list of operations Return subgraph of matches according to the list of node & edge matchers - If any matchers are named, add a correspondingly named boolean-valued column to the output :param ops: List[ASTobject] Various node and edge matchers - :returns: Plotter + :returns: Plottable **Example: Find nodes of some type** - :: + + :: from graphistry.ast import n people_nodes_df = g.chain([ n({"type": "person"}) ])._nodes **Example: Find 2-hop edge sequences with some attribute** - :: + + :: from graphistry.ast import e_forward From f66856c817bd677bb2780ce67029f5bdf47532e1 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 02:52:06 +0530 Subject: [PATCH 335/432] test 6 --- graphistry/PlotterBase.py | 94 --------------------------------------- 1 file changed, 94 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 628b74991d..ceb0f5f55a 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -398,31 +398,15 @@ def encode_point_color(self, column, """Set point color with more control than bind() :param column: Data column name - :type column: str - :param palette: Optional list of color-like strings. Ex: ["black, "#FF0", "rgb(255,255,255)" ]. Used as a gradient for continuous and round-robin for categorical. - :type palette: Optional[list] - :param as_categorical: Interpret column values as categorical. Ex: Uses palette via round-robin when more values than palette entries. - :type as_categorical: Optional[bool] - :param as_continuous: Interpret column values as continuous. Ex: Uses palette for an interpolation gradient when more values than palette entries. - :type as_continuous: Optional[bool] - :param categorical_mapping: Mapping from column values to color-like strings. Ex: {"car": "red", "truck": #000"} - :type categorical_mapping: Optional[dict] - :param default_mapping: Augment categorical_mapping with mapping for values not in categorical_mapping. Ex: default_mapping="gray". - :type default_mapping: Optional[str] - :param for_default: Use encoding for when no user override is set. Default on. - :type for_default: Optional[bool] - :param for_current: Use encoding as currently active. Clearing the active encoding resets it to default, which may be different. Default on. - :type for_current: Optional[bool] :returns: Plotter - :rtype: Plotter **Example: Set a palette-valued column for the color, same as bind(point_color='my_column')** :: @@ -459,31 +443,15 @@ def encode_edge_color(self, column, """Set edge color with more control than bind() :param column: Data column name - :type column: str - :param palette: Optional list of color-like strings. Ex: ["black, "#FF0", "rgb(255,255,255)" ]. Used as a gradient for continuous and round-robin for categorical. - :type palette: Optional[list] - :param as_categorical: Interpret column values as categorical. Ex: Uses palette via round-robin when more values than palette entries. - :type as_categorical: Optional[bool] - :param as_continuous: Interpret column values as continuous. Ex: Uses palette for an interpolation gradient when more values than palette entries. - :type as_continuous: Optional[bool] - :param categorical_mapping: Mapping from column values to color-like strings. Ex: {"car": "red", "truck": #000"} - :type categorical_mapping: Optional[dict] - :param default_mapping: Augment categorical_mapping with mapping for values not in categorical_mapping. Ex: default_mapping="gray". - :type default_mapping: Optional[str] - :param for_default: Use encoding for when no user override is set. Default on. - :type for_default: Optional[bool] - :param for_current: Use encoding as currently active. Clearing the active encoding resets it to default, which may be different. Default on. - :type for_current: Optional[bool] :returns: Plotter - :rtype: Plotter **Example: See encode_point_color** """ @@ -541,34 +509,16 @@ def encode_point_icon(self, column, """Set node icon with more control than bind(). Values from Font Awesome 4 such as "laptop": https://fontawesome.com/v4.7.0/icons/ , image URLs (http://...), and data URIs (data:...). When as_text=True is enabled, values are instead interpreted as raw strings. :param column: Data column name - :type column: str - :param categorical_mapping: Mapping from column values to icon name strings. Ex: {"toyota": 'car', "ford": 'truck'} - :type categorical_mapping: Optional[dict] - :param default_mapping: Augment categorical_mapping with mapping for values not in categorical_mapping. Ex: default_mapping=50. - :type default_mapping: Optional[Union[int,float]] - :param for_default: Use encoding for when no user override is set. Default on. - :type for_default: Optional[bool] - :param for_current: Use encoding as currently active. Clearing the active encoding resets it to default, which may be different. Default on. - :type for_current: Optional[bool] - :param as_text: Values should instead be treated as raw strings, instead of icons and images. (Default False.) - :type as_text: Optional[bool] - :param blend_mode: CSS blend mode - :type blend_mode: Optional[str] - :param style: CSS filter properties - opacity, saturation, luminosity, grayscale, and more - :type style: Optional[dict] - :param border: Border properties - 'width', 'color', and 'storke' - :type border: Optional[dict] :returns: Plotter - :rtype: Plotter **Example: Set a string column of icons for the point icons, same as bind(point_icon='my_column')** :: @@ -608,25 +558,13 @@ def encode_edge_icon(self, column, """Set edge icon with more control than bind() Values from Font Awesome 4 such as "laptop": https://fontawesome.com/v4.7.0/icons/ , image URLs (http://...), and data URIs (data:...). When as_text=True is enabled, values are instead interpreted as raw strings. :param column: Data column name - :type column: str - :param categorical_mapping: Mapping from column values to icon name strings. Ex: {"toyota": 'car', "ford": 'truck'} - :type categorical_mapping: Optional[dict] - :param default_mapping: Augment categorical_mapping with mapping for values not in categorical_mapping. Ex: default_mapping=50. - :type default_mapping: Optional[Union[int,float]] - :param for_default: Use encoding for when no user override is set. Default on. - :type for_default: Optional[bool] - :param for_current: Use encoding as currently active. Clearing the active encoding resets it to default, which may be different. Default on. - :type for_current: Optional[bool] - :param as_text: Values should instead be treated as raw strings, instead of icons and images. (Default False.) - :type as_text: Optional[bool] :returns: Plotter - :rtype: Plotter **Example: Set a string column of icons for the edge icons, same as bind(edge_icon='my_column')** :: @@ -828,55 +766,23 @@ def bind(self, source=None, destination=None, node=None, edge=None, """Relate data attributes to graph structure and visual representation. To facilitate reuse and replayable notebooks, the binding call is chainable. Invocation does not effect the old binding: it instead returns a new Plotter instance with the new bindings added to the existing ones. Both the old and new bindings can then be used for different graphs. :param source: Attribute containing an edge's source ID - :type source: str - :param destination: Attribute containing an edge's destination ID - :type destination: str - :param node: Attribute containing a node's ID - :type node: str - :param edge: Attribute containing an edge's ID - :type edge: str - :param edge_title: Attribute overriding edge's minimized label text. By default, the edge source and destination is used. - :type edge_title: str - :param edge_label: Attribute overriding edge's expanded label text. By default, scrollable list of attribute/value mappings. - :type edge_label: str - :param edge_color: Attribute overriding edge's color. rgba (int64) or int32 palette index, see `palette `_ definitions for values. Based on Color Brewer. - :type edge_color: str - :param edge_source_color: Attribute overriding edge's source color if no edge_color, as an rgba int64 value. - :type edge_source_color: str - :param edge_destination_color: Attribute overriding edge's destination color if no edge_color, as an rgba int64 value. - :type edge_destination_color: str - :param edge_weight: Attribute overriding edge weight. Default is 1. Advanced layout controls will relayout edges based on this value. - :type edge_weight: str - :param point_title: Attribute overriding node's minimized label text. By default, the node ID is used. - :type point_title: str - :param point_label: Attribute overriding node's expanded label text. By default, scrollable list of attribute/value mappings. - :type point_label: str - :param point_color: Attribute overriding node's color.rgba (int64) or int32 palette index, see `palette `_ definitions for values. Based on Color Brewer. - :type point_color: str - :param point_size: Attribute overriding node's size. By default, uses the node degree. The visualization will normalize point sizes and adjust dynamically using semantic zoom. - :type point_size: str - :param point_x: Attribute overriding node's initial x position. Combine with ".settings(url_params={'play': 0}))" to create a custom layout - :type point_x: str - :param point_y: Attribute overriding node's initial y position. Combine with ".settings(url_params={'play': 0}))" to create a custom layout - :type point_y: str :returns: Plotter - :rtype: Plotter **Example: Minimal** From 06f26528e4b227fc5af738a745f035c0c49cb5b8 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 03:01:23 +0530 Subject: [PATCH 336/432] test 7 --- docs/source/conf.py | 3 +++ graphistry/compute/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index c7bbd195fc..b28a305f8e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,6 +40,9 @@ "sphinx_autodoc_typehints", ] +# mock imports +todoc_mock_imports = ["graphistry.compute.ast.ASTObject"] + #FIXME Why is sphinx/autodoc failing here? nitpick_ignore = [ ('py:class', '1'), # Ex: api : Optional[Literal[1, 3]] diff --git a/graphistry/compute/__init__.py b/graphistry/compute/__init__.py index 3c7c7f45e3..4f6cef58f7 100644 --- a/graphistry/compute/__init__.py +++ b/graphistry/compute/__init__.py @@ -1,4 +1,4 @@ from .ComputeMixin import ComputeMixin from .ast import ( - n, e_forward, e_reverse, e_undirected + n, e_forward, e_reverse, e_undirected, ASTObject ) From 254b31bc4af97ac220a2e46169baa8f54dd468d6 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 03:07:50 +0530 Subject: [PATCH 337/432] test 8 --- graphistry/compute/chain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/compute/chain.py b/graphistry/compute/chain.py index a06c913018..2e4124415e 100644 --- a/graphistry/compute/chain.py +++ b/graphistry/compute/chain.py @@ -95,7 +95,7 @@ def chain(self: Plottable, ops: List[ASTObject]) -> Plottable: Return subgraph of matches according to the list of node & edge matchers If any matchers are named, add a correspondingly named boolean-valued column to the output - :param ops: List[ASTobject] Various node and edge matchers + :param ops: List[ASTObject] Various node and edge matchers :returns: Plottable From 1d9722c7fdb38d6e8c8a02b01f6899a3988a9389 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 03:12:47 +0530 Subject: [PATCH 338/432] test 9 --- graphistry/plugins/igraph.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphistry/plugins/igraph.py b/graphistry/plugins/igraph.py index 0dbcb80dc3..09278743a5 100644 --- a/graphistry/plugins/igraph.py +++ b/graphistry/plugins/igraph.py @@ -48,8 +48,7 @@ def from_igraph(self, :param merge_if_existing: Whether to merge with existing node/edge dataframes (default True) :param merge_if_existing: bool - :returns: Plotter - :rtype: Plotter + :returns: Plottable **Example: Convert from igraph, including all node/edge properties** :: From 073eaec8e0348c7c8ce0d62cc4dac3d99b339860 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 03:18:45 +0530 Subject: [PATCH 339/432] test 10 --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index b28a305f8e..e830756af3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -93,6 +93,7 @@ ('py:data', 'typing.List'), ('py:data', 'typing.Literal'), ('py:data', 'typing.Optional'), + ('py:data', 'typing.Callable'), ('py:data', 'typing.Tuple'), ('py:data', 'typing.Union'), ('py:class','pandas.core.frame.DataFrame') From 1e8207636466453d197e3a276c2c47a33578bfa2 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 03:25:54 +0530 Subject: [PATCH 340/432] test 11 --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index e830756af3..20d3ede7d4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,6 +69,7 @@ ('py:class', 'graphistry.text_utils.SearchToGraphMixin'), ('py:class', 'graphistry.embed_utils.HeterographEmbedModuleMixin'), ('py:class', 'graphistry.PlotterBase.PlotterBase'), + ('py:class', 'graphistry.plotter.Plotter'), ('py:class', 'IGraph graph'), ('py:class', 'igraph'), ('py:class', 'dgl'), From 1a68d747fdb574a6a802d7a9db37b45eb2e321af Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 03:29:45 +0530 Subject: [PATCH 341/432] test 12 --- docs/source/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 20d3ede7d4..6f46d1ea85 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,8 +40,6 @@ "sphinx_autodoc_typehints", ] -# mock imports -todoc_mock_imports = ["graphistry.compute.ast.ASTObject"] #FIXME Why is sphinx/autodoc failing here? nitpick_ignore = [ @@ -69,6 +67,7 @@ ('py:class', 'graphistry.text_utils.SearchToGraphMixin'), ('py:class', 'graphistry.embed_utils.HeterographEmbedModuleMixin'), ('py:class', 'graphistry.PlotterBase.PlotterBase'), + ('py:class', 'graphistry.compute.ast.ASTObject'), ('py:class', 'graphistry.plotter.Plotter'), ('py:class', 'IGraph graph'), ('py:class', 'igraph'), From 8320dd505082f632a9c121d9e0403170386bc093 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 03:37:16 +0530 Subject: [PATCH 342/432] test 13 --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6f46d1ea85..12b39f3270 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,7 +68,7 @@ ('py:class', 'graphistry.embed_utils.HeterographEmbedModuleMixin'), ('py:class', 'graphistry.PlotterBase.PlotterBase'), ('py:class', 'graphistry.compute.ast.ASTObject'), - ('py:class', 'graphistry.plotter.Plotter'), + ('py:class', 'Plotter'), ('py:class', 'IGraph graph'), ('py:class', 'igraph'), ('py:class', 'dgl'), From a1941ba0484cf6584178b67e30f3122221070caf Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 03:45:05 +0530 Subject: [PATCH 343/432] test 14 --- graphistry/PlotterBase.py | 2 +- graphistry/compute/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index ceb0f5f55a..30ac752e02 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -959,7 +959,7 @@ def edges(self, edges: Union[Callable, Any], source=None, destination=None, edge If a callable, will be called with current Plotter and whatever positional+named arguments :param edges: Edges and their attributes, or transform from Plotter to edges - :type edges: Pandas dataframe, NetworkX graph, or IGraph graph. + :type edges: Pandas dataframe, NetworkX graph, or IGraph graph :returns: Plotter :rtype: Plotter diff --git a/graphistry/compute/__init__.py b/graphistry/compute/__init__.py index 4f6cef58f7..3c7c7f45e3 100644 --- a/graphistry/compute/__init__.py +++ b/graphistry/compute/__init__.py @@ -1,4 +1,4 @@ from .ComputeMixin import ComputeMixin from .ast import ( - n, e_forward, e_reverse, e_undirected, ASTObject + n, e_forward, e_reverse, e_undirected ) From 4361ae6aa1dd11be4f6b597a648ca3c8dc4d2853 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 04:28:31 +0530 Subject: [PATCH 344/432] final fix --- graphistry/PlotterBase.py | 94 +++++++++++++++++++++++++++++++++++ graphistry/compute/chain.py | 3 +- graphistry/plugins/cugraph.py | 31 ++++++++++++ graphistry/plugins/igraph.py | 28 ++++++++++- 4 files changed, 154 insertions(+), 2 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 30ac752e02..9c64d0e1f5 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -398,15 +398,31 @@ def encode_point_color(self, column, """Set point color with more control than bind() :param column: Data column name + :type column: str + :param palette: Optional list of color-like strings. Ex: ["black, "#FF0", "rgb(255,255,255)" ]. Used as a gradient for continuous and round-robin for categorical. + :type palette: Optional[list] + :param as_categorical: Interpret column values as categorical. Ex: Uses palette via round-robin when more values than palette entries. + :type as_categorical: Optional[bool] + :param as_continuous: Interpret column values as continuous. Ex: Uses palette for an interpolation gradient when more values than palette entries. + :type as_continuous: Optional[bool] + :param categorical_mapping: Mapping from column values to color-like strings. Ex: {"car": "red", "truck": #000"} + :type categorical_mapping: Optional[dict] + :param default_mapping: Augment categorical_mapping with mapping for values not in categorical_mapping. Ex: default_mapping="gray". + :type default_mapping: Optional[str] + :param for_default: Use encoding for when no user override is set. Default on. + :type for_default: Optional[bool] + :param for_current: Use encoding as currently active. Clearing the active encoding resets it to default, which may be different. Default on. + :type for_current: Optional[bool] :returns: Plotter + :rtype: Plotter **Example: Set a palette-valued column for the color, same as bind(point_color='my_column')** :: @@ -443,15 +459,31 @@ def encode_edge_color(self, column, """Set edge color with more control than bind() :param column: Data column name + :type column: str + :param palette: Optional list of color-like strings. Ex: ["black, "#FF0", "rgb(255,255,255)" ]. Used as a gradient for continuous and round-robin for categorical. + :type palette: Optional[list] + :param as_categorical: Interpret column values as categorical. Ex: Uses palette via round-robin when more values than palette entries. + :type as_categorical: Optional[bool] + :param as_continuous: Interpret column values as continuous. Ex: Uses palette for an interpolation gradient when more values than palette entries. + :type as_continuous: Optional[bool] + :param categorical_mapping: Mapping from column values to color-like strings. Ex: {"car": "red", "truck": #000"} + :type categorical_mapping: Optional[dict] + :param default_mapping: Augment categorical_mapping with mapping for values not in categorical_mapping. Ex: default_mapping="gray". + :type default_mapping: Optional[str] + :param for_default: Use encoding for when no user override is set. Default on. + :type for_default: Optional[bool] + :param for_current: Use encoding as currently active. Clearing the active encoding resets it to default, which may be different. Default on. + :type for_current: Optional[bool] :returns: Plotter + :rtype: Plotter **Example: See encode_point_color** """ @@ -509,16 +541,34 @@ def encode_point_icon(self, column, """Set node icon with more control than bind(). Values from Font Awesome 4 such as "laptop": https://fontawesome.com/v4.7.0/icons/ , image URLs (http://...), and data URIs (data:...). When as_text=True is enabled, values are instead interpreted as raw strings. :param column: Data column name + :type column: str + :param categorical_mapping: Mapping from column values to icon name strings. Ex: {"toyota": 'car', "ford": 'truck'} + :type categorical_mapping: Optional[dict] + :param default_mapping: Augment categorical_mapping with mapping for values not in categorical_mapping. Ex: default_mapping=50. + :type default_mapping: Optional[Union[int,float]] + :param for_default: Use encoding for when no user override is set. Default on. + :type for_default: Optional[bool] + :param for_current: Use encoding as currently active. Clearing the active encoding resets it to default, which may be different. Default on. + :type for_current: Optional[bool] + :param as_text: Values should instead be treated as raw strings, instead of icons and images. (Default False.) + :type as_text: Optional[bool] + :param blend_mode: CSS blend mode + :type blend_mode: Optional[str] + :param style: CSS filter properties - opacity, saturation, luminosity, grayscale, and more + :type style: Optional[dict] + :param border: Border properties - 'width', 'color', and 'storke' + :type border: Optional[dict] :returns: Plotter + :rtype: Plotter **Example: Set a string column of icons for the point icons, same as bind(point_icon='my_column')** :: @@ -558,13 +608,25 @@ def encode_edge_icon(self, column, """Set edge icon with more control than bind() Values from Font Awesome 4 such as "laptop": https://fontawesome.com/v4.7.0/icons/ , image URLs (http://...), and data URIs (data:...). When as_text=True is enabled, values are instead interpreted as raw strings. :param column: Data column name + :type column: str + :param categorical_mapping: Mapping from column values to icon name strings. Ex: {"toyota": 'car', "ford": 'truck'} + :type categorical_mapping: Optional[dict] + :param default_mapping: Augment categorical_mapping with mapping for values not in categorical_mapping. Ex: default_mapping=50. + :type default_mapping: Optional[Union[int,float]] + :param for_default: Use encoding for when no user override is set. Default on. + :type for_default: Optional[bool] + :param for_current: Use encoding as currently active. Clearing the active encoding resets it to default, which may be different. Default on. + :type for_current: Optional[bool] + :param as_text: Values should instead be treated as raw strings, instead of icons and images. (Default False.) + :type as_text: Optional[bool] :returns: Plotter + :rtype: Plotter **Example: Set a string column of icons for the edge icons, same as bind(edge_icon='my_column')** :: @@ -766,23 +828,55 @@ def bind(self, source=None, destination=None, node=None, edge=None, """Relate data attributes to graph structure and visual representation. To facilitate reuse and replayable notebooks, the binding call is chainable. Invocation does not effect the old binding: it instead returns a new Plotter instance with the new bindings added to the existing ones. Both the old and new bindings can then be used for different graphs. :param source: Attribute containing an edge's source ID + :type source: str + :param destination: Attribute containing an edge's destination ID + :type destination: str + :param node: Attribute containing a node's ID + :type node: str + :param edge: Attribute containing an edge's ID + :type edge: str + :param edge_title: Attribute overriding edge's minimized label text. By default, the edge source and destination is used. + :type edge_title: str + :param edge_label: Attribute overriding edge's expanded label text. By default, scrollable list of attribute/value mappings. + :type edge_label: str + :param edge_color: Attribute overriding edge's color. rgba (int64) or int32 palette index, see `palette `_ definitions for values. Based on Color Brewer. + :type edge_color: str + :param edge_source_color: Attribute overriding edge's source color if no edge_color, as an rgba int64 value. + :type edge_source_color: str + :param edge_destination_color: Attribute overriding edge's destination color if no edge_color, as an rgba int64 value. + :type edge_destination_color: str + :param edge_weight: Attribute overriding edge weight. Default is 1. Advanced layout controls will relayout edges based on this value. + :type edge_weight: str + :param point_title: Attribute overriding node's minimized label text. By default, the node ID is used. + :type point_title: str + :param point_label: Attribute overriding node's expanded label text. By default, scrollable list of attribute/value mappings. + :type point_label: str + :param point_color: Attribute overriding node's color.rgba (int64) or int32 palette index, see `palette `_ definitions for values. Based on Color Brewer. + :type point_color: str + :param point_size: Attribute overriding node's size. By default, uses the node degree. The visualization will normalize point sizes and adjust dynamically using semantic zoom. + :type point_size: str + :param point_x: Attribute overriding node's initial x position. Combine with ".settings(url_params={'play': 0}))" to create a custom layout + :type point_x: str + :param point_y: Attribute overriding node's initial y position. Combine with ".settings(url_params={'play': 0}))" to create a custom layout + :type point_y: str :returns: Plotter + :rtype: Plotter **Example: Minimal** diff --git a/graphistry/compute/chain.py b/graphistry/compute/chain.py index 2e4124415e..4920b74c9f 100644 --- a/graphistry/compute/chain.py +++ b/graphistry/compute/chain.py @@ -97,7 +97,8 @@ def chain(self: Plottable, ops: List[ASTObject]) -> Plottable: :param ops: List[ASTObject] Various node and edge matchers - :returns: Plottable + :returns: Plotter + :rtype: Plotter **Example: Find nodes of some type** diff --git a/graphistry/plugins/cugraph.py b/graphistry/plugins/cugraph.py index 0930efed42..da68452d11 100644 --- a/graphistry/plugins/cugraph.py +++ b/graphistry/plugins/cugraph.py @@ -222,13 +222,25 @@ def compute_cugraph( """Run cugraph algorithm on graph. For algorithm parameters, see cuGraph docs. :param alg: algorithm name + :type alg: str + :param out_col: node table output column name, defaults to alg param + :type out_col: Optional[str] + :param params: algorithm parameters passed to cuGraph as kwargs + :type params: dict + :param kind: kind of cugraph to use + :type kind: CuGraphKind + :param directed: whether graph is directed + :type directed: bool + :param G: cugraph graph to use; if None, use self + :type G: Optional[cugraph.Graph] :return: Plottable + :rtype: Plottable **Example: Pagerank** :: @@ -325,15 +337,34 @@ def layout_cugraph( """Layout the grpah using a cuGraph algorithm. For a list of layouts, see cugraph documentation (currently just force_atlas2). :param layout: Name of an cugraph layout method like `force_atlas2` + :type layout: str + :param params: Any named parameters to pass to the underlying cugraph method + :type params: dict + :param kind: The kind of cugraph Graph + :type kind: CuGraphKind + :param directed: During the to_cugraph conversion, whether to be directed. (default True) + :type directed: bool + :param G: The cugraph graph (G) to layout. If None, the current graph is used. + :type G: Optional[Any] + :param bind_position: Whether to call bind(point_x=, point_y=) (default True) + :type bind_position: bool + :param x_out_col: Attribute to write x position to. (default 'x') + :type x_out_col: str + :param y_out_col: Attribute to write x position to. (default 'y') + :type y_out_col: str + :param play: If defined, set settings(url_params={'play': play}). (default 0) + :type play: Optional[str] + :returns: Plotter + :rtype: Plotter **Example: ForceAtlas2 layout** :: diff --git a/graphistry/plugins/igraph.py b/graphistry/plugins/igraph.py index 09278743a5..b7bdc0d405 100644 --- a/graphistry/plugins/igraph.py +++ b/graphistry/plugins/igraph.py @@ -48,7 +48,7 @@ def from_igraph(self, :param merge_if_existing: Whether to merge with existing node/edge dataframes (default True) :param merge_if_existing: bool - :returns: Plottable + :returns: Plotter **Example: Convert from igraph, including all node/edge properties** :: @@ -293,12 +293,22 @@ def compute_igraph( """Enrich or replace graph using igraph methods :param alg: Name of an igraph.Graph method like `pagerank` + :type alg: str + :param out_col: For algorithms that generate a node attribute column, `out_col` is the desired output column name. When `None`, use the algorithm's name. (default None) + :type out_col: Optional[str] + :param directed: During the to_igraph conversion, whether to be directed. If None, try directed and then undirected. (default None) + :type directed: Optional[bool] + :param use_vids: During the to_igraph conversion, whether to interpret IDs as igraph vertex IDs (non-negative integers) or arbitrary values (False, default) + :type use_vids: bool + :param params: Any named parameters to pass to the underlying igraph method + :type params: dict :returns: Plotter + :rtype: Plotter **Example: Pagerank** :: @@ -414,15 +424,31 @@ def layout_igraph( """Compute graph layout using igraph algorithm. For a list of layouts, see layout_algs or igraph documentation. :param layout: Name of an igraph.Graph.layout method like `sugiyama` + :type layout: str + :param directed: During the to_igraph conversion, whether to be directed. If None, try directed and then undirected. (default None) + :type directed: Optional[bool] + :param use_vids: Whether to use igraph vertex ids (non-negative integers) or arbitary node ids (False, default) + :type use_vids: bool + :param bind_position: Whether to call bind(point_x=, point_y=) (default True) + :type bind_position: bool + :param x_out_col: Attribute to write x position to. (default 'x') + :type x_out_col: str + :param y_out_col: Attribute to write x position to. (default 'y') + :type y_out_col: str + :param play: If defined, set settings(url_params={'play': play}). (default 0) + :type play: Optional[str] + :param params: Any named parameters to pass to the underlying igraph method + :type params: dict :returns: Plotter + :rtype: Plotter **Example: Sugiyama layout** :: From db8c22855643ab4d5631ad4a65e3054cf8ed80cb Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 04:33:51 +0530 Subject: [PATCH 345/432] final fix 1 --- docs/source/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 12b39f3270..0d639ff942 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,6 +69,8 @@ ('py:class', 'graphistry.PlotterBase.PlotterBase'), ('py:class', 'graphistry.compute.ast.ASTObject'), ('py:class', 'Plotter'), + ('py:class', 'Plottable'), + ('py:class', 'CuGraphKind'), ('py:class', 'IGraph graph'), ('py:class', 'igraph'), ('py:class', 'dgl'), From 5e9a577d2084907e4089be437f3e39e275d0fe42 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Fri, 7 Apr 2023 04:41:40 +0530 Subject: [PATCH 346/432] final fix 2 --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0d639ff942..e8a0c2f36c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -71,6 +71,7 @@ ('py:class', 'Plotter'), ('py:class', 'Plottable'), ('py:class', 'CuGraphKind'), + ('py:class', 'cugraph.Graph'), ('py:class', 'IGraph graph'), ('py:class', 'igraph'), ('py:class', 'dgl'), From 52506b1f865a63ebafe9c59f3ccf4efdf574326c Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 19:25:18 -0400 Subject: [PATCH 347/432] fix(conf.py): added converter for badges --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index e8a0c2f36c..a9e22f14e7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -38,6 +38,7 @@ #'sphinx.ext.intersphinx', "sphinx.ext.ifconfig", "sphinx_autodoc_typehints", + "sphinx.ext.imgconverter" ] From 7114bc8af608efe6476b0f4d23b21e1a0cfc7328 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 19:29:30 -0400 Subject: [PATCH 348/432] fix(conf.py): removed img converter --- docs/source/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index a9e22f14e7..1f47612c1a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -37,8 +37,7 @@ #'sphinx.ext.autosummary', #'sphinx.ext.intersphinx', "sphinx.ext.ifconfig", - "sphinx_autodoc_typehints", - "sphinx.ext.imgconverter" + "sphinx_autodoc_typehints" ] From 224351f55798287cc23f9d99c9a4d6c5a8805cdb Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 20:02:27 -0400 Subject: [PATCH 349/432] test(conf.py): using only directive for ci testing --- docs/source/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index ce0734f357..ddbf98922f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,5 +1,6 @@ PyGraphistry: Explore Relationships ======================================== +.. only:: html .. image:: https://readthedocs.org/projects/pygraphistry/badge/?version=latest :target: https://pygraphistry.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status From cf25d29cdd56641fbed6cdcce7e941e8dd053151 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 20:07:34 -0400 Subject: [PATCH 350/432] test(conf.py): testing only directive --- docs/source/index.rst | 55 ++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index ddbf98922f..e8943f800f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,44 +1,45 @@ PyGraphistry: Explore Relationships ======================================== .. only:: html -.. image:: https://readthedocs.org/projects/pygraphistry/badge/?version=latest - :target: https://pygraphistry.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status + .. image:: https://readthedocs.org/projects/pygraphistry/badge/?version=latest + :target: https://pygraphistry.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status -.. image:: https://github.com/graphistry/pygraphistry/workflows/CI%20Tests/badge.svg - :target: https://github.com/graphistry/pygraphistry/workflows/CI%20Tests/badge.svg - :alt: Build Status + .. image:: https://github.com/graphistry/pygraphistry/workflows/CI%20Tests/badge.svg + :target: https://github.com/graphistry/pygraphistry/workflows/CI%20Tests/badge.svg + :alt: Build Status -.. image:: https://github.com/graphistry/pygraphistry/workflows/CodeQL/badge.svg - :target: https://github.com/graphistry/pygraphistry/actions?query=workflow%3ACodeQL - :alt: CodeQL Status -.. image:: https://img.shields.io/pypi/v/graphistry.svg - :target: https://pypi.python.org/pypi/graphistry - :alt: PyPi Status + .. image:: https://github.com/graphistry/pygraphistry/workflows/CodeQL/badge.svg + :target: https://github.com/graphistry/pygraphistry/actions?query=workflow%3ACodeQL + :alt: CodeQL Status -.. image:: https://img.shields.io/pypi/dm/graphistry - :target: https://img.shields.io/pypi/dm/graphistry - :alt: PyPi Downloads + .. image:: https://img.shields.io/pypi/v/graphistry.svg + :target: https://pypi.python.org/pypi/graphistry + :alt: PyPi Status + .. image:: https://img.shields.io/pypi/dm/graphistry + :target: https://img.shields.io/pypi/dm/graphistry + :alt: PyPi Downloads -.. image:: https://img.shields.io/pypi/l/graphistry.svg - :target: https://pypi.python.org/pypi/graphistry - :alt: License -.. image:: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com - :target: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com - :alt: License + .. image:: https://img.shields.io/pypi/l/graphistry.svg + :target: https://pypi.python.org/pypi/graphistry + :alt: License -.. image:: https://img.shields.io/badge/slack-Graphistry%20chat-orange.svg?logo=slack - :target: https://join.slack.com/t/graphistry-community/shared_invite/zt-53ik36w2-fpP0Ibjbk7IJuVFIRSnr6g - :alt: Slack + .. image:: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com + :target: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com + :alt: License -.. image:: https://img.shields.io/twitter/follow/graphistry - :target: https://twitter.com/graphistry - :alt: Twitter + .. image:: https://img.shields.io/badge/slack-Graphistry%20chat-orange.svg?logo=slack + :target: https://join.slack.com/t/graphistry-community/shared_invite/zt-53ik36w2-fpP0Ibjbk7IJuVFIRSnr6g + :alt: Slack + + .. image:: https://img.shields.io/twitter/follow/graphistry + :target: https://twitter.com/graphistry + :alt: Twitter .. Quickstart: .. `Read our tutorial `_ From b17af60aed4d2730fd78f0689912c2dff2d9108a Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 23:23:42 -0400 Subject: [PATCH 351/432] fix(docstr): removed slack badge --- docs/source/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index e8943f800f..2b6cf75d44 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -33,9 +33,9 @@ PyGraphistry: Explore Relationships :target: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com :alt: License - .. image:: https://img.shields.io/badge/slack-Graphistry%20chat-orange.svg?logo=slack - :target: https://join.slack.com/t/graphistry-community/shared_invite/zt-53ik36w2-fpP0Ibjbk7IJuVFIRSnr6g - :alt: Slack + .. .. image:: https://img.shields.io/badge/slack-Graphistry%20chat-orange.svg?logo=slack + .. :target: https://join.slack.com/t/graphistry-community/shared_invite/zt-53ik36w2-fpP0Ibjbk7IJuVFIRSnr6g + .. :alt: Slack .. image:: https://img.shields.io/twitter/follow/graphistry :target: https://twitter.com/graphistry From 385a0ad4409bd6f26cc43d7bd68ab0d4651f63f0 Mon Sep 17 00:00:00 2001 From: Desirree Adegunle <87389186+dess890@users.noreply.github.com> Date: Thu, 6 Apr 2023 23:32:18 -0400 Subject: [PATCH 352/432] test(docstr): testing to see if uptime is failing --- docs/source/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 2b6cf75d44..9b10c2c91c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -29,9 +29,9 @@ PyGraphistry: Explore Relationships :target: https://pypi.python.org/pypi/graphistry :alt: License - .. image:: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com - :target: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com - :alt: License + .. .. image:: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com + .. :target: https://img.shields.io/uptimerobot/status/m787548531-e9c7b7508fc76fea927e2313?label=hub.graphistry.com + .. :alt: License .. .. image:: https://img.shields.io/badge/slack-Graphistry%20chat-orange.svg?logo=slack .. :target: https://join.slack.com/t/graphistry-community/shared_invite/zt-53ik36w2-fpP0Ibjbk7IJuVFIRSnr6g From 7a74dc870a01c07634de83d5f71432d04b84f1f9 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Fri, 7 Apr 2023 15:51:54 +0800 Subject: [PATCH 353/432] cudf edge reqs --- graphistry/feature_utils.py | 66 +++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 84c65675fd..0250c2eb52 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -60,7 +60,7 @@ GapEncoder = Any SimilarityEncoder = Any try: - from sklearn.preprocessing import FunctionTransformer + from cuml.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin except: FunctionTransformer = Any @@ -328,11 +328,10 @@ def remove_node_column_from_symbolic(X_symbolic, node): logger.info(f"Removing `{node}` from input X_symbolic list") X_symbolic.remove(node) return X_symbolic - if isinstance(X_symbolic, pd.DataFrame): + if isinstance(X_symbolic, pd.DataFrame) or 'cudf' in str(getmodule(X_symbolic)): logger.info(f"Removing `{node}` from input X_symbolic DataFrame") return X_symbolic.drop(columns=[node], errors="ignore") - def remove_internal_namespace_if_present(df: pd.DataFrame): """Some tranformations below add columns to the DataFrame, this method removes them before featurization will not drop if suffix is added during UMAP-ing @@ -609,11 +608,19 @@ def get_preprocessing_pipeline( :return: scaled array, imputer instances or None, scaler instance or None """ from sklearn.preprocessing import ( + # FunctionTransformer, + # KBinsDiscretizer, + # MinMaxScaler, + MultiLabelBinarizer, + QuantileTransformer, + # RobustScaler, + # StandardScaler, + ) + from cuml.preprocessing import ( FunctionTransformer, KBinsDiscretizer, MinMaxScaler, - MultiLabelBinarizer, - QuantileTransformer, + # QuantileTransformer, ## cuml 23 only RobustScaler, StandardScaler, ) @@ -886,7 +893,7 @@ def __call__(self, *args, **kwargs): def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. - from sklearn.preprocessing import FunctionTransformer + from cuml.preprocessing import FunctionTransformer label_encoder = False data_encoder = False y_ = y @@ -947,8 +954,9 @@ def process_dirty_dataframes( if feature_engine == 'dirty_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder elif feature_engine == 'cu_cat': + # assert_cuml_cucat() ## tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - from sklearn.preprocessing import FunctionTransformer + from cuml.preprocessing import FunctionTransformer t = time() if not is_dataframe_all_numeric(ndf): @@ -963,7 +971,6 @@ def process_dirty_dataframes( ) logger.info(":: Encoding DataFrame might take a few minutes ------") - X_enc = data_encoder.fit_transform(ndf, y) X_enc = make_array(X_enc) @@ -992,9 +999,14 @@ def process_dirty_dataframes( ) X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version elif 'cudf.core.dataframe' in str(getmodule(ndf)): - X_enc = cudf.DataFrame( - X_enc, columns=features_transformed, index=ndf.index - ) + import cudf + X_enc = cudf.DataFrame.from_arrow(X_enc) + X_enc.index = ndf.index + # features_transformed=np.array([item.as_py() for item in features_transformed.key()]) + # X_enc.columns = features_transformed.as_py() + # = features_transformed #.to_numpy() ##error suggests this -- not working + + #X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version else: logger.info("-*-*- DataFrame is completely numeric") @@ -1219,7 +1231,6 @@ def process_nodes_dataframes( logger.debug( f"--The entire Encoding process took {(time()-t)/60:.2f} minutes" ) - X_encs, y_encs, scaling_pipeline, scaling_pipeline_target = smart_scaler( # noqa X_enc, y_enc, @@ -1234,7 +1245,6 @@ def process_nodes_dataframes( strategy=strategy, keep_n_decimals=keep_n_decimals, ) - return ( X_enc, y_enc, @@ -1309,7 +1319,7 @@ def encode_edges(edf, src, dst, mlb, fit=False): edf (pd.DataFrame): edge dataframe src (string): source column dst (string): destination column - mlb (sklearn): multilabelBinarizer + mlb (sklearn): multilabelBinarizer ##not in cuml yet so cast down to pandas fit (bool, optional): If true, fits multilabelBinarizer. Defaults to False. Returns: tuple: pd.DataFrame, multilabelBinarizer @@ -1319,16 +1329,19 @@ def encode_edges(edf, src, dst, mlb, fit=False): logger.debug("Encoding Edges using MultiLabelBinarizer") edf_type = str(getmodule(edf)) - if 'cudf.core.dataframe' in edf_type: - source = edf.to_pandas()[src] - destination = edf.to_pandas()[dst] - else: - source = edf[src] - destination = edf[dst] - if fit: + source = edf[src] + destination = edf[dst] + source_dtype = str(getmodule(source)) + + if fit and 'cudf' not in source_dtype: T = mlb.fit_transform(zip(source, destination)) - else: + elif fit and 'cudf' in source_dtype: + T = mlb.fit_transform(zip(source.to_pandas(), destination.to_pandas())) + elif not fit and 'cudf' not in source_dtype: T = mlb.transform(zip(source, destination)) + elif not fit and 'cudf' in source_dtype: + T = mlb.transform(zip(source.to_pandas(), destination.to_pandas())) + T = 1.0 * T # coerce to float columns = [ str(k) for k in mlb.classes_ @@ -1336,6 +1349,7 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf.core.dataframe' in edf_type: + import cudf T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1756,7 +1770,6 @@ def _hecho(self, res): logger.info("\n-- Setting Encoder Parts from Fit ::") logger.info(f'Feature Columns In: {self.feature_names_in}') logger.info(f'Target Columns In: {self.target_names_in}') - for name, value in zip(self.res_names, res): if name not in ["X_enc", "y_enc"]: logger.info("-" * 90) @@ -2033,11 +2046,14 @@ def _featurize_nodes( res = self.copy() ndf = res._nodes node = res._node - + # print(['ndf:',ndf]) + # print(['X:',X]) + # print(['node:',res._node]) + if remove_node_column: ndf = remove_node_column_from_symbolic(ndf, node) X = remove_node_column_from_symbolic(X, node) - + if ndf is None: logger.info( "! Materializing Nodes and setting `embedding=True`" From 6b0056a0def08d1f33ff830e09ef119e5c928481 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Sat, 8 Apr 2023 07:50:30 +0800 Subject: [PATCH 354/432] cu_cat refactor --- graphistry/feature_utils.py | 49 ++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index af1bc24ef1..37747fce74 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -328,11 +328,10 @@ def remove_node_column_from_symbolic(X_symbolic, node): logger.info(f"Removing `{node}` from input X_symbolic list") X_symbolic.remove(node) return X_symbolic - if isinstance(X_symbolic, pd.DataFrame): + if isinstance(X_symbolic, pd.DataFrame) or 'cudf' in str(getmodule(X_symbolic)): logger.info(f"Removing `{node}` from input X_symbolic DataFrame") return X_symbolic.drop(columns=[node], errors="ignore") - def remove_internal_namespace_if_present(df: pd.DataFrame): """Some tranformations below add columns to the DataFrame, this method removes them before featurization will not drop if suffix is added during UMAP-ing @@ -955,6 +954,7 @@ def process_dirty_dataframes( if feature_engine == 'dirty_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder elif feature_engine == 'cu_cat': + # assert_cuml_cucat() ## tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder from cuml.preprocessing import FunctionTransformer t = time() @@ -993,21 +993,17 @@ def process_dirty_dataframes( # now just set the feature names, since dirty cat changes them in # a weird way... data_encoder.get_feature_names_out = callThrough(features_transformed) - if 'cudf.core.dataframe' not in str(getmodule(ndf)): + if 'cudf' not in str(getmodule(ndf)): X_enc = pd.DataFrame( X_enc, columns=features_transformed, index=ndf.index ) X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version - elif 'cudf.core.dataframe' in str(getmodule(ndf)): - import cudf - X_enc = cudf.DataFrame.from_arrow(X_enc) + elif 'cudf' in str(getmodule(ndf)): + # X_enc = cudf.DataFrame.from_arrow(X_enc) X_enc.index = ndf.index - # features_transformed=np.array([item.as_py() for item in features_transformed.key()]) - # X_enc.columns = features_transformed.as_py() - # = features_transformed #.to_numpy() ##error suggests this -- not working + X_enc.columns = np.array(features_transformed) + X_enc = X_enc.fillna(0.0) - - #X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version else: logger.info("-*-*- DataFrame is completely numeric") X_enc, _, data_encoder, _ = get_numeric_transformers(ndf, None) @@ -1319,7 +1315,7 @@ def encode_edges(edf, src, dst, mlb, fit=False): edf (pd.DataFrame): edge dataframe src (string): source column dst (string): destination column - mlb (sklearn): multilabelBinarizer + mlb (sklearn): multilabelBinarizer ##not in cuml yet so cast down to pandas fit (bool, optional): If true, fits multilabelBinarizer. Defaults to False. Returns: tuple: pd.DataFrame, multilabelBinarizer @@ -1329,23 +1325,27 @@ def encode_edges(edf, src, dst, mlb, fit=False): logger.debug("Encoding Edges using MultiLabelBinarizer") edf_type = str(getmodule(edf)) - if 'cudf.core.dataframe' in edf_type: - source = edf.to_pandas()[src] - destination = edf.to_pandas()[dst] - else: - source = edf[src] - destination = edf[dst] - if fit: + source = edf[src] + destination = edf[dst] + source_dtype = str(getmodule(source)) + + if fit and 'cudf' not in source_dtype: T = mlb.fit_transform(zip(source, destination)) - else: + elif fit and 'cudf' in source_dtype: + T = mlb.fit_transform(zip(source.to_pandas(), destination.to_pandas())) + elif not fit and 'cudf' not in source_dtype: T = mlb.transform(zip(source, destination)) + elif not fit and 'cudf' in source_dtype: + T = mlb.transform(zip(source.to_pandas(), destination.to_pandas())) + T = 1.0 * T # coerce to float columns = [ str(k) for k in mlb.classes_ ] # stringify the column names or scikits.base throws error mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] - if 'cudf.core.dataframe' in edf_type: + if 'cudf' in edf_type: + import cudf T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -2042,11 +2042,14 @@ def _featurize_nodes( res = self.copy() ndf = res._nodes node = res._node - + # print(['ndf:',ndf]) + # print(['X:',X]) + # print(['node:',res._node]) + if remove_node_column: ndf = remove_node_column_from_symbolic(ndf, node) X = remove_node_column_from_symbolic(X, node) - + if ndf is None: logger.info( "! Materializing Nodes and setting `embedding=True`" From e79a3e667e73adf6ed26665554245d6d4b8a6e3e Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Tue, 11 Apr 2023 01:57:03 +0530 Subject: [PATCH 355/432] typo: graphistry.rst compute.cluster --- docs/source/graphistry.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/graphistry.rst b/docs/source/graphistry.rst index cd22422c02..2fd55094a2 100644 --- a/docs/source/graphistry.rst +++ b/docs/source/graphistry.rst @@ -58,7 +58,7 @@ Semantic Search DBScan ================== -.. automodule:: graphistry.computecluster +.. automodule:: graphistry.compute.cluster :members: :undoc-members: :show-inheritance: From 9c445f19b130d39eb6e9720ac4555238c7a18c74 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Tue, 11 Apr 2023 02:09:40 +0530 Subject: [PATCH 356/432] fix: duplicate entries docs --- docs/source/index.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 8b6e9c5387..9b10c2c91c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -65,9 +65,7 @@ For self-hosting and access to a free API key, refer to our Graphistry `Hub Date: Tue, 11 Apr 2023 11:17:16 +0800 Subject: [PATCH 357/432] TestFeatureCUMLProcessors --- graphistry/tests/test_feature_utils.py | 140 +++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 10 deletions(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index f3738b3707..08358112f5 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -15,13 +15,19 @@ process_nodes_dataframes, resolve_feature_engine, lazy_import_has_min_dependancy, + lazy_import_has_cu_cat_dependancy, lazy_import_has_dependancy_text, FastEncoder ) +from graphistry.features import topic_model, ngrams_model +from graphistry.constants import SCALERS + +np.random.seed(137) has_min_dependancy, _ = lazy_import_has_min_dependancy() has_min_dependancy_text, _, _ = lazy_import_has_dependancy_text() +has_cu_cat_dependancy_text, _, _ = lazy_import_has_cu_cat_dependancy() logger = logging.getLogger(__name__) warnings.filterwarnings("ignore") @@ -127,7 +133,7 @@ target_names_node = [['label'], ['label', 'type']] # test also sending in a dataframe for target double_target_reddit = pd.DataFrame( - {"label": ndf_reddit.label.values, "type": ndf_reddit["type"].values} + {"label": ndf_reddit.label.values, "type": ndf_reddit["type"].values}, index=ndf_reddit.index ) single_target_reddit = pd.DataFrame({"label": ndf_reddit.label.values}) @@ -136,6 +142,14 @@ edge_df2['dst'] = np.random.random_integers(0, 120, size=len(edge_df2)) edge2_target_df = pd.DataFrame({'label': edge_df2.label}) +# ############################################################################################################# +what = ['whatever', 'on what', 'what do', 'what do you', 'what do you think', + 'to what', 'but what', 'what is', 'what it', 'what kind', 'what kind of', + 'of what', 'know what', 'what are', 'what are the', 'what to', 'what to do', + 'from what', 'with what', 'and what', 'what you', 'whats', 'know what to', 'don know what', 'what the'] +freedom = ['title: dyslexics, experience, language', + 'label: languagelearning, agile, leaves', + 'title: freedom, finally, moved'] # ################################################ # data to test textual and numeric DataFrame # ndf_stocks, price_df_stocks = get_stocks_dataframe() @@ -162,6 +176,44 @@ def check_allclose_fit_transform_on_same_data(X, x, Y=None, y=None): allclose_stats(Y, y, value, name) +class TestFeaturizeGetMethods(unittest.TestCase): + + @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") + def setUp(self) -> None: + g = graphistry.nodes(ndf_reddit) + g2 = g.featurize(y=double_target_reddit, # ngrams + use_ngrams=True, + ngram_range=(1, 4) + ) + + g3 = g.featurize(**topic_model # topic model + ) + self.g = g + self.g2 = g2 + self.g3 = g3 + + @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") + def test_get_col_matrix(self): + # no edges so this should be None + assert self.g2.get_matrix(kind='edges') is None + + # test target methods + assert all(self.g2.get_matrix(target=True).columns == self.g2._node_target.columns) + assert self.g2.get_matrix('Anxiety', target=True).shape[0] == len(self.g2._node_target) + # test str vs list + assert (self.g2.get_matrix('Anxiety', target=True) == self.g2.get_matrix(['Anxiety'], target=True)).all().values[0] + + # assert list(self.g2.get_matrix(['Anxiety', 'education', 'computer'], target=True).columns) == ['label_Anxiety', 'label_education', 'label_computervision'] + + # test feature methods + # ngrams + assert (self.g2.get_matrix().columns == self.g2._node_features.columns).all() + assert list(self.g2.get_matrix('what').columns) == what, list(self.g2.get_matrix('what').columns) + + # topic + assert all(self.g3.get_matrix().columns == self.g3._node_features.columns) + assert list(self.g3.get_matrix(['language', 'freedom']).columns) == freedom, self.g3.get_matrix(['language', 'freedom']).columns + class TestFastEncoder(unittest.TestCase): # we test how far off the fit returned values different from the transformed @@ -237,7 +289,8 @@ def test_process_node_dataframes_min_words(self): 2, 4000, ]: # last one should skip encoding, and throw all to dirty_cat - X_enc, y_enc, data_encoder, label_encoder, ordinal_pipeline, ordinal_pipeline_target, text_model, text_cols = process_nodes_dataframes( + + X_enc, y_enc, X_encs, y_encs, data_encoder, label_encoder, ordinal_pipeline, ordinal_pipeline_target, text_model, text_cols = process_nodes_dataframes( ndf_reddit, y=double_target_reddit, use_scaler=None, @@ -260,6 +313,71 @@ def test_multi_label_binarizer(self): assert y.shape == (4, 4) assert sum(y.sum(1).values - np.array([1., 2., 1., 0.])) == 0 +class TestFeatureCUMLProcessors(unittest.TestCase): + def cases_tests(self, x, y, data_encoder, target_encoder, name, value): + import cu_cat + self.assertIsInstance( + x, + cudf.DataFrame, + f"Returned data matrix is not cudf DataFrame for {name} {value}", + ) + self.assertFalse( + x.empty, + f"cudf DataFrame should not be empty for {name} {value}", + ) + self.assertIsInstance( + y, + pd.DataFrame, + f"Returned Target is not a cudf DataFrame for {name} {value}", + ) + self.assertFalse( + y.empty, + f"cudf Target DataFrame should not be empty for {name} {value}", + ) + self.assertIsInstance( + data_encoder, + cu_cat.super_vectorizer.TableVectorizer, + f"Data Encoder is not a cu_cat.super_vectorizer.TableVectorizer instance for {name} {value}", + ) + self.assertIsInstance( + target_encoder, + cu_cat.super_vectorizer.TableVectorizer, + f"Data Target Encoder is not a cu_cat.super_vectorizer.TableVectorizer instance for {name} {value}", + ) + + @pytest.mark.skipif(not has_cu_cat_dependancy or not has_cu_cat_dependancy, reason="requires cu_cat feature dependencies") + def test_process_node_dataframes_min_words(self): + # test different target cardinality + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + for min_words in [ + 2, + 4000, + ]: # last one should skip encoding, and throw all to dirty_cat + + X_enc, y_enc, X_encs, y_encs, data_encoder, label_encoder, ordinal_pipeline, ordinal_pipeline_target, text_model, text_cols = process_nodes_dataframes( + ndf_reddit, + y=double_target_reddit, + use_scaler=None, + cardinality_threshold=40, + cardinality_threshold_target=40, + n_topics=20, + min_words=min_words, + model_name=model_avg_name, + feature_engine=resolve_feature_engine('auto') + ) + self.cases_tests(X_enc, y_enc, data_encoder, label_encoder, "min_words", min_words) + + @pytest.mark.skipif(not has_cu_cat_dependancy, reason="requires minimal feature dependencies") + def test_multi_label_binarizer(self): + g = graphistry.nodes(bad_df) # can take in a list of lists and convert to multiOutput + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=UserWarning) + g2 = g.featurize(y=['list_str'], X=['src'], multilabel=True) + y = g2._get_target('node') + assert y.shape == (4, 4) + assert sum(y.sum(1).values - np.array([1., 2., 1., 0.])) == 0 + class TestFeatureMethods(unittest.TestCase): def _check_attributes(self, g, attributes): @@ -370,19 +488,21 @@ def test_edge_featurization(self): def test_node_scaling(self): g = graphistry.nodes(ndf_reddit) g2 = g.featurize(X="title", y='label', use_scaler=None, use_scaler_target=None) - scalers = ['quantile', 'zscale', 'kbins', 'robust', 'minmax'] - for scaler in scalers: - a, b, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', use_scaler=scaler, use_scaler_target=np.random.choice(scalers)) - - + for scaler in SCALERS: + X, y, c, d = g2.scale(ndf_reddit, single_target_reddit, kind='nodes', + use_scaler=scaler, + use_scaler_target=np.random.choice(SCALERS), + return_scalers=True) @pytest.mark.skipif(not has_min_dependancy or not has_min_dependancy_text, reason="requires ai feature dependencies") def test_edge_scaling(self): g = graphistry.edges(edge_df2, "src", "dst") g2 = g.featurize(y='label', kind='edges', use_scaler=None, use_scaler_target=None) - scalers = ['quantile', 'zscale', 'kbins', 'robust', 'minmax'] - for scaler in scalers: - a, b, c, d = g2.scale(edge_df2, edge2_target_df, kind='edges', use_scaler=scaler, use_scaler_target=np.random.choice(scalers)) + for scaler in SCALERS: + X, y, c, d = g2.scale(edge_df2, edge2_target_df, kind='edges', + use_scaler=scaler, + use_scaler_target=np.random.choice(SCALERS), + return_scalers=True) From ce17b8ecafaae765d86e483f205cc7fb344a019c Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 11 Apr 2023 22:51:16 +0530 Subject: [PATCH 358/432] tests: add cudf umap pass through --- .gitignore | 4 ++++ graphistry/tests/test_umap_utils.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/.gitignore b/.gitignore index 9fb2c10c4c..f8a1ee9544 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# vim temporary files +*.swp +*.swo + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 564ffcdbfa..ab916f1563 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -780,6 +780,28 @@ def test_filter_edges(self): self.assertGreaterEqual(shape[0], last_shape) last_shape = shape[0] +class TestCudfUmap(unittest.TestCase): + # temporary tests for cudf pass thru umap + @pytest.mark.skipif( + not has_dependancy or not has_cuml, + reason="requires cuml dependencies", + ) + @pytest.mark.skipif( + not has_cudf, reason="requires cudf" + ) + + def setUp(self): + self.samples=1000 + df = pd.DataFrame(np.random.randint(18,75,size=(self.samples, 1)), columns=['age']) + df['user_id'] = np.random.randint(0,200,size=(self.samples, 1)) + df['profile'] = np.random.randint(0,1000,size=(self.samples, 1)) + self.df = cudf.from_pandas(df) + + def test_base(self): + print('working') + graphistry.nodes(self.df).umap('auto')._node_embedding.shape == (self.samples, 2) + graphistry.nodes(self.df).umap('engine')._node_embedding.shape == (self.samples, 2) + if __name__ == "__main__": unittest.main() From 5ff14f87a7dc4d7431bcdf3740f40590239cf95f Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 11 Apr 2023 22:56:05 +0530 Subject: [PATCH 359/432] lint: flake8 fixes --- graphistry/tests/test_umap_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index ab916f1563..d6d96dbd7b 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -789,9 +789,8 @@ class TestCudfUmap(unittest.TestCase): @pytest.mark.skipif( not has_cudf, reason="requires cudf" ) - def setUp(self): - self.samples=1000 + self.samples = 1000 df = pd.DataFrame(np.random.randint(18,75,size=(self.samples, 1)), columns=['age']) df['user_id'] = np.random.randint(0,200,size=(self.samples, 1)) df['profile'] = np.random.randint(0,1000,size=(self.samples, 1)) From 77eb8c2635ddfecd6833fca35b5f7dba8809df75 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Tue, 11 Apr 2023 23:50:35 +0530 Subject: [PATCH 360/432] fix: cudf umap skip --- graphistry/tests/test_umap_utils.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index d6d96dbd7b..c281ae75fe 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -782,13 +782,6 @@ def test_filter_edges(self): class TestCudfUmap(unittest.TestCase): # temporary tests for cudf pass thru umap - @pytest.mark.skipif( - not has_dependancy or not has_cuml, - reason="requires cuml dependencies", - ) - @pytest.mark.skipif( - not has_cudf, reason="requires cudf" - ) def setUp(self): self.samples = 1000 df = pd.DataFrame(np.random.randint(18,75,size=(self.samples, 1)), columns=['age']) @@ -796,8 +789,9 @@ def setUp(self): df['profile'] = np.random.randint(0,1000,size=(self.samples, 1)) self.df = cudf.from_pandas(df) + @pytest.mark.skipif(not has_dependancy or not has_cuml, reason="requires cuml dependencies") + @pytest.mark.skipif(not has_cudf, reason="requires cudf") def test_base(self): - print('working') graphistry.nodes(self.df).umap('auto')._node_embedding.shape == (self.samples, 2) graphistry.nodes(self.df).umap('engine')._node_embedding.shape == (self.samples, 2) From c2d2fcb6eb1588e28171c2b6b8dcfde01ba66b39 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 12 Apr 2023 17:28:08 +0800 Subject: [PATCH 361/432] need to make cudf import for edges lazy --- graphistry/feature_utils.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index c03a3cde52..2d26dd119b 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -692,15 +692,16 @@ def fit_pipeline( columns = X.columns index = X.index X_type= str(getmodule(X)) - if 'cudf.core.dataframe' not in X_type: + if 'cudf' not in X_type: X = transformer.fit_transform(X) if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa X=pd.DataFrame(X, columns=columns, index=index) - elif 'cudf.core.dataframe' in X_type: - X = transformer.fit_transform(X.to_numpy()) + else: + X = transformer.fit_transform(X.to_numpy()) ## why numpy here? if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa + import cudf X=cudf.DataFrame(X, columns=columns, index=index) return X @@ -954,7 +955,7 @@ def process_dirty_dataframes( if feature_engine == 'dirty_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder elif feature_engine == 'cu_cat': - # assert_cuml_cucat() ## tried to use this rather than importing below + lazy_import_has_cu_cat_dependancy() ## tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder from cuml.preprocessing import FunctionTransformer t = time() @@ -997,12 +998,13 @@ def process_dirty_dataframes( X_enc = pd.DataFrame( X_enc, columns=features_transformed, index=ndf.index ) - elif 'cudf.core.dataframe' in str(getmodule(ndf)): - import cudf - X_enc = cudf.DataFrame( - X_enc, columns=features_transformed, index=ndf.index - ) - X_enc = X_enc.fillna(0.0) + X_enc = X_enc.fillna(0.0) # TODO -- this is a hack in cuml version + else: + # X_enc = cudf.DataFrame.from_arrow(X_enc) + X_enc.index = ndf.index + X_enc.columns = np.array(features_transformed) + X_enc = X_enc.fillna(0.0) + else: logger.info("-*-*- DataFrame is completely numeric") X_enc, _, data_encoder, _ = get_numeric_transformers(ndf, None) @@ -1344,6 +1346,7 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf' in edf_type: + lazy_import_has_cu_cat_dependancy() import cudf T = cudf.DataFrame(T, columns=columns, index=edf.index) else: @@ -1420,6 +1423,11 @@ def process_edge_dataframes( MultiLabelBinarizer() ) # create new one so we can use encode_edges later in # transform with fit=False + edf_type = str(getmodule(edf)) + if 'cudf' in edf_type: + import cudf + lazy_import_has_cu_cat_dependancy() + T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) @@ -1506,12 +1514,10 @@ def process_edge_dataframes( logger.debug("-" * 60) logger.debug("<= Found Edges and Dirty_cat encoding =>") T_type= str(getmodule(T)) - if 'cudf.core.dataframe' not in T_type: + if 'cudf' not in T_type: X_enc = pd.concat([T, X_enc], axis=1) - elif 'cudf.core.dataframe' not in T_type: - X_enc = cudf.concat([T, X_enc], axis=1) else: - X_enc = pd.concat([T, X_enc], axis=1) + X_enc = cudf.concat([T, X_enc], axis=1) elif not T.empty and X_enc.empty: logger.debug("-" * 60) logger.debug("<= Found only Edges =>") From c703a427df9daa5f539430615f9e9c4f980142b1 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Wed, 12 Apr 2023 21:22:33 +0530 Subject: [PATCH 362/432] test: cudf tests with docker flag --- graphistry/tests/test_embed_utils.py | 15 ++++++++++----- graphistry/tests/test_umap_utils.py | 8 +++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/graphistry/tests/test_embed_utils.py b/graphistry/tests/test_embed_utils.py index 2ecc30a677..f0cb82b8d0 100644 --- a/graphistry/tests/test_embed_utils.py +++ b/graphistry/tests/test_embed_utils.py @@ -1,3 +1,4 @@ +import os import pytest import pandas as pd import unittest @@ -20,6 +21,10 @@ def check_cudf(): has_cudf, cudf = check_cudf() +TEST_CUDF = False +if "TEST_CUDF" in os.environ and os.environ["TEST_CUDF"] == "1": + TEST_CUDF = True + class TestEmbed(unittest.TestCase): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") @@ -114,7 +119,7 @@ def test_chaining(self): class TestEmbedCUDF(unittest.TestCase): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not has_cudf, reason="requires cudf") + @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") def setUp(self): self.edf = cudf.DataFrame([[0, 1, 0], [1, 2, 0], [2, 0, 1]], columns=['src', 'dst', 'rel'] @@ -138,7 +143,7 @@ def setUp(self): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not has_cudf, reason="requires cudf") + @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") def test_embed_out_basic(self): for name, g in self.graphs: g = g.embed('rel', embedding_dim=self.d, **self.kwargs) @@ -150,7 +155,7 @@ def test_embed_out_basic(self): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not has_cudf, reason="requires cudf") + @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") def test_predict_links(self): source = pd.Series([0,2]) relation = None @@ -166,7 +171,7 @@ def test_predict_links(self): self.assertIn("score", g_new._edges.columns) @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not has_cudf, reason="requires cudf") + @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") def test_predict_links_all(self): g = self.graph_no_feat.embed('rel', embedding_dim=self.d, **self.kwargs) g_new = g.predict_links_all(threshold=0) @@ -175,7 +180,7 @@ def test_predict_links_all(self): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not has_cudf, reason="requires cudf") + @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") def test_chaining(self): for name, g in self.graphs: logging.debug('name: %s test changing embedding dim with feats' % name) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index c281ae75fe..041e840952 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -5,6 +5,7 @@ import graphistry +import os import logging import numpy as np import pandas as pd @@ -43,6 +44,10 @@ warnings.filterwarnings("ignore") +TEST_CUDF = False +if "TEST_CUDF" in os.environ and os.environ["TEST_CUDF"] == "1": + TEST_CUDF = True + triangleEdges = pd.DataFrame( { @@ -782,6 +787,7 @@ def test_filter_edges(self): class TestCudfUmap(unittest.TestCase): # temporary tests for cudf pass thru umap + @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") def setUp(self): self.samples = 1000 df = pd.DataFrame(np.random.randint(18,75,size=(self.samples, 1)), columns=['age']) @@ -790,7 +796,7 @@ def setUp(self): self.df = cudf.from_pandas(df) @pytest.mark.skipif(not has_dependancy or not has_cuml, reason="requires cuml dependencies") - @pytest.mark.skipif(not has_cudf, reason="requires cudf") + @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") def test_base(self): graphistry.nodes(self.df).umap('auto')._node_embedding.shape == (self.samples, 2) graphistry.nodes(self.df).umap('engine')._node_embedding.shape == (self.samples, 2) From 006aa7d30ecfcf286b54df67e232f9eb3f87a427 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Wed, 12 Apr 2023 21:26:43 +0530 Subject: [PATCH 363/432] delete: .swp files --- graphistry/.dgl_utils.py.swp | Bin 40960 -> 0 bytes graphistry/.feature_utils.py.swp | Bin 122880 -> 0 bytes graphistry/tests/.test_embed_utils.py.swp | Bin 40960 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 graphistry/.dgl_utils.py.swp delete mode 100644 graphistry/.feature_utils.py.swp delete mode 100644 graphistry/tests/.test_embed_utils.py.swp diff --git a/graphistry/.dgl_utils.py.swp b/graphistry/.dgl_utils.py.swp deleted file mode 100644 index 46316aea9ac305deae72075eac60f9f2bebb0cb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40960 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 diff --git a/graphistry/tests/.test_embed_utils.py.swp b/graphistry/tests/.test_embed_utils.py.swp deleted file mode 100644 index 764bc178db5ec513d75fcb7f821f1d712ce2b5d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40960 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 OYU#c~#y0 Date: Thu, 13 Apr 2023 00:24:48 +0530 Subject: [PATCH 364/432] add: test_umap_utils on test-gpu-local.sh --- docker/test-gpu-local.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/test-gpu-local.sh b/docker/test-gpu-local.sh index 12667b3a04..158584f9b6 100755 --- a/docker/test-gpu-local.sh +++ b/docker/test-gpu-local.sh @@ -45,5 +45,4 @@ docker run \ graphistry/test-gpu:${TEST_CPU_VERSION} \ --maxfail=1 \ --ignore=graphistry/tests/test_feature_utils.py \ - --ignore=graphistry/tests/test_umap_utils.py \ $@ From 400b63262a02f338d6439bff2bceb415d982cfc1 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Thu, 13 Apr 2023 21:38:56 +0530 Subject: [PATCH 365/432] fix: revert back to addStyle from add_style --- graphistry/PlotterBase.py | 2 +- graphistry/__init__.py | 2 +- graphistry/pygraphistry.py | 6 +++--- graphistry/tests/test_plotter.py | 22 +++++++++++----------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 9c64d0e1f5..0b9dbe9235 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -224,7 +224,7 @@ def __repr__(self): else: return str(rep) - def add_style(self, fg=None, bg=None, page=None, logo=None): + def addStyle(self, fg=None, bg=None, page=None, logo=None): """Set general visual styles See .bind() and .settings(url_params={}) for additional styling options, and style() for another way to set the same attributes. diff --git a/graphistry/__init__.py b/graphistry/__init__.py index 511b543e57..dd13cb60c5 100644 --- a/graphistry/__init__.py +++ b/graphistry/__init__.py @@ -15,7 +15,7 @@ description, bind, style, - add_style, + addStyle, edges, nodes, graph, diff --git a/graphistry/pygraphistry.py b/graphistry/pygraphistry.py index e6582928e6..2051a32523 100644 --- a/graphistry/pygraphistry.py +++ b/graphistry/pygraphistry.py @@ -1209,7 +1209,7 @@ def description(description): return Plotter().description(description) @staticmethod - def add_style(bg=None, fg=None, logo=None, page=None): + def addStyle(bg=None, fg=None, logo=None, page=None): """Creates a base plotter with some style settings. For parameters, see ``plotter.addStyle``. @@ -1225,7 +1225,7 @@ def add_style(bg=None, fg=None, logo=None, page=None): graphistry.addStyle(bg={'color': 'black'}) """ - return Plotter().add_style(bg=bg, fg=fg, logo=logo, page=page) + return Plotter().addStyle(bg=bg, fg=fg, logo=logo, page=page) @staticmethod def style(bg=None, fg=None, logo=None, page=None): @@ -2417,7 +2417,7 @@ def _handle_api_response(response): api_token = PyGraphistry.api_token verify_token = PyGraphistry.verify_token bind = PyGraphistry.bind -add_style = PyGraphistry.add_style +addStyle = PyGraphistry.addStyle style = PyGraphistry.style encode_point_color = PyGraphistry.encode_point_color encode_edge_color = PyGraphistry.encode_edge_color diff --git a/graphistry/tests/test_plotter.py b/graphistry/tests/test_plotter.py index 35142e8026..c1518a8160 100644 --- a/graphistry/tests/test_plotter.py +++ b/graphistry/tests/test_plotter.py @@ -720,32 +720,32 @@ def test_addStyle_good(self): logo = {"url": "zzz"} page = {"title": "zzz"} - assert g.add_style()._style == {} + assert g.addStyle()._style == {} - g.add_style(fg={"blendMode": "screen"}) - assert g.add_style()._style == {} + g.addStyle(fg={"blendMode": "screen"}) + assert g.addStyle()._style == {} - assert g.add_style(bg=copy.deepcopy(bg))._style == {"bg": bg} - assert g.add_style(bg={"color": "blue"}).add_style( + assert g.addStyle(bg=copy.deepcopy(bg))._style == {"bg": bg} + assert g.addStyle(bg={"color": "blue"}).addStyle( bg=copy.deepcopy(bg) )._style == {"bg": bg} - assert g.add_style(bg={"image": {"url": "http://asdf.com/b.png"}}).add_style( + assert g.addStyle(bg={"image": {"url": "http://asdf.com/b.png"}}).addStyle( bg=copy.deepcopy(bg) )._style == {"bg": {**bg, "image": {"url": "http://asdf.com/b.png"}}} assert ( - g.add_style( + g.addStyle( bg=copy.deepcopy(bg), fg=copy.deepcopy(fg), logo=copy.deepcopy(logo), page=copy.deepcopy(page), )._style == {"bg": bg, "fg": fg, "logo": logo, "page": page} ) - assert g.add_style( + assert g.addStyle( bg=copy.deepcopy(bg), fg=copy.deepcopy(fg), logo=copy.deepcopy(logo), page=copy.deepcopy(page), - ).add_style(bg={"color": "green"})._style == { + ).addStyle(bg={"color": "green"})._style == { "bg": {"color": "green"}, "fg": fg, "logo": logo, @@ -755,7 +755,7 @@ def test_addStyle_good(self): g2 = graphistry.edges(pd.DataFrame({"s": [0], "d": [0]})).bind( source="s", destination="d" ) - ds = g2.add_style( + ds = g2.addStyle( bg=copy.deepcopy(bg), fg=copy.deepcopy(fg), page=copy.deepcopy(page), @@ -783,7 +783,7 @@ def test_styleApi_reject(self): g2 = graphistry.edges(pd.DataFrame({"s": [0], "d": [0]})).bind( source="s", destination="d" ) - g3 = g2.add_style( + g3 = g2.addStyle( bg=copy.deepcopy(bg), fg=copy.deepcopy(fg), page=copy.deepcopy(page), From e6fc323c0d80d4187a95a3a4e4ca727a44f866c7 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Thu, 13 Apr 2023 21:49:08 +0530 Subject: [PATCH 366/432] added warnings for predict_links --- graphistry/embed_utils.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index 9ab4d07d00..dcbf3a88fd 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -1,4 +1,5 @@ import logging +import warnings import numpy as np import pandas as pd from typing import Optional, Union, Callable, List, TYPE_CHECKING, Any, Tuple @@ -405,40 +406,35 @@ def predict_links( where score >= threshold if anamalous if False else score <= threshold, or a dataframe """ - + warnings.warn("currently `predict_links` is cpu only, gpu compatibility will be added in \ + future releases") all_nodes = self._node2id.values() all_relations = self._relation2id.values() if source is None: src = pd.Series(all_nodes) else: - # this is temporary - try: + # this is temporary, will be removed after gpu feature utils + if not isinstance(source, pd.DataFrame): source = source.to_pandas() # type: ignore - except: - pass src = pd.Series(source) src = src.map(self._node2id) if relation is None: rel = pd.Series(all_relations) else: - # this is temporary - try: + # this is temporary, will be removed after gpu feature utils + if not isinstance(relation, pd.DataFrame): relation = relation.to_pandas() # type: ignore - except: - pass rel = pd.Series(relation) rel = rel.map(self._relation2id) if destination is None: dst = pd.Series(all_nodes) else: - # this is temporary - try: + # this is temporary, will be removed after gpu feature utils + if not isinstance(destination, pd.DataFrame): destination = destination.to_pandas() # type: ignore - except: - pass dst = pd.Series(destination) dst = dst.map(self._node2id) From c300050e4a0916dc9fe0c9838ea5842ecb3897f8 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Thu, 13 Apr 2023 21:57:45 +0530 Subject: [PATCH 367/432] fix: test cudf flag --- graphistry/tests/test_embed_utils.py | 15 +++++++-------- graphistry/tests/test_umap_utils.py | 10 ++++------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/graphistry/tests/test_embed_utils.py b/graphistry/tests/test_embed_utils.py index f0cb82b8d0..a3001424d4 100644 --- a/graphistry/tests/test_embed_utils.py +++ b/graphistry/tests/test_embed_utils.py @@ -21,9 +21,8 @@ def check_cudf(): has_cudf, cudf = check_cudf() -TEST_CUDF = False -if "TEST_CUDF" in os.environ and os.environ["TEST_CUDF"] == "1": - TEST_CUDF = True +# enable tests if has cudf and env didn't explicitly disable +is_test_cudf = has_cudf and os.environ["TEST_CUDF"] != "0" class TestEmbed(unittest.TestCase): @@ -119,7 +118,7 @@ def test_chaining(self): class TestEmbedCUDF(unittest.TestCase): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") + @pytest.mark.skipif(not is_test_cudf, reason="requires cudf") def setUp(self): self.edf = cudf.DataFrame([[0, 1, 0], [1, 2, 0], [2, 0, 1]], columns=['src', 'dst', 'rel'] @@ -143,7 +142,7 @@ def setUp(self): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") + @pytest.mark.skipif(not is_test_cudf, reason="requires cudf") def test_embed_out_basic(self): for name, g in self.graphs: g = g.embed('rel', embedding_dim=self.d, **self.kwargs) @@ -155,7 +154,7 @@ def test_embed_out_basic(self): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") + @pytest.mark.skipif(not is_test_cudf, reason="requires cudf") def test_predict_links(self): source = pd.Series([0,2]) relation = None @@ -171,7 +170,7 @@ def test_predict_links(self): self.assertIn("score", g_new._edges.columns) @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") + @pytest.mark.skipif(not is_test_cudf, reason="requires cudf") def test_predict_links_all(self): g = self.graph_no_feat.embed('rel', embedding_dim=self.d, **self.kwargs) g_new = g.predict_links_all(threshold=0) @@ -180,7 +179,7 @@ def test_predict_links_all(self): @pytest.mark.skipif(not dep_flag, reason="requires ai feature dependencies") - @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") + @pytest.mark.skipif(not is_test_cudf, reason="requires cudf") def test_chaining(self): for name, g in self.graphs: logging.debug('name: %s test changing embedding dim with feats' % name) diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 041e840952..6bd57785a2 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -44,10 +44,8 @@ warnings.filterwarnings("ignore") -TEST_CUDF = False -if "TEST_CUDF" in os.environ and os.environ["TEST_CUDF"] == "1": - TEST_CUDF = True - +# enable tests if has cudf and env didn't explicitly disable +is_test_cudf = has_cudf and os.environ["TEST_CUDF"] != "0" triangleEdges = pd.DataFrame( { @@ -787,7 +785,7 @@ def test_filter_edges(self): class TestCudfUmap(unittest.TestCase): # temporary tests for cudf pass thru umap - @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") + @pytest.mark.skipif(not is_test_cudf, reason="requires cudf") def setUp(self): self.samples = 1000 df = pd.DataFrame(np.random.randint(18,75,size=(self.samples, 1)), columns=['age']) @@ -796,7 +794,7 @@ def setUp(self): self.df = cudf.from_pandas(df) @pytest.mark.skipif(not has_dependancy or not has_cuml, reason="requires cuml dependencies") - @pytest.mark.skipif(not TEST_CUDF, reason="requires cudf") + @pytest.mark.skipif(not is_test_cudf, reason="requires cudf") def test_base(self): graphistry.nodes(self.df).umap('auto')._node_embedding.shape == (self.samples, 2) graphistry.nodes(self.df).umap('engine')._node_embedding.shape == (self.samples, 2) From 1305db11d0fb04ce908126f2cf51bb87aa1410f3 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Thu, 13 Apr 2023 22:02:42 +0530 Subject: [PATCH 368/432] doc: changelog update for _dgl_graph --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1ed4185a..69fd423074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added * AI: moves public `g.g_dgl` from KG `embed` method to private method `g._kg_dgl` +* AI: moves public `g.DGL_graph` to private attribute `g._dgl_graph` * AI: BREAKING CHANGES: to return matrices during transform, set the flag: `X, y = g.transform(df, return_graph=False)` default behavior is ~ `g2 = g.transform(df)` returning a Plottable instance. ## [0.28.7 - 2022-12-22] From 7ad6449ed4d6cf688f7c6aede00f80dd15c35e44 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Thu, 13 Apr 2023 22:06:00 +0530 Subject: [PATCH 369/432] passing gpu test_feature_utils --- docker/test-gpu-local.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/test-gpu-local.sh b/docker/test-gpu-local.sh index 158584f9b6..f7c5c5c5ad 100755 --- a/docker/test-gpu-local.sh +++ b/docker/test-gpu-local.sh @@ -44,5 +44,4 @@ docker run \ ${NETWORK} \ graphistry/test-gpu:${TEST_CPU_VERSION} \ --maxfail=1 \ - --ignore=graphistry/tests/test_feature_utils.py \ $@ From 23951474d23900b3c665d8e69276cf3286df00bf Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Thu, 13 Apr 2023 23:12:00 +0530 Subject: [PATCH 370/432] additional checks for embed utils --- graphistry/embed_utils.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index dcbf3a88fd..3dbba83156 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -1,5 +1,4 @@ import logging -import warnings import numpy as np import pandas as pd from typing import Optional, Union, Callable, List, TYPE_CHECKING, Any, Tuple @@ -7,6 +6,7 @@ from .PlotterBase import Plottable from .compute.ComputeMixin import ComputeMixin + def lazy_embed_import_dep(): try: import torch @@ -21,6 +21,11 @@ def lazy_embed_import_dep(): except: return False, None, None, None, None, None, None, None +try: + import cudf +except: + cudf = object + if TYPE_CHECKING: _, torch, _, _, _, _, _, _ = lazy_embed_import_dep() @@ -290,6 +295,11 @@ def embed( ------- self : graphistry instance """ + # this is temporary, will be fixed in future releases + if isinstance(self._nodes, cudf.DataFrame): + self._nodes = self._nodes.to_pandas() + if isinstance(self._edges, cudf.DataFrame): + self._edges = self._edges.to_pandas() if inplace: res = self else: @@ -406,7 +416,7 @@ def predict_links( where score >= threshold if anamalous if False else score <= threshold, or a dataframe """ - warnings.warn("currently `predict_links` is cpu only, gpu compatibility will be added in \ + logging.warning("currently `predict_links` is cpu only, gpu compatibility will be added in \ future releases") all_nodes = self._node2id.values() all_relations = self._relation2id.values() @@ -415,7 +425,7 @@ def predict_links( src = pd.Series(all_nodes) else: # this is temporary, will be removed after gpu feature utils - if not isinstance(source, pd.DataFrame): + if isinstance(source, cudf.DataFrame): source = source.to_pandas() # type: ignore src = pd.Series(source) src = src.map(self._node2id) @@ -424,7 +434,7 @@ def predict_links( rel = pd.Series(all_relations) else: # this is temporary, will be removed after gpu feature utils - if not isinstance(relation, pd.DataFrame): + if isinstance(relation, cudf.DataFrame): relation = relation.to_pandas() # type: ignore rel = pd.Series(relation) rel = rel.map(self._relation2id) @@ -433,7 +443,7 @@ def predict_links( dst = pd.Series(all_nodes) else: # this is temporary, will be removed after gpu feature utils - if not isinstance(destination, pd.DataFrame): + if isinstance(destination, cudf.DataFrame): destination = destination.to_pandas() # type: ignore dst = pd.Series(destination) dst = dst.map(self._node2id) From 8e99fe37bcb32ebda13e09bc0a7b4bf6aa6b16b8 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Fri, 14 Apr 2023 14:31:22 +0800 Subject: [PATCH 371/432] cu_cat flag in umap --- graphistry/umap_utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 47186bb7a1..f9a133ef26 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -126,9 +126,9 @@ def safe_cudf(X, y): new_kwargs = {} kwargs = {'X': X, 'y': y} for key, value in kwargs.items(): - if isinstance(value, cudf.DataFrame) and engine == "pandas": + if isinstance(value, cudf.DataFrame) and engine in ["pandas", "umap_learn", "dirty_cat"]: new_kwargs[key] = value.to_pandas() - elif isinstance(value, pd.DataFrame) and engine == "cuml": + elif isinstance(value, pd.DataFrame) and engine in ["cuml", "cu_cat"]: new_kwargs[key] = cudf.from_pandas(value) else: new_kwargs[key] = value @@ -352,10 +352,13 @@ def transform_umap(self, df: pd.DataFrame, def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 - if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)): + if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)) and not hasattr(emb, 'device'): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): emb.rename(columns={0: config.X, 1: config.Y}, inplace=True) + elif emb.shape[1] == 2 and hasattr(emb, 'device'): + import cudf + emb = cudf.DataFrame(emb, columns=[config.X, config.Y], index=index) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1]) From 2e9820c6370459d0d268f245bc706bd5ad159bb0 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Fri, 14 Apr 2023 14:34:18 +0800 Subject: [PATCH 372/432] added tanmoy umap changes --- graphistry/umap_utils.py | 116 ++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/graphistry/umap_utils.py b/graphistry/umap_utils.py index 9be5616786..f9a133ef26 100644 --- a/graphistry/umap_utils.py +++ b/graphistry/umap_utils.py @@ -50,17 +50,6 @@ def lazy_cuml_import_has_dependancy(): return True, "ok", cuml except ModuleNotFoundError as e: return False, e, None - -def lazy_cudf_import_has_dependancy(): - try: - import warnings - - warnings.filterwarnings("ignore") - import cudf # type: ignore - - return True, "ok", cudf - except ModuleNotFoundError as e: - return False, e, None def lazy_cudf_import_has_dependancy(): try: @@ -190,7 +179,7 @@ class UMAPMixin(MIXIN_BASE): def __init__(self, *args, **kwargs): #self._umap_initialized = False - #self.umap_engine = self.umap_engine if hasattr(self, "engine") else None + #self.engine = self.engine if hasattr(self, "engine") else None pass @@ -205,18 +194,18 @@ def umap_lazy_init( negative_sample_rate: int = 5, n_components: int = 2, metric: str = "euclidean", - umap_engine: UMAPEngine = "auto", + engine: UMAPEngine = "auto", suffix: str = "", verbose: bool = False, ): from graphistry.features import ModelDict - engine_resolved = resolve_umap_engine(umap_engine) + engine_resolved = resolve_umap_engine(engine) # FIXME remove as set_new_kwargs will always replace? if engine_resolved == UMAP_LEARN: - _, _, umap_engine_ = lazy_umap_import_has_dependancy() + _, _, umap_engine = lazy_umap_import_has_dependancy() elif engine_resolved == CUML: - _, _, umap_engine_ = lazy_cuml_import_has_dependancy() + _, _, umap_engine = lazy_cuml_import_has_dependancy() else: raise ValueError( "No umap engine, ensure 'auto', 'umap_learn', or 'cuml', and the library is installed" @@ -242,6 +231,7 @@ def umap_lazy_init( print(umap_kwargs) if verbose else None # set new umap kwargs res._umap_params = umap_kwargs + res._n_components = n_components res._metric = metric res._n_neighbors = n_neighbors @@ -250,8 +240,8 @@ def umap_lazy_init( res._local_connectivity = local_connectivity res._repulsion_strength = repulsion_strength res._negative_sample_rate = negative_sample_rate - res._umap = umap_engine_.UMAP(**umap_kwargs) - res.umap_engine = engine_resolved + res._umap = umap_engine.UMAP(**umap_kwargs) + res.engine = engine_resolved res._suffix = suffix return res @@ -287,7 +277,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None, verbose logger.info("-" * 90) logger.info(f"Starting UMAP-ing data of shape {X.shape}") - if self.umap_engine == CUML and is_legacy_cuml(): # type: ignore + if self.engine == CUML and is_legacy_cuml(): # type: ignore from cuml.neighbors import NearestNeighbors knn = NearestNeighbors(n_neighbors=self._n_neighbors) # type: ignore @@ -300,7 +290,7 @@ def umap_fit(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = None, verbose self._weighted_adjacency = self._umap.graph_ # if changing, also update fresh_res self._weighted_edges_df = umap_graph_to_weighted_edges( - self._umap.graph_, self.umap_engine, is_legacy_cuml() # type: ignore + self._umap.graph_, self.engine, is_legacy_cuml() # type: ignore ) mins = (time() - t) / 60 @@ -317,17 +307,39 @@ def _umap_fit_transform(self, X: pd.DataFrame, y: Union[pd.DataFrame, None] = No emb = self._bundle_embedding(emb, index=X.index) return emb - def transform_umap( # noqa: E303 - self, df: pd.DataFrame, ydf: pd.DataFrame, kind: str = "nodes" - ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: - try: - logger.debug(f"Going into Transform umap {df.shape}, {ydf.shape}") - except: - pass - x, y = self.transform(df, ydf, kind=kind) - emb = self._umap.transform(x) # type: ignore - emb = self._bundle_embedding(emb, index=df.index) + def transform_umap(self, df: pd.DataFrame, + y: Optional[pd.DataFrame] = None, + kind: str = 'nodes', + min_dist: Union[str, float, int] = 'auto', + n_neighbors: int = 7, + merge_policy: bool = False, + sample: Optional[int] = None, + return_graph: bool = True, + fit_umap_embedding: bool = True, + verbose: bool = False + ) -> Union[Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame], Plottable]: + """Transforms data into UMAP embedding + + Args: + :df: Dataframe to transform + :y: Target column + :kind: One of `nodes` or `edges` + :min_dist: Epsilon for including neighbors in infer_graph + :n_neighbors: Number of neighbors to use for contextualization + :merge_policy: if True, use previous graph, adding new batch to existing graph's neighbors + useful to contextualize new data against existing graph. If False, `sample` is irrelevant. + + sample: Sample number of existing graph's neighbors to use for contextualization -- helps make denser graphs + return_graph: Whether to return a graph or just the embeddings + fit_umap_embedding: Whether to infer graph from the UMAP embedding on the new data, default True + verbose: Whether to print information about the graph inference + """ + df, y = make_safe_gpu_dataframes(df, y, 'pandas') + X, y_ = self.transform(df, y, kind=kind, return_graph=False, verbose=verbose) + X, y_ = make_safe_gpu_dataframes(X, y_, self.engine) # type: ignore + emb = self._umap.transform(X) # type: ignore + emb = self._bundle_embedding(emb, index=df.index) if return_graph and kind not in ["edges"]: emb, _ = make_safe_gpu_dataframes(emb, None, 'pandas') # for now so we don't have to touch infer_edges, force to pandas X, y_ = make_safe_gpu_dataframes(X, y_, 'pandas') @@ -340,32 +352,23 @@ def transform_umap( # noqa: E303 def _bundle_embedding(self, emb, index): # Converts Embedding into dataframe and takes care if emb.dim > 2 - - try: - emb.get() - import cupy as cp - emb_dtype=str(cp.get_array_module(emb)) - except AttributeError: - emb_dtype = str(getmodule(emb)) - - - if emb.shape[1] == 2 and 'cudf' not in emb_dtype and 'cupy' not in emb_dtype: + if emb.shape[1] == 2 and 'cudf.core.dataframe' not in str(getmodule(emb)) and not hasattr(emb, 'device'): emb = pd.DataFrame(emb, columns=[config.X, config.Y], index=index) - elif emb.shape[1] == 2 and 'cudf' in emb_dtype: + elif emb.shape[1] == 2 and 'cudf.core.dataframe' in str(getmodule(emb)): emb.rename(columns={0: config.X, 1: config.Y}, inplace=True) - elif emb.shape[1] == 2 and 'cupy' in emb_dtype: + elif emb.shape[1] == 2 and hasattr(emb, 'device'): import cudf emb = cudf.DataFrame(emb, columns=[config.X, config.Y], index=index) else: columns = [config.X, config.Y] + [ f"umap_{k}" for k in range(2, emb.shape[1]) ] - if 'cudf' not in emb_dtype: + if 'cudf.core.dataframe' not in str(getmodule(emb)): emb = pd.DataFrame(emb, columns=columns, index=index) - elif 'cudf' in emb_dtype: + elif 'cudf.core.dataframe' in str(getmodule(emb)): emb.columns = columns return emb - + def _process_umap( self, res, @@ -468,7 +471,7 @@ def umap( encode_position: bool = True, encode_weight: bool = True, dbscan: bool = False, - umap_engine: UMAPEngine = "auto", + engine: UMAPEngine = "auto", feature_engine: str = "auto", inplace: bool = False, memoize: bool = True, @@ -478,7 +481,7 @@ def umap( """UMAP the featurized nodes or edges data, or pass in your own X, y (optional) dataframes of values Example - + >>> import graphistry >>> g = graphistry.nodes(pd.DataFrame({'node': [0,1,2], 'data': [1,2,3], 'meta': ['a', 'b', 'c']})) >>> g2 = g.umap(n_components=3, spread=1.0, min_dist=0.1, n_neighbors=12, negative_sample_rate=5, local_connectivity=1, repulsion_strength=1.0, metric='euclidean', suffix='', play=0, encode_position=True, encode_weight=True, dbscan=False, engine='auto', feature_engine='auto', inplace=False, memoize=True, verbose=False) @@ -530,9 +533,9 @@ def umap( :return: self, with attributes set with new data """ - if umap_engine == UMAP_LEARN: + if engine == UMAP_LEARN: assert_imported() - elif umap_engine == CUML: + elif engine == CUML: assert_imported_cuml() umap_kwargs = dict( @@ -544,7 +547,7 @@ def umap( local_connectivity=local_connectivity, repulsion_strength=repulsion_strength, negative_sample_rate=negative_sample_rate, - umap_engine=umap_engine, + engine=engine, suffix=suffix, ) logger.debug("umap_kwargs: %s", umap_kwargs) @@ -578,8 +581,6 @@ def umap( featurize_kwargs = self._set_features( res, X, y, kind, feature_engine, {**featurize_kwargs, "memoize": memoize} ) - # umap_kwargs = {**umap_kwargs, - # 'featurize_kwargs': featurize_kwargs or {}} if kind == "nodes": index = res._nodes.index @@ -610,7 +611,10 @@ def umap( if isinstance(X_, pd.DataFrame): index_to_nodes_dict = dict(zip(range(len(nodes)), nodes)) elif 'cudf.core.dataframe' in str(getmodule(X_)): - index_to_nodes_dict = nodes + index_to_nodes_dict = nodes # {}? + + # add the safe coercion here + X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) # type: ignore res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, verbose, **umap_kwargs @@ -640,7 +644,7 @@ def umap( ) # add the safe coercion here - X_, y_ = make_safe_gpu_dataframes(X_, y_, res.umap_engine) # type: ignore + X_, y_ = make_safe_gpu_dataframes(X_, y_, res.engine) # type: ignore res = res._process_umap( res, X_, y_, kind, memoize, featurize_kwargs, **umap_kwargs @@ -685,7 +689,7 @@ def umap( res, kind, encode_position, encode_weight, play ) # noqa: E501 - if res.umap_engine == CUML and is_legacy_cuml(): # type: ignore + if res.engine == CUML and is_legacy_cuml(): # type: ignore res = res.prune_self_edges() if dbscan: @@ -715,7 +719,7 @@ def _bind_xy_from_umap( if type(df) == type(emb): df[x_name] = emb.values.T[0] df[y_name] = emb.values.T[1] - elif isinstance(df, pd.DataFrame) and 'cudf' in str(getmodule(emb)): + elif isinstance(df, pd.DataFrame) and 'cudf.core.dataframe' in str(getmodule(emb)): df[x_name] = emb.to_numpy().T[0] df[y_name] = emb.to_numpy().T[1] From e06ce0c8b4129202bc509a267e2267260efc4c79 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Mon, 17 Apr 2023 22:49:59 +0530 Subject: [PATCH 373/432] flake8 fix --- graphistry/embed_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index 3dbba83156..50e2d7aca7 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -21,6 +21,7 @@ def lazy_embed_import_dep(): except: return False, None, None, None, None, None, None, None + try: import cudf except: From 8f5a40fffb679edab54028657d4dd10fc773e32c Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Mon, 17 Apr 2023 22:54:50 +0530 Subject: [PATCH 374/432] mypy ignore hyperdask --- graphistry/hyper_dask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/hyper_dask.py b/graphistry/hyper_dask.py index 5da16298f9..1b4ee15647 100644 --- a/graphistry/hyper_dask.py +++ b/graphistry/hyper_dask.py @@ -602,7 +602,7 @@ def df_coercion( # noqa: C901 if engine == Engine.DASK: import dask.dataframe if isinstance(df, pd.DataFrame): - out = dask.dataframe.from_pandas(df, **{ + out = dask.dataframe.from_pandas(df, **{ # type: ignore **({'npartitions': npartitions} if npartitions is not None else {}) , **({'chunksize': chunksize} if chunksize is not None else {}) }) From 16607741320e73d8948af1143a952ef2e961a0e7 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Mon, 17 Apr 2023 22:58:52 +0530 Subject: [PATCH 375/432] mypy ignore _version.py --- graphistry/_version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphistry/_version.py b/graphistry/_version.py index c9b3980271..786d68a210 100644 --- a/graphistry/_version.py +++ b/graphistry/_version.py @@ -52,7 +52,8 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} +# TODO: type annotation +LONG_VERSION_PY = {} # type: ignore HANDLERS = {} From 7574e97dffb2ab07a2ebb962424b5ac8cc1b5b5d Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Mon, 17 Apr 2023 23:37:37 +0530 Subject: [PATCH 376/432] temp fix for cudf objects in embed --- graphistry/embed_utils.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index 50e2d7aca7..f590918e4d 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -297,10 +297,16 @@ def embed( self : graphistry instance """ # this is temporary, will be fixed in future releases - if isinstance(self._nodes, cudf.DataFrame): - self._nodes = self._nodes.to_pandas() - if isinstance(self._edges, cudf.DataFrame): - self._edges = self._edges.to_pandas() + try: + if isinstance(self._nodes, cudf.DataFrame): + self._nodes = self._nodes.to_pandas() + except: + pass + try: + if isinstance(self._edges, cudf.DataFrame): + self._edges = self._edges.to_pandas() + except: + pass if inplace: res = self else: @@ -426,8 +432,11 @@ def predict_links( src = pd.Series(all_nodes) else: # this is temporary, will be removed after gpu feature utils - if isinstance(source, cudf.DataFrame): - source = source.to_pandas() # type: ignore + try: + if isinstance(source, cudf.DataFrame): + source = source.to_pandas() # type: ignore + except: + pass src = pd.Series(source) src = src.map(self._node2id) @@ -435,8 +444,11 @@ def predict_links( rel = pd.Series(all_relations) else: # this is temporary, will be removed after gpu feature utils - if isinstance(relation, cudf.DataFrame): - relation = relation.to_pandas() # type: ignore + try: + if isinstance(relation, cudf.DataFrame): + relation = relation.to_pandas() # type: ignore + except: + pass rel = pd.Series(relation) rel = rel.map(self._relation2id) @@ -444,8 +456,11 @@ def predict_links( dst = pd.Series(all_nodes) else: # this is temporary, will be removed after gpu feature utils - if isinstance(destination, cudf.DataFrame): - destination = destination.to_pandas() # type: ignore + try: + if isinstance(destination, cudf.DataFrame): + destination = destination.to_pandas() # type: ignore + except: + pass dst = pd.Series(destination) dst = dst.map(self._node2id) From a9017fa28463a23fb1c6dd476ad684675ea545f5 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 09:50:37 +0800 Subject: [PATCH 377/432] lint --- graphistry/tests/test_feature_utils.py | 6 +++--- graphistry/tests/test_umap_utils.py | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index 08358112f5..85476e2764 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -315,7 +315,7 @@ def test_multi_label_binarizer(self): class TestFeatureCUMLProcessors(unittest.TestCase): def cases_tests(self, x, y, data_encoder, target_encoder, name, value): - import cu_cat + import cu_cat,cudf,cuml self.assertIsInstance( x, cudf.DataFrame, @@ -345,7 +345,7 @@ def cases_tests(self, x, y, data_encoder, target_encoder, name, value): f"Data Target Encoder is not a cu_cat.super_vectorizer.TableVectorizer instance for {name} {value}", ) - @pytest.mark.skipif(not has_cu_cat_dependancy or not has_cu_cat_dependancy, reason="requires cu_cat feature dependencies") + @pytest.mark.skipif(not lazy_import_has_cu_cat_dependancy, reason="requires cu_cat feature dependencies") def test_process_node_dataframes_min_words(self): # test different target cardinality with warnings.catch_warnings(): @@ -368,7 +368,7 @@ def test_process_node_dataframes_min_words(self): ) self.cases_tests(X_enc, y_enc, data_encoder, label_encoder, "min_words", min_words) - @pytest.mark.skipif(not has_cu_cat_dependancy, reason="requires minimal feature dependencies") + @pytest.mark.skipif(not lazy_import_has_cu_cat_dependancy, reason="requires minimal feature dependencies") def test_multi_label_binarizer(self): g = graphistry.nodes(bad_df) # can take in a list of lists and convert to multiOutput with warnings.catch_warnings(): diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index bc247e4216..34fbbac86b 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -30,10 +30,7 @@ lazy_cuml_import_has_dependancy, lazy_cudf_import_has_dependancy, ) -<<<<<<< HEAD from graphistry.umap_utils import lazy_umap_import_has_dependancy, lazy_cuml_import_has_dependancy, lazy_cudf_import_has_dependancy -======= ->>>>>>> cudf-alex3 has_dependancy, _ = lazy_import_has_min_dependancy() has_cuml, _, _ = lazy_cuml_import_has_dependancy() From 65ce26b3022d89d3bd233abc2f56a5d5023c87b3 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:03:17 +0800 Subject: [PATCH 378/432] lint --- graphistry/embed_utils.py | 1 + graphistry/feature_utils.py | 13 +++++++------ graphistry/tests/test_umap_utils.py | 1 - 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index 3dbba83156..50e2d7aca7 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -21,6 +21,7 @@ def lazy_embed_import_dep(): except: return False, None, None, None, None, None, None, None + try: import cudf except: diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index d401dc0083..740dd108ca 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -197,6 +197,7 @@ def safe_cudf(X, y): # # _featurize_or_get_edges_dataframe_if_X_is_None + FeatureEngineConcrete = Literal["none", "pandas", "dirty_cat", "torch", "cu_cat"] FeatureEngine = Literal[FeatureEngineConcrete, "auto"] @@ -688,18 +689,18 @@ def fit_pipeline( """ columns = X.columns index = X.index - X_type= str(getmodule(X)) + X_type = str(getmodule(X)) if 'cudf' not in X_type: X = transformer.fit_transform(X) if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa - X=pd.DataFrame(X, columns=columns, index=index) + X = pd.DataFrame(X, columns=columns, index=index) else: - X = transformer.fit_transform(X.to_numpy()) ## why numpy here? + X = transformer.fit_transform(X.to_numpy()) if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa import cudf - X=cudf.DataFrame(X, columns=columns, index=index) + X = udf.DataFrame(X, columns=columns, index=index) return X @@ -954,7 +955,7 @@ def process_dirty_dataframes( if feature_engine == 'dirty_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder elif feature_engine == 'cu_cat': - lazy_import_has_cu_cat_dependancy() ## tried to use this rather than importing below + lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder from cuml.preprocessing import FunctionTransformer t = time() @@ -1511,7 +1512,7 @@ def process_edge_dataframes( if not X_enc.empty and not T.empty: logger.debug("-" * 60) logger.debug("<= Found Edges and Dirty_cat encoding =>") - T_type= str(getmodule(T)) + T_type = str(getmodule(T)) if 'cudf' not in T_type: X_enc = pd.concat([T, X_enc], axis=1) else: diff --git a/graphistry/tests/test_umap_utils.py b/graphistry/tests/test_umap_utils.py index 34fbbac86b..c6b4f0aa74 100644 --- a/graphistry/tests/test_umap_utils.py +++ b/graphistry/tests/test_umap_utils.py @@ -30,7 +30,6 @@ lazy_cuml_import_has_dependancy, lazy_cudf_import_has_dependancy, ) -from graphistry.umap_utils import lazy_umap_import_has_dependancy, lazy_cuml_import_has_dependancy, lazy_cudf_import_has_dependancy has_dependancy, _ = lazy_import_has_min_dependancy() has_cuml, _, _ = lazy_cuml_import_has_dependancy() From ab7fd8e200ac898b658c81aef84f3357838e0c9c Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:05:31 +0800 Subject: [PATCH 379/432] lint --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 740dd108ca..6064d7e076 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -700,7 +700,7 @@ def fit_pipeline( if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa import cudf - X = udf.DataFrame(X, columns=columns, index=index) + X = cudf.DataFrame(X, columns=columns, index=index) return X From d3d3071cce63ef0bc73af550bd2fa371e50514ea Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:13:26 +0800 Subject: [PATCH 380/432] type: ignore cu_cat import --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 6064d7e076..22eb190be2 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -54,7 +54,7 @@ SuperVectorizer, GapEncoder, SimilarityEncoder, - ) + ) # type: ignore except: SuperVectorizer = Any GapEncoder = Any From 88adafc30574cc10ceb8438ac9677cbfd087a4cf Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:15:12 +0800 Subject: [PATCH 381/432] type: ignore cu_cat import --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 22eb190be2..9e6238be7f 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -54,7 +54,7 @@ SuperVectorizer, GapEncoder, SimilarityEncoder, - ) # type: ignore + ) # type: ignore except: SuperVectorizer = Any GapEncoder = Any From 63f60442cbe552f82977faa10cb26b80b2de2992 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:19:31 +0800 Subject: [PATCH 382/432] base_extras_heavy[cu_cat] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b568ba9e00..77eb0dd5be 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -# base_extras_heavy['cu_cat'] = ['cu-cat @ git+https://github.com/graphistry/cu_cat.git@cudf-cat'] +base_extras_heavy['cu_cat'] = base_extras_heavy['ai'] + ['cu-cat @ git+https://github.com/graphistry/cu_cat.git@cu_cat_regpt'] # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] From 1887a8289cd171f197e38d5fb36f6809f386b52a Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:21:01 +0800 Subject: [PATCH 383/432] base_extras_heavy[cu_cat] --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 77eb0dd5be..fe0b9c693d 100755 --- a/setup.py +++ b/setup.py @@ -41,11 +41,12 @@ def unique_flatten_dict(d): base_extras_heavy = { 'umap-learn': ['umap-learn', 'dirty-cat==0.2.0', 'scikit-learn>=1.0'], } -base_extras_heavy['cu_cat'] = base_extras_heavy['ai'] + ['cu-cat @ git+https://github.com/graphistry/cu_cat.git@cu_cat_regpt'] # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] +base_extras_heavy['cu_cat'] = base_extras_heavy['ai'] + ['cu-cat @ git+https://github.com/graphistry/cu_cat.git@cu_cat_regpt'] + base_extras = {**base_extras_light, **base_extras_heavy} extras_require = { From 8ec6c6e1b367416f121476e05e8765adebb87fae Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:26:37 +0800 Subject: [PATCH 384/432] base_extras_heavy[cu-cat] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fe0b9c693d..f5e6125697 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def unique_flatten_dict(d): # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] -base_extras_heavy['cu_cat'] = base_extras_heavy['ai'] + ['cu-cat @ git+https://github.com/graphistry/cu_cat.git@cu_cat_regpt'] +base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu-cat @ git+https://github.com/graphistry/cu-cat.git@cu_cat_regpt'] base_extras = {**base_extras_light, **base_extras_heavy} From 6f85ee154c4a701bac978f2b8e8ac4c5be10bd4c Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:29:43 +0800 Subject: [PATCH 385/432] base_extras_heavy[cu-cat] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f5e6125697..5eb75098fd 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def unique_flatten_dict(d): # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] -base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu-cat @ git+https://github.com/graphistry/cu-cat.git@cu_cat_regpt'] +base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+https://github.com/graphistry/cu-cat.git@cu_cat_regpt'] base_extras = {**base_extras_light, **base_extras_heavy} From 4acc4d80eb163d0f5e4badee52f7600652427401 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 10:56:14 +0800 Subject: [PATCH 386/432] base_extras_heavy[cu-cat] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5eb75098fd..f5e6125697 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def unique_flatten_dict(d): # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] -base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+https://github.com/graphistry/cu-cat.git@cu_cat_regpt'] +base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu-cat @ git+https://github.com/graphistry/cu-cat.git@cu_cat_regpt'] base_extras = {**base_extras_light, **base_extras_heavy} From 0e0ae32c136b6aef9789a21f52a196f63e454d88 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 11:04:07 +0800 Subject: [PATCH 387/432] long_version_py ignore type --- graphistry/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/_version.py b/graphistry/_version.py index c9b3980271..3bb0a87502 100644 --- a/graphistry/_version.py +++ b/graphistry/_version.py @@ -52,7 +52,7 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} +LONG_VERSION_PY = {} # type: ignore HANDLERS = {} From 7ec91f7b89fe4bc1217d6f13c464eb01a4d06e4a Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 11:15:42 +0800 Subject: [PATCH 388/432] egg-0.02.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f5e6125697..ee2fb106b5 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def unique_flatten_dict(d): # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] -base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu-cat @ git+https://github.com/graphistry/cu-cat.git@cu_cat_regpt'] +base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+http://github.com/graphistry/cu-cat/tarball/cu_cat_regpt#egg=packages-0.02.0'] base_extras = {**base_extras_light, **base_extras_heavy} From d78628b141a32a06fbdaf46b821d37c858e3c770 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 15:46:59 +0800 Subject: [PATCH 389/432] egg-0.02.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ee2fb106b5..fe9353e434 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def unique_flatten_dict(d): # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] -base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+http://github.com/graphistry/cu-cat/tarball/cu_cat_regpt#egg=packages-0.02.0'] +base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+http://github.com/graphistry/cu-cat/tarball/cu_cat_regpt#egg=package-0.02.0'] base_extras = {**base_extras_light, **base_extras_heavy} From 429f6a391b865020d077ee005250e7042d056493 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 18 Apr 2023 15:52:16 +0800 Subject: [PATCH 390/432] rm egg --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fe9353e434..35207de8a3 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def unique_flatten_dict(d): # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] -base_extras_heavy['cu-cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+http://github.com/graphistry/cu-cat/tarball/cu_cat_regpt#egg=package-0.02.0'] +base_extras_heavy['cu_cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+http://github.com/graphistry/cu-cat/tarball/cu_cat_regpt'] #egg=package-0.02.0'] base_extras = {**base_extras_light, **base_extras_heavy} From 6d6ae2544e550a53b90669a76362867d51b37b74 Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Tue, 18 Apr 2023 21:06:46 +0530 Subject: [PATCH 391/432] fix: cu_cat missing stubs ignore --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index 898e001146..5b4403e91f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -94,3 +94,6 @@ ignore_missing_imports = True [mypy-cuml.*] ignore_missing_imports = True + +[mypy-cu_cat.*] +ignore_missing_imports = true From 4204713414f6e25938d299189c984bfe6158dc21 Mon Sep 17 00:00:00 2001 From: tanmoyio Date: Wed, 19 Apr 2023 01:54:31 +0530 Subject: [PATCH 392/432] skip feature_utils cudf tests --- docker/test-gpu-local.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/test-gpu-local.sh b/docker/test-gpu-local.sh index f7c5c5c5ad..158584f9b6 100755 --- a/docker/test-gpu-local.sh +++ b/docker/test-gpu-local.sh @@ -44,4 +44,5 @@ docker run \ ${NETWORK} \ graphistry/test-gpu:${TEST_CPU_VERSION} \ --maxfail=1 \ + --ignore=graphistry/tests/test_feature_utils.py \ $@ From 037975e258ebca4964a89febdcdd7271c9e4c645 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 19 Apr 2023 13:12:44 +0800 Subject: [PATCH 393/432] sklearn FunctionTransformer, no lazy cuml import --- graphistry/feature_utils.py | 2 +- graphistry/tests/test_feature_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 9e6238be7f..e052b22651 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -60,7 +60,7 @@ GapEncoder = Any SimilarityEncoder = Any try: - from cuml.preprocessing import FunctionTransformer + from sklearn.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin except: FunctionTransformer = Any diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index 85476e2764..6f71b5d3ae 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -315,7 +315,7 @@ def test_multi_label_binarizer(self): class TestFeatureCUMLProcessors(unittest.TestCase): def cases_tests(self, x, y, data_encoder, target_encoder, name, value): - import cu_cat,cudf,cuml + import cu_cat,cudf #,cuml self.assertIsInstance( x, cudf.DataFrame, From 263156439495ed37b3cf991afc699cfc42f6c302 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 19 Apr 2023 13:14:19 +0800 Subject: [PATCH 394/432] sklearn FunctionTransformer, no lazy cuml import --- graphistry/tests/test_feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index 6f71b5d3ae..4cb99217c5 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -315,7 +315,7 @@ def test_multi_label_binarizer(self): class TestFeatureCUMLProcessors(unittest.TestCase): def cases_tests(self, x, y, data_encoder, target_encoder, name, value): - import cu_cat,cudf #,cuml + import cu_cat,cudf # ,cuml self.assertIsInstance( x, cudf.DataFrame, From 71cfcd59c8cd98257cbea8b2d1220f5ed887e3ef Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Wed, 19 Apr 2023 22:00:55 +0530 Subject: [PATCH 395/432] some fixes for cpu checks(gpu issues are still there) --- graphistry/feature_utils.py | 61 ++++++++++++++++---------- graphistry/tests/test_feature_utils.py | 13 +++++- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index e052b22651..a0c19572f7 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -611,23 +611,29 @@ def get_preprocessing_pipeline( `uniform`, `quantile`, `kmeans`, default 'quantile' :return: scaled array, imputer instances or None, scaler instance or None """ - from sklearn.preprocessing import ( - # FunctionTransformer, - # KBinsDiscretizer, - # MinMaxScaler, - MultiLabelBinarizer, - QuantileTransformer, - # RobustScaler, - # StandardScaler, - ) - from cuml.preprocessing import ( - FunctionTransformer, - KBinsDiscretizer, - MinMaxScaler, - # QuantileTransformer, ## cuml 23 only - RobustScaler, - StandardScaler, - ) + try: + from sklearn.preprocessing import ( + FunctionTransformer, + KBinsDiscretizer, + MinMaxScaler, + MultiLabelBinarizer, + QuantileTransformer, + RobustScaler, + StandardScaler, + ) + except: + pass + try: + from cuml.preprocessing import ( + FunctionTransformer, + KBinsDiscretizer, + MinMaxScaler, + # QuantileTransformer, ## cuml 23 only + RobustScaler, + StandardScaler, + ) + except: + pass from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer available_preprocessors = [ @@ -892,7 +898,11 @@ def __call__(self, *args, **kwargs): def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. - from cuml.preprocessing import FunctionTransformer + try: + from cuml.preprocessing import FunctionTransformer + except: + from sklearn.preprocessing import FunctionTransformer + label_encoder = False data_encoder = False y_ = y @@ -952,12 +962,19 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ - if feature_engine == 'dirty_cat': - from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - elif feature_engine == 'cu_cat': + try: lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - from cuml.preprocessing import FunctionTransformer + from cuml.preprocessing import FunctionTransformer + temporary_feat_engine_key = 'cu_cat' + except: + from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + temporary_feat_engine_key = 'dirty_cat' + + # TODO: should be handled at resolve_feature_engine level + if not is_dataframe_all_numeric(ndf) and feature_engine == 'torch': + feature_engine = temporary_feat_engine_key + t = time() if not is_dataframe_all_numeric(ndf): diff --git a/graphistry/tests/test_feature_utils.py b/graphistry/tests/test_feature_utils.py index 4cb99217c5..c357381365 100644 --- a/graphistry/tests/test_feature_utils.py +++ b/graphistry/tests/test_feature_utils.py @@ -29,6 +29,14 @@ has_min_dependancy_text, _, _ = lazy_import_has_dependancy_text() has_cu_cat_dependancy_text, _, _ = lazy_import_has_cu_cat_dependancy() +HAS_CUCAT = False +try: + import cu_cat, cudf + HAS_CUCAT = True +except: + cu_cat = object + cudf = pd + logger = logging.getLogger(__name__) warnings.filterwarnings("ignore") logging.getLogger("graphistry.feature_utils").setLevel(logging.DEBUG) @@ -314,8 +322,9 @@ def test_multi_label_binarizer(self): assert sum(y.sum(1).values - np.array([1., 2., 1., 0.])) == 0 class TestFeatureCUMLProcessors(unittest.TestCase): + @pytest.mark.skipif(not lazy_import_has_cu_cat_dependancy, reason="requires cu_cat feature dependencies") + @pytest.mark.skipif(not HAS_CUCAT, reason="requires cu_cat, cudf") def cases_tests(self, x, y, data_encoder, target_encoder, name, value): - import cu_cat,cudf # ,cuml self.assertIsInstance( x, cudf.DataFrame, @@ -346,6 +355,7 @@ def cases_tests(self, x, y, data_encoder, target_encoder, name, value): ) @pytest.mark.skipif(not lazy_import_has_cu_cat_dependancy, reason="requires cu_cat feature dependencies") + @pytest.mark.skipif(not HAS_CUCAT, reason="requires cu_cat, cudf") def test_process_node_dataframes_min_words(self): # test different target cardinality with warnings.catch_warnings(): @@ -369,6 +379,7 @@ def test_process_node_dataframes_min_words(self): self.cases_tests(X_enc, y_enc, data_encoder, label_encoder, "min_words", min_words) @pytest.mark.skipif(not lazy_import_has_cu_cat_dependancy, reason="requires minimal feature dependencies") + @pytest.mark.skipif(not HAS_CUCAT, reason="requires cu_cat, cudf") def test_multi_label_binarizer(self): g = graphistry.nodes(bad_df) # can take in a list of lists and convert to multiOutput with warnings.catch_warnings(): From 9755e448f05e6d670c354b88c9cca735de1c6a40 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Wed, 19 Apr 2023 22:09:54 +0530 Subject: [PATCH 396/432] flake: fix --- graphistry/feature_utils.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index a0c19572f7..241fe7a0bb 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -612,28 +612,25 @@ def get_preprocessing_pipeline( :return: scaled array, imputer instances or None, scaler instance or None """ try: - from sklearn.preprocessing import ( + from sklearn.preprocessing import QuantileTransformer + from cuml.preprocessing import ( FunctionTransformer, KBinsDiscretizer, MinMaxScaler, - MultiLabelBinarizer, - QuantileTransformer, + # QuantileTransformer, ## cuml 23 only RobustScaler, StandardScaler, ) except: - pass - try: - from cuml.preprocessing import ( + from sklearn.preprocessing import ( FunctionTransformer, KBinsDiscretizer, MinMaxScaler, - # QuantileTransformer, ## cuml 23 only + MultiLabelBinarizer, + QuantileTransformer, RobustScaler, StandardScaler, ) - except: - pass from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer available_preprocessors = [ From 19e9f6cf436c90125d28227cc580998c52e7477d Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 20 Apr 2023 07:17:35 +0800 Subject: [PATCH 397/432] merge with cudf-final embed --- graphistry/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/_version.py b/graphistry/_version.py index c9b3980271..3bb0a87502 100644 --- a/graphistry/_version.py +++ b/graphistry/_version.py @@ -52,7 +52,7 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} +LONG_VERSION_PY = {} # type: ignore HANDLERS = {} From 9d673b452064e53bc7b398eaf8dd9e79dcf90f62 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 20 Apr 2023 16:04:18 +0800 Subject: [PATCH 398/432] assert cudf not import --- graphistry/feature_utils.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 9e6238be7f..0a275329d2 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -699,7 +699,8 @@ def fit_pipeline( X = transformer.fit_transform(X.to_numpy()) if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa - import cudf + # import cudf + assert_cuml_cucat() X = cudf.DataFrame(X, columns=columns, index=index) return X @@ -1345,8 +1346,9 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf' in edf_type: - lazy_import_has_cu_cat_dependancy() - import cudf + # lazy_import_has_cu_cat_dependancy() + # import cudf + assert_cuml_cucat() T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1423,10 +1425,10 @@ def process_edge_dataframes( ) # create new one so we can use encode_edges later in # transform with fit=False edf_type = str(getmodule(edf)) - if 'cudf' in edf_type: - import cudf - lazy_import_has_cu_cat_dependancy() - + # if 'cudf' in edf_type: + # import cudf + # lazy_import_has_cu_cat_dependancy() + assert_cuml_cucat() T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From 03a404253dc5bdceeb24fb4e1ff47520b78134e0 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 20 Apr 2023 16:05:25 +0800 Subject: [PATCH 399/432] assert cudf not import --- graphistry/feature_utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 241fe7a0bb..2aba6666c7 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -702,7 +702,8 @@ def fit_pipeline( X = transformer.fit_transform(X.to_numpy()) if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa - import cudf + # import cudf + assert_cuml_cucat() X = cudf.DataFrame(X, columns=columns, index=index) return X @@ -1359,8 +1360,9 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf' in edf_type: - lazy_import_has_cu_cat_dependancy() - import cudf + # lazy_import_has_cu_cat_dependancy() + # import cudf + assert_cuml_cucat() T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1438,9 +1440,9 @@ def process_edge_dataframes( # transform with fit=False edf_type = str(getmodule(edf)) if 'cudf' in edf_type: - import cudf - lazy_import_has_cu_cat_dependancy() - + # import cudf + # lazy_import_has_cu_cat_dependancy() + assert_cuml_cucat() T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From 071f1c177d90431e301c5e6797ef8093a847a0f9 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 20 Apr 2023 16:16:55 +0800 Subject: [PATCH 400/432] lazy not assert --- graphistry/feature_utils.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 2aba6666c7..43c8d235dd 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -703,7 +703,8 @@ def fit_pipeline( if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa # import cudf - assert_cuml_cucat() + # assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() X = cudf.DataFrame(X, columns=columns, index=index) return X @@ -1360,9 +1361,9 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf' in edf_type: - # lazy_import_has_cu_cat_dependancy() + _, _, cudf = lazy_import_has_cu_cat_dependancy() # import cudf - assert_cuml_cucat() + # assert_cuml_cucat() T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1441,8 +1442,8 @@ def process_edge_dataframes( edf_type = str(getmodule(edf)) if 'cudf' in edf_type: # import cudf - # lazy_import_has_cu_cat_dependancy() - assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() + # assert_cuml_cucat() T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From dd38945840dd50b37387d3a6605facfb907a9e1f Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Tue, 25 Apr 2023 01:05:32 +0530 Subject: [PATCH 401/432] Update embed_utils.py --- graphistry/embed_utils.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index f590918e4d..e7c17b5723 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -21,12 +21,13 @@ def lazy_embed_import_dep(): except: return False, None, None, None, None, None, None, None - -try: - import cudf -except: - cudf = object - +def check_cudf(): + try: + import cudf + return True, cudf + except: + return False, Object + if TYPE_CHECKING: _, torch, _, _, _, _, _, _ = lazy_embed_import_dep() @@ -37,6 +38,8 @@ def lazy_embed_import_dep(): MIXIN_BASE = object torch = Any +has_cudf, cudf = check_cudf() + XSymbolic = Optional[Union[List[str], str, pd.DataFrame]] ProtoSymbolic = Optional[Union[str, Callable[[TT, TT, TT], TT]]] # type: ignore From e96bf01b5aa7cc75be14a60f89ffbaef3dadce2c Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Tue, 25 Apr 2023 01:06:32 +0530 Subject: [PATCH 402/432] migrate check_cudf to embed_utils.py --- graphistry/tests/test_embed_utils.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/graphistry/tests/test_embed_utils.py b/graphistry/tests/test_embed_utils.py index a3001424d4..307bdd0266 100644 --- a/graphistry/tests/test_embed_utils.py +++ b/graphistry/tests/test_embed_utils.py @@ -5,20 +5,12 @@ import graphistry import numpy as np -from graphistry.embed_utils import lazy_embed_import_dep +from graphistry.embed_utils import lazy_embed_import_dep, check_cudf import logging logger = logging.getLogger(__name__) dep_flag, _, _, _, _, _, _, _ = lazy_embed_import_dep() -def check_cudf(): - try: - import cudf - return True, cudf - except: - return False, None - - has_cudf, cudf = check_cudf() # enable tests if has cudf and env didn't explicitly disable From 827984d23f45fcfbaa2ef5d4d66ff026de0e7407 Mon Sep 17 00:00:00 2001 From: Tanmoy Date: Tue, 25 Apr 2023 01:09:08 +0530 Subject: [PATCH 403/432] Update embed_utils.py --- graphistry/embed_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/embed_utils.py b/graphistry/embed_utils.py index e7c17b5723..9e64fdfa10 100644 --- a/graphistry/embed_utils.py +++ b/graphistry/embed_utils.py @@ -26,7 +26,7 @@ def check_cudf(): import cudf return True, cudf except: - return False, Object + return False, object if TYPE_CHECKING: From 13f6b7ef143d69f6e4fbd047bbd0d01ca2c5038b Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 10:15:34 +0800 Subject: [PATCH 404/432] lint --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 0bd54f141b..e8790591ea 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1442,7 +1442,7 @@ def process_edge_dataframes( # if 'cudf' in edf_type: # import cudf # lazy_import_has_cu_cat_dependancy() - assert_cuml_cucat() + assert_cuml_cucat() T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From 339779b25612e68485322770df781015cfcd3170 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 10:19:18 +0800 Subject: [PATCH 405/432] lint --- graphistry/feature_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index e8790591ea..569640dde7 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -703,7 +703,7 @@ def fit_pipeline( if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa # import cudf - assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() X = cudf.DataFrame(X, columns=columns, index=index) return X @@ -1360,9 +1360,9 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf' in edf_type: - # lazy_import_has_cu_cat_dependancy() # import cudf - assert_cuml_cucat() + # assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1441,8 +1441,8 @@ def process_edge_dataframes( edf_type = str(getmodule(edf)) # if 'cudf' in edf_type: # import cudf - # lazy_import_has_cu_cat_dependancy() - assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() + # assert_cuml_cucat() T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From ff213fb18ebe09b00370d86adb0ff5e994cc96e5 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 10:21:14 +0800 Subject: [PATCH 406/432] lint --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 569640dde7..370006abc2 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -703,7 +703,7 @@ def fit_pipeline( if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa # import cudf - _, _, cudf = lazy_import_has_cu_cat_dependancy() + _, _, cudf = lazy_import_has_cu_cat_dependancy() X = cudf.DataFrame(X, columns=columns, index=index) return X From 084395a50bb3e21a8f46cea1230a5d269f9ea25c Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 10:23:36 +0800 Subject: [PATCH 407/432] lint --- graphistry/feature_utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 370006abc2..392f10b324 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1438,9 +1438,6 @@ def process_edge_dataframes( MultiLabelBinarizer() ) # create new one so we can use encode_edges later in # transform with fit=False - edf_type = str(getmodule(edf)) - # if 'cudf' in edf_type: - # import cudf _, _, cudf = lazy_import_has_cu_cat_dependancy() # assert_cuml_cucat() T, mlb_pairwise_edge_encoder = encode_edges( From 70b50d95c11b104f5573868241d7712cded51c10 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 10:33:41 +0800 Subject: [PATCH 408/432] lint --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 0a275329d2..028caab99b 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1428,7 +1428,7 @@ def process_edge_dataframes( # if 'cudf' in edf_type: # import cudf # lazy_import_has_cu_cat_dependancy() - assert_cuml_cucat() + assert_cuml_cucat() T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From 06a691bd7b25ced86f3de0ce8bb0c17360f712f9 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 10:45:51 +0800 Subject: [PATCH 409/432] lazy cudf import --- graphistry/feature_utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 028caab99b..380ff5c8c5 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -700,7 +700,8 @@ def fit_pipeline( if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa # import cudf - assert_cuml_cucat() + # assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() X = cudf.DataFrame(X, columns=columns, index=index) return X @@ -1348,7 +1349,8 @@ def encode_edges(edf, src, dst, mlb, fit=False): if 'cudf' in edf_type: # lazy_import_has_cu_cat_dependancy() # import cudf - assert_cuml_cucat() + # assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1428,7 +1430,8 @@ def process_edge_dataframes( # if 'cudf' in edf_type: # import cudf # lazy_import_has_cu_cat_dependancy() - assert_cuml_cucat() + # assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From 4b779ac710b44557c4654bc106966355913cb98a Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 10:47:57 +0800 Subject: [PATCH 410/432] lazy cudf import --- graphistry/feature_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 380ff5c8c5..41f008d1e0 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1432,6 +1432,7 @@ def process_edge_dataframes( # lazy_import_has_cu_cat_dependancy() # assert_cuml_cucat() _, _, cudf = lazy_import_has_cu_cat_dependancy() + T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From c1a0ccafd502b2b600eda9762ee62eeb31dad5b4 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 10:52:55 +0800 Subject: [PATCH 411/432] lint --- graphistry/feature_utils.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 74e9776063..0511e35bf2 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1438,11 +1438,7 @@ def process_edge_dataframes( MultiLabelBinarizer() ) # create new one so we can use encode_edges later in # transform with fit=False - edf_type = str(getmodule(edf)) - # if 'cudf' in edf_type: - # import cudf - # lazy_import_has_cu_cat_dependancy() - # assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() T, mlb_pairwise_edge_encoder = encode_edges( From f853472cfe06149dba74e94c9a56570c952cdf2b Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 11:24:33 +0800 Subject: [PATCH 412/432] better lazy cudf import --- graphistry/feature_utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 0a275329d2..116a9ad712 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -700,7 +700,8 @@ def fit_pipeline( if keep_n_decimals: X = np.round(X, decimals=keep_n_decimals) # type: ignore # noqa # import cudf - assert_cuml_cucat() + # assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() X = cudf.DataFrame(X, columns=columns, index=index) return X @@ -1348,7 +1349,8 @@ def encode_edges(edf, src, dst, mlb, fit=False): if 'cudf' in edf_type: # lazy_import_has_cu_cat_dependancy() # import cudf - assert_cuml_cucat() + # assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() T = cudf.DataFrame(T, columns=columns, index=edf.index) else: T = pd.DataFrame(T, columns=columns, index=edf.index) @@ -1427,8 +1429,9 @@ def process_edge_dataframes( edf_type = str(getmodule(edf)) # if 'cudf' in edf_type: # import cudf - # lazy_import_has_cu_cat_dependancy() - assert_cuml_cucat() + _, _, cudf = lazy_import_has_cu_cat_dependancy() + # assert_cuml_cucat() + T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True ) From b6b148b130723750a7970bf7c204e71f26e73bb7 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 13:43:18 +0800 Subject: [PATCH 413/432] lazy merge --- graphistry/feature_utils.py | 66 ++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index d81d0622c2..116a9ad712 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -60,7 +60,7 @@ GapEncoder = Any SimilarityEncoder = Any try: - from sklearn.preprocessing import FunctionTransformer + from cuml.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin except: FunctionTransformer = Any @@ -611,26 +611,23 @@ def get_preprocessing_pipeline( `uniform`, `quantile`, `kmeans`, default 'quantile' :return: scaled array, imputer instances or None, scaler instance or None """ - try: - from sklearn.preprocessing import QuantileTransformer - from cuml.preprocessing import ( - FunctionTransformer, - KBinsDiscretizer, - MinMaxScaler, - # QuantileTransformer, ## cuml 23 only - RobustScaler, - StandardScaler, - ) - except: - from sklearn.preprocessing import ( - FunctionTransformer, - KBinsDiscretizer, - MinMaxScaler, - MultiLabelBinarizer, - QuantileTransformer, - RobustScaler, - StandardScaler, - ) + from sklearn.preprocessing import ( + # FunctionTransformer, + # KBinsDiscretizer, + # MinMaxScaler, + MultiLabelBinarizer, + QuantileTransformer, + # RobustScaler, + # StandardScaler, + ) + from cuml.preprocessing import ( + FunctionTransformer, + KBinsDiscretizer, + MinMaxScaler, + # QuantileTransformer, ## cuml 23 only + RobustScaler, + StandardScaler, + ) from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer available_preprocessors = [ @@ -897,11 +894,7 @@ def __call__(self, *args, **kwargs): def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. - try: - from cuml.preprocessing import FunctionTransformer - except: - from sklearn.preprocessing import FunctionTransformer - + from cuml.preprocessing import FunctionTransformer label_encoder = False data_encoder = False y_ = y @@ -961,19 +954,12 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ - try: + if feature_engine == 'dirty_cat': + from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + elif feature_engine == 'cu_cat': lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - from cuml.preprocessing import FunctionTransformer - temporary_feat_engine_key = 'cu_cat' - except: - from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - temporary_feat_engine_key = 'dirty_cat' - - # TODO: should be handled at resolve_feature_engine level - if not is_dataframe_all_numeric(ndf) and feature_engine == 'torch': - feature_engine = temporary_feat_engine_key - + from cuml.preprocessing import FunctionTransformer t = time() if not is_dataframe_all_numeric(ndf): @@ -1361,6 +1347,7 @@ def encode_edges(edf, src, dst, mlb, fit=False): mlb.get_feature_names_out = callThrough(columns) mlb.columns_ = [src, dst] if 'cudf' in edf_type: + # lazy_import_has_cu_cat_dependancy() # import cudf # assert_cuml_cucat() _, _, cudf = lazy_import_has_cu_cat_dependancy() @@ -1439,8 +1426,11 @@ def process_edge_dataframes( MultiLabelBinarizer() ) # create new one so we can use encode_edges later in # transform with fit=False - + edf_type = str(getmodule(edf)) + # if 'cudf' in edf_type: + # import cudf _, _, cudf = lazy_import_has_cu_cat_dependancy() + # assert_cuml_cucat() T, mlb_pairwise_edge_encoder = encode_edges( edf, src, dst, mlb_pairwise_edge_encoder, fit=True From 17f0af6380829a09755855c0a5fe833027d935a6 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 13:46:38 +0800 Subject: [PATCH 414/432] lint --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 116a9ad712..89f2ff8aaf 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1426,7 +1426,7 @@ def process_edge_dataframes( MultiLabelBinarizer() ) # create new one so we can use encode_edges later in # transform with fit=False - edf_type = str(getmodule(edf)) + # edf_type = str(getmodule(edf)) # if 'cudf' in edf_type: # import cudf _, _, cudf = lazy_import_has_cu_cat_dependancy() From 2a5c879c4a25510452585c0a4b67ca679ff72a00 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 14:05:54 +0800 Subject: [PATCH 415/432] lint --- graphistry/feature_utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 89f2ff8aaf..3cafbec0e1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -1426,9 +1426,6 @@ def process_edge_dataframes( MultiLabelBinarizer() ) # create new one so we can use encode_edges later in # transform with fit=False - # edf_type = str(getmodule(edf)) - # if 'cudf' in edf_type: - # import cudf _, _, cudf = lazy_import_has_cu_cat_dependancy() # assert_cuml_cucat() From 80ba095ce7834bb094529ea351bba3620b304128 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 14:25:16 +0800 Subject: [PATCH 416/432] functiontransform cuml import --- graphistry/feature_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 3cafbec0e1..38a22696a1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -59,6 +59,13 @@ SuperVectorizer = Any GapEncoder = Any SimilarityEncoder = Any + try: + from sklearn.preprocessing import FunctionTransformer + from sklearn.base import BaseEstimator, TransformerMixin + except: + FunctionTransformer = Any + BaseEstimator = object + TransformerMixin = object try: from cuml.preprocessing import FunctionTransformer from sklearn.base import BaseEstimator, TransformerMixin From 6709bea99725d187b75c4e3cfd155c915be6671d Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 14:30:00 +0800 Subject: [PATCH 417/432] functiontransform cuml import --- graphistry/feature_utils.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 38a22696a1..7e426bc7f7 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -901,7 +901,11 @@ def __call__(self, *args, **kwargs): def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. - from cuml.preprocessing import FunctionTransformer + from sklearn.preprocessing import FunctionTransformer + try: + from cuml.preprocessing import FunctionTransformer + except: + pass label_encoder = False data_encoder = False y_ = y @@ -966,7 +970,11 @@ def process_dirty_dataframes( elif feature_engine == 'cu_cat': lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - from cuml.preprocessing import FunctionTransformer + from sklearn.preprocessing import FunctionTransformer + try: + from cuml.preprocessing import FunctionTransformer + except: + pass t = time() if not is_dataframe_all_numeric(ndf): From 901846c0bae167f00a87a972e052e28aab865fc8 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 14:33:00 +0800 Subject: [PATCH 418/432] functiontransform cuml import --- graphistry/feature_utils.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 7e426bc7f7..d853c41b22 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -901,11 +901,6 @@ def __call__(self, *args, **kwargs): def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. - from sklearn.preprocessing import FunctionTransformer - try: - from cuml.preprocessing import FunctionTransformer - except: - pass label_encoder = False data_encoder = False y_ = y @@ -970,11 +965,6 @@ def process_dirty_dataframes( elif feature_engine == 'cu_cat': lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - from sklearn.preprocessing import FunctionTransformer - try: - from cuml.preprocessing import FunctionTransformer - except: - pass t = time() if not is_dataframe_all_numeric(ndf): From 118ea809dd665f5f949d3e8c73b230026898244f Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 14:54:54 +0800 Subject: [PATCH 419/432] functiontransform cuml import --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index d853c41b22..09ab1eee37 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -619,7 +619,7 @@ def get_preprocessing_pipeline( :return: scaled array, imputer instances or None, scaler instance or None """ from sklearn.preprocessing import ( - # FunctionTransformer, + FunctionTransformer, # KBinsDiscretizer, # MinMaxScaler, MultiLabelBinarizer, From f1ee230e461529d059b286127d6eeb78a21324e6 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Tue, 25 Apr 2023 14:59:37 +0800 Subject: [PATCH 420/432] functiontransform cuml import --- graphistry/feature_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 09ab1eee37..a81e36cf06 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -619,7 +619,7 @@ def get_preprocessing_pipeline( :return: scaled array, imputer instances or None, scaler instance or None """ from sklearn.preprocessing import ( - FunctionTransformer, + # FunctionTransformer, # KBinsDiscretizer, # MinMaxScaler, MultiLabelBinarizer, @@ -628,7 +628,7 @@ def get_preprocessing_pipeline( # StandardScaler, ) from cuml.preprocessing import ( - FunctionTransformer, + # FunctionTransformer, KBinsDiscretizer, MinMaxScaler, # QuantileTransformer, ## cuml 23 only From 7fc02e235f579cd3ee302fa6af022e774f48c993 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 10:26:48 +0800 Subject: [PATCH 421/432] use dirty_cat superVec for torch/etc, except if cu_cat --- graphistry/feature_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index a81e36cf06..9333770879 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -960,9 +960,9 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ - if feature_engine == 'dirty_cat': - from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - elif feature_engine == 'cu_cat': + # if feature_engine == 'dirty_cat': + from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + if feature_engine == 'cu_cat': lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder t = time() From d38f469e23b833943d5d64e8cf76e3bcc9775529 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 10:29:53 +0800 Subject: [PATCH 422/432] use dirty_cat superVec for torch/etc, except if cu_cat --- graphistry/feature_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 9333770879..907eb656d1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -960,8 +960,8 @@ def process_dirty_dataframes( :return: Encoded data matrix and target (if not None), the data encoder, and the label encoder. """ - # if feature_engine == 'dirty_cat': - from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + if feature_engine != 'cu_cat': + from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder if feature_engine == 'cu_cat': lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder From 6436067d8d6b3fcbc20aff3c906fab15d7a52d27 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 10:34:44 +0800 Subject: [PATCH 423/432] use dirty_cat superVec for torch/etc, except if cu_cat --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 907eb656d1..73eb717181 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -962,7 +962,7 @@ def process_dirty_dataframes( """ if feature_engine != 'cu_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder - if feature_engine == 'cu_cat': + elif feature_engine == 'cu_cat': lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder t = time() From 25573eaf4db2768f19ce269a684e9c9819e4b8a4 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 10:43:21 +0800 Subject: [PATCH 424/432] sklearn functiontransformer & MLB --- graphistry/feature_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 73eb717181..e5bfe7a234 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -619,7 +619,7 @@ def get_preprocessing_pipeline( :return: scaled array, imputer instances or None, scaler instance or None """ from sklearn.preprocessing import ( - # FunctionTransformer, + FunctionTransformer, # KBinsDiscretizer, # MinMaxScaler, MultiLabelBinarizer, From dee5ad4b9ca22e22a7892925e794dcd6ae32f3a9 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 10:49:57 +0800 Subject: [PATCH 425/432] sklearn functiontransformer & MLB --- graphistry/feature_utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index e5bfe7a234..3de26a3f95 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -66,13 +66,13 @@ FunctionTransformer = Any BaseEstimator = object TransformerMixin = object - try: - from cuml.preprocessing import FunctionTransformer - from sklearn.base import BaseEstimator, TransformerMixin - except: - FunctionTransformer = Any - BaseEstimator = object - TransformerMixin = object + # try: + # from cuml.preprocessing import FunctionTransformer + # from sklearn.base import BaseEstimator, TransformerMixin + # except: + # FunctionTransformer = Any + # BaseEstimator = object + # TransformerMixin = object else: MIXIN_BASE = object Pipeline = Any From cac6cc42b7e51e7d1e74196636ffa8d209aa9799 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 11:09:46 +0800 Subject: [PATCH 426/432] all preprocess back to sklearn --- graphistry/feature_utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 3de26a3f95..c3c0906e6d 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -620,21 +620,21 @@ def get_preprocessing_pipeline( """ from sklearn.preprocessing import ( FunctionTransformer, - # KBinsDiscretizer, - # MinMaxScaler, - MultiLabelBinarizer, - QuantileTransformer, - # RobustScaler, - # StandardScaler, - ) - from cuml.preprocessing import ( - # FunctionTransformer, KBinsDiscretizer, MinMaxScaler, - # QuantileTransformer, ## cuml 23 only + MultiLabelBinarizer, + QuantileTransformer, RobustScaler, StandardScaler, ) + # from cuml.preprocessing import ( + # # FunctionTransformer, + # KBinsDiscretizer, + # MinMaxScaler, + # # QuantileTransformer, ## cuml 23 only + # RobustScaler, + # StandardScaler, + # ) from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer available_preprocessors = [ From 3757b10e69bbb9d9d633ad9f8512fd177ca82b32 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 12:04:16 +0800 Subject: [PATCH 427/432] import FT again --- graphistry/feature_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index c3c0906e6d..bd53d3a1f1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -962,11 +962,13 @@ def process_dirty_dataframes( """ if feature_engine != 'cu_cat': from dirty_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + from sklearn.preprocessing import FunctionTransformer elif feature_engine == 'cu_cat': lazy_import_has_cu_cat_dependancy() # tried to use this rather than importing below from cu_cat import SuperVectorizer, GapEncoder, SimilarityEncoder + from cuml.preprocessing import FunctionTransformer t = time() - + if not is_dataframe_all_numeric(ndf): data_encoder = SuperVectorizer( auto_cast=True, From 523d180378d2d6afa4aef0439437495b8f536508 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 14:37:39 +0800 Subject: [PATCH 428/432] rewrite g_n_t --- graphistry/feature_utils.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index bd53d3a1f1..635721bea1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -898,29 +898,24 @@ def __call__(self, *args, **kwargs): return self.x -def get_numeric_transformers(ndf, y=None): +def get_numeric_transformers(ndf=None, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. - label_encoder = False - data_encoder = False - y_ = y - if y is not None: - y_ = y.select_dtypes(include=[np.number]) - label_encoder = FunctionTransformer( - partial(passthrough_df_cols, columns=y_.columns) - ) # takes dataframe and memorizes which cols to use. + label_encoder = None + data_encoder = None + y_ = y.select_dtypes(include=[np.number]) if y is not None else None + if y_ is not None: + label_encoder = FunctionTransformer(partial(passthrough_df_cols, columns=y_.columns)) label_encoder.get_feature_names_out = callThrough(y_.columns) label_encoder.columns_ = y_.columns - if ndf is not None: - ndf_ = ndf.select_dtypes(include=[np.number]) - data_encoder = FunctionTransformer( - partial(passthrough_df_cols, columns=ndf_.columns) - ) + ndf_ = ndf.select_dtypes(include=[np.number]) if ndf is not None else None + if ndf_ is not None: + data_encoder = StandardScaler() + data_encoder.fit(ndf_) data_encoder.get_feature_names_out = callThrough(ndf_.columns) - #data_encoder.columns_ = ndf_.columns data_encoder.get_feature_names_in = callThrough(ndf_.columns) - + return ndf_, y_, data_encoder, label_encoder From 97b725d3280f3619ebe2337614000772426d7edf Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 26 Apr 2023 15:49:07 +0800 Subject: [PATCH 429/432] revert g_n_t --- graphistry/feature_utils.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 635721bea1..bd53d3a1f1 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -898,24 +898,29 @@ def __call__(self, *args, **kwargs): return self.x -def get_numeric_transformers(ndf=None, y=None): +def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. - label_encoder = None - data_encoder = None - y_ = y.select_dtypes(include=[np.number]) if y is not None else None - if y_ is not None: - label_encoder = FunctionTransformer(partial(passthrough_df_cols, columns=y_.columns)) + label_encoder = False + data_encoder = False + y_ = y + if y is not None: + y_ = y.select_dtypes(include=[np.number]) + label_encoder = FunctionTransformer( + partial(passthrough_df_cols, columns=y_.columns) + ) # takes dataframe and memorizes which cols to use. label_encoder.get_feature_names_out = callThrough(y_.columns) label_encoder.columns_ = y_.columns - ndf_ = ndf.select_dtypes(include=[np.number]) if ndf is not None else None - if ndf_ is not None: - data_encoder = StandardScaler() - data_encoder.fit(ndf_) + if ndf is not None: + ndf_ = ndf.select_dtypes(include=[np.number]) + data_encoder = FunctionTransformer( + partial(passthrough_df_cols, columns=ndf_.columns) + ) data_encoder.get_feature_names_out = callThrough(ndf_.columns) + #data_encoder.columns_ = ndf_.columns data_encoder.get_feature_names_in = callThrough(ndf_.columns) - + return ndf_, y_, data_encoder, label_encoder From 7ab97a4869ceea117f97607f9590f61b9f479108 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 27 Apr 2023 09:24:53 +0800 Subject: [PATCH 430/432] import FT in get_numeric_transform --- graphistry/feature_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index bd53d3a1f1..033e3d6472 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -901,6 +901,11 @@ def __call__(self, *args, **kwargs): def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. + from sklearn.preprocessing import FunctionTransformer + try: + from cuml.preprocessing import FunctionTransformer + except: + pass label_encoder = False data_encoder = False y_ = y From aba0c558d7bef721515790df935bf4b28079d0f7 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Thu, 27 Apr 2023 09:26:48 +0800 Subject: [PATCH 431/432] import FT in get_numeric_transform --- graphistry/feature_utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/graphistry/feature_utils.py b/graphistry/feature_utils.py index 033e3d6472..2a60194ca2 100644 --- a/graphistry/feature_utils.py +++ b/graphistry/feature_utils.py @@ -902,10 +902,6 @@ def get_numeric_transformers(ndf, y=None): # numeric selector needs to embody memorization of columns # for later .transform consistency. from sklearn.preprocessing import FunctionTransformer - try: - from cuml.preprocessing import FunctionTransformer - except: - pass label_encoder = False data_encoder = False y_ = y From fb96400a3f4d1e8f68cc38fb1e438b7dcde7f055 Mon Sep 17 00:00:00 2001 From: dcolinmorgan Date: Wed, 10 May 2023 16:56:02 +0800 Subject: [PATCH 432/432] latest release opt-in install --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35207de8a3..3ad1513235 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def unique_flatten_dict(d): # https://github.com/facebookresearch/faiss/issues/1589 for faiss-cpu 1.6.1, #'setuptools==67.4.0' removed base_extras_heavy['ai'] = base_extras_heavy['umap-learn'] + ['scipy', 'dgl', 'torch<2', 'sentence-transformers', 'faiss-cpu', 'joblib'] -base_extras_heavy['cu_cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+http://github.com/graphistry/cu-cat/tarball/cu_cat_regpt'] #egg=package-0.02.0'] +base_extras_heavy['cu_cat'] = base_extras_heavy['ai'] + ['cu_cat @ git+http://github.com/graphistry/cu-cat.git@0.03.0'] base_extras = {**base_extras_light, **base_extras_heavy}