"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"et3I3l-iBltF"},"source":["# Network epidemiology\n","\n","The goal of this assignment is to implement a working SIS model on a network and reproduce a figure from this week's readings. We will start with an SI model as a template which you can then expand upon to build the SIS model.\n","\n","## SI model\n","\n","We will put together a function that will be able to take as input networkx graphs. In the SI model, a fraction of nodes begin as infected and then new infections spread throughout the network across the links with some probability $\\beta$. If the dice rolls out of the nodes favor then it becomes infected and can then infect other nodes that it neighbors.\n","\n","In a connected graph, the SI model should eventually infect everyone as $t \\rightarrow \\infty$ because there will always be a non-zero probability of transmission. Alternatively, the SIS model will reach an equilibrium point where there is a balance between infections and reversions to susceptibility.\n","\n","Let's build the SI model!"]},{"cell_type":"code","execution_count":2,"metadata":{"executionInfo":{"elapsed":408,"status":"ok","timestamp":1648464750012,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"W2gd8rYsBltF"},"outputs":[],"source":["# We will be using numpy and networkx for our function\n","import networkx as nx\n","import numpy as np"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"lc9CvcP-CHtu"},"source":["The simulation first requires a network. Once a network is given, we can then randomly choose a small fraction of initially infected nodes. We also need to keep track of every node's state (infected or susceptible). In each time step, we go through every node and figure out what would be the next state of the node based on its current state and neighbors. For instance, if the node is already infected, then it will remain infected regardless of the states of the neighbors (SI model). \n","\n","We can play with each step first. Let's first create a network. "]},{"cell_type":"code","execution_count":4,"metadata":{"executionInfo":{"elapsed":416,"status":"ok","timestamp":1648465079814,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"7WjEkBsDJ5Ic"},"outputs":[],"source":["G = nx.barabasi_albert_graph(10**4, 5)"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"7U8uddN0LPf3"},"source":["We can create an attribute to store the nodes' states. "]},{"cell_type":"code","execution_count":7,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":263,"status":"ok","timestamp":1648465170308,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"xzPd867cLPRQ","outputId":"9adbd7a0-343e-4475-c631-87a52d095c55"},"outputs":[{"data":{"text/plain":["[0]"]},"execution_count":7,"metadata":{},"output_type":"execute_result"}],"source":["nx.set_node_attributes(G, {node: [0] for node in G.nodes()}, 'inf')\n","G.nodes[0]['inf']"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"YPA1eSbCLmGg"},"source":["We are using a list to store the state so that we can keep a time-series of every node's state. This also makes the simulation easier. \n","\n","Now we can randomly choose a certain fraction of nodes and mark them as initially infected using `choice()` function in numpy. "]},{"cell_type":"code","execution_count":10,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":8,"status":"ok","timestamp":1648465374102,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"aIMIziOZL-8w","outputId":"dc2b6bf8-30b8-48dc-8c1f-e7b8dbc32fd3"},"outputs":[{"data":{"text/plain":["{1455, 3868, 3878, 5390, 5753, 7700, 7901, 8819, 9000, 9647}"]},"execution_count":10,"metadata":{},"output_type":"execute_result"}],"source":["init_infected_fraction = 0.001\n","init_infected = set(np.random.choice(G.nodes(), \n"," size=int(len(G) * init_infected_fraction), \n"," replace=False))\n","init_infected"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"Da9t1uoCMgEq"},"source":["We should set these nodes' states to be `1` and then we can begin the simulation. "]},{"cell_type":"code","execution_count":11,"metadata":{"colab":{"base_uri":"https://localhost:8080/"},"executionInfo":{"elapsed":510,"status":"ok","timestamp":1648465499752,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"RgO-hI6AMpUq","outputId":"afc43957-b2c4-4fc8-d03b-7444e085c9ed"},"outputs":[{"data":{"text/plain":["[1]"]},"execution_count":11,"metadata":{},"output_type":"execute_result"}],"source":["for node in init_infected:\n"," G.nodes[node]['inf'][0] = 1\n","G.nodes[1455]['inf']"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"Cuh5w7hPNULr"},"source":["But how should we update the states and run the simulation? \n","\n","There is an important issue here. If we immediately change the state of a particular node, that may affect the updating of other nodes! Imagine an extreme case where we are simulating a highly infectious disease on a chain-like network where node 0 is connected to node 1, node 1 is connected to node 0 and 2, and so on. In the simulation we are going through each node one by one from node 0 to node N and update them immediately. But it happens to be that node 0 was infected. Then, it can propagate to many nodes within a single time step because it is possible that node 1 gets updated to be infected by node 0, and then node 2 gets infected by node 1, and so on. This is clearly unrealistic. \n","\n","If we change a node's state, then the next node will be updating with respect to a network that is now in a different state!\n","\n","When modelling discrete-time dynamical systems there are generally two different update strategies: synchronous and asynchronous updating. In the asynchronous updating, a random node is picked and its state is updated according to the current network state. In the synchronous updating, there is a global time clock that all nodes are synced to, so nodes only update according to the state of the network at the _current time-step_ and all nodes move to the next time step simultaneously. \n","\n","Choosing the updating scheme can have a huge impact on dynamics. We will be using the synchronous updating scheme, which means we need to store the _next state_ of the system before updating everyone all at once. There are many ways to accomplish this, but for the sake of simplicity, we will just keep the whole history. "]},{"cell_type":"code","execution_count":12,"metadata":{"executionInfo":{"elapsed":208,"status":"ok","timestamp":1648467910491,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"BunVNgrZBltG"},"outputs":[],"source":["def run_SI(graph, tmax: int, beta: float, initial_inf: float):\n"," \"\"\"Runs the SI model on the given graph with given parameters. \n","\n"," Parameters\n"," ----------\n"," graph : networkx graph object\n"," The network (graph) on which the simulation will be run\n"," tmax : int\n"," The maximum time step that we will run the model \n"," beta : float\n"," The transmission probability\n"," initial_inf : float\n"," The initial fraction of infected nodes\n","\n"," Returns\n"," -------\n"," list[float]\n"," the time-series of the fraction of infected nodes\n"," \"\"\"\n"," # First lets generate a set of initially infected nodes.\n"," init_infected = set(\n"," np.random.choice(graph.nodes(), size=int(len(graph) * initial_inf), replace=False)\n"," )\n"," \n"," # The code below uses a dictionary comprehension to generate a dictionary\n"," # with keys=nodes and values=a list of 0's and 1's. The 1 is for infected\n"," # and 0 is for susceptible. We then give that dictionary to networkx's\n"," # attribute function which then gives all the nodes the 'inf' attribute.\n"," nx.set_node_attributes(\n"," graph, \n"," {node: ([1] if node in init_infected else [0]) for node in graph.nodes()},\n"," 'inf'\n"," )\n"," \n"," # Now we need to loop through for `tmax` time step. One time step equals to \n"," # updating the whole network once. \n"," for t in range(tmax):\n"," for node in graph.nodes():\n"," \n"," # Now we check if the node if susceptible to infection\n"," # If it is, we need to determine the probability of it switching\n"," # and then switch it for the next time-step\n"," if graph.nodes[node]['inf'][t] == 0:\n"," \n"," # First determine how many infected neighbors the node has at time t:\n"," num_inf_neighbors = np.sum(\n"," [ graph.nodes[neighbor]['inf'][t] for neighbor in graph.neighbors(node)]\n"," )\n"," \n"," # Instead of drawing a bunch of random numbers for each neighbor\n"," # we can just calculate the cumulative probability of getting\n"," # infected since these events are independent and then just\n"," # draw 1 random number to check against:\n"," if np.random.random() < (1 - (1 - beta)**num_inf_neighbors):\n"," # If infection occurs we add a 1 to the state list of the node.\n"," # Note that by doing this we don't change how the other \n"," # nodes update, because they will be using time index t not t+1\n"," graph.nodes[node]['inf'].append(1)\n"," \n"," else:\n"," # If no infection occurs, then just append the current state (0)\n"," graph.nodes[node]['inf'].append(0)\n"," \n"," # Similarly, if the node is already infected it can't change back\n"," # So we append the current state if it wasn't susceptible\n"," else:\n"," graph.nodes[node]['inf'].append(1)\n"," # the function returns a time series of the fraction of infected in each time step. \n"," return [ np.mean([ graph.nodes[node]['inf'][t] for node in graph.nodes() ]) for t in range(tmax)]"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"Z-DB-9N4BltH"},"source":["And there we have our SI model. The function is mostly comments, there are only a dozen lines of code involved in the whole process. Lets give it a run:"]},{"cell_type":"code","execution_count":13,"metadata":{"executionInfo":{"elapsed":195,"status":"ok","timestamp":1648467998110,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"7vJ4gcT0BltH"},"outputs":[],"source":["# Lets generate a random graph for testing\n","rnd_graph = nx.erdos_renyi_graph(100, 0.1)\n","\n","# We want to make sure that the graph is connected, so we will only take the largest\n","# connected component, as disconnected parts can't be infected or transmit infection:\n","largest_component = max(nx.connected_components(rnd_graph), key=len)\n","# above returns a set of nodes, so we use it to creat a subgraph\n","largest_component = rnd_graph.subgraph(largest_component)"]},{"cell_type":"code","execution_count":15,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":57},"executionInfo":{"elapsed":9,"status":"ok","timestamp":1648468005007,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"RqFS7albBltH","outputId":"94922e5c-40e7-4d67-de4e-1b4644baad14"},"outputs":[{"data":{"application/vnd.google.colaboratory.intrinsic+json":{"type":"string"},"text/plain":["'Graph with 100 nodes and 486 edges'"]},"execution_count":15,"metadata":{},"output_type":"execute_result"}],"source":["nx.info(largest_component)"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"5o2NLrC1Wgot"},"source":["Now we can run it and plot it. "]},{"cell_type":"code","execution_count":16,"metadata":{"executionInfo":{"elapsed":310,"status":"ok","timestamp":1648468021624,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"kbaM5FSxBltH"},"outputs":[],"source":["import matplotlib.pyplot as plt"]},{"cell_type":"code","execution_count":18,"metadata":{"colab":{"base_uri":"https://localhost:8080/","height":303},"executionInfo":{"elapsed":1081,"status":"ok","timestamp":1648468028065,"user":{"displayName":"Yong Yeol Ahn","photoUrl":"https://lh3.googleusercontent.com/a/default-user=s64","userId":"00405984523192108505"},"user_tz":240},"id":"3qjDbTvVBltH","outputId":"3bb26a59-fd89-4339-a190-3b62923222ce"},"outputs":[{"data":{"text/plain":["[]"]},"execution_count":18,"metadata":{},"output_type":"execute_result"},{"data":{"image/png":"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","text/plain":["
"]},"metadata":{"needs_background":"light"},"output_type":"display_data"}],"source":["# Since it is a random process we want to do a couple\n","# sample runs to smooth out the curve\n","\n","plt.plot( np.mean([run_SI(rnd_graph, tmax=20, beta=0.05, initial_inf=0.1) for i in range(50)], axis=0) )"]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"71yaTNYFBltI"},"source":["The axis argument in `numpy.mean` tells you which axis to apply the average over, since we have a two-dimensional array (time on one axis and trials on the other). If I picked `axis=1` instead, it would have run the average over time rather than the number of trials.\n","\n","This curve is much smoother than the previous one. You will find that this sort of averaging over trials is necessary when dealing with noisy or random models.\n","\n","We can see that at 10% initial infected population and an infection rate of 5% we infect the whole 100 node network within 20 time steps. Most of the growth occurs in the middle after the disease ramps up, and then slows as most of the population is already infected."]},{"attachments":{},"cell_type":"markdown","metadata":{"id":"K0X_c5sIBltI"},"source":["## Building the SIS model\n","The example SI model should give you a good starting point from which to create the SIS version. In the SIS model, infected nodes can transform back to susceptible nodes, which means you will have one additional parameter that needs to be provided as an argument to the model. Lets call this probability of reversion `mu`. \n","\n","The SI model implementation is simple but far from optimal, it will be slower to run on larger and more dense graphs. If you want more of a challenge try comming up with an SIS version that can run efficiently on larger graphs. This could be done by relying more heavily on numpy by vectorizing operations and/or by using other faster libraries. \n","\n","Here are your goals:\n","\n","1. Create an SIS version of the function.\n","2. Plot your model's results using a sparse random graph and play with the parameters to get a feel for how `mu`, `beta`, and `initial_inf` change the equilibrium point of the system. The equilibrium point occurs when the system settles on a stable fraction of infected (see Fig 10.7 in Barabasi's textbook). Also take note of how long it takes for the system to reach equilibrium.\n","3. Finally, construct a graph like the figure from Barabasi's book that shows the difference between Erdos-Renyi graphs and Scale-free graphs for the SIS-model. The Y-axis in the figure will be the equilibrium point of the system. This will generally be the last time point of your simulation, assuming you run it long enough to let it reach equilibrium. The X-axis is the parameter `lambda` which is just `beta / mu`. The exact location of the critical point for the SIS model on the ER graph will vary depending upon parameters, but the key take-away is that the Scale-free graph's is lower (and eventually vanishes depending upon scaling exponent). If you are not getting this relationship, you can try running SIS on Erdos-Renyi and Scale-free networks with more nodes. Make sure to adjust the connection probability (p) in the ER graph function so that the two networks have a similar number of edges. Lastly, remember to use averaging to smooth the curves over many trials for each data-point. Note: The BA algorithm only generates exponents of 3. You can generate a directed scale-free graph with varying power-law exponent using networkx's [`scafe_free_graph`](https://networkx.github.io/documentation/stable/reference/generated/networkx.generators.directed.scale_free_graph.html?%20scale_free_graph#networkx.generators.directed.scale_free_graph) function. However, it needs to be converted to an undirected graph. You can make a power-law exponent of ~2.5 with the following parameters `alpha=0.35`, `beta=0.60`, `gamma=0.05`, `delta_in=0.4`, `delta_out=0.4`. \n","\n","4. When you are done submit your notebook to Canvas."]},{"cell_type":"code","execution_count":null,"metadata":{"id":"ahg8aNqxBltI"},"outputs":[],"source":[]},{"cell_type":"code","execution_count":null,"metadata":{"id":"MMgR40-UBltI"},"outputs":[],"source":[]}],"metadata":{"anaconda-cloud":{},"colab":{"collapsed_sections":[],"name":"m10-networkepidemiology.ipynb","provenance":[{"file_id":"https://github.com/yy/netsci-course/blob/master/m10-epidemics/epidemics_assignment.ipynb","timestamp":1648429000871}]},"kernelspec":{"display_name":"Python 3","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.6.7"}},"nbformat":4,"nbformat_minor":0}
diff --git a/m12-diffusion/threshold_model_assignment.ipynb b/m12-diffusion/threshold_model_assignment.ipynb
deleted file mode 100755
index cfdd738..0000000
--- a/m12-diffusion/threshold_model_assignment.ipynb
+++ /dev/null
@@ -1,315 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# The Threshold model\n",
- "We will build a basic threshold model and then use it to explore the dynamical space of the model on a real network and then to compare spreading processes between real graphs and randomized versions of those graphs. In the reading we learned that simple contagions and complex contagions differ in the way they spread which can have a huge impact on the resulting dynamics of the system. This is particularly important when considering social systems.\n",
- "\n",
- "In Granovetter's paper we saw how adjusting the standard deviation of thresholds in a crowd induced a phase-transition where the crowd went from exhibiting no/limited rioting behavior to a full blown riot. However, the Granovetter model assumes perfect mixing in the population. Granovetter was aware of this and alluded to extensions of the model that could include friendship information and social ties. Lets try implementing a network version of the model that takes into account binary social ties between individuals."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [],
- "source": [
- "import networkx as nx\n",
- "import numpy as np\n",
- "import random\n",
- "%matplotlib inline\n",
- "import matplotlib.pyplot as plt"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "# We will take three arguments for our function. The first\n",
- "# being a networkx graph, the second being the mean of the\n",
- "# threshold distribution, the third being the standard\n",
- "# deviation of the threshold distribution\n",
- "def threshold_model(graph, mean, std):\n",
- " \n",
- " # First lets create our distribution of thresholds.\n",
- " # Numpy has many different kinds of distributions, and\n",
- " # you can easily replace 'normal' with a 'gamma' or 'beta'\n",
- " # or 'uniform' or any other of your choosing, though the\n",
- " # parameters will change.\n",
- " threshold_distribution = np.random.normal(mean, std, size=len(graph))\n",
- " # I create a dictionary with keys=nodes and values=thresholds\n",
- " # that will store the threshold value for each node for future reference\n",
- " node_thresholds = { node: threshold_distribution[i] for i, node in enumerate(graph.nodes()) }\n",
- " \n",
- " # Now lets select a seed. We will pick a random node from the graph\n",
- " # along with its immediate neighbors as the set of initially active nodes\n",
- " # (alternatively we could just pick one node at random as the seed)\n",
- " seed_node = np.random.choice(graph.nodes())\n",
- " seed_neighbors = list(graph[seed_node].keys())\n",
- " seed_neighbors.append(seed_node)\n",
- " seeds = set(seed_neighbors)\n",
- " \n",
- " # In our model we will only allow for activation, not deactivation, so we\n",
- " # can cut down on processing time a bit by only looping over inactive nodes\n",
- " # when updating. The inactive set is just the rest of the nodes less the seeds:\n",
- " inactive_nodes = set(graph.nodes()) - seeds\n",
- " \n",
- " # We want as our output the fraction of nodes that are active in the graph \n",
- " # when the process has reached equilibrium. We can check if the process\n",
- " # is at equilibrium if between two time steps no new nodes become active.\n",
- " # So we will use the 'previous_set' variable to keep track of this\n",
- " # and iterate through a 'while loop' that exits when equilibrium is \n",
- " # reached (since we can only activate nodes we are guaranteed to reach\n",
- " # some equilibrium, even if it takes a long time)\n",
- " previous_set = set([])\n",
- " while (previous_set != inactive_nodes):\n",
- " previous_set = set(inactive_nodes)\n",
- " \n",
- " # We will be using an asynchronous update scheme that randomly\n",
- " # orders the inactive nodes and then updates them in that sequence.\n",
- " # First turn the inactive nodes into a list and shuffle it using\n",
- " # the shuffle function from python, then we iterate over that list.\n",
- " update_sequence = list(inactive_nodes)\n",
- " random.shuffle(update_sequence)\n",
- " for node in update_sequence:\n",
- " # We add up the number of active neighbors that the node has.\n",
- " # You can easily extend this to work with weighted graphs by\n",
- " # substituting 1 with edge weights. \n",
- " num_active_neighbors = np.sum([ 0 if neighbor in inactive_nodes else 1 \n",
- " for neighbor in graph.neighbors(node)])\n",
- " \n",
- " # Now we check if the activity from our neighbors pushes us over\n",
- " # the threshold...\n",
- " if (num_active_neighbors >= node_thresholds[node]):\n",
- " # if it does, then we just remove that node from the inactive list\n",
- " inactive_nodes.remove(node)\n",
- " \n",
- " # Return the fraction of active nodes at equilibrium\n",
- " return 1 - len(inactive_nodes) / len(graph)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Our threshold model is complete. It is only one variant of many. For instance, with a few small modifications we could change it to use the fraction of neighbors instead of an absolute threshold.\n",
- "\n",
- "Lets go ahead and load in a real graph and then apply this model to it and randomized versions of the [Americal college football graph](http://www-personal.umich.edu/~mejn/netdata/) which I choose because it has a distict community structure based on the geographic location of the teams:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Name: \n",
- "Type: Graph\n",
- "Number of nodes: 115\n",
- "Number of edges: 613\n",
- "Average degree: 10.6609\n"
- ]
- }
- ],
- "source": [
- "football = nx.read_gml('football.gml')\n",
- "football = nx.Graph(football)\n",
- "print(nx.info(football))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We can do two different randomizations: one in which we hold only the number of nodes and edges roughly constant, and one where we hold the degree sequence constant. We can then compare the results."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Lets pick a range of standard deviations and means to loop over\n",
- "# You will have to adjust this range and play with it so that it fits\n",
- "# your graph. I explored the dynamical space of the system\n",
- "# before in order to figure out what range of values to pick,\n",
- "# that part will be left to you in the homework.\n",
- "stds = np.linspace(0.01, 8.0, 30)\n",
- "means = np.linspace(1.0, 6.0, 30)\n",
- "# And of course we need the degree sequence for the graph\n",
- "deg_seq = list(dict(nx.degree(football)).values())"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 6,
- "metadata": {},
- "output_type": "execute_result"
- },
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- "