{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "971f5210-cade-4db5-be11-e6311a825e32",
   "metadata": {},
   "source": [
    "# Basic Graph Theory. Methods of Graph Representation in Python.\n",
    "Compiled by [cyterat](https://github.com/cyterat)<br>\n",
    "<sub>* <span style=\"color:#FFAC1C\">Most information in this notebook can be found on YouTube: [David Amos: Graph Theory With Python](https://www.youtube.com/playlist?list=PLLIPpKeh9v3ZFEHvNd5xqUrCkqLgXnekL)</sub><br>\n",
    "<sub>* <span style=\"color:#FFAC1C\">Visuals in the **Graph Theory (basics)** section were made by [cyterat](https://github.com/cyterat) using [draw.io](https://www.drawio.com/)</sub><br>\n",
    "<sub>* <span style=\"color:#FFAC1C\">Gif in the **(PyVis) FrankenGraph** section was made by [cyterat](https://github.com/cyterat) using [screentogif](https://www.screentogif.com/)</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "477e7576-07df-4104-a538-b64cecdfd55b",
   "metadata": {},
   "source": [
    "Things worth looking into:\n",
    "* YouTube playlist [David Amos: Graph Theory With Python](https://www.youtube.com/playlist?list=PLLIPpKeh9v3ZFEHvNd5xqUrCkqLgXnekL)\n",
    "* Book [Intro to Graph Theory, Richard J. Trudeau](https://www.amazon.com/Introduction-Graph-Theory-Dover-Mathematics/dp/0486678709)\n",
    "* Brilliant's [Graph Theory](https://brilliant.org/wiki/graph-theory/) (_Königsberg Bridge Problem_ is here as well)\n",
    "* PyVis library graphs examples https://visjs.github.io/vis-network/examples/\n",
    "* https://maelfabien.github.io/machinelearning/graph_1/#basic-graph-notions"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "79c23633-0988-498f-a9f2-2e5a5786eda1",
   "metadata": {},
   "source": [
    "## Graph Theory (basics)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aa61620f-ef97-4dcd-9c8c-91a3f7a65228",
   "metadata": {},
   "source": [
    "**$G = (V, E)$**\n",
    "* **$G$** - graph\n",
    "* **$V$** - vertex set (nodes)\n",
    "* **$E$** - edge set (lines)<br>\n",
    "![graph](images/graph.png)<br>\n",
    "In the graph above:<br>\n",
    "**$V$** = **{** 0, 1, 2, 3 **}** - 4 vertices (nodes)<br>\n",
    "**$E$** = **{** ( 0, 0 ), ( 0, 1 ), ( 0, 3 ), ( 1, 3 ), ( 1, 2 ), ( 2, 3 ) **}** - 6 edges (lines)<br>\n",
    "\\*_loop counts as one edge_"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aeb4a323-f532-4968-b98b-b9f132b6b379",
   "metadata": {},
   "source": [
    "There are multiple ways of graphical representation of the vertex and edge sets from the previous graph. All variants below are correct, and represent the same graph.<br>\n",
    "![graph](images/graphs.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b81b37dd-b565-477e-9f5b-749a0ac37107",
   "metadata": {},
   "source": [
    "A **multigraph** can have several edges (lines), often called **parallel**, connecting the same vertices (nodes).\n",
    "\n",
    "![multigraph-graph](images/multi-graphs.png)<br>\n",
    "In a graph above:<br>\n",
    "**$V$** = **{** 0, 1, 2, 3 **}** - 4 vertices (nodes)<br>\n",
    "**$E$** = **{** ( 0, 1 ), ( 0, 1 ), ( 0, 3 ), ( 1, 2 ), ( 2, 3 ), ( 2, 3 ) **}** - 6 edges (lines)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42560c8a-91f0-4959-bd3c-27f5c32351de",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7ab3fa2-7722-4e18-839c-604fb39fe073",
   "metadata": {},
   "source": [
    "### Orientation"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3bf71f93-36b1-427f-909b-5634c502ace4",
   "metadata": {},
   "source": [
    "Graphs serve as a great tool to model relationships between objects. Such models can have a \"direction\" in their relationship, called **orientation** in a graph theory. This concept might be better understood by looking at social networks.<br>\n",
    "![social-networks-graphs](images/social-networks-graphs.png)<br>\n",
    "The examples above represent 2 types of graphs:\n",
    "* **Undirected** (_Facebook_) - edge represents relationship between nodes. (i.e. no arrows)\n",
    "    * _Luna and Mike are friends, therefore if Luna has Mike in a her friendlist then Mike will automatically have Luna in his friendlist._\n",
    "* **Directed** (_Instagram_) - edge represents relationship between the node at the start of the edge and a node at the end of the edge. Not the other way around. (i.e. with arrows)\n",
    "    * _Mike is a follower of a Luna, however Luna does not follow Mike._"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2a74a3e7-ca05-4f24-94c4-97f1f53bde85",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7f4daf5b-fa4b-47d4-9183-03e9b4e10226",
   "metadata": {},
   "source": [
    "### Degree\n",
    "The vertices (nodes) of a graph have a property called **degree** (valency), denoted as **deg(v)**. It is basically a number of edges (lines) connected (incident) to a vertex (node).<br>\n",
    "\n",
    "The degree sequence of a graph is a list of all degrees of a graph, e.g. $(3,\\ 3,\\ 1,\\ 1,\\ 1,\\ 1)$.<br> These degrees can be written in any order. In general however the degree sequence is written from max to min (non-increasing order).\n",
    "\n",
    "A degree sequence with negative numbers **cannot** exist, since nodes cannot have negative degrees, e.g. $(-1,\\ 2,\\ 3,-3)$.<br>\n",
    "A degree sequence with 0 **cannot** exist, e.g. $(3,\\ 2,\\ 3,\\ 0)$.\n",
    "\n",
    "In most cases, a single degree sequence can represent multiple graphs, which may not look alike."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c83b7e1e-da51-426d-9b7c-a86ef424a14d",
   "metadata": {},
   "source": [
    "Can you tell if a sequence is graphic, i.e. if it can be represented using a graph?<br>Yes, here is a simple rule:\n",
    "> **The Handshaking Lemma**: The sum of the degrees of a graph is even.<br>\n",
    "\n",
    "$(3,\\ 3,\\ 3,\\ 3,\\ 3)$ --- is this a graph?\n",
    "\n",
    "$3 \\times 5 = 15$ , not an even number, so it is not a graphic sequence.\n",
    "\n",
    "**The Handshaking Lemma is not a theorem**, there is not enough information taken into account to make a strong conclusion.<br>\n",
    "![graphs-handshaking-fail](images/graphs-handshaking-fail.png)<br>\n",
    "For example, let's look at a degree sequence $(2,\\ 2)$ which represents the graph above:<br>\n",
    "$2 + 2 = 4$ ,  the result is an even number, however it **is not graphical**, beacuse it tries to show 2 connected nodes, with 1 edge each, not connected to anything.<br>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "996328ff-efdc-4a15-bacb-2339dad71c97",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "409d0a5e-2acc-42c8-9c31-32d9ea179d96",
   "metadata": {},
   "source": [
    "#### Degree (Undirected Graph)\n",
    "![graphs-degree](images/graphs-degree.png)<br>\n",
    "<sub>*Note: the example was reduced to essentials, thats why some other nodes which should be connected to the ones displayed are excluded.</sub>\n",
    "\n",
    "In the example above: \n",
    "* vertex CP-0 has 3 edges, therefore $deg($CP-0$) = 3$\n",
    "* vertex CP-1 has 2 edges and a loop which contributes 2 to the vertex's degree, therefore $deg($CP-1$) = 4$\n",
    "\n",
    "Some other concepts worth mentioning are:\n",
    "* Min Degree $\\delta$ -- the smallest degree among all nodes in a graph;\n",
    "* Max Degree $\\Delta$ -- the largest degree among all nodes in a graph;"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c74704eb-9b62-4d7d-8fae-3f5bb964f71c",
   "metadata": {},
   "source": [
    "#### Degree (Directed Graph)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc77870c-e5c8-4980-abad-f2f1955cd099",
   "metadata": {},
   "source": [
    "The concept of orientation in this type of graphs introduces 2 new things: **indegree** and **outdegree**.\n",
    "* $deg^-(v)$ - indegree -- number of edge \"heads\" connected to a vertex (node);\n",
    "* $deg^+(v)$ - outdegree -- number of \"tails\" connected to a vertex (node);\n",
    "\n",
    "\\*_loop contributes to +1 to indegree and +1 to outdegree_\n",
    "\n",
    "![directed-graphs-degree](images/directed-graphs-degree.png)<br>\n",
    "<sub>*Note: the example was reduced to essentials, thats why some other nodes which should be connected to the ones displayed are excluded.</sub>\n",
    "\n",
    "In the example above:<br>\n",
    "\\- degree of the vertex AA can be denoted as:\n",
    "* $deg^-(AA) = 1$ \n",
    "* $deg^+(AA) = 2$\n",
    "\n",
    "\\- degree of the vertex BA can be denoted as:\n",
    "* $deg^-(BA) = 1$\n",
    "* $deg^+(BA) = 3$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5520f1de-2891-432e-93cf-7f61469ea6f4",
   "metadata": {},
   "source": [
    "Some other related concepts worth mentioning are:<br>\n",
    "a) Min In-Degree $\\delta$<sub>in</sub> -- the smallest indegree among all nodes in a graph;<br>\n",
    "b) Max In-Degree $\\Delta$<sub>in</sub> -- the largest indegree among all nodes in a graph;<br>\n",
    "\n",
    "a) Min Out-Degree $\\delta$<sub>out</sub> -- the smallest outdegree among all nodes in a graph;<br>\n",
    "b) Max Out-Degree $\\Delta$<sub>out</sub> -- the largest outdegree among all nodes in a graph;<br>\n",
    "\n",
    "a) Min Total Degree $\\delta$<sub>total</sub> -- the smallest degree among all nodes in a graph;<br>\n",
    "b) Max Total Degree $\\Delta$<sub>total</sub> -- the largest degree among all nodes in a graph."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd7f88c4-fe78-4eea-9c82-24be4116a1e6",
   "metadata": {},
   "source": [
    "If $deg^+(v) = deg^−(v)$, the graph is called a balanced directed graph:<br>\n",
    "![graphs-equal-degree](images/graphs-equal-degree.png)<br>\n",
    "<sub>*Note: the example was reduced to essentials, thats why some other nodes which should be connected to the one displayed are excluded.</sub>\n",
    "\n",
    "A vertex with $deg^−(v) = 0$ is called a source, as it is the origin of each of its outcoming edges. Similarly, a vertex with $deg^+(v) = 0$ is called a sink, since it is the end of each of its incoming edges:<br>\n",
    "![degree](images/source-sink.png)<br>\n",
    "<sub>*Note: the example was reduced to essentials, thats why some other nodes which should be connected to the ones displayed are excluded.</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "015b9c62-0fcc-42f3-86d8-9f4c706e1096",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac7e47a2-e651-421e-93de-e5b197c07d28",
   "metadata": {},
   "source": [
    "### Families of Graphs "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0de98ec4-055d-4a9d-a07b-8803bca3ba3f",
   "metadata": {},
   "source": [
    "#### Path Graphs\n",
    "Path is denoted as $P$<sub>$n$</sub> where <sub>$n$</sub> is a number of vertices (nodes) in a path.<br>\n",
    "$V = \\{$ 0, 1, ..., n-1 $\\}$<br>\n",
    "$E = \\{$ (0, 1), (1, 2), ..., (n-2, n-1) $\\}$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00c17406-2461-4085-96a2-25062a9eb41f",
   "metadata": {},
   "source": [
    "##### Undirected Path Graph\n",
    "![graphs-path](images/graphs-path.png)<br>\n",
    "In the example above, the path can be denoted as $P$<sub>$3$</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cb529285-66c2-41b3-8419-3200a5222deb",
   "metadata": {},
   "source": [
    "##### Directed Path Graph\n",
    "![graphs-directed-path](images/graphs-directed-path.png)<br>\n",
    "In the example above, the path can be denoted as $P$<sub>$4$</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "49eae2fe-ee6a-4e7c-8bc8-252d4a421d14",
   "metadata": {},
   "source": [
    "#### Cycle Graphs\n",
    "Cycle is denoted as $C$<sub>$n$</sub> where <sub>$n$</sub> is a number of vertices (nodes) in a cycle.<br>\n",
    "They are somewhat related to paths. If you remove one of the edges on a cycle you are left with path.<br>\n",
    "$V = \\{$ 0, 1, ..., n-1 $\\}$<br>\n",
    "$E = \\{$ (0, 1), (1, 2), ..., (n-2, n-1), (n-1, 0) $\\}$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8e554fd-79c1-43aa-85a3-9c7641756d68",
   "metadata": {},
   "source": [
    "##### Undirected Cycle Graph\n",
    "![graphs-undirected-cycle](images/graphs-undirected-cycle.png)<br>\n",
    "In the example above, the cycle can be denoted as $C$<sub>$4$</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "abc9d06a-83f8-4767-8ec5-77d315e3a96f",
   "metadata": {},
   "source": [
    "##### Directed Cycle Graph\n",
    "![graphs-directed-cycle](images/graphs-directed-cycle.png)<br>\n",
    "In the example above, the cycle can be denoted as $C$<sub>$3$</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "075c7845-4c53-45ba-8ad0-2f68fb48dcb2",
   "metadata": {},
   "source": [
    "#### Complete Graphs\n",
    "Complete graph is denoted as $K$<sub>$n$</sub> where <sub>$n$</sub> is a number of vertices (nodes) in a complete graph.<br>\n",
    "Complete graphs do not include multiple (parallel) edges and are undirected.<br>\n",
    "$V = \\{$ 0, 1, ..., n-1 $\\}$<br>\n",
    "$E = \\{$ \n",
    "(0, 1), (0, 2), ..., (0, n-1),\n",
    "(1, 2), (1, 3), ..., (1, n-1),\n",
    "(2, 3), (2, 4), ..., (2, n-1),\n",
    "...\n",
    "$\\}$<br>\n",
    "![graphs-complete](images/graphs-complete.png)<br>\n",
    "In the example above, the graph can be denoted as $K$<sub>$5$</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "99682f61-9d43-4a7f-af14-445b72e7776e",
   "metadata": {},
   "source": [
    "#### Tournament Graphs\n",
    "Tournament graph is basically a directed complete graph.<br>\n",
    "![graphs-tournament](images/graphs-tournament.png)<br>\n",
    "In the example above, the graph can be denoted as $K$<sub>$5$</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b44d207f-e717-47d9-a139-d78fef8cfaa7",
   "metadata": {},
   "source": [
    "#### Star Graphs\n",
    "Star graph is denoted as $S$<sub>$n$</sub> where <sub>$n$</sub> is a number of vertices (nodes) in a star graph.<br>\n",
    "$V = \\{$ 0, 1, ..., n-1 $\\}$<br>\n",
    "$E = \\{$ (0, 1), (1, 2), ..., (0, n-1) $\\}$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6837d309-3692-453f-9a48-db707f1d11bb",
   "metadata": {},
   "source": [
    "##### Undirected Star Graph\n",
    "![graphs-undirected-star](images/graphs-undirected-star.png)<br>\n",
    "In the example above, the graph can be denoted as $S$<sub>$5$</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9e3f362a-4830-4d8e-a701-6d83295a0dfb",
   "metadata": {},
   "source": [
    "##### Directed Star Graph\n",
    "![graphs-directed-star](images/graphs-directed-star.png)<br>\n",
    "In the example above, the graph can be denoted as $S$<sub>$4$</sub>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adea43c2-cd1f-482e-896b-178bf3119acc",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "795c9cb4-6c8f-40b1-bd71-53ef9aa9d359",
   "metadata": {},
   "source": [
    "## Mathematical Representation of Graphs"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad63ad73-b60c-4269-bb6b-5e266d2f8a9c",
   "metadata": {},
   "source": [
    "### Example: Social Networks Relationships"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b65cf2fb-d568-4b51-aa07-ed581cfda623",
   "metadata": {},
   "source": [
    "![social-networks-graphs](images/social-networks-graphs.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25bb1393-875f-4663-b849-776b6b682bcf",
   "metadata": {},
   "source": [
    "**Facebook**: here nodes are accounts, and an edge (relationship) exists between them if the accounts are friends with each other. It's a 2-way relationship:\n",
    "\n",
    "**Adjacency List**\n",
    "* Bob: _Alice_\n",
    "* Alice: _Bob, Luna_\n",
    "* Luna: _Mike, Alice_\n",
    "* Mike: _Luna, Aurora, John_\n",
    "* Aurora: _Mike, John_\n",
    "* John: _Mike, Aurora_\n",
    "\n",
    "**Adjacency Matrix**\n",
    "$$\n",
    "Facebook = \n",
    "\\begin{bmatrix}\n",
    "BobBob & \\color{#007FFF}BobAlice & BobLuna & BobMike & BobAurora & BobJohn \\\\\n",
    "\\color{#007FFF}AliceBob & AliceAlice & \\color{#007FFF}AliceLuna & AliceMike & AliceAurora & AliceJohn \\\\\n",
    "LunaBob & \\color{#007FFF}LunaAlice & LunaLuna & \\color{#007FFF}LunaMike & LunaAurora & LunaJohn \\\\\n",
    "MikeBob & MikeAlice & \\color{#007FFF}MikeLuna & MikeMike & \\color{#007FFF}MikeAurora & \\color{#007FFF}MikeJohn \\\\\n",
    "AuroraBob & AuroraAlice & AuroraLuna & \\color{#007FFF}AuroraMike & AuroraAurora & \\color{#007FFF}AuroraJohn \\\\\n",
    "JohnBob & JohnAlice & JohnLuna & \\color{#007FFF}JohnMike & \\color{#007FFF}JohnAurora & JohnJohn\n",
    "\\end{bmatrix}\n",
    "=\n",
    "\\begin{bmatrix}\n",
    "0 & \\color{#007FFF}1 & 0 & 0 & 0 & 0\\\\\n",
    "\\color{#007FFF}1 & 0 & \\color{#007FFF}1 & 0 & 0 & 0\\\\\n",
    "0 & \\color{#007FFF}1 & 0 & \\color{#007FFF}1 & 0 & 0\\\\\n",
    "0 & 0 & \\color{#007FFF}1 & 0 & \\color{#007FFF}1 & \\color{#007FFF}1\\\\\n",
    "0 & 0 & 0 & \\color{#007FFF}1 & 0 & \\color{#007FFF}1\\\\\n",
    "0 & 0 & 0 & \\color{#007FFF}1 & \\color{#007FFF}1 & 0\n",
    "\\end{bmatrix}\n",
    "$$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a9ed8cb3-d0f9-4569-869d-a1e269f15f27",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9799412e-d6b9-4dd4-a152-ce00f36752fc",
   "metadata": {},
   "source": [
    "**Instagram**: accounts are nodes as well, and an edge exists if one account follows the other account. Moreover, when Mike follows Luna, but Luna doesn't follow Mike, then there is some direction to the \"follow\" relationship.\n",
    "\n",
    "**Adjacency List**\n",
    "* Bob: _Alice_\n",
    "* Alice : _Bob, Luna_\n",
    "* Luna:\n",
    "* Mike: _Luna, Aurora, John_\n",
    "* Aurora: _Mike, John_\n",
    "* John:\n",
    "\n",
    "**Adjacency Matrix**\n",
    "$$\n",
    "Instagram = \n",
    "\\begin{bmatrix}\n",
    "BobBob & \\color{#FF9933}BobAlice & BobLuna & BobMike & BobAurora & BobJohn \\\\\n",
    "\\color{#FF9933}AliceBob & AliceAlice & \\color{#FF9933}AliceLuna & AliceMike & AliceAurora & AliceJohn \\\\\n",
    "LunaBob & LunaAlice & LunaLuna & LunaMike & LunaAurora & LunaJohn \\\\\n",
    "MikeBob & MikeAlice & \\color{#FF9933}MikeLuna & MikeMike & \\color{#FF9933}MikeAurora & \\color{#FF9933}MikeJohn \\\\\n",
    "AuroraBob & AuroraAlice & AuroraLuna & \\color{#FF9933}AuroraMike & AuroraAurora & \\color{#FF9933}AuroraJohn \\\\\n",
    "JohnBob & JohnAlice & JohnLuna & JohnMike & JohnAurora & JohnJohn\n",
    "\\end{bmatrix}\n",
    "=\n",
    "\\begin{bmatrix}\n",
    "0 & \\color{#FF9933}1 & 0 & 0 & 0 & 0\\\\\n",
    "\\color{#FF9933}1 & 0 & \\color{#FF9933}1 & 0 & 0 & 0\\\\\n",
    "0 & 0 & 0 & 0 & 0 & 0\\\\\n",
    "0 & 0 & \\color{#FF9933}1 & 0 & \\color{#FF9933}1 & \\color{#FF9933}1\\\\\n",
    "0 & 0 & 0 & \\color{#FF9933}1 & 0 & \\color{#FF9933}1\\\\\n",
    "0 & 0 & 0 & 0 & 0 & 0\n",
    "\\end{bmatrix}\n",
    "$$"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0fbd1894-8e77-4561-a865-db7b762c6062",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f4755293-6ec8-4f58-88d2-badc66a8d77f",
   "metadata": {},
   "source": [
    "### Example: Königsberg Bridge Problem\n",
    "Can all seven bridges over the river Preger be traversed in a single trip without doubling back, with the additional requirement that the trip ends in the same place it began?\n",
    "\n",
    "Answer: _For a walk that crosses every edge exactly once to be possible, at most two vertices can have an odd number of edges attached to them. In fact there have to be either two vertices with an odd number of edges or none at all. In the Königsberg problem, however, all vertices have an odd number of edges attached to them, so a walk that crosses every bridge is **impossible**. (See [Euler's solution](https://simple.wikipedia.org/wiki/Seven_Bridges_of_K%C3%B6nigsberg#Euler's_analysis))_<br>\n",
    "![königsberg](images/the-seven-bridges-of-königsberg.jpg)\n",
    "![python-graph-1](images/python-graph-1.png)<br>\n",
    "Vertices (landmasses): { $A$, $B$, $C$, $D$ }<br>\n",
    "Edges (bridges): { $AaB$, $AbB$, $AcC$, $AdC$, $AeD$, $BfD$, $CgD$ }<br>\n",
    "Degrees (bridges per landmass): $deg(A)=5$, $deg(B)=3$, $deg(C)=3$, $deg(D)=3$\n",
    "\n",
    "The sum of the degrees of each landmass is even and is twice the number of the existing edges. In the example there are 7 bridges. When we add up the bridges of each landmass, we get 14, exactly twice of the number of bridges. The result follows the **Handshaking Lemma**."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8d14f5b8-7bf5-4b69-9c34-54d277f9a3d6",
   "metadata": {},
   "source": [
    "***\n",
    "#### Named Tuple (Königsberg Bridge Problem)\n",
    "Despite this method being probably the best mathematical representation of a graph, it may not be the most convenient in some other cases."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "81d4e3c3-e84e-4359-8810-36bf426abaad",
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import namedtuple"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "64bac4f6-2fa7-4390-a4e6-6ab225da3197",
   "metadata": {},
   "outputs": [],
   "source": [
    "Graph = namedtuple(\"Graph\",[\"nodes\",\"edges\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "b2b2acd5-ec55-461c-8dd9-0677134c8df2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Graph(nodes=['A', 'B', 'C', 'D'], edges=[('A', 'B'), ('A', 'B'), ('A', 'C'), ('A', 'C'), ('A', 'D'), ('B', 'D'), ('C', 'D')])\n"
     ]
    }
   ],
   "source": [
    "nodes = [\"A\",\"B\",\"C\",\"D\"]\n",
    "edges = [\n",
    "    (\"A\",\"B\"),\n",
    "    (\"A\",\"B\"),\n",
    "    (\"A\",\"C\"),\n",
    "    (\"A\",\"C\"),\n",
    "    (\"A\",\"D\"),\n",
    "    (\"B\",\"D\"),\n",
    "    (\"C\",\"D\"),\n",
    "]\n",
    "\n",
    "G = Graph(nodes, edges)\n",
    "print(G)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b473044e-c2fc-4e18-a7bf-f8cacca16598",
   "metadata": {},
   "source": [
    "***\n",
    "#### Adjacency List (Königsberg Bridge Problem)\n",
    "$A: B,  B,  C,  C,  D$<br>\n",
    "$B: A,  A,  D$<br>\n",
    "$C: A,  A,  D$<br>\n",
    "$D: A,  B,  C$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "40046545-fd0d-4164-a698-175341adb86a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def adjacency_dict(graph):\n",
    "    \"\"\"\n",
    "    Returns the adjacency list representation\n",
    "    of the graph.\n",
    "    \"\"\"\n",
    "    adj = {node: [] for node in graph.nodes}\n",
    "    for edge in graph.edges:\n",
    "        node1, node2 = edge[0], edge[1]\n",
    "        adj[node1].append(node2)\n",
    "        adj[node2].append(node1)\n",
    "    return adj"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "9aa4256d-9077-4e55-bbfc-e592b3f83e87",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'A': ['B', 'B', 'C', 'C', 'D'],\n",
       " 'B': ['A', 'A', 'D'],\n",
       " 'C': ['A', 'A', 'D'],\n",
       " 'D': ['A', 'B', 'C']}"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "adjacency_dict(G)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "644569a6-dfa8-4fa5-a42d-052984244d5d",
   "metadata": {},
   "source": [
    "It can be further improved by using `integer` instead of `string` node and edge values, and by adding the orientation parameter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "04bdd9cc-c164-4a45-b43c-129479954328",
   "metadata": {},
   "outputs": [],
   "source": [
    "Graph = namedtuple(\"Graph\",[\"nodes\",\"edges\",\"is_directed\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "dbcf2231-3228-4b21-90e8-55a0ad42bfe6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Graph(nodes=range(0, 4), edges=[(0, 1), (0, 1), (0, 2), (0, 2), (0, 3), (1, 3), (2, 3)], is_directed=False)\n"
     ]
    }
   ],
   "source": [
    "G = Graph(\n",
    "    nodes = range(4),\n",
    "    edges = [\n",
    "        (0,1),\n",
    "        (0,1),\n",
    "        (0,2),\n",
    "        (0,2),\n",
    "        (0,3),\n",
    "        (1,3),\n",
    "        (2,3),\n",
    "    ],\n",
    "    is_directed = False\n",
    ")\n",
    "\n",
    "print(G)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "8e8a345e-2cbc-4df8-83b7-561685dbeead",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Imporoved adjacency list function\n",
    "def adjacency_dict(graph):\n",
    "    \"\"\"\n",
    "    Returns the adjacency list representation\n",
    "    of the graph.\n",
    "    \"\"\"\n",
    "    adj = {node: [] for node in graph.nodes}\n",
    "    for edge in graph.edges:\n",
    "        node1, node2 = edge[0], edge[1]\n",
    "        adj[node1].append(node2)\n",
    "        if not graph.is_directed: # Added this line\n",
    "            adj[node2].append(node1)\n",
    "    return adj"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "ad03a296-9ebb-427b-9148-b9913e3bbb4c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{0: [1, 1, 2, 2, 3], 1: [0, 0, 3], 2: [0, 0, 3], 3: [0, 1, 2]}"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "adjacency_dict(G)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1373cd81-9580-47c3-8c2b-5676e89b73c4",
   "metadata": {},
   "source": [
    "***\n",
    "#### Adjacency Matrix (Königsberg Bridge Problem)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0bdc5957-da01-447e-85e0-3a2faba424b6",
   "metadata": {},
   "source": [
    "$$\n",
    "Bridges = \n",
    "\\begin{bmatrix}\n",
    "AA & \\color{#2DCDD2}AB & \\color{#2DCDD2}AC & \\color{#2DCDD2}AD \\\\\n",
    "\\color{#2DCDD2}BA & BB & BC & \\color{#2DCDD2}BD \\\\\n",
    "\\color{#2DCDD2}CA & CB & CC & \\color{#2DCDD2}CD \\\\\n",
    "\\color{#2DCDD2}DA & \\color{#2DCDD2}DB & \\color{#2DCDD2}DC & DD \n",
    "\\end{bmatrix}\n",
    "=\n",
    "\\begin{bmatrix}\n",
    "0 & \\color{#2DCDD2}2 & \\color{#2DCDD2}2 & \\color{#2DCDD2}1 \\\\\n",
    "\\color{#2DCDD2}2 & 0 & 0 & \\color{#2DCDD2}1 \\\\\n",
    "\\color{#2DCDD2}2 & 0 & 0 & \\color{#2DCDD2}1 \\\\\n",
    "\\color{#2DCDD2}1 & \\color{#2DCDD2}1 & \\color{#2DCDD2}1 & 0\n",
    "\\end{bmatrix}\n",
    "$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "5ed517d6-8fe5-40de-b9bc-3a472f4c1877",
   "metadata": {},
   "outputs": [],
   "source": [
    "Graph = namedtuple(\"Graph\",[\"nodes\",\"edges\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "a4f51ba5-3816-4c81-bb00-c132c6f1162e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Graph(nodes=['A', 'B', 'C', 'D'], edges=[('A', 'B'), ('A', 'B'), ('A', 'C'), ('A', 'C'), ('A', 'D'), ('B', 'D'), ('C', 'D')])\n"
     ]
    }
   ],
   "source": [
    "nodes = [\"A\",\"B\",\"C\",\"D\"]\n",
    "edges = [\n",
    "    (\"A\",\"B\"),\n",
    "    (\"A\",\"B\"),\n",
    "    (\"A\",\"C\"),\n",
    "    (\"A\",\"C\"),\n",
    "    (\"A\",\"D\"),\n",
    "    (\"B\",\"D\"),\n",
    "    (\"C\",\"D\"),\n",
    "]\n",
    "\n",
    "G = Graph(nodes, edges)\n",
    "print(G)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "daa67f13-f155-46d4-bf23-f274d6a97e73",
   "metadata": {},
   "outputs": [],
   "source": [
    "def adjacency_matrix(graph):\n",
    "    \"\"\"\n",
    "    Returns the adjacency matrix representation\n",
    "    of the graph.\n",
    "    Assumes that graph.nodes is equivalent to range(len(graph.nodes)).\n",
    "    \"\"\"\n",
    "    adj = [[0 for node in graph.nodes] for node in graph.nodes]\n",
    "    for edge in graph.edges:\n",
    "        node1, node2 = edge[0], edge[1]\n",
    "        adj[node1][node2] += 1\n",
    "        adj[node2][node2] += 1\n",
    "    return adj"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "5053cfa6-8892-4a50-ae75-0fee68f8d4dd",
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "list indices must be integers or slices, not str",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "Cell \u001b[1;32mIn[13], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[43madjacency_matrix\u001b[49m\u001b[43m(\u001b[49m\u001b[43mG\u001b[49m\u001b[43m)\u001b[49m\n",
      "Cell \u001b[1;32mIn[12], line 10\u001b[0m, in \u001b[0;36madjacency_matrix\u001b[1;34m(graph)\u001b[0m\n\u001b[0;32m      8\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m edge \u001b[38;5;129;01min\u001b[39;00m graph\u001b[38;5;241m.\u001b[39medges:\n\u001b[0;32m      9\u001b[0m     node1, node2 \u001b[38;5;241m=\u001b[39m edge[\u001b[38;5;241m0\u001b[39m], edge[\u001b[38;5;241m1\u001b[39m]\n\u001b[1;32m---> 10\u001b[0m     \u001b[43madj\u001b[49m\u001b[43m[\u001b[49m\u001b[43mnode1\u001b[49m\u001b[43m]\u001b[49m[node2] \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m     11\u001b[0m     adj[node2][node2] \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m     12\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m adj\n",
      "\u001b[1;31mTypeError\u001b[0m: list indices must be integers or slices, not str"
     ]
    }
   ],
   "source": [
    "adjacency_matrix(G)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c8e0c73-504f-4b82-98c6-0c834fe23e47",
   "metadata": {},
   "source": [
    "Since nodes are represented as `string` in this case, they should be relabled using `integer` in order to put them into a matrix:<br>\n",
    "A $\\to$ 0<br>\n",
    "B $\\to$ 1<br>\n",
    "C $\\to$ 2<br>\n",
    "D $\\to$ 3<br>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "635f20cf-9b18-4a56-bc66-196437d51be3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Graph(nodes=range(0, 4), edges=[(0, 1), (0, 1), (0, 2), (0, 2), (0, 3), (1, 3), (2, 3)])\n"
     ]
    }
   ],
   "source": [
    "nodes = range(4)\n",
    "edges = [\n",
    "    (0,1),\n",
    "    (0,1),\n",
    "    (0,2),\n",
    "    (0,2),\n",
    "    (0,3),\n",
    "    (1,3),\n",
    "    (2,3),\n",
    "]\n",
    "\n",
    "G = Graph(nodes, edges)\n",
    "print(G)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "ec1c0f49-6bf7-4cdf-a160-5c5dc19bdc5b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[[0, 2, 2, 1], [0, 2, 0, 1], [0, 0, 2, 1], [0, 0, 0, 3]]"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "adjacency_matrix(G)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea5ec00f-bcc2-49e6-8882-8d818365a976",
   "metadata": {},
   "source": [
    "Again, this graph can be improved by adding the orientation parameter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "1c747e30-a9fd-4d7d-a814-009058c71c85",
   "metadata": {},
   "outputs": [],
   "source": [
    "Graph = namedtuple(\"Graph\",[\"nodes\",\"edges\",\"is_directed\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "861d9ccd-8f30-4431-9c31-21aa1dcb1650",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Graph(nodes=range(0, 4), edges=[(0, 1), (0, 1), (0, 2), (0, 2), (0, 3), (1, 3), (2, 3)], is_directed=False)\n"
     ]
    }
   ],
   "source": [
    "G = Graph(\n",
    "    nodes = range(4),\n",
    "    edges = [\n",
    "        (0,1),\n",
    "        (0,1),\n",
    "        (0,2),\n",
    "        (0,2),\n",
    "        (0,3),\n",
    "        (1,3),\n",
    "        (2,3),\n",
    "    ],\n",
    "    is_directed = False\n",
    ")\n",
    "\n",
    "print(G)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "c15da923-634a-4721-b126-72196f755b7c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Imporoved adjacency matrix function\n",
    "def adjacency_matrix(graph):\n",
    "    \"\"\"\n",
    "    Returns the adjacency matrix representation\n",
    "    of the graph.\n",
    "    Assumes that graph.nodes is equivalent to range(len(graph.nodes)).\n",
    "    \"\"\"\n",
    "    adj = [[0 for node in graph.nodes] for node in graph.nodes]\n",
    "    for edge in graph.edges:\n",
    "        node1, node2 = edge[0], edge[1]\n",
    "        adj[node1][node2] += 1\n",
    "        if not graph.is_directed: # Added this line\n",
    "            adj[node2][node1] += 1\n",
    "    return adj"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "87d7c421-7039-4962-9840-16fac564c92a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[[0, 2, 2, 1], [2, 0, 0, 1], [2, 0, 0, 1], [1, 1, 1, 0]]"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "adjacency_matrix(G)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a036e4da-5469-46da-8172-fc5546a3765a",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e858e6e7-7185-4ab3-9410-6f3259579858",
   "metadata": {},
   "source": [
    "| **Adjacent List (Python)**                          | vs. | **Adjacent Matrix (Python)**                             |\r\n",
    "|-----------------------------------------------------|-----|----------------------------------------------------------|\r\n",
    "| * Can handle arbitrary hashable nodes.              |     | * Only works for graphs whose nodes are integers.        |\r\n",
    "| * Good for graphs with few edges, uses less memory. |     | * Not a good choice for sparse graphs, uses most memory. |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d206704b-24a7-4df3-9b68-76e44d5d4b79",
   "metadata": {},
   "source": [
    "***\n",
    "#### Degrees (Königsberg Bridge Problem)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "66e2f651-5455-409b-bbe9-f4c789c616db",
   "metadata": {},
   "outputs": [],
   "source": [
    "Graph = namedtuple(\"Graph\",[\"nodes\",\"edges\",\"is_directed\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "be7fa522-19e9-408b-a678-95c84328afe1",
   "metadata": {},
   "outputs": [],
   "source": [
    "G = Graph(\n",
    "    nodes = range(4),\n",
    "    edges = [\n",
    "        (0,1),\n",
    "        (0,1),\n",
    "        (0,2),\n",
    "        (0,2),\n",
    "        (0,3),\n",
    "        (1,3),\n",
    "        (2,3),\n",
    "    ],\n",
    "    is_directed = False\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "a76c803f-6bf0-43cd-b6f3-4a8e54d1f9b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "def degrees(graph):\n",
    "    \"\"\"\n",
    "    Return a dictionary of degrees\n",
    "    for each node in the graph.\n",
    "    \"\"\" \n",
    "    adj_list = adjacency_dict(graph)\n",
    "    degrees = {\n",
    "        node: len(neighbors)\n",
    "        for node, neighbors in adj_list.items()\n",
    "    }\n",
    "    return degrees"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "0e50db8a-cd95-4741-bce0-b862699a1de1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{0: 5, 1: 3, 2: 3, 3: 3}"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "degrees(G)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c77b7f6b-b920-4435-90f2-87f1f4dc8cc6",
   "metadata": {},
   "source": [
    "***"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e9bef0f-ffae-4602-a49d-473752c7a632",
   "metadata": {},
   "source": [
    "## (Python) Graph Visualization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "bba9254f-6a78-481e-a1b0-f6a51adef5e8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import namedtuple\n",
    "from itertools import combinations # used in complete graph\n",
    "from pyvis.network import Network"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "09cc8bdb-23f4-4ba1-8cc3-35ea87112172",
   "metadata": {},
   "outputs": [],
   "source": [
    "Graph = namedtuple(\"Graph\",[\"nodes\",\"edges\",\"is_directed\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "950984bd-ee70-4f26-8822-42e77456d8aa",
   "metadata": {},
   "source": [
    "### (PyVis) Example Graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "ac6fa84d-c232-45b7-b555-e86610d24825",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "html/example-graph.html\n"
     ]
    }
   ],
   "source": [
    "# Instantiate a Network class\n",
    "graph = Network(directed=False)\n",
    "\n",
    "# Add vertices (nodes) to the network\n",
    "graph.add_node(0, label=\"AAA\") # '0' is an id, 'AAA' is a label diplayed in the visualization\n",
    "graph.add_node(1, label=\"ABA\")\n",
    "\n",
    "# Add edges (lines) between vertices (nodes)\n",
    "graph.add_edge(0,1)\n",
    "\n",
    "# Save output in an html\n",
    "graph.show(\n",
    "    \"html/example-graph.html\", \n",
    "    notebook=False # notebook configuration, throws en error if not set to False \n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "cbb02b29-1788-4a66-a579-543408dc790e",
   "metadata": {},
   "source": [
    "![pyvis-undirected-graph](images/pyvis-undirected-graph.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "517db2e7-1d4e-4a05-aeba-bc8c5fa61661",
   "metadata": {},
   "source": [
    "### (PyVis) Generate Graphs\n",
    "The **validate_num_nodes()** function is used within other functions to validate the input."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "935c893a-bd15-499f-8f8d-e6861cc399e6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Used within graph generating functions\n",
    "def _validate_num_nodes(num_nodes):\n",
    "    \"\"\"\n",
    "    Check whether or not 'num_nodes' is a\n",
    "    positive integer, and raise a TypeError\n",
    "    or ValueError if it is not.\n",
    "    \"\"\"\n",
    "    if not isinstance(num_nodes, int):\n",
    "        raise TypeError(f\"num_nodes must be an integer; {type(num_nodes)=}\")\n",
    "    if num_nodes < 1:\n",
    "        raise ValueError(f\"num_nodes must be positive; {num_nodes=}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a01f3c27-48e3-45d4-973c-37d7c120daa1",
   "metadata": {},
   "source": [
    "Use with **show_graph()** to visualize the output.<br>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "ae709819-8eca-4ea8-a384-301403c0db6d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_graph(graph, output_filename):\n",
    "    \"\"\"\n",
    "    Saves an HTML file locally containing\n",
    "    a visualization  of the graph, and returns\n",
    "    a pyvis Network instance of the graph.\n",
    "    \"\"\"\n",
    "    g = Network(directed=graph.is_directed)\n",
    "    g.add_nodes(graph.nodes)\n",
    "    g.add_edges(graph.edges)\n",
    "    g.show(\"html/\" + output_filename, notebook=False) # 'html' is a folder name\n",
    "    return g"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "067e99a8-c1ba-4423-a2a9-165128895665",
   "metadata": {},
   "source": [
    "#### (PyVis) Path Graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "636221d4-0f07-4e63-88d5-4f01a47b39d2",
   "metadata": {},
   "outputs": [],
   "source": [
    "def path_graph(num_nodes, is_directed=False):\n",
    "    \"\"\"\n",
    "    Return a Graph instance representing\n",
    "    an undirected path in 'num_ndes' nodes.\n",
    "    \"\"\"\n",
    "    _validate_num_nodes(num_nodes)\n",
    "    nodes = range(num_nodes)\n",
    "    edges = [(i, i+1) for i in range(num_nodes - 1)]\n",
    "    return Graph(nodes, edges, is_directed=is_directed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "f5c9223f-1b3d-4a62-bf95-14b738e234fd",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "html/gen-path-graph.html\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<class 'pyvis.network.Network'> |N|=5 |E|=4"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_graph(\n",
    "    path_graph(5, is_directed=True),\n",
    "    \"gen-path-graph.html\"\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "0df1eea8-7c6e-46e5-b552-7f39a8a95bba",
   "metadata": {},
   "source": [
    "![gen-path-graph](images/gen-path-graph.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2cb037d4-81f7-4be5-a393-3381a0146b18",
   "metadata": {},
   "source": [
    "#### (PyVis) Cycle Graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "f125a843-c759-47a5-9c54-b7ffe75651a6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def cycle_graph(num_nodes, is_directed=False):\n",
    "    \"\"\"\n",
    "    Return a Graph instance representing\n",
    "    an undirected cycle in 'num_ndes' nodes.\n",
    "    \"\"\"\n",
    "    _validate_num_nodes(num_nodes)\n",
    "    base_path = path_graph(num_nodes,is_directed) # uses path_graph() output\n",
    "    base_path.edges.append((num_nodes - 1, 0))\n",
    "    return base_path"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "04bb76df-ada4-444c-a2a1-b1708d6fe39d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "html/gen-cycle-graph.html\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<class 'pyvis.network.Network'> |N|=10 |E|=10"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_graph(\n",
    "    cycle_graph(10),\n",
    "    \"gen-cycle-graph.html\"\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "3b6dab26-323e-49ed-926b-c2509dab54dc",
   "metadata": {},
   "source": [
    "![gen-cycle-graph](images/gen-cycle-graph.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a36da4d-9a8e-4a0e-b012-4cd66fbd4fce",
   "metadata": {},
   "source": [
    "#### (PyVis) Complete Graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "ce291087-2f31-4659-ac90-c0cb5f8881e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def complete_graph(num_nodes):\n",
    "    \"\"\"\n",
    "    Return a Graph instance representing\n",
    "    a complete graph in 'num_ndes' nodes.\n",
    "    \"\"\"\n",
    "    _validate_num_nodes(num_nodes)\n",
    "    nodes = range(num_nodes)\n",
    "    edges = list(combinations(nodes, 2))\n",
    "    # edges = []\n",
    "    # for i in range(num_nodes - 1):\n",
    "    #     for j in range(i + 1, num_nodes):\n",
    "    #         edges.append(i, j)\n",
    "    return Graph(nodes, edges, is_directed=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "b99ca2cf-a38a-4164-80d4-aa2381f5c89d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "html/gen-complete-graph.html\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<class 'pyvis.network.Network'> |N|=10 |E|=45"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_graph(\n",
    "    complete_graph(10), \n",
    "    \"gen-complete-graph.html\"\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "62a9499f-f5ac-48f5-932c-0bf49b8cb9cb",
   "metadata": {},
   "source": [
    "![gen-complete-graph](images/gen-complete-graph.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7ddce73b-53cb-4b0a-b587-12a02b814582",
   "metadata": {},
   "source": [
    "#### (PyVis) Tournament Graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "b85160a2-3755-4484-93eb-788c2ba5b19c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def tournament_graph(num_nodes):\n",
    "    \"\"\"\n",
    "    Return a Graph instance representing\n",
    "    a complete graph in 'num_ndes' nodes.\n",
    "    \"\"\"\n",
    "    _validate_num_nodes(num_nodes)\n",
    "    nodes = range(num_nodes)\n",
    "    edges = list(combinations(nodes, 2))\n",
    "    return Graph(nodes, edges, is_directed=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "4346f981-a896-4b94-8943-d856506ea90c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "html/gen-tournament-graph.html\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<class 'pyvis.network.Network'> |N|=5 |E|=10"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_graph(\n",
    "    tournament_graph(5), \n",
    "    \"gen-tournament-graph.html\"\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "0096754e-7a06-4cb2-a147-7a09108b42cc",
   "metadata": {},
   "source": [
    "![gen-tournament-graph](images/gen-tournament-graph.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c730b6e3-9133-4662-b5e9-708586d61889",
   "metadata": {},
   "source": [
    "#### (PyVis) Star Graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "08557f01-1a30-46d5-ac54-c13ca708e78d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def star_graph(num_nodes, is_directed=False):\n",
    "    \"\"\"\n",
    "    Return a Graph instance representing\n",
    "    an undirected cycle in 'num_ndes' nodes.\n",
    "    \"\"\"\n",
    "    _validate_num_nodes(num_nodes)\n",
    "    nodes = range(num_nodes)\n",
    "    edges = [(0,i) for i in range(1, num_nodes)]\n",
    "    return Graph(nodes, edges, is_directed=is_directed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "b1289781-66ec-46a2-b190-c60f72d404fa",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "html/gen-star-graph.html\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<class 'pyvis.network.Network'> |N|=20 |E|=19"
      ]
     },
     "execution_count": 38,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "show_graph(\n",
    "    star_graph(20), \n",
    "    \"gen-star-graph.html\"\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "6c872033-d96a-4c01-9b43-66bdd7c7a990",
   "metadata": {},
   "source": [
    "![gen-star-graph](images/gen-star-graph.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c294a4b7-bf61-495e-9e86-dd979222eae2",
   "metadata": {},
   "source": [
    "***\n",
    "### (PyVis) Additional Style Options"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d8a044e-e120-4ac8-8cd3-3b8b35d52c15",
   "metadata": {},
   "source": [
    "#### (PyVis) FrankenGraph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "20860874-cb32-40ad-bc16-7deda08b09c9",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "html/additional-style-franken-graph.html\n"
     ]
    }
   ],
   "source": [
    "# Assign color values to variables\n",
    "background_color = '#f5f5f5'\n",
    "font_color_light = '#111111'\n",
    "font_color_dark = '#0b0c10'\n",
    "node_color_not_highlighted = '#f5f5f5'\n",
    "node_color_highlighted = '#26c8cd'\n",
    "border_color = '#333333'\n",
    "\n",
    "# Base64 encoded image for the node\n",
    "node_image_base64 = 'data:image/jpeg;base64,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'\n",
    "\n",
    "# URL of the image for the node\n",
    "node_image_url = 'https://www.kingstonpolice.ca/en/services-and-reporting/resources/fingerprints-sm.jpg'\n",
    "\n",
    "# Custom style for the nodes\n",
    "node_style = {\n",
    "    'background': node_color_not_highlighted,\n",
    "    'border': border_color,\n",
    "    'highlight': { # style on click\n",
    "        'background': node_color_highlighted,\n",
    "        'border': border_color\n",
    "    }\n",
    "}\n",
    "\n",
    "# Instantiate a Network class\n",
    "graph = Network(\n",
    "    width='100%', # background width\n",
    "    height='900px', # background height\n",
    "    bgcolor=background_color # background color\n",
    ")\n",
    "\n",
    "# Add nodes to the graph with various parameters\n",
    "graph.add_node(\n",
    "    0, \n",
    "    label='Created by:\\ncyterat\\n\\ncircle with\\nlink\\non hover',\n",
    "    margin=20, # distance between a node border and label\n",
    "    title=\"<a href='https://github.com/cyterat'>https://github.com/cyterat\", # hover text\n",
    "    shape='circle', # node shape (this one allows text inside)\n",
    "    color=node_style,\n",
    "    font={\n",
    "        'color':'black',\n",
    "        'size': 18,\n",
    "        'face': 'courier',\n",
    "        'strokeWidth': 2,\n",
    "        'strokeColor': 'orange',\n",
    "    },\n",
    "    borderWidth=5, # node border width\n",
    "    size=60, # node size\n",
    "    shadow=True # node shadow\n",
    ")\n",
    "graph.add_node(\n",
    "    1, \n",
    "    label='base64\\ncircular\\nimage',\n",
    "    font={\n",
    "        'background':node_color_highlighted # highlighted text effect\n",
    "    },\n",
    "    color=node_style,\n",
    "    shape='circularImage',\n",
    "    image=node_image_base64,\n",
    "    borderWidth=10,\n",
    "    shadow=True,\n",
    "    opacity=1\n",
    ")\n",
    "graph.add_node(\n",
    "    2, \n",
    "    label=\"<b><code>base64</code></b> <i>image</i>\",\n",
    "    font={\n",
    "        'multi': 'html' # enables the use of HTML tags in label\n",
    "    },\n",
    "    color=node_style,\n",
    "    shape='image',\n",
    "    image=node_image_base64,\n",
    "    size=50,\n",
    "    shadow=True\n",
    ")\n",
    "graph.add_node(\n",
    "    3, \n",
    "    label='url image',\n",
    "    color=node_style,\n",
    "    shape='image',\n",
    "    image=node_image_url,\n",
    "    size=40,\n",
    "    shadow=True\n",
    ")\n",
    "graph.add_node(\n",
    "    4, \n",
    "    label='dot shape',\n",
    "    color=node_style,\n",
    "    borderWidth=5,\n",
    "    size=30,\n",
    "    shadow=True\n",
    ")\n",
    "graph.add_node(\n",
    "    6,\n",
    "    title=\"<img src='https://miro.medium.com/v2/resize:fit:/1*4rSaugyj7_jf0uXozP0oxg.gif' width=300 alt='Flowers in Chania'>\",\n",
    "    label=\"box shape\\nwith image\\non hover\\nmargin=20\\nand left align\", \n",
    "    color=node_style,\n",
    "    shape='box',\n",
    "    font={\n",
    "        'align':'left'\n",
    "    },\n",
    "    size=20,\n",
    "    borderWidth=5,\n",
    "    margin=20,\n",
    "    shadow=True\n",
    ")\n",
    "graph.add_node(\n",
    "    7, \n",
    "    label='triangle no border', \n",
    "    color=node_style,\n",
    "    shape='triangle',\n",
    "    borderWidth=0,\n",
    "    shadow=True\n",
    ")\n",
    "graph.add_node(\n",
    "    8, \n",
    "    label='semi-transparent star', \n",
    "    color=node_style,\n",
    "    shape='star',\n",
    "    shadow=True,\n",
    "    opacity=0.3,\n",
    ")\n",
    "\n",
    "# Add edges (lines) between vertices (nodes)\n",
    "graph.add_edge(0, 1, width=10, shadow=True)\n",
    "graph.add_edge(0, 4, width=5, label='top 5.0', font={'align':'top'}, shadow=True)\n",
    "graph.add_edge(0, 6, width=15, label='middle', font={'color':node_color_highlighted, 'align':'middle', 'strokeWidth': 0}, shadow=True)\n",
    "graph.add_edge(1,2,shadow=True)\n",
    "graph.add_edge(1,3,shadow=True)\n",
    "graph.add_edge(6,7,shadow=True)\n",
    "graph.add_edge(6,8,shadow=True)\n",
    "\n",
    "# Set the repulsion (distance) between nodes\n",
    "graph.repulsion(node_distance=200)\n",
    "\n",
    "# Save output in an html\n",
    "graph.show(\n",
    "    \"html/additional-style-franken-graph.html\", \n",
    "    notebook=False\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f7eaac62-4081-4007-8302-c9f5f252e95d",
   "metadata": {},
   "source": [
    "<img src=\"images/additional-style-franken-graph.gif\" width=\"800\"/>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f34c0c15-5ecd-42a4-8166-dd667a2e6def",
   "metadata": {},
   "source": [
    "#### (PyVis) Node Groups"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "e4acdbd9-73b7-4ab1-bcf9-094065310f71",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "html/additional-style-groups.html\n"
     ]
    }
   ],
   "source": [
    "# Instantiate a Network class\n",
    "net = Network()\n",
    "\n",
    "# Data\n",
    "data = [\n",
    "    ('A', 'B', 'anomaly'),\n",
    "    ('B', 'A', 'anomaly'),\n",
    "    ('C', 'E', 'normal'),\n",
    "    ('D', 'C', 'normal'),\n",
    "    ('E', 'A', 'anomaly')\n",
    "]\n",
    "\n",
    "# Set colors for each group\n",
    "colors = {\n",
    "    'anomaly': '#e54848',\n",
    "    'normal': '#333333'\n",
    "}\n",
    "\n",
    "# Set fonts for each group\n",
    "fonts = {\n",
    "    'anomaly': {\n",
    "        'color': 'orange',\n",
    "        'size': 20, \n",
    "        'face': 'arial'\n",
    "    },\n",
    "    'normal': {\n",
    "        'color': colors.get('black'),\n",
    "        'size': 15\n",
    "    }\n",
    "}\n",
    "\n",
    "# Add nodes with color and font based on their group\n",
    "for source, target, group in data:\n",
    "    net.add_node(source, color=colors[group], font=fonts[group])\n",
    "    net.add_node(target, color=colors[group], font=fonts[group])\n",
    "    net.add_edge(source, target)\n",
    "\n",
    "# Disable physics\n",
    "net.toggle_physics(False)\n",
    "\n",
    "# Show the network\n",
    "net.show('html/additional-style-groups.html', notebook=False)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "05cc7010-03a9-4010-b3b2-86a02a3afba9",
   "metadata": {},
   "source": [
    "![pyvis-groups-of-nodes](images/pyvis-groups-of-nodes.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e076dd5-dd24-4f17-9764-2e831d9196c9",
   "metadata": {},
   "source": [
    "#### (PyVis & NetworkX) Circular Graph Layout"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "b82653b9-259a-4053-9289-4827bb00a210",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "NetworkX module example graph:\n",
      "\n",
      "Nodes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]\n",
      "\n",
      "Edges: [(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 10), (0, 11), (0, 12), (0, 13), (0, 17), (0, 19), (0, 21), (0, 31), (1, 2), (1, 3), (1, 7), (1, 13), (1, 17), (1, 19), (1, 21), (1, 30), (2, 3), (2, 7), (2, 8), (2, 9), (2, 13), (2, 27), (2, 28), (2, 32), (3, 7), (3, 12), (3, 13), (4, 6), (4, 10), (5, 6), (5, 10), (5, 16), (6, 16), (8, 30), (8, 32), (8, 33), (9, 33), (13, 33), (14, 32), (14, 33), (15, 32), (15, 33), (18, 32), (18, 33), (19, 33), (20, 32), (20, 33), (22, 32), (22, 33), (23, 25), (23, 27), (23, 29), (23, 32), (23, 33), (24, 25), (24, 27), (24, 31), (25, 31), (26, 29), (26, 33), (27, 33), (28, 31), (28, 33), (29, 32), (29, 33), (30, 32), (30, 33), (31, 32), (31, 33), (32, 33)]\n",
      "\n",
      "Degrees: [(0, 16), (1, 9), (2, 10), (3, 6), (4, 3), (5, 4), (6, 4), (7, 4), (8, 5), (9, 2), (10, 3), (11, 1), (12, 2), (13, 5), (14, 2), (15, 2), (16, 2), (17, 2), (18, 2), (19, 3), (20, 2), (21, 2), (22, 2), (23, 5), (24, 3), (25, 3), (26, 2), (27, 4), (28, 3), (29, 4), (30, 4), (31, 6), (32, 12), (33, 17)]\n",
      "\n",
      "html/additional-style-circular.html\n"
     ]
    }
   ],
   "source": [
    "import networkx as nx\n",
    "from pyvis.network import Network\n",
    "\n",
    "# Store graph object\n",
    "G = nx.karate_club_graph() # NetworkX module example graph\n",
    "\n",
    "# Overview\n",
    "print(f\"\"\"\n",
    "NetworkX module example graph:\\n\n",
    "Nodes: {G.nodes}\\n\n",
    "Edges: {G.edges}\\n\n",
    "Degrees: {G.degree}\n",
    "\"\"\")\n",
    "\n",
    "# Set graph layout\n",
    "pos = nx.circular_layout(G, scale=500)\n",
    "\n",
    "# Instantiate a Network class\n",
    "net = Network()\n",
    "net.from_nx(G) # convert NetworkX graph into PyVis\n",
    "\n",
    "# Configure each node\n",
    "for node in net.get_nodes():\n",
    "    net.get_node(node)['x'] = pos[node][0]\n",
    "    net.get_node(node)['y'] = -pos[node][1] #the minus is needed here to respect networkx y-axis convention \n",
    "    net.get_node(node)['label'] = str(node) #set the node label as a string so that it can be displayed\n",
    "    net.get_node(node)['color'] = '#26c8cd'\n",
    "    net.get_node(node)['size'] = G.degree[node]*2 # set node size equal to its degree x2\n",
    "\n",
    "# Disable physics (effects on drag)\n",
    "net.toggle_physics(False)\n",
    "\n",
    "# Store graph in HTML file and show it\n",
    "net.show('html/additional-style-circular.html', notebook=False)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "03ad7d3f-dc94-4ae7-b5dd-75be91558c51",
   "metadata": {},
   "source": [
    "![pyvis-circular-shape-graph](images/pyvis-circular-shape-graph.png)"
   ]
  }
 ],
 "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"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}