Skip to content

Instantly share code, notes, and snippets.

@jtrive84
Created April 26, 2024 20:38
Show Gist options
  • Save jtrive84/26100fd58eef298cbc64828e43bddd70 to your computer and use it in GitHub Desktop.
Save jtrive84/26100fd58eef298cbc64828e43bddd70 to your computer and use it in GitHub Desktop.
Betweenness Centrality
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## CuGraph Demonstration"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Betweenness Centrality\n",
"Betweenness centrality is a measure of the relative importance based on measuring the number of shortest paths that pass through each vertex or over each edge. High betweenness centrality vertices have a greater number of path cross through the vertex. Likewise, high centrality edges have more shortest paths that pass over the edge.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Betweenness centrality of a node 𝑣 is the sum of the fraction of all-pairs shortest paths that pass through 𝑣\n",
"\n",
"<img src=\"https://latex.codecogs.com/png.latex?c_B(v)&space;=\\sum_{s,t&space;\\in&space;V}&space;\\frac{\\sigma(s,&space;t|v)}{\\sigma(s,&space;t)}\" title=\"c_B(v) =\\sum_{s,t \\in V} \\frac{\\sigma(s, t|v)}{\\sigma(s, t)}\" />\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>source</th>\n",
" <th>target</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>1265124607</td>\n",
" <td>1265124605</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>1055951276</td>\n",
" <td>1055951278</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>1192198750</td>\n",
" <td>1192199485</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>1201913687</td>\n",
" <td>1201913688</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>28235035</td>\n",
" <td>28235080</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" source target\n",
"0 1265124607 1265124605\n",
"1 1055951276 1055951278\n",
"2 1192198750 1192199485\n",
"3 1201913687 1201913688\n",
"4 28235035 28235080"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"\n",
"import os\n",
"import timeit\n",
"import warnings\n",
"\n",
"import matplotlib as mpl\n",
"import matplotlib.pyplot as plt\n",
"import networkx as nx\n",
"import numpy as np\n",
"import pandas as pd\n",
"import cugraph\n",
"import cudf\n",
"\n",
"warnings.simplefilter(action='ignore', category=FutureWarning)\n",
"\n",
"edge_list_path = \"/home/rapids/data/charlotte-edge-list-link-as-node.csv\"\n",
"\n",
"dfedges = pd.read_csv(edge_list_path, low_memory=False)\n",
"dfedges = (\n",
" dfedges[dfedges.source != dfedges.target]\n",
" .reset_index(drop=True)\n",
" )\n",
"\n",
"dfedges.head()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### NetworkX vs. CuGraph Betweenness Centrality for different node counts"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"\n",
"num_nodes = [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 15000, 25000, 50000]\n",
"\n",
"rng = np.random.default_rng()\n",
"\n",
"all_nodes = dfedges.source.unique()\n",
"\n",
"results = []\n",
"\n",
"for n in num_nodes:\n",
" sample_nodes = rng.choice(all_nodes, size=n)\n",
" dfedges2 = dfedges[(dfedges.source.isin(sample_nodes)) | (dfedges.target.isin(sample_nodes))]\n",
" Gs = nx.from_edgelist(dfedges2[[\"source\", \"target\"]].values)\n",
" Gs.remove_edges_from(nx.selfloop_edges(Gs))\n",
" bc1 = timeit.timeit(lambda: nx.betweenness_centrality(Gs), number=1)\n",
" bc2 = timeit.timeit(lambda: cugraph.betweenness_centrality(Gs), number=1)\n",
" speedup = bc1 / bc2\n",
" results.append({\"n\": n, \"networkx\": bc1, \"cugraph\": bc2, \"speedup\": speedup})\n",
" print(f\"n={n}: bc1={bc1:.3f}, bc2={bc2:.3f}, speedup={speedup:.1f}\")\n",
" \n",
"dfresults = pd.DataFrame.from_dict(results)\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>n</th>\n",
" <th>networkx</th>\n",
" <th>cugraph</th>\n",
" <th>speedup</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>10</td>\n",
" <td>0.001118</td>\n",
" <td>0.503808</td>\n",
" <td>0.002219</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>50</td>\n",
" <td>0.013554</td>\n",
" <td>0.211838</td>\n",
" <td>0.063984</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>100</td>\n",
" <td>0.051152</td>\n",
" <td>0.415733</td>\n",
" <td>0.123040</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>250</td>\n",
" <td>0.265840</td>\n",
" <td>0.959617</td>\n",
" <td>0.277027</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>500</td>\n",
" <td>0.975226</td>\n",
" <td>1.877632</td>\n",
" <td>0.519391</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>1000</td>\n",
" <td>4.027643</td>\n",
" <td>3.849096</td>\n",
" <td>1.046387</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>2500</td>\n",
" <td>26.979091</td>\n",
" <td>9.624603</td>\n",
" <td>2.803138</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>5000</td>\n",
" <td>109.695305</td>\n",
" <td>20.227116</td>\n",
" <td>5.423181</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>10000</td>\n",
" <td>383.497137</td>\n",
" <td>42.656435</td>\n",
" <td>8.990370</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>15000</td>\n",
" <td>910.768422</td>\n",
" <td>76.623885</td>\n",
" <td>11.886221</td>\n",
" </tr>\n",
" <tr>\n",
" <th>10</th>\n",
" <td>25000</td>\n",
" <td>2396.076872</td>\n",
" <td>139.674776</td>\n",
" <td>17.154686</td>\n",
" </tr>\n",
" <tr>\n",
" <th>11</th>\n",
" <td>50000</td>\n",
" <td>6079.395899</td>\n",
" <td>283.494384</td>\n",
" <td>21.444502</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" n networkx cugraph speedup\n",
"0 10 0.001118 0.503808 0.002219\n",
"1 50 0.013554 0.211838 0.063984\n",
"2 100 0.051152 0.415733 0.123040\n",
"3 250 0.265840 0.959617 0.277027\n",
"4 500 0.975226 1.877632 0.519391\n",
"5 1000 4.027643 3.849096 1.046387\n",
"6 2500 26.979091 9.624603 2.803138\n",
"7 5000 109.695305 20.227116 5.423181\n",
"8 10000 383.497137 42.656435 8.990370\n",
"9 15000 910.768422 76.623885 11.886221\n",
"10 25000 2396.076872 139.674776 17.154686\n",
"11 50000 6079.395899 283.494384 21.444502"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"dfresults.head(20)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Runtime: NetworkX vs. CuGraph"
]
},
{
"cell_type": "code",
"execution_count": 53,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"image/png": "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lCljXcgtXToN6q9//WumdX777beqVq2aqlatKj8/P/Xu3fu2jik/Ilh4sNjYWA0ePFjPPvusSpYsqXLlymn06NFW+x9//KG+ffuqbNmyCggIUMuWLbV+/XpJl05FGjNmjNavXy+HwyGHw6GPPvpIQ4cO1X333WetY/z48XI4HPr66z+P3lSrVk2TJk2SdOkeEi+88IIqVKggHx8fRUdH65tvvrHmTUlJkcPh0KxZsxQbGytfX19Nnz49y1iOHj2qhg0bKi4uTufOndMLL7yg8uXL6+jRo9Y8cXFxat68uVwuV649hwAAIH/o0KGDNm7cqF9++UVvvPGGTp8+rYiICOsUpoiICK1du1Y7d+7U5MmTM/2oy8yZMzNdXyFJXbt21bZt27Rt2za9/vrr/AjMDeAZ8nBTp05V0aJFlZCQoH/961964YUXtHjxYhljdM899+jgwYOaP3++1q5dq3r16qlVq1Y6duyYunbtqqFDh6pGjRo6cOCADhw4oK5duyo2NlY//PCDtfO+fPlylS5dWsuXL5ckHTx4UNu2bVOLFi0kSW+99ZZef/11vfbaa9qwYYPatWunuLg4bd++PVM///a3v2nw4MHasmWL2rVrl6lt7969uuuuuxQZGanPP/9cvr6+eu655xQWFqbHH39ckjRx4kR9//33mjZtGm98AACgokWLatasWe7uRoHCNRYernbt2nr++eclXUrq77zzjr799lsVKlRIGzdu1OHDh60byLz22muaO3euZs+erb59+6pYsWLy8vLKdGPA5s2bKzU1VT///LPq1aunH374QcOGDbN+23np0qUKDAxUZGSktc6//e1v6tatm6RLhyGXLl2q8ePH691337XW+9RTT6lz585Z+r9t2za1adNG999/v9566y3rFxwKFSqk6dOnKzo6WsOHD9e///1vTZ48mbtqAwAAuAnBwsPVrl070+OgoCAdPnxYa9eu1alTp1SqVKlM7WfPntXOnTuvur7ixYsrOjpay5Ytk7e3t5xOp/r166fnn39eqampWrZsmXW04uTJk9q/f7+aNm2aaR1Nmza1TrnKEBMTk2VbZ8+eVbNmzfTggw/qrbfeytIeHh6u1157Tf369VPXrl3VvXv3az8ZAAAAuGUIFh7O29s702OHwyGXyyWXy6WgoCAtW7YsyzLX+5nX2NhYLVu2TIULF1aLFi10xx13qEaNGlq5cqWWLVump556Kss2L2eMyTItu9+G9vHxUevWrfX111/rmWeeUYUKFbLM8/3336tQoUJKSUlRenp6prtsAgAA4PbhZPQCql69ejp48KC8vLxUpUqVTP+VLl1aklS4cGFdvHgxy7IZ11l89913io2NlSS1aNFCn3zySabrKwICAlS+fHmtWLEi0/I//vijqlevft0+Op1OTZs2TfXr11fLli21f//+TO0zZ87U559/rmXLlmnPnj365z//mZOnAgAAALmAYFFAtW7dWo0bN1Z8fLwWLlyolJQU/fjjj/rHP/6hxMRESVJYWJiSk5OVlJSkI0eO6Pz585L+vM7iq6++soJFbGyspk+frjJlyigqKsrazjPPPKNx48Zp5syZ+vXXXzV8+HAlJSXpySefvKF+FipUSP/9739Vp04dtWzZUgcPHpR06YLuJ554QuPGjVOzZs300Ucf6eWXX9bq1atz8VkCAADAjeK8kQLK4XBo/vz5eu6559SrVy/9/vvvKleunJo3b67AwEBJUpcuXfT555/r7rvv1okTJ/Thhx+qZ8+eKl68uOrWravdu3dbIeKuu+6Sy+WyjlZkGDx4sE6ePKmhQ4fq8OHDioqK0pdffqmIiIgb7quXl5dmzJihrl27qmXLllq6dKl69uyphg0batCgQZKkNm3aaNCgQXr44YeVlJSkYsWK5dIzBQAA8rLbfcPiDG81dstm8zSHybi3uRucP39eQ4cO1cKFC1W4cGHVrVs323sYTJkyRa+88opcLpdatWqlCRMmWOfSz5s3T8OGDVN6errq1KmjqVOnWjuVCQkJ6tevn86cOaOKFStq+vTpmW7fnuHkyZMqXry4/vjjDwUEBGRqO3PmjLZs2aLq1avLz8/vFjwLuF0KQi3T0tI0f/58dezYMcv1NcjfqK3noraei9reHu4LFukForbX2k++kltPhRo+fLicTqe2bdumTZs26dVXX80yT3JyskaOHKkVK1Zox44dOnjwoKZMmSJJOnXqlHr37q25c+dqx44dCgoK0tixYyVdukC4e/fuGj9+vLZt26YOHTpoyJAht3V8AAAAQEHhtmBx+vRpffjhh3rppZesXwjK7mjC7Nmz1alTJwUGBsrhcKh///6aMWOGJGnBggWKiYmx7pkwYMAAqy0xMVE+Pj7WNQD9+vXT3LlzlZaWdhtGBwAAABQsbrvGYufOnSpVqpRefPFFLVmyREWKFNHo0aPVqlWrTPPt3r07003PwsLCtHv37qu27du3Ty6XK0ubv7+//P39deDAAYWEhGTbp7S0tCzBgyDiebKrs6fIGJenjq8go7aei9p6Lmrr+QpCbW9mjG4LFmlpafrtt98UFRWlV155RevXr1fr1q21efNmlSlTJtO8l9/z4MpLQq68H8K12q53OcmiRYuynHvv7e1tXcwMz7By5UqP/0OwePFid3cBtwi19VzU1nNR21vNfb9FVBBqe+bMmRue122VCA0NldPptO6WXKdOHVWqVEmbNm2yTl+SpJCQEKWkpFiPd+3aZR1xCAkJ0XfffWe1paSkKDg4WE6nM8tyqampSk1NzfZ0qwxt27bN9uLtHTt22Bgp8pqmTZt69MXbixcvVps2bTz+YrKChtp6Lmrruajt7fHkqkVu23ZBqO3JkydveF63BYvSpUurVatWWrhwoTp27Khdu3YpOTlZ1apV04gRIxQcHKxBgwapS5cuatasmUaNGqWyZctq4sSJ6tatmySpffv2GjhwoLZu3arIyEhNmDDBaqtfv77OnTunZcuWKTY2VpMmTVJ8fPw1i+/t7Z2l3dNfLAVRdnX2NAVhjAUVtfVc1NZzUVvPVRBqezPjc+t9LCZOnKhevXrpb3/7mwoVKqTJkycrKChIGzZsUP369SVJ4eHhGjNmjJo2bSqXy6WWLVuqd+/eki5dN/HBBx8oPj5e6enpqlWrlqZOnSrp0l2bp0+frv79++vs2bMKDg7O9qdsAQAAANjn1mARHh6uZcuWZZrmcrl05MgRde7c2ZrWp08f9enTJ9t1xMXFKS4uLtu2xo0ba/369bnWXwAAAADZy3N33nY6nUpISHB3NwAAAADcBLfeIA+4lmXLlsnhcOjEiRPu7goAAACuI88dschXRhe/zdv74/ZuDwAAALhBHLFArvL0+0MAAAAgewQLD+dyuTRu3DhVqVJFPj4+CgkJ0dixY7M9zSgpKUkOhyPT/T/ef/99VaxYUX5+furUqZPeeOMNlShRwmofPXq0oqOj9Z///Efh4eHy8fGRMUbffPONmjVrphIlSqhUqVK69957tXPnTmu5lJQUORwOffLJJ2rSpIl8fX1Vo0aNLBfzS9LatWsVExMjPz8/NWnSRL/++usteKYAAABgB8HCw40YMULjxo3TyJEjtXnzZv3vf/+74TuJr1y5Uv3799eTTz6ppKQktWnTRmPHjs0y344dOzRr1ix99tlnSkpKkiSdPn1aQ4YM0U8//aRvv/1WTqdTnTp1ksvlyrTsM888o6FDh+rnn39WkyZNFBcXp6NHj2aa57nnntPrr7+uxMREeXl5qVevXjl7MgAAAHDLcI2FB0tNTdVbb72ld955R48++qgkqXLlymrWrFm2Rwau9O9//1sdOnTQsGHDJElVq1bVjz/+qHnz5mWa78KFC5o2bZrKlCljTevSpUumeaZMmaKyZctq8+bNqlmzpjU94yaIkvTee+/pm2++0ZQpU/Tss89a84wdO1YtWrSQJA0fPlz33HOPzp07J19f35t4NgAAAHArccTCg23ZskXnz59Xq1atcrT8r7/+qoYNG2aaduVjSQoNDc0UKiRp586deuihhxQeHq6AgABVqlRJkrR79+5M8zVu3Nj6t5eXl2JiYrRly5ZM89SuXdv6d1BQkCTp8OHDORgRAAAAbhWOWHiwIkWKXLXN6byUKY0x1rQrL7w2xsjhcGSZdqWiRYtmmXbfffepYsWKev/991W+fHm5XC7VrFlTFy5cuG6/r9zm5beSz2i78pQqAAAAuBdHLDxYRESEihQpom+//TZLW8YRhgMHDljTMq6PyBAZGak1a9ZkmpaYmHjd7R49elRbtmzRP/7xD7Vq1UrVq1fX8ePHs5139erV1r/T09O1du1aRUZGXncbAAAAyFs4YuHBfH199be//U3PPvusChcurKZNm+r333/Xpk2b1KNHD1WsWFGjR4/Wiy++qO3bt+v111/PtPz//d//qXnz5nrjjTd033336bvvvtOCBQuyHFG40h133KFSpUpp8uTJCgoK0u7duzV8+PBs53333XcVERGh6tWr680339Tx48e5OBsAACAf4oiFhxs5cqSGDh2qUaNGqXr16uratasOHz4sb29vzZgxQ1u3blWdOnU0btw4vfjii5mWbdq0qSZOnKg33nhDderU0TfffKOnn376uhdNO51OffLJJ1q7dq1q1qypp59+Wq+++mq2877yyisaN26c6tSpox9++EFffPGFSpcunWvjBwAAwO3BEQs78sGdsJ1Op5577jk999xzWdqaNm2qDRs2ZJp25TUUffr0UZ8+fTI9rlKlivV49OjRGj16dJZ1t27d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Og32448/1n333efm3gLZI1gAeRjfVCGnzpw5o9DQUA0bNkzSpdPv6tSpoxo1auiFF15wc+9g15dffqlWrVqpaNGi1rRVq1apXLlyWX5RCHlTu3bt9Je//EXz589XhQoV9NNPP1113nHjxun5559X5cqVVaZMGetCbiCv4RqLfGLPnj165JFHdPjwYXl5eWnkyJFq37697rrrLmue5ORkjRkzRk899ZT7Oopcdfk3Vffee68+/vhj9erVy93dQj4wduxYNWrUyHr8xRdfKCAgQBcvXlSzZs103333qW7dum7sIeyYNWuWevTokWVa165d3dQj3KyFCxdetS02NlaxsbHW44iICK1du/Y29AqwhyMW+YSXl5fGjx+vzZs3a8mSJXr66afldDo5BJ5PZfdN1eOPP67GjRtrw4YNqlChgubMmSPp1nxTtWfPHsXGxioqKkq1a9fWp59+Kklas2aNatSooSpVqvCtdj62fft2bd26VR07drSmBQQESJIuXLigCxcucEpdPnby5EmtXLkyU32NMfr888/1l7/8xY09A1DQccQinwgKCrJOgSlbtqxKliypY8eOWYfBb9Uh8E6dOmnZsmVq1aqV9ctEa9as0WOPPabz58+rR48eGjVqVK5usyDI7puqBg0aZDvvrfimKiOoRkdH6/Dhw6pXr546duyogQMHasaMGYqKilLjxo3VuXNn1axZM1e3jVtv2LBhevXVV/Xjjz9mmt6kSRNt3LhRAwYMUHR0tHs6B9u++OILtWvXTr6+vta0FStWKCQkRBUrVnRjzwAUdByxyIcSExPlcrkyfYDcqkPg2d1vIWPnc+vWrfrqq6/0yy+/5Pp2cWsFBQVZO5YZQfXIkSNKT09X7dq15eXlpYceeoibMOVDX3zxhapWraqqVatmafvxxx+1f/9+JSUl8b7Nx7L7e89pUADyAo5Y5DNHjx5Vjx499MEHH1jTMg6Br1y5Mte3d+X9Fvbv32/tfEqydj75Vjv/ygiqv//+u4KDg63pFSpU0PLly93YM+TE6tWr9cknn+jTTz/VqVOnlJaWpoCAAOvIor+/v1q2bKkFCxbwvs2H/vjjD61Zs0afffaZNc3lcmnOnDnXvPgXAG4HjljkI+fPn1enTp00YsQINWnSxJp+Ow+B79+/P8vOJ/dVyL8ygurkyZP5aVsP8fLLL2vPnj1KSUnR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pbZ5X3afr9Q3/8vu85W/y7mDYJHPvfHGG5oxY4YWL16sEiVKWNNHjBihd955R5J04MABuVwuSZde4PPmzVPdunUlSe3bt9dPP/2krVu3SpImTJigbt26SZLq16+vc+fOadmyZZKkSZMmKT4+Xt7e3rdpdAXX6dOndeLECevxjBkzrJpdXtsuXbpozpw5OnTokIwxmjhxolU/apu3/eUvf9G7774rSfrpp5908OBBNWvWTJL0zjvvaMSIEZJyXsfL7+Mj6ar38UHuSk9P16FDh6zHn332mQIDA62dFmqbf2T3+Up9879rfb7ydzkX3O6LOpB79uzZYySZ8PBwU6dOHVOnTh3TsGFDY4wxHTt2NJ9++qkx5tKvWURFRZnatWubqKgo8/zzz1u/dGGMMV988YWpVq2aqVy5somPjzd//PGH1fbjjz+a2rVrm4iICBMbG2v27t17ewdZQO3cudNER0ebWrVqmZo1a5q4uDjr4q7La2uMMZMnTzaVK1c2lSpVMr179850ATa1db8BAwaY4OBgU6hQIRMYGGhdAHrw4EHTpk0bU6VKFRMVFWWWLVuWaZlXX33VepzTOm7dutXceeedJiIiwtSvX9/88ssvt2HEBUd2tT116pSpX7++qVmzpqldu7Zp2bKlSUpKyrQMtc37rvb5Sn3zv2t9vvJ32T6Pv0FeQeRyudS4cWOtWrVKTicHpTwJtS0YWrRooXnz5snf39/dXUEuo7aejfp6Lmp7YwgWAAAAAGzjK08AAAAAthEsAAAAANhGsAAAAABgG8ECAAAAgG0ECwAAAAC2ESwAAAAA2EawAADkWaNHj1Z0dLS7uwEAuAEECwAAAAC2ESwAAAAA2EawAADkitjYWA0ePFjPPvusSpYsqXLlymn06NFW++7du3X//ferWLFiCggI0F//+lcdOnQo0zpeeeUVBQYGyt/fX71799a5c+eybOfDDz9U9erV5evrq8jISE2YMMFqu3DhggYNGqSgoCD5+voqLCxML7/88i0bMwDgTwQLAECumTp1qooWLaqEhAT961//0gsvvKDFixfLGKP4+HgdO3ZMy5cv1+LFi7Vz50517drVWnbWrFl6/vnnNXbsWCUmJiooKChTaJCk999/X88995zGjh2rLVu26KWXXtLIkSM1depUSdLbb7+tL7/8UrNmzdKvv/6q6dOnKyws7HY+BQBQYDmMMcbdnQAA5H+xsbG6ePGifvjhB2taw4YN1bJlS7Vq1UodOnRQcnKyKlasKEnavHmzatSooTVr1qhBgwZq0qSJ6tSpo/fee89a/s4779S5c+eUlJQkSQoJCdG4ceP04IMPWvO8+OKLmj9/vn788UcNHjxYmzZt0pIlS+RwOG7PwAEAkjhiAQDIRbVr1870OCgoSIcPH9aWLVtUsWJFK1RIUlRUlEqUKKEtW7ZIkrZs2aLGjRtnWv7yx7///rv27Nmj3r17q1ixYtZ/L774onbu3ClJ6tmzp5KSklStWjUNHjxYixYtulVDBQBcwcvdHQAAeA5vb+9Mjx0Oh1wul4wx2R5BuNr07LhcLkmXTodq1KhRprZChQpJkurVq6fk5GQtWLBAS5Ys0V//+le1bt1as2fPzslwAAA3gSMWAIBbLioqSrt379aePXusaZs3b9Yff/yh6tWrS5KqV6+u1atXZ1ru8seBgYEKDg7Wb7/9pipVqmT6r1KlStZ8AQEB6tq1q95//33NnDlTn332mY4dO3aLRwgA4IgFAOCWa926tWrXrq3u3btr/PjxSk9P14ABA9SiRQvFxMRIkp588kk9+uijiomJUbNmzfTf//5XmzZtUnh4uLWe0aNHa/DgwQoICFCHDh10/vx5JSYm6vjx4xoyZIjefPNNBQUFKTo6Wk6nU59++qnKlSunEiVKuGnkAFBwcMQCAHDLORwOzZ07V3fccYeaN2+u1q1bKzw8XDNnzrTm6dq1q0aNGqW//e1vql+/vnbt2qUnnngi03oef/xxffDBB/roo49Uq1YttWjRQh999JF1xKJYsWIaN26cYmJi1KBBA6WkpGj+/PlyOvm4A4BbjV+FAgAAAGAbX+EAAAAAsI1gAQAAAMA2ggUAAAAA2wgWAAAAAGwjWAAAAACwjWABAAAAwDaCBQAAAADbCBYAAAAAbCNYAAAAALCNYAEAAADANoIFAAAAANsIFgAAAABs+3+Tii9c0RAeYgAAAABJRU5ErkJggg==",
"text/plain": [
"<Figure size 800x550 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"\n",
"\n",
"def add_value_labels(ax, spacing=3, annotate_font=6, rotation=0, color=\"#000000\", weight=\"normal\"):\n",
" for rect in ax.patches:\n",
" y_value = rect.get_height()\n",
" x_value = rect.get_x() + rect.get_width() / 2\n",
" label = \"{0:,.0f}\".format(y_value)\n",
" ax.annotate(\n",
" label, (x_value, y_value), xytext=(0, spacing), textcoords=\"offset points\", \n",
" ha=\"center\", va=\"bottom\", fontsize=annotate_font, rotation=rotation, color=color, \n",
" weight=weight\n",
" )\n",
"\n",
"\n",
"dfr = dfresults[dfresults[\"n\"] > 1000].reset_index(drop=True)\n",
"dfr = dfr[[\"n\", \"networkx\", \"cugraph\"]].sort_values(\"n\", ascending=True)\n",
"dfr[\"n\"] = dfr[\"n\"].map(lambda v: f\"{v:,}\")\n",
"\n",
"fig, ax = plt.subplots(1, 1, figsize=(8, 5.5), tight_layout=True) \n",
"dfr.plot.bar(ax=ax)\n",
"ax.set_title(\"Betweenness Centrality: NetworkX vs. CuGraph\", fontsize=10, weight=\"bold\")\n",
"ax.set_xticklabels(dfr[\"n\"].values, rotation=0)\n",
"ax.set_xlabel(\"nodes\", size=10)\n",
"ax.set_ylabel(\"seconds\", size=10)\n",
"ax.yaxis.set_major_formatter(mpl.ticker.StrMethodFormatter(\"{x:,.0f}\"))\n",
"ax.tick_params(axis=\"x\", which=\"major\", direction='in', labelsize=8)\n",
"ax.tick_params(axis=\"x\", which=\"minor\", direction='in', labelsize=8)\n",
"ax.tick_params(axis=\"y\", which=\"major\", direction='in', labelsize=8)\n",
"ax.tick_params(axis=\"y\", which=\"minor\", direction='in', labelsize=8)\n",
"ax.xaxis.set_ticks_position(\"none\")\n",
"ax.yaxis.set_ticks_position(\"none\")\n",
"ax.legend(loc=\"best\", fancybox=True, framealpha=1, fontsize=\"medium\")\n",
"ax.grid(True) \n",
"ax.set_axisbelow(True) \n",
"# ax.bar_label(ax.containers[0], fmt=\"%.0f\")\n",
"# ax.bar_label(ax.containers[1], fmt=\"%.0f\")\n",
"add_value_labels(ax, annotate_font=7)\n",
"plt.show()\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Speedup using CuGraph vs. NetworkX"
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"image/png": "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t3LlSsXFxSkmJkZNmjTRoUOHbms/mZHRfpzS4sWLFRQUZL3vcXFxunDhgjXe2/q+0X58PW99r7Pib627/Xnq2Vk36jmj38fX87ae3f07dD1v6zsrjqm8omeDm1qyZInZt2+fKV26tNm4caM1/JFHHjFDhw41xhizatUqExUVZVwul4eqzHrX95vMl/p2573997//bXr16mWMMWbXrl0mLCzMnDhxIrtLd8uFCxfMt99+a5KSkowxxrz33numWbNmxhhjGjVqZL755pt05/Pmno1xb1/2lp5vtA+ntGvXLhMREWEOHz5skpKSTOvWrc2ECROMMcacOXPGFC9e3GzZssUYY0zfvn3NoEGDjDHGJCUlmXLlyplFixYZY4wZPXq06dKly+1v6iYy2o9TWrRokalVq1a6y/DGvjN6j5N583tt92+tnf4aN25sJk6caIwx5quvvjL16tW7DR2mdaOeM/p9nJI39uzu36GUvLFvu8dU3tIzweIWXL9TBAcHm6NHj1qva9eubb2pvuBGPwS+2PetvLexsbFm1apV1riOHTtaP7DeZvXq1aZcuXLGmIx/oXt7z+7sy97Wc0YHnaNGjTJ9+vSxXn/77bemUaNGxhhjpk2bZlq2bGmN27RpkyldurQx5tofudjYWGvc6dOnTd68ec3ly5ezvgEbUu7HKWUULLyx78wEC194r939W+tuf0eOHDEFChSwDuaSkpJMWFiYSUxMzNrGMuBusPDmnpNl9u9QSt7Yt91jKm/pOcdeCpXTHT9+XElJSSpWrJg1LDo6Wnv37vVgVVmve/fuqlq1qh577DH99ddfftH3zXrcu3evSpcune44b/Puu++qdevW1uuBAweqatWq6ty5s3bt2mUN94Web3Vf9oWek2XUS3rjDhw4oKSkpDTjQkJCFBISkiMuC0rp+v04pa1bt6pmzZqqXbu2xo0bZw331r6v34+v52vv9a38zXG3v4yeneVJN/p9nJIv9JzZv0MpeWvfdo6pvKVngoUN13+9rcmZ98G7benSpfrtt9+0bt06FSlSRL169ZLk+31LN+8x5Xhv7f/111/X9u3bNXz4cEnSp59+qi1btuj333/XPffco1atWqWa3pt7dndf9uaer5dRLxl9VXdO/3m/fj9OqWbNmtq/f7/WrVunWbNmacKECZo2bZo13tv6vtF+fD1fe69vpS53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",
"text/plain": [
"<Figure size 800x550 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"\n",
"dfr = dfresults[[\"n\", \"speedup\"]].sort_values(\"n\", ascending=True)\n",
"dfr[\"n\"] = dfr[\"n\"].map(lambda v: f\"{v:,}\")\n",
"\n",
"fig, ax = plt.subplots(1, 1, figsize=(8, 5.5), tight_layout=True) \n",
"ax.set_title(\"Speedup using CuGraph vs. NetworkX\", fontsize=10, weight=\"bold\")\n",
"dfr.plot.bar(color=\"#7400ff\", xlabel=\"nodes\", ylabel=\"speedup\", legend=False, ax=ax)\n",
"ax.set_xticklabels(dfr[\"n\"].values, rotation=0, fontsize=7)\n",
"ax.yaxis.set_major_formatter(mpl.ticker.StrMethodFormatter(\"{x:,.0f}\"))\n",
"ax.tick_params(axis=\"x\", which=\"major\", direction='in', labelsize=8)\n",
"ax.tick_params(axis=\"x\", which=\"minor\", direction='in', labelsize=8)\n",
"ax.tick_params(axis=\"y\", which=\"major\", direction='in', labelsize=8)\n",
"ax.tick_params(axis=\"y\", which=\"minor\", direction='in', labelsize=8)\n",
"ax.xaxis.set_ticks_position(\"none\")\n",
"ax.yaxis.set_ticks_position(\"none\")\n",
"ax.grid(True) \n",
"ax.set_axisbelow(True) \n",
"ax.bar_label(ax.containers[0], fmt=\"%.0f\")\n",
"plt.show()\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Betweenness centrality: Full Charlotte network"
]
},
{
"cell_type": "code",
"execution_count": 55,
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Gn.number_of_nodes: 205,932\n",
"Gn.number_of_edges: 376,667\n",
"nx.is_connected(G): True\n",
"Betweenness centrality for Charlotte: 30,888.688321590424 seconds.\n"
]
}
],
"source": [
"\n",
"import time\n",
"\n",
"G0 = nx.from_edgelist(dfedges[[\"source\", \"target\"]].values)\n",
"G0.remove_edges_from(nx.selfloop_edges(G0))\n",
"ccs = sorted(nx.connected_components(G0), key=len, reverse=True)\n",
"G = G0.subgraph(ccs[0])\n",
"\n",
"print(f\"Gn.number_of_nodes: {G.number_of_nodes():,.0f}\")\n",
"print(f\"Gn.number_of_edges: {G.number_of_edges():,.0f}\")\n",
"print(f\"nx.is_connected(G): {nx.is_connected(G)}\")\n",
"\n",
"# Betweenness centrality: Full Charlotte network.\n",
"t_init = time.time()\n",
"bc = cugraph.betweenness_centrality(G)\n",
"t_total = time.time() - t_init\n",
"\n",
"print(f\"Betweenness centrality for Charlotte: {t_total:,} seconds.\")\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": 63,
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Speedup vs. NetworkX parallel bc (4 cores): 10.5\n",
"Speedup vs. NetworkX single-core bc : 21.0\n"
]
}
],
"source": [
"\n",
"\n",
"# Speedup vs. NetworkX parallel and serial implementations.\n",
"print(f\"Speedup vs. NetworkX parallel bc (4 cores): {90 * 60 * 60 / 30889:,.1f}\")\n",
"print(f\"Speedup vs. NetworkX single-core bc : {180 * 60 * 60 / 30889:,.1f}\")\n"
]
}
],
"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.10.13"
},
"vscode": {
"interpreter": {
"hash": "f708a36acfaef0acf74ccd43dfb58100269bf08fb79032a1e0a6f35bd9856f51"
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment