From 3d38d4d8b2f5248115731e673d8f762ff557db82 Mon Sep 17 00:00:00 2001
From: Don Acosta <97529984+acostadon@users.noreply.github.com>
Date: Fri, 8 Jul 2022 12:43:31 -0400
Subject: [PATCH] Moving Centrality notebooks to new structure and
updating/testing (#2388)
Authors:
- Don Acosta (https://github.com/acostadon)
Approvers:
- Brad Rees (https://github.com/BradReesWork)
URL: https://github.com/rapidsai/cugraph/pull/2388
---
notebooks/README.md | 16 +-
notebooks/algorithms/README.md | 69 ++
.../centrality/Betweenness.ipynb | 61 +-
.../algorithms/centrality/Centrality.ipynb | 470 +++++++++++
notebooks/algorithms/centrality/Degree.ipynb | 323 ++++++++
.../algorithms/centrality/Eigenvector.ipynb | 340 ++++++++
.../{ => algorithms}/centrality/Katz.ipynb | 68 +-
notebooks/algorithms/centrality/README.md | 41 +
notebooks/centrality/Centrality.ipynb | 761 ------------------
notebooks/img/zachary_black_lines.png | Bin 58104 -> 65807 bytes
notebooks/img/zachary_graph_centrality.png | Bin 0 -> 141188 bytes
notebooks/img/zachary_graph_pagerank.png | Bin 114250 -> 140203 bytes
12 files changed, 1292 insertions(+), 857 deletions(-)
create mode 100644 notebooks/algorithms/README.md
rename notebooks/{ => algorithms}/centrality/Betweenness.ipynb (86%)
create mode 100644 notebooks/algorithms/centrality/Centrality.ipynb
create mode 100644 notebooks/algorithms/centrality/Degree.ipynb
create mode 100644 notebooks/algorithms/centrality/Eigenvector.ipynb
rename notebooks/{ => algorithms}/centrality/Katz.ipynb (88%)
create mode 100644 notebooks/algorithms/centrality/README.md
delete mode 100644 notebooks/centrality/Centrality.ipynb
create mode 100644 notebooks/img/zachary_graph_centrality.png
diff --git a/notebooks/README.md b/notebooks/README.md
index 98e421603ad..90bbd8142d4 100644
--- a/notebooks/README.md
+++ b/notebooks/README.md
@@ -10,9 +10,11 @@ This repository contains a collection of Jupyter Notebooks that outline how to r
| Folder | Notebook | Description |
| --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Centrality | | |
-| | [Centrality](centrality/Centrality.ipynb) | Compute and compare multiple (currently 4) centrality scores |
-| | [Katz](centrality/Katz.ipynb) | Compute the Katz centrality for every vertex |
-| | [Betweenness](centrality/Betweenness.ipynb) | Compute both Edge and Vertex Betweenness centrality |
+| | [Centrality](algorithms/centrality/Centrality.ipynb) | Compute and compare multiple (currently 5) centrality scores |
+| | [Katz](algorithms/centrality/Katz.ipynb) | Compute the Katz centrality for every vertex |
+| | [Betweenness](algorithms/centrality/Betweenness.ipynb) | Compute both Edge and Vertex Betweenness centrality |
+| | [Degree](algorithms/centrality/Degree.ipynb) | Compute Degree Centraility for each vertex |
+| | [Eigenvector](algorithms/centrality/Eigenvector.ipynb) | Compute Eigenvector for every vertex |
| Community | | |
| | [Louvain](community/Louvain.ipynb) and Leiden | Identify clusters in a graph using both the Louvain and Leiden algorithms |
| | [ECG](community/ECG.ipynb) | Identify clusters in a graph using the Ensemble Clustering for Graph |
@@ -51,10 +53,10 @@ Running the example in these notebooks requires:
* The latest version of RAPIDS with cuGraph.
* Download via Docker, Conda (See [__Getting Started__](https://rapids.ai/start.html))
-* cuGraph is dependent on the latest version of cuDF. Please install all components of RAPIDS
-* Python 3.7+
+* cuGraph is dependent on the latest version of cuDF. Please install all components of RAPIDS
+* Python 3.8+
* A system with an NVIDIA GPU: Pascal architecture or better
-* CUDA 11.0+
+* CUDA 11.4+
* NVIDIA driver 450.51+
@@ -73,7 +75,7 @@ Test Hardware
##### Copyright
-Copyright (c) 2019-2020, NVIDIA CORPORATION. All rights reserved.
+Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
diff --git a/notebooks/algorithms/README.md b/notebooks/algorithms/README.md
new file mode 100644
index 00000000000..14b2929efac
--- /dev/null
+++ b/notebooks/algorithms/README.md
@@ -0,0 +1,69 @@
+# cuGraph Algorithm Notebooks
+
+As all the algorithm Notebooks are updated and migrated to this area, they will show in this Readme. Until then they are available [here](../README.md)
+
+![GraphAnalyticsFigure](../img/GraphAnalyticsFigure.jpg)
+
+This repository contains a collection of Jupyter Notebooks that outline how to run various cuGraph analytics. The notebooks do not address a complete data science problem. The notebooks are simply examples of how to run the graph analytics. Manipulation of the data before or after the graph analytic is not covered here. Extended, more problem focused, notebooks are being created and available https://github.com/rapidsai/notebooks-extended
+
+## Summary
+
+| Folder | Notebook | Description |
+| --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
+| Centrality | | |
+| | [Centrality](centrality/Centrality.ipynb) | Compute and compare multiple (currently 5) centrality scores |
+| | [Katz](centrality/Katz.ipynb) | Compute the Katz centrality for every vertex |
+| | [Betweenness](centrality/Betweenness.ipynb) | Compute both Edge and Vertex Betweenness centrality |
+| | [Degree](centrality/Degree.ipynb) | Compute Degree Centraility for each vertex |
+| | [Eigenvector](centrality/Eigenvector.ipynb) | Compute Eigenvector for every vertex |
+
+
+
+[System Requirements](../README.md#requirements)
+
+| Author Credit | Date | Update | cuGraph Version | Test Hardware |
+| --------------|------------|------------------|-----------------|----------------|
+| Brad Rees | 04/19/2021 | created | 0.19 | GV100, CUDA 11.0
+| Don Acosta | 07/05/2022 | tested / updated | 22.08 nightly | DGX Tesla V100 CUDA 11.5
+
+### Copyright
+
+Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+
+
+
+
+![RAPIDS](img/rapids_logo.png)
diff --git a/notebooks/centrality/Betweenness.ipynb b/notebooks/algorithms/centrality/Betweenness.ipynb
similarity index 86%
rename from notebooks/centrality/Betweenness.ipynb
rename to notebooks/algorithms/centrality/Betweenness.ipynb
index 79e5383ed94..8860819b3ad 100644
--- a/notebooks/centrality/Betweenness.ipynb
+++ b/notebooks/algorithms/centrality/Betweenness.ipynb
@@ -8,16 +8,11 @@
"\n",
"In this notebook, we will compute the Betweenness centrality for both vertices and edges in our test database using cuGraph and NetworkX. The NetworkX and cuGraph processes will be interleaved so that each step can be compared.\n",
"\n",
- "Notebook Credits\n",
- "* Original Authors: Bradley Rees\n",
- "* Created: 04/24/2019\n",
- "* Last Edit: 08/16/2020\n",
- "\n",
- "RAPIDS Versions: 0.15 \n",
- "\n",
- "Test Hardware\n",
- "\n",
- "* GV100 32G, CUDA 10.2\n"
+ "| Author Credit | Date | Update | cuGraph Version | Test Hardware |\n",
+ "| --------------|------------|------------------|-----------------|----------------|\n",
+ "| Brad Rees | 04/24/2019 | created | 0.15 | GV100, CUDA 11.0\n",
+ "| Brad Rees | 08/16/2020 | tested / updated | 21.10 nightly | RTX 3090 CUDA 11.4\n",
+ "| Don Acosta | 07/05/2022 | tested / updated | 22.08 nightly | DGX Tesla V100 CUDA 11.5"
]
},
{
@@ -79,7 +74,7 @@
"metadata": {},
"source": [
"#### Some notes about vertex IDs...\n",
- "* The current version of cuGraph requires that vertex IDs be representable as 32-bit integers, meaning graphs currently can contain at most 2^32 unique vertex IDs. However, this limitation is being actively addressed and a version of cuGraph that accommodates more than 2^32 vertices will be available in the near future.\n",
+ "\n",
"* cuGraph will automatically renumber graphs to an internal format consisting of a contiguous series of integers starting from 0, and convert back to the original IDs when returning data to the caller. If the vertex IDs of the data are already a contiguous series of integers starting from 0, the auto-renumbering step can be skipped for faster graph creation times.\n",
" * To skip auto-renumbering, set the `renumber` boolean arg to `False` when calling the appropriate graph creation API (eg. `G.from_cudf_edgelist(gdf_r, source='src', destination='dst', renumber=False)`).\n",
" * For more advanced renumbering support, see the examples in `structure/renumber.ipynb` and `structure/renumber-2.ipynb`\n"
@@ -95,7 +90,7 @@
"Anthropological Research 33, 452-473 (1977).*\n",
"\n",
"\n",
- "![Karate Club](../img/zachary_black_lines.png)\n",
+ "\n",
"\n",
"\n",
"Because the test data has vertex IDs starting at 1, the auto-renumber feature of cuGraph (mentioned above) will be used so the starting vertex ID is zero for maximum efficiency. The resulting data will then be auto-unrenumbered, making the entire renumbering process transparent to users.\n"
@@ -143,7 +138,7 @@
"outputs": [],
"source": [
"# Define the path to the test data \n",
- "datafile='../data/karate-data.csv'"
+ "datafile='../../data/karate-data.csv'"
]
},
{
@@ -221,33 +216,6 @@
"Let's now look at the results"
]
},
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Find the most important vertex using the scores\n",
- "# This methods should only be used for small graph\n",
- "def print_top_scores(_df, txt) :\n",
- " m = _df['betweenness_centrality'].max()\n",
- " _d = _df.query('betweenness_centrality == @m')\n",
- " print(txt)\n",
- " print(_d)\n",
- " print()\n",
- " "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print_top_scores(vertex_bc, \"top vertex centrality scores\")\n",
- "print_top_scores(edge_bc, \"top edge centrality scores\")"
- ]
- },
{
"cell_type": "code",
"execution_count": null,
@@ -342,7 +310,7 @@
"metadata": {},
"source": [
"___\n",
- "Copyright (c) 2019-2020, NVIDIA CORPORATION.\n",
+ "Copyright (c) 2019-2022, NVIDIA CORPORATION.\n",
"\n",
"Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n",
"\n",
@@ -353,9 +321,9 @@
],
"metadata": {
"kernelspec": {
- "display_name": "cugraph_dev",
+ "display_name": "Python 3.8.13 ('cugraph_dev')",
"language": "python",
- "name": "cugraph_dev"
+ "name": "python3"
},
"language_info": {
"codemirror_mode": {
@@ -367,7 +335,12 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.6"
+ "version": "3.8.13"
+ },
+ "vscode": {
+ "interpreter": {
+ "hash": "cee8a395f2f0c5a5bcf513ae8b620111f4346eff6dc64e1ea99c951b2ec68604"
+ }
}
},
"nbformat": 4,
diff --git a/notebooks/algorithms/centrality/Centrality.ipynb b/notebooks/algorithms/centrality/Centrality.ipynb
new file mode 100644
index 00000000000..4a584a7fb87
--- /dev/null
+++ b/notebooks/algorithms/centrality/Centrality.ipynb
@@ -0,0 +1,470 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Centrality"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In this notebook, we will compute vertex centrality scores using the various cuGraph algorithms. We will then compare the similarities and differences.\n",
+ "\n",
+ "| Author Credit | Date | Update | cuGraph Version | Test Hardware |\n",
+ "| --------------|------------|------------------|-----------------|----------------|\n",
+ "| Brad Rees | 04/16/2021 | created | 0.19 | GV100, CUDA 11.0\n",
+ "| Brad Rees | 08/05/2021 | tested / updated | 21.10 nightly | RTX 3090 CUDA 11.4\n",
+ "| Don Acosta | 07/05/2022 | tested / updated | 22.08 nightly | DGX Tesla V100 CUDA 11.5\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Centrality is measure of how important, or central, a node or edge is within a graph. It is useful for identifying influencer in social networks, key routing nodes in communication/computer network infrastructures, \n",
+ "\n",
+ "The seminal paper on centrality is: Freeman, L. C. (1978). Centrality in social networks conceptual clarification. Social networks, 1(3), 215-239.\n",
+ "\n",
+ "__Degree Centrality__
\n",
+ "Degree centrality is based on the notion that whoever has the most connections must be important. \n",
+ "\n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "___Closeness centrality – coming soon___
\n",
+ "Closeness is a measure of the shortest path to every other node in the graph. A node that is close to every other node, can reach over other node in the fewest number of hops, means that it has greater influence on the network versus a node that is not close.\n",
+ "\n",
+ "__Betweenness Centrality__
\n",
+ "Betweenness is a measure of the number of shortest paths that cross through a node, or over an edge. A node with high betweenness means that it had a greater influence on the flow of information. \n",
+ "\n",
+ "Betweenness centrality of a node 𝑣 is the sum of the fraction of all-pairs shortest paths that pass through 𝑣\n",
+ "\n",
+ "\n",
+ " \n",
+ "\n",
+ "\n",
+ "To speedup runtime of betweenness centrailty, the metric can be computed on a limited number of nodes (randomly selected) and then used to estimate the other scores. For this example, the graphs are relatively small (under 5,000 nodes) so betweenness on every node will be computed.\n",
+ "\n",
+ "__Katz Centrality__
\n",
+ "Katz is a variant of degree centrality and of eigenvector centrality. \n",
+ "Katz centrality is a measure of the relative importance of a node within the graph based on measuring the influence across the total number of walks between vertex pairs.\n",
+ "\n",
+ "\n",
+ " \n",
+ "\n",
+ "\n",
+ "See:\n",
+ "* [Katz on Wikipedia](https://en.wikipedia.org/wiki/Katz_centrality) for more details on the algorithm.\n",
+ "* [Learn more about Katz Centrality](https://www.sci.unich.it/~francesc/teaching/network/katz.html)\n",
+ "\n",
+ "__Eigenvector Centrality__
\n",
+ "Eigenvectors can be thought of as the balancing points of a graph, or center of gravity of a 3D object. High centrality means that more of the graph is balanced around that node.\n",
+ "The eigenvector centrality for node i is the\n",
+ "i-th element of the vector x defined by the eigenvector equation.\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "Where M(v) is the adjacency list for the set of vertices(v) and λ is a constant.\n",
+ "\n",
+ "See:\n",
+ "* [Eigenvector Centrality on Wikipedia](https://en.wikipedia.org/wiki/Eigenvector_centrality) for more details on the algorithm.\n",
+ "* [Learn more about EigenVector Centrality](https://www.sci.unich.it/~francesc/teaching/network/eigenvector.html)\n",
+ "\n",
+ "\n",
+ "__PageRank__
\n",
+ "PageRank is classified as both a Link Analysis tool and a centrality measure. PageRank is based on the assumption that important nodes point (directed edge) to other important nodes. From a social network perspective, the question is who do you seek for an answer and then who does that person seek. PageRank is good when there is implied importance in the data, for example a citation network, web page linkages, or trust networks. Pagerank also dilutes the importance of nodes which have exceedingly high outward degree which is an improvement over Katz.\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "See:\n",
+ "* [Pagerank on Wikipedia](https://en.wikipedia.org/wiki/Pagerank) for more details on the algorithm and its commercial use.\n",
+ "* [Learn more about Pagerank Centrality](https://www.sci.unich.it/~francesc/teaching/network/pagerank.html) which is a centrality measure highly related to the Pagerank algorithm."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Test Data\n",
+ "We will be using the Zachary Karate club dataset \n",
+ "*W. W. Zachary, An information flow model for conflict and fission in small groups, Journal of\n",
+ "Anthropological Research 33, 452-473 (1977).*\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "Because the test data has vertex IDs starting at 1, the auto-renumber feature of cuGraph (mentioned above) will be used so the starting vertex ID is zero for maximum efficiency. The resulting data will then be auto-unrenumbered, making the entire renumbering process transparent to users."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Import the cugraph modules\n",
+ "import cugraph\n",
+ "import cudf"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "#import the networkX required modules\n",
+ "import numpy as np\n",
+ "import pandas as pd \n",
+ "from IPython.display import display_html "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Functions\n",
+ "using underscore variable names to avoid collisions. \n",
+ "non-underscore names are expected to be global names"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Compute Centrality\n",
+ "# the centrality calls are very straightforward with the graph being the primary argument\n",
+ "# we are using the default argument values for all centrality functions\n",
+ "\n",
+ "def compute_centrality(_graph) :\n",
+ " # Compute Degree Centrality\n",
+ " _d = cugraph.degree_centrality(_graph)\n",
+ " \n",
+ " # Compute the Betweenness Centrality\n",
+ " _b = cugraph.betweenness_centrality(_graph)\n",
+ "\n",
+ " # Compute Katz Centrality\n",
+ " _k = cugraph.katz_centrality(_graph)\n",
+ " \n",
+ " # Compute PageRank Centrality\n",
+ " _p = cugraph.pagerank(_graph)\n",
+ "\n",
+ " # Compute EigenVector Centrality\n",
+ " _e = cugraph.eigenvector_centrality(_graph, max_iter=1000, tol=1.0e-3)\n",
+ " \n",
+ " return _d, _b, _k, _p, _e"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Print function\n",
+ "def print_centrality(k,dc,bc,katz,pr, ev):\n",
+ "\n",
+ " dc_top = dc.sort_values(by='degree_centrality', ascending=False).head(k).to_pandas()\n",
+ " bc_top = bc.sort_values(by='betweenness_centrality', ascending=False).head(k).to_pandas()\n",
+ " katz_top = katz.sort_values(by='katz_centrality', ascending=False).head(k).to_pandas()\n",
+ " pr_top = pr.sort_values(by='pagerank', ascending=False).head(k).to_pandas()\n",
+ " ev_top = ev.sort_values(by='eigenvector_centrality', ascending=False).head(k).to_pandas()\n",
+ " \n",
+ " df1_styler = dc_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Degree').hide(axis='index')\n",
+ " df2_styler = bc_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Betweenness').hide(axis='index')\n",
+ " df3_styler = katz_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Katz').hide(axis='index')\n",
+ " df4_styler = pr_top.style.set_table_attributes(\"style='display:inline'\").set_caption('PageRank').hide(axis='index')\n",
+ " df5_styler = ev_top.style.set_table_attributes(\"style='display:inline'\").set_caption('EigenVector').hide(axis='index')\n",
+ "\n",
+ " display_html(df1_styler._repr_html_()+df2_styler._repr_html_()+df3_styler._repr_html_()+df4_styler._repr_html_()+df5_styler._repr_html_(), raw=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Read the data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Define the path to the test data \n",
+ "datafile='../../data/karate-data.csv'"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "cuGraph does not do any data reading or writing and is dependent on other tools for that, with cuDF being the preferred solution. \n",
+ "\n",
+ "The data file contains an edge list, which represents the connection of a vertex to another. The `source` to `destination` pairs is in what is known as Coordinate Format (COO). In this test case, the data is just two columns. However a third, `weight`, column is also possible"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gdf = cudf.read_csv(datafile, delimiter='\\t', names=['src', 'dst'], dtype=['int32', 'int32'] )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "it was that easy to load data"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Create a Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n",
+ "G = cugraph.Graph()\n",
+ "G.from_cudf_edgelist(gdf, source='src', destination='dst')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Compute Centrality"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dc, bc, katz, pr, ev = compute_centrality(G)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Results\n",
+ "Typically, analysts just look at the top 10% of results. Basically just those vertices that are the most central or important. \n",
+ "The karate data has 32 vertices, so let's round a little and look at the top 5 vertices"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print_centrality(5, dc, bc, katz, pr, ev)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### A Different Dataset\n",
+ "The Karate dataset is not that large or complex, which makes it a perfect test dataset since it is easy to visually verify results. Let's look at a larger dataset with a lot more edges"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Define the path to the test data \n",
+ "datafile='../../data/netscience.csv'\n",
+ "\n",
+ "gdf = cudf.read_csv(datafile, delimiter=' ', names=['src', 'dst', 'wt'], dtype=['int32', 'int32', 'float'] )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n",
+ "G = cugraph.Graph()\n",
+ "G.from_cudf_edgelist(gdf, source='src', destination='dst')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "(G.number_of_nodes(), G.number_of_edges())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dc, bc, katz, pr, ev = compute_centrality(G)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print_centrality(5, dc, bc, katz, pr, ev)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can now see a larger discrepancy between the centrality scores and which nodes rank highest.\n",
+ "Which centrality measure to use is left to the analyst to decide and does require insight into the difference algorithms and graph structure."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### And One More Dataset\n",
+ "Let's look at a Cyber dataset. The vertex ID are IP addresses"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Define the path to the test data \n",
+ "datafile='../../data/cyber.csv'\n",
+ "\n",
+ "gdf = cudf.read_csv(datafile, delimiter=',', names=['idx', 'src', 'dst'], dtype=['int32', 'str', 'str'] )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n",
+ "G = cugraph.Graph()\n",
+ "G.from_cudf_edgelist(gdf, source='src', destination='dst')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "(G.number_of_nodes(), G.number_of_edges())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dc, bc, katz, pr, ev = compute_centrality(G)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Here the results of all the measures can be seen side by side."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print_centrality(5, dc, bc, katz, pr, ev)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "There are differences in how each centrality measure ranks the nodes. In some cases, every algorithm returns similar results, and in others, the results are different. Understanding how the centrality measure is computed and what edge represent is key to selecting the right centrality metric."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "----\n",
+ "Copyright (c) 2019-2022, NVIDIA CORPORATION.\n",
+ "\n",
+ "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n",
+ "\n",
+ "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3.8.13 ('cugraph_dev')",
+ "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.13"
+ },
+ "vscode": {
+ "interpreter": {
+ "hash": "cee8a395f2f0c5a5bcf513ae8b620111f4346eff6dc64e1ea99c951b2ec68604"
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/notebooks/algorithms/centrality/Degree.ipynb b/notebooks/algorithms/centrality/Degree.ipynb
new file mode 100644
index 00000000000..bb3779d9980
--- /dev/null
+++ b/notebooks/algorithms/centrality/Degree.ipynb
@@ -0,0 +1,323 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Degree Centrality\n",
+ "\n",
+ "In this notebook, we will compute the degree centrality for vertices in our test database using cuGraph and NetworkX. The NetworkX and cuGraph processes will be interleaved so that each step can be compared.\n",
+ "\n",
+ "| Author Credit | Date | Update | cuGraph Version | Test Hardware |\n",
+ "| --------------|------------|------------------|-----------------|----------------|\n",
+ "| Don Acosta | 07/05/2022 | created | 22.08 nightly | DGX Tesla V100 CUDA 11.5"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Introduction\n",
+ "Degree centrality is the simplest measure of the relative importance based on counting the connections with each vertex. Vertices with the most connections are the most central by this measure.\n",
+ "\n",
+ "See [Degree Centrality on Wikipedia](https://en.wikipedia.org/wiki/Degree_centrality) for more details on the algorithm.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Degree centrality of a vertex 𝑣 is the sum of the edges incident on that node.\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To compute the Degree centrality scores for a graph in cuGraph we use:
\n",
+ "__df_v = cugraph.degree_centrality(G)__\n",
+ " G: cugraph.Graph object\n",
+ " \n",
+ "\n",
+ "Returns:\n",
+ "\n",
+ " df: a cudf.DataFrame object with two columns:\n",
+ " df['vertex']: The vertex identifier for the vertex\n",
+ " df['degree_centrality']: The degree centrality score for the vertex\n",
+ "\n",
+ "\n",
+ "### _NOTICE_\n",
+ "cuGraph does not currently support the ‘endpoints’ and ‘weight’ parameters as seen in the corresponding networkX call. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Some notes about vertex IDs...\n",
+ "* cuGraph will automatically renumber graphs to an internal format consisting of a contiguous series of integers starting from 0, and convert back to the original IDs when returning data to the caller. If the vertex IDs of the data are already a contiguous series of integers starting from 0, the auto-renumbering step can be skipped for faster graph creation times.\n",
+ " * To skip auto-renumbering, set the `renumber` boolean arg to `False` when calling the appropriate graph creation API (eg. `G.from_cudf_edgelist(gdf_r, source='src', destination='dst', renumber=False)`).\n",
+ " * For more advanced renumbering support, see the examples in `structure/renumber.ipynb` and `structure/renumber-2.ipynb`\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Test Data\n",
+ "We will be using the Zachary Karate club dataset \n",
+ "*W. W. Zachary, An information flow model for conflict and fission in small groups, Journal of\n",
+ "Anthropological Research 33, 452-473 (1977).*\n",
+ "\n",
+ "\n",
+ "![Karate Club](../../img/zachary_black_lines.png)\n",
+ "\n",
+ "\n",
+ "Because the test data has vertex IDs starting at 1, the auto-renumber feature of cuGraph (mentioned above) will be used so the starting vertex ID is zero for maximum efficiency. The resulting data will then be auto-unrenumbered, making the entire renumbering process transparent to users.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Prep"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Import needed libraries\n",
+ "import cugraph\n",
+ "import cudf"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# NetworkX libraries\n",
+ "import networkx as nx"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Some Prep"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Define the path to the test data \n",
+ "datafile='../../data/karate-data.csv'"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Read in the data - GPU\n",
+ "cuGraph depends on cuDF for data loading and the initial Dataframe creation\n",
+ "\n",
+ "The data file contains an edge list, which represents the connection of a vertex to another. The `source` to `destination` pairs is in what is known as Coordinate Format (COO). In this test case, the data is just two columns. However a third, `weight`, column is also possible"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gdf = cudf.read_csv(datafile, delimiter='\\t', names=['src', 'dst'], dtype=['int32', 'int32'] )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Create a Graph "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n",
+ "G = cugraph.Graph()\n",
+ "G.from_cudf_edgelist(gdf, source='src', destination='dst')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Call the Degree Centrality algorithm"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Call cugraph.degree_centrality \n",
+ "vertex_bc = cugraph.degree_centrality(G)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "_It was that easy!_ \n",
+ "\n",
+ "----\n",
+ "\n",
+ "Let's now look at the results"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Find the most important vertex using the scores\n",
+ "def print_top_scores(_df, txt) :\n",
+ " m = _df['degree_centrality'].max()\n",
+ " _d = _df.query('degree_centrality == @m')\n",
+ " print(txt)\n",
+ " print(_d)\n",
+ " print()\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print_top_scores(vertex_bc, \"top degree centrality score\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# let's sort the data and look at the top 5 vertices\n",
+ "vertex_bc.sort_values(by='degree_centrality', ascending=False).head(5)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Now compute using NetworkX"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Read the data, this also created a NetworkX Graph \n",
+ "file = open(datafile, 'rb')\n",
+ "Gnx = nx.read_edgelist(file)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dc_nx_vert = nx.degree_centrality(Gnx)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dc_nx_sv = sorted(((value, key) for (key,value) in dc_nx_vert.items()), reverse=True)\n",
+ "dc_nx_sv[:5]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As mentioned, the scores are different but the ranking is the same."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "___\n",
+ "Copyright (c) 2022, NVIDIA CORPORATION.\n",
+ "\n",
+ "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n",
+ "\n",
+ "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n",
+ "___"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3.8.13 ('cugraph_dev')",
+ "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.13"
+ },
+ "vscode": {
+ "interpreter": {
+ "hash": "cee8a395f2f0c5a5bcf513ae8b620111f4346eff6dc64e1ea99c951b2ec68604"
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/notebooks/algorithms/centrality/Eigenvector.ipynb b/notebooks/algorithms/centrality/Eigenvector.ipynb
new file mode 100644
index 00000000000..c7fc3506c89
--- /dev/null
+++ b/notebooks/algorithms/centrality/Eigenvector.ipynb
@@ -0,0 +1,340 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Eigenvector Centrality\n",
+ "\n",
+ "In this notebook, we will compute the Eigenvector centrality for vertice in our test database using cuGraph and NetworkX. The NetworkX and cuGraph processes will be interleaved so that each step can be compared.\n",
+ "\n",
+ "| Author Credit | Date | Update | cuGraph Version | Test Hardware |\n",
+ "| --------------|------------|------------------|-----------------|----------------|\n",
+ "| Don Acosta | 07/05/2022 | created | 22.08 nightly | DGX Tesla V100 CUDA 11.5"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Introduction\n",
+ "Eigenvector centrality computes the centrality for a vertex based on the\n",
+ "centrality of its neighbors. The Eigenvector of a node measures influence within a graph by taking into account a vertex's connections to other highly connected vertices.\n",
+ "\n",
+ "\n",
+ "See [Eigenvector Centrality on Wikipedia](https://en.wikipedia.org/wiki/Eigenvector_centrality) for more details on the algorithm.\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The eigenvector centrality for node i is the\n",
+ "i-th element of the vector x defined by the eigenvector equation.\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "Where M(v) is the adjacency list for the set of vertices(v) and λ is a constant.\n",
+ "\n",
+ "[Learn more about EigenVector Centrality](https://www.sci.unich.it/~francesc/teaching/network/eigenvector.html)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "To compute the Eigenvector centrality scores for a graph in cuGraph we use:
\n",
+ "__df = cugraph.eigenvector_centrality(G, max_iter=100, tol=1.0e-6, normalized=True)__\n",
+ "\n",
+ " G : cuGraph.Graph or networkx.Graph\n",
+ " max_iter : int, optional (default=100)\n",
+ " The maximum number of iterations before an answer is returned. This can\n",
+ " be used to limit the execution time and do an early exit before the\n",
+ " solver reaches the convergence tolerance.\n",
+ " tol : float, optional (default=1e-6)\n",
+ " Set the tolerance the approximation, this parameter should be a small\n",
+ " magnitude value.\n",
+ " The lower the tolerance the better the approximation. If this value is\n",
+ " 0.0f, cuGraph will use the default value which is 1.0e-6.\n",
+ " Setting too small a tolerance can lead to non-convergence due to\n",
+ " numerical roundoff. Usually values between 1e-2 and 1e-6 are\n",
+ " acceptable.\n",
+ " normalized : bool, optional, default=True\n",
+ " If True normalize the resulting eigenvector centrality values\n",
+ "\n",
+ " \n",
+ "\n",
+ "Returns:\n",
+ "\n",
+ " df : cudf.DataFrame or Dictionary if using NetworkX\n",
+ " GPU data frame containing two cudf.Series of size V: the vertex\n",
+ " identifiers and the corresponding eigenvector centrality values.\n",
+ " df['vertex'] : cudf.Series\n",
+ " Contains the vertex identifiers\n",
+ " df['eigenvector_centrality'] : cudf.Series\n",
+ " Contains the eigenvector centrality of vertices\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "#### Some notes about vertex IDs...\n",
+ "* cuGraph will automatically renumber graphs to an internal format consisting of a contiguous series of integers starting from 0, and convert back to the original IDs when returning data to the caller. If the vertex IDs of the data are already a contiguous series of integers starting from 0, the auto-renumbering step can be skipped for faster graph creation times.\n",
+ " * To skip auto-renumbering, set the `renumber` boolean arg to `False` when calling the appropriate graph creation API (eg. `G.from_cudf_edgelist(gdf_r, source='src', destination='dst', renumber=False)`).\n",
+ " * For more advanced renumbering support, see the examples in `structure/renumber.ipynb` and `structure/renumber-2.ipynb`\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Test Data\n",
+ "We will be using the Zachary Karate club dataset \n",
+ "*W. W. Zachary, An information flow model for conflict and fission in small groups, Journal of\n",
+ "Anthropological Research 33, 452-473 (1977).*\n",
+ "\n",
+ "\n",
+ "![Karate Club](../../img/zachary_black_lines.png)\n",
+ "\n",
+ "\n",
+ "Because the test data has vertex IDs starting at 1, the auto-renumber feature of cuGraph (mentioned above) will be used so the starting vertex ID is zero for maximum efficiency. The resulting data will then be auto-unrenumbered, making the entire renumbering process transparent to users.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Prep"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Import needed libraries\n",
+ "import cugraph\n",
+ "import cudf"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# NetworkX libraries\n",
+ "import networkx as nx\n",
+ "import pandas as pd"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Some Prep"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Define the path to the test data \n",
+ "datafile='../../data/karate-data.csv'"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Method to show most influencial nodes"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def topKVertices(eigen, col, k):\n",
+ " top = eigen.nlargest(n=k, columns=col)\n",
+ " top = top.sort_values(by=col, ascending=False)\n",
+ " return top\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Read in the data - GPU\n",
+ "cuGraph depends on cuDF for data loading and the initial Dataframe creation\n",
+ "\n",
+ "The data file contains an edge list, which represents the connection of a vertex to another. The `source` to `destination` pairs is in what is known as Coordinate Format (COO). In this test case, the data is just two columns. However a third, `weight`, column is also possible"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gdf = cudf.read_csv(datafile, delimiter='\\t', names=['src', 'dst'], dtype=['int32', 'int32'] )"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Create a Graph "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n",
+ "G = cugraph.Graph(directed=True)\n",
+ "G.from_cudf_edgelist(gdf, source='src', destination='dst')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Call the Eigenvector Centrality algorithm"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Call cugraph.eigenvector_centrality \n",
+ "k_df = cugraph.eigenvector_centrality(G, max_iter=1000)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "_It was that easy!_ \n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "---"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Now compute using NetworkX"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Read the data, this also created a NetworkX Graph \n",
+ "file = open(datafile, 'rb')\n",
+ "Gnx = nx.read_edgelist(file)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Compute and Display the top 5 eigenvector centrality values with their nodes in NetworkX."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "nx_evc = nx.eigenvector_centrality(Gnx)\n",
+ "evc_nx_sv = sorted(((value, key) for (key,value) in nx_evc.items()), reverse=True)\n",
+ "evc_nx_sv[:5]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Display the top 5 from the earlier cugraph-based computation."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "k_df = k_df.rename(columns={\"eigenvector_centrality\": \"cu_eigen\"}, copy=False)\n",
+ "k_df = k_df.sort_values(\"cu_eigen\").reset_index(drop=True)\n",
+ "topKVertices(k_df, \"cu_eigen\", 5)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "As mentioned, the scores are slightly different but the ranking is the same."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "___\n",
+ "Copyright (c) 2022, NVIDIA CORPORATION.\n",
+ "\n",
+ "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n",
+ "\n",
+ "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.\n",
+ "___"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3.8.13 ('cugraph_dev')",
+ "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.13"
+ },
+ "vscode": {
+ "interpreter": {
+ "hash": "cee8a395f2f0c5a5bcf513ae8b620111f4346eff6dc64e1ea99c951b2ec68604"
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/notebooks/centrality/Katz.ipynb b/notebooks/algorithms/centrality/Katz.ipynb
similarity index 88%
rename from notebooks/centrality/Katz.ipynb
rename to notebooks/algorithms/centrality/Katz.ipynb
index 6e2396efd47..f3537fe75e7 100755
--- a/notebooks/centrality/Katz.ipynb
+++ b/notebooks/algorithms/centrality/Katz.ipynb
@@ -8,16 +8,11 @@
"\n",
"In this notebook, we will compute the Katz centrality of each vertex in our test datase using both cuGraph and NetworkX. Additionally, NetworkX also contains a Numpy implementation that will used. The NetworkX and cuGraph processes will be interleaved so that each step can be compared.\n",
"\n",
- "Notebook Credits\n",
- "* Original Authors: Bradley Rees\n",
- "* Created: 10/15/2019\n",
- "* Last Edit: 08/16/2020\n",
- "\n",
- "RAPIDS Versions: 0.14 \n",
- "\n",
- "Test Hardware\n",
- "\n",
- "* GV100 32G, CUDA 10.2\n"
+ "| Author Credit | Date | Update | cuGraph Version | Test Hardware |\n",
+ "| --------------|------------|------------------|-----------------|----------------|\n",
+ "| Brad Rees | 10/15/2019 | created | 0.14 | GV100, CUDA 10.2\n",
+ "| Brad Rees | 08/16/2020 | tested / updated | 0.15.1 nightly | RTX 3090 CUDA 11.4\n",
+ "| Don Acosta | 07/05/2022 | tested / updated | 22.08 nightly | DGX Tesla V100 CUDA 11.5"
]
},
{
@@ -75,7 +70,6 @@
"metadata": {},
"source": [
"#### Some notes about vertex IDs...\n",
- "* The current version of cuGraph requires that vertex IDs be representable as 32-bit integers, meaning graphs currently can contain at most 2^32 unique vertex IDs. However, this limitation is being actively addressed and a version of cuGraph that accommodates more than 2^32 vertices will be available in the near future.\n",
"* cuGraph will automatically renumber graphs to an internal format consisting of a contiguous series of integers starting from 0, and convert back to the original IDs when returning data to the caller. If the vertex IDs of the data are already a contiguous series of integers starting from 0, the auto-renumbering step can be skipped for faster graph creation times.\n",
" * To skip auto-renumbering, set the `renumber` boolean arg to `False` when calling the appropriate graph creation API (eg. `G.from_cudf_edgelist(gdf_r, source='src', destination='dst', renumber=False)`).\n",
" * For more advanced renumbering support, see the examples in `structure/renumber.ipynb` and `structure/renumber-2.ipynb`\n"
@@ -91,7 +85,7 @@
"Anthropological Research 33, 452-473 (1977).*\n",
"\n",
"\n",
- "![Karate Club](../img/zachary_black_lines.png)\n",
+ "\n",
"\n",
"\n",
"Because the test data has vertex IDs starting at 1, the auto-renumber feature of cuGraph (mentioned above) will be used so the starting vertex ID is zero for maximum efficiency. The resulting data will then be auto-unrenumbered, making the entire renumbering process transparent to users.\n"
@@ -101,7 +95,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "### Prep"
+ "### Importing needed Libraries"
]
},
{
@@ -110,7 +104,7 @@
"metadata": {},
"outputs": [],
"source": [
- "# Import needed libraries\n",
+ "# Import rapids libraries\n",
"import cugraph\n",
"import cudf"
]
@@ -121,7 +115,7 @@
"metadata": {},
"outputs": [],
"source": [
- "# NetworkX libraries\n",
+ "# Import NetworkX libraries\n",
"import networkx as nx"
]
},
@@ -129,7 +123,10 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "### Some Prep"
+ "### parameters\n",
+ "max_iter determines the number of iterations the algorithm will run to seek convergence\n",
+ "tol is the maximum difference that indicates convergence.\n",
+ "The algorithm will fail if it fails to converge within the tolerance (tol) after max_iter number of runs. This is often due to dataset characteristics.\n"
]
},
{
@@ -150,7 +147,7 @@
"outputs": [],
"source": [
"# Define the path to the test data \n",
- "datafile='../data/karate-data.csv'"
+ "datafile='../../data/karate-data.csv'"
]
},
{
@@ -247,30 +244,6 @@
"Let's now look at the results"
]
},
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Find the most important vertex using the scores\n",
- "# This methods should only be used for small graph\n",
- "def find_top_scores(_df) :\n",
- " m = _df['katz_centrality'].max()\n",
- " return _df.query('katz_centrality >= @m')\n",
- " "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "top_df = find_top_scores(gdf_katz)\n",
- "top_df"
- ]
- },
{
"cell_type": "code",
"execution_count": null,
@@ -364,7 +337,7 @@
"metadata": {},
"source": [
"___\n",
- "Copyright (c) 2019-2020, NVIDIA CORPORATION.\n",
+ "Copyright (c) 2019-2022, NVIDIA CORPORATION.\n",
"\n",
"Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n",
"\n",
@@ -375,9 +348,9 @@
],
"metadata": {
"kernelspec": {
- "display_name": "cugraph_dev",
+ "display_name": "Python 3.8.13 ('cugraph_dev')",
"language": "python",
- "name": "cugraph_dev"
+ "name": "python3"
},
"language_info": {
"codemirror_mode": {
@@ -389,7 +362,12 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.7.6"
+ "version": "3.8.13"
+ },
+ "vscode": {
+ "interpreter": {
+ "hash": "cee8a395f2f0c5a5bcf513ae8b620111f4346eff6dc64e1ea99c951b2ec68604"
+ }
}
},
"nbformat": 4,
diff --git a/notebooks/algorithms/centrality/README.md b/notebooks/algorithms/centrality/README.md
new file mode 100644
index 00000000000..608dd239029
--- /dev/null
+++ b/notebooks/algorithms/centrality/README.md
@@ -0,0 +1,41 @@
+
+# cuGraph Centrality Notebooks
+
+
+
+cuGraph Centrality notebooks contain a collection of Jupyter Notebooks that demonstrate algorithms to identify and quantify importance of vertices to the structure of the graph. In the diagram above, the highlighted vertices are highly important and are likely answers to questions like:
+
+* Which vertices have the highest degree (most direct links) ?
+* Which vertices are on the most efficient paths through the graph?
+* Which vertices connect the most important vertices to each other?
+
+But which vertices are most important? The answer depends on which measure/algorithm is run. Manipulation of the data before or after the graph analytic is not covered here. Extended, more problem focused, notebooks are being created and available https://github.com/rapidsai/notebooks-extended
+
+## Summary
+
+|Algorithm |Notebooks Containing |Description |
+| --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
+|Degree Centrality| [Centrality](centrality/Centrality.ipynb), [Degree](centrality/Degree.ipynb) |Measure based on counting direct connections for each vertex|
+|Betweenness Centrality| [Centrality](centrality/Centrality.ipynb), [Betweenness](centrality/Betweenness.ipynb) |Number of shortest paths through the vertex|
+|Eigenvector Centrality|[Centrality](centrality/Centrality.ipynb), [Eigenvector](centrality/Eigenvector.ipynb)|Measure of connectivity to other important vertices (which also have high connectivity) often referred to as the influence measure of a vertex|
+|Katz Centrality|[Centrality](centrality/Centrality.ipynb), [Katz](centrality/Katz.ipynb) |Similar to Eigenvector but has tweaks to measure more weakly connected graph |
+|Pagerank|[Centrality](centrality/Centrality.ipynb), [Pagerank](../../link_analysis/Pagerank.ipynb) |Classified as both a link analysis and centrality measure by quantifying incoming links from central vertices. |
+
+[System Requirements](../../README.md#requirements)
+
+| Author Credit | Date | Update | cuGraph Version | Test Hardware |
+| --------------|------------|------------------|-----------------|----------------|
+| Brad Rees | 04/19/2021 | created | 0.19 | GV100, CUDA 11.0
+| Don Acosta | 07/05/2022 | tested / updated | 22.08 nightly | DGX Tesla V100 CUDA 11.5
+
+## Copyright
+
+Copyright (c) 2019-2022, NVIDIA CORPORATION. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
+
+![RAPIDS](../../img/rapids_logo.png)
diff --git a/notebooks/centrality/Centrality.ipynb b/notebooks/centrality/Centrality.ipynb
deleted file mode 100644
index 8272d208cf5..00000000000
--- a/notebooks/centrality/Centrality.ipynb
+++ /dev/null
@@ -1,761 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Centrality"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "In this notebook, we will compute vertex centrality scores using the various cuGraph algorithms. We will then compare the similarities and differences.\n",
- "\n",
- "| Author Credit | Date | Update | cuGraph Version | Test Hardware |\n",
- "| --------------|------------|------------------|-----------------|----------------|\n",
- "| Brad Rees | 04/16/2021 | created | 0.19 | GV100, CUDA 11.0\n",
- "| | 08/05/2021 | tested / updated | 21.10 nightly | RTX 3090 CUDA 11.4\n",
- "\n",
- " "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Centrality is measure of how important, or central, a node or edge is within a graph. It is useful for identifying influencer in social networks, key routing nodes in communication/computer network infrastructures, \n",
- "\n",
- "The seminal paper on centrality is: Freeman, L. C. (1978). Centrality in social networks conceptual clarification. Social networks, 1(3), 215-239.\n",
- "\n",
- "\n",
- "__Degree centrality__ – _done but needs an API_
\n",
- "Degree centrality is based on the notion that whoever has the most connections must be important. \n",
- "\n",
- "\n",
- " Cd(v) = degree(v)\n",
- "\n",
- "\n",
- "cuGraph currently does not have a Degree Centrality function call. However, since Degree Centrality is just the degree of a node, we can use _G.degree()_ function.\n",
- "Degree Centrality for a Directed graph can be further divided in _indegree centrality_ and _outdegree centrality_ and can be obtained using _G.degrees()_\n",
- "\n",
- "\n",
- "___Closeness centrality – coming soon___
\n",
- "Closeness is a measure of the shortest path to every other node in the graph. A node that is close to every other node, can reach over other node in the fewest number of hops, means that it has greater influence on the network versus a node that is not close.\n",
- "\n",
- "__Betweenness Centrality__
\n",
- "Betweenness is a measure of the number of shortest paths that cross through a node, or over an edge. A node with high betweenness means that it had a greater influence on the flow of information. \n",
- "\n",
- "Betweenness centrality of a node 𝑣 is the sum of the fraction of all-pairs shortest paths that pass through 𝑣\n",
- "\n",
- "\n",
- " \n",
- "\n",
- "\n",
- "To speedup runtime of betweenness centrailty, the metric can be computed on a limited number of nodes (randomly selected) and then used to estimate the other scores. For this example, the graphs are relatively small (under 5,000 nodes) so betweenness on every node will be computed.\n",
- "\n",
- "___Eigenvector Centrality - coming soon___
\n",
- "Eigenvectors can be thought of as the balancing points of a graph, or center of gravity of a 3D object. High centrality means that more of the graph is balanced around that node.\n",
- "\n",
- "__Katz Centrality__
\n",
- "Katz is a variant of degree centrality and of eigenvector centrality. \n",
- "Katz centrality is a measure of the relative importance of a node within the graph based on measuring the influence across the total number of walks between vertex pairs. \n",
- "\n",
- "\n",
- " \n",
- "\n",
- "\n",
- "See:\n",
- "* [Katz on Wikipedia](https://en.wikipedia.org/wiki/Katz_centrality) for more details on the algorithm.\n",
- "* https://www.sci.unich.it/~francesc/teaching/network/katz.html\n",
- "\n",
- "__PageRank__
\n",
- "PageRank is classified as both a Link Analysis tool and a centrality measure. PageRank is based on the assumption that important nodes point (directed edge) to other important nodes. From a social network perspective, the question is who do you seek for an answer and then who does that person seek. PageRank is good when there is implied importance in the data, for example a citation network, web page linkages, or trust networks. \n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Test Data\n",
- "We will be using the Zachary Karate club dataset \n",
- "*W. W. Zachary, An information flow model for conflict and fission in small groups, Journal of\n",
- "Anthropological Research 33, 452-473 (1977).*\n",
- "\n",
- "\n",
- "![Karate Club](../img/zachary_black_lines.png)\n",
- "\n",
- "\n",
- "Because the test data has vertex IDs starting at 1, the auto-renumber feature of cuGraph (mentioned above) will be used so the starting vertex ID is zero for maximum efficiency. The resulting data will then be auto-unrenumbered, making the entire renumbering process transparent to users."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Import the modules\n",
- "import cugraph\n",
- "import cudf"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "import numpy as np\n",
- "import pandas as pd \n",
- "from IPython.display import display_html "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Functions\n",
- "using underscore variable names to avoid collisions. \n",
- "non-underscore names are expected to be global names"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Compute Centrality\n",
- "# the centrality calls are very straightforward with the graph being the primary argument\n",
- "# we are using the default argument values for all centrality functions\n",
- "\n",
- "def compute_centrality(_graph) :\n",
- " # Compute Degree Centrality\n",
- " _d = _graph.degree()\n",
- " \n",
- " # Compute the Betweenness Centrality\n",
- " _b = cugraph.betweenness_centrality(_graph)\n",
- "\n",
- " # Compute Katz Centrality\n",
- " _k = cugraph.katz_centrality(_graph)\n",
- " \n",
- " # Compute PageRank Centrality\n",
- " _p = cugraph.pagerank(_graph)\n",
- " \n",
- " return _d, _b, _k, _p"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Print function\n",
- "# being lazy and requiring that the dataframe names are not changed versus passing them in\n",
- "def print_centrality(_n):\n",
- " dc_top = dc.sort_values(by='degree', ascending=False).head(_n).to_pandas()\n",
- " bc_top = bc.sort_values(by='betweenness_centrality', ascending=False).head(_n).to_pandas()\n",
- " katz_top = katz.sort_values(by='katz_centrality', ascending=False).head(_n).to_pandas()\n",
- " pr_top = pr.sort_values(by='pagerank', ascending=False).head(_n).to_pandas()\n",
- " \n",
- " df1_styler = dc_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Degree').hide_index()\n",
- " df2_styler = bc_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Betweenness').hide_index()\n",
- " df3_styler = katz_top.style.set_table_attributes(\"style='display:inline'\").set_caption('Katz').hide_index()\n",
- " df4_styler = pr_top.style.set_table_attributes(\"style='display:inline'\").set_caption('PageRank').hide_index()\n",
- "\n",
- " display_html(df1_styler._repr_html_()+df2_styler._repr_html_()+df3_styler._repr_html_()+df4_styler._repr_html_(), raw=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Read the data"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Define the path to the test data \n",
- "datafile='../data/karate-data.csv'"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "cuGraph does not do any data reading or writing and is dependent on other tools for that, with cuDF being the preferred solution. \n",
- "\n",
- "The data file contains an edge list, which represents the connection of a vertex to another. The `source` to `destination` pairs is in what is known as Coordinate Format (COO). In this test case, the data is just two columns. However a third, `weight`, column is also possible"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [],
- "source": [
- "gdf = cudf.read_csv(datafile, delimiter='\\t', names=['src', 'dst'], dtype=['int32', 'int32'] )"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "it was that easy to load data"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Create a Graph"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [],
- "source": [
- "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n",
- "G = cugraph.Graph()\n",
- "G.from_cudf_edgelist(gdf, source='src', destination='dst')"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Compute Centrality"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [],
- "source": [
- "dc, bc, katz, pr = compute_centrality(G)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Results\n",
- "Typically, analysts just look at the top 10% of results. Basically just those vertices that are the most central or important. \n",
- "The karate data has 32 vertices, so let's round a little and look at the top 5 vertices"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "Degree degree | vertex |
\n",
- " \n",
- " 34 | \n",
- " 34 | \n",
- "
\n",
- " \n",
- " 32 | \n",
- " 1 | \n",
- "
\n",
- " \n",
- " 24 | \n",
- " 33 | \n",
- "
\n",
- " \n",
- " 20 | \n",
- " 3 | \n",
- "
\n",
- " \n",
- " 18 | \n",
- " 2 | \n",
- "
\n",
- "
Betweenness betweenness_centrality | vertex |
\n",
- " \n",
- " 0.437635 | \n",
- " 1 | \n",
- "
\n",
- " \n",
- " 0.304075 | \n",
- " 34 | \n",
- "
\n",
- " \n",
- " 0.145247 | \n",
- " 33 | \n",
- "
\n",
- " \n",
- " 0.143657 | \n",
- " 3 | \n",
- "
\n",
- " \n",
- " 0.138276 | \n",
- " 32 | \n",
- "
\n",
- "
Katz katz_centrality | vertex |
\n",
- " \n",
- " 0.436256 | \n",
- " 34 | \n",
- "
\n",
- " \n",
- " 0.418408 | \n",
- " 1 | \n",
- "
\n",
- " \n",
- " 0.328650 | \n",
- " 33 | \n",
- "
\n",
- " \n",
- " 0.296005 | \n",
- " 3 | \n",
- "
\n",
- " \n",
- " 0.256614 | \n",
- " 2 | \n",
- "
\n",
- "
PageRank pagerank | vertex |
\n",
- " \n",
- " 0.100917 | \n",
- " 34 | \n",
- "
\n",
- " \n",
- " 0.096999 | \n",
- " 1 | \n",
- "
\n",
- " \n",
- " 0.071692 | \n",
- " 33 | \n",
- "
\n",
- " \n",
- " 0.057078 | \n",
- " 3 | \n",
- "
\n",
- " \n",
- " 0.052877 | \n",
- " 2 | \n",
- "
\n",
- "
"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "print_centrality(5)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### A Different Dataset\n",
- "The Karate dataset is not that large or complex, which makes it a perfect test dataset since it is easy to visually verify results. Let's look at a larger dataset with a lot more edges"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Define the path to the test data \n",
- "datafile='../data/netscience.csv'\n",
- "\n",
- "gdf = cudf.read_csv(datafile, delimiter=' ', names=['src', 'dst', 'wt'], dtype=['int32', 'int32', 'float'] )"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {},
- "outputs": [],
- "source": [
- "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n",
- "G = cugraph.Graph()\n",
- "G.from_cudf_edgelist(gdf, source='src', destination='dst')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 12,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "(1461, 2742)"
- ]
- },
- "execution_count": 12,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "(G.number_of_nodes(), G.number_of_edges())"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 13,
- "metadata": {},
- "outputs": [],
- "source": [
- "dc, bc, katz, pr = compute_centrality(G)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 14,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "Degree degree | vertex |
\n",
- " \n",
- " 68 | \n",
- " 33 | \n",
- "
\n",
- " \n",
- " 54 | \n",
- " 34 | \n",
- "
\n",
- " \n",
- " 54 | \n",
- " 78 | \n",
- "
\n",
- " \n",
- " 42 | \n",
- " 54 | \n",
- "
\n",
- " \n",
- " 40 | \n",
- " 294 | \n",
- "
\n",
- "
Betweenness betweenness_centrality | vertex |
\n",
- " \n",
- " 0.026572 | \n",
- " 78 | \n",
- "
\n",
- " \n",
- " 0.023090 | \n",
- " 150 | \n",
- "
\n",
- " \n",
- " 0.019135 | \n",
- " 516 | \n",
- "
\n",
- " \n",
- " 0.018074 | \n",
- " 281 | \n",
- "
\n",
- " \n",
- " 0.017088 | \n",
- " 216 | \n",
- "
\n",
- "
Katz katz_centrality | vertex |
\n",
- " \n",
- " 0.158191 | \n",
- " 1429 | \n",
- "
\n",
- " \n",
- " 0.158191 | \n",
- " 1430 | \n",
- "
\n",
- " \n",
- " 0.158191 | \n",
- " 1431 | \n",
- "
\n",
- " \n",
- " 0.154591 | \n",
- " 645 | \n",
- "
\n",
- " \n",
- " 0.154591 | \n",
- " 1432 | \n",
- "
\n",
- "
PageRank pagerank | vertex |
\n",
- " \n",
- " 0.004183 | \n",
- " 78 | \n",
- "
\n",
- " \n",
- " 0.003771 | \n",
- " 33 | \n",
- "
\n",
- " \n",
- " 0.002800 | \n",
- " 34 | \n",
- "
\n",
- " \n",
- " 0.002387 | \n",
- " 281 | \n",
- "
\n",
- " \n",
- " 0.002373 | \n",
- " 294 | \n",
- "
\n",
- "
"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "print_centrality(5)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can now see a larger discrepancy between the centrality scores and which nodes rank highest.\n",
- "Which centrality measure to use is left to the analyst to decide and does require insight into the difference algorithms and graph structure."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### And One More Dataset\n",
- "Let's look at a Cyber dataset. The vertex ID are IP addresses"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 15,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Define the path to the test data \n",
- "datafile='../data/cyber.csv'\n",
- "\n",
- "gdf = cudf.read_csv(datafile, delimiter=',', names=['idx', 'src', 'dst'], dtype=['int32', 'str', 'str'] )"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 16,
- "metadata": {},
- "outputs": [],
- "source": [
- "# create a Graph using the source (src) and destination (dst) vertex pairs from the Dataframe \n",
- "G = cugraph.Graph()\n",
- "G.from_cudf_edgelist(gdf, source='src', destination='dst')"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 17,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "(54, 174)"
- ]
- },
- "execution_count": 17,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "(G.number_of_nodes(), G.number_of_edges())"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "metadata": {},
- "outputs": [],
- "source": [
- "dc, bc, katz, pr = compute_centrality(G)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 19,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/html": [
- "Degree degree | vertex |
\n",
- " \n",
- " 26 | \n",
- " 175.45.176.1 | \n",
- "
\n",
- " \n",
- " 26 | \n",
- " 175.45.176.3 | \n",
- "
\n",
- " \n",
- " 26 | \n",
- " 175.45.176.0 | \n",
- "
\n",
- " \n",
- " 26 | \n",
- " 175.45.176.2 | \n",
- "
\n",
- " \n",
- " 22 | \n",
- " 149.171.126.6 | \n",
- "
\n",
- "
Betweenness betweenness_centrality | vertex |
\n",
- " \n",
- " 0.112091 | \n",
- " 10.40.85.1 | \n",
- "
\n",
- " \n",
- " 0.052250 | \n",
- " 224.0.0.5 | \n",
- "
\n",
- " \n",
- " 0.048621 | \n",
- " 10.40.182.1 | \n",
- "
\n",
- " \n",
- " 0.033745 | \n",
- " 175.45.176.1 | \n",
- "
\n",
- " \n",
- " 0.033745 | \n",
- " 175.45.176.3 | \n",
- "
\n",
- "
Katz katz_centrality | vertex |
\n",
- " \n",
- " 0.213361 | \n",
- " 149.171.126.6 | \n",
- "
\n",
- " \n",
- " 0.206289 | \n",
- " 59.166.0.4 | \n",
- "
\n",
- " \n",
- " 0.206289 | \n",
- " 59.166.0.1 | \n",
- "
\n",
- " \n",
- " 0.206289 | \n",
- " 59.166.0.5 | \n",
- "
\n",
- " \n",
- " 0.206289 | \n",
- " 59.166.0.2 | \n",
- "
\n",
- "
PageRank pagerank | vertex |
\n",
- " \n",
- " 0.038591 | \n",
- " 175.45.176.1 | \n",
- "
\n",
- " \n",
- " 0.038591 | \n",
- " 175.45.176.3 | \n",
- "
\n",
- " \n",
- " 0.038591 | \n",
- " 175.45.176.0 | \n",
- "
\n",
- " \n",
- " 0.038591 | \n",
- " 175.45.176.2 | \n",
- "
\n",
- " \n",
- " 0.028716 | \n",
- " 10.40.85.1 | \n",
- "
\n",
- "
"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "print_centrality(5)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "There are differences in how each centrality measure ranks the nodes. In some cases, every algorithm returns similar results, and in others, the results are different. Understanding how the centrality measure is computed and what edge represent is key to selecting the right centrality metric."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "----\n",
- "Copyright (c) 2019-2021, NVIDIA CORPORATION.\n",
- "\n",
- "Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0\n",
- "\n",
- "Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License."
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "cugraph_dev",
- "language": "python",
- "name": "cugraph_dev"
- },
- "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.10"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
diff --git a/notebooks/img/zachary_black_lines.png b/notebooks/img/zachary_black_lines.png
index 937137e9649e293211979f3a3228461199546631..3681dbfb90251666d7763bc6b991f953e88b359a 100644
GIT binary patch
literal 65807
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
Gu^y#h
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