Note

You can download this demonstration as a Jupyter Notebook
`here`

# Virus spread¶

This notebook presents an agent-based model that simulates the propagation of a disease through a network. It demonstrates how to use the agentpy package to create and visualize networks, use the interactive module, and perform different types of sensitivity analysis.

```
[1]:
```

```
# Model design
import agentpy as ap
import networkx as nx
import random
# Visualization
import matplotlib.pyplot as plt
import seaborn as sns
import IPython
```

## About the model¶

The agents of this model are people, which can be in one of the following three conditions: susceptible to the disease (S), infected (I), or recovered (R). The agents are connected to each other through a small-world network of peers. At every time-step, infected agents can infect their peers or recover from the disease based on random chance.

## Defining the model¶

We define a new agent type `Person`

by creating a subclass of `Agent`

.
This agent has two methods: `setup()`

will be called automatically at the agent’s creation,
and `being_sick()`

will be called by the `Model.step()`

function.
Three tools are used within this class:

`Agent.p`

returns the parameters of the model`Agent.neighbors()`

returns a list of the agents’ peers in the network`random.random()`

returns a uniform random draw between 0 and 1

```
[2]:
```

```
class Person(ap.Agent):
def setup(self):
""" Initialize a new variable at agent creation. """
self.condition = 0 # Susceptible = 0, Infected = 1, Recovered = 2
def being_sick(self):
""" Spread disease to peers in the network. """
for n in self.neighbors():
if n.condition == 0 and self.p.infection_chance > random.random():
n.condition = 1 # Infect susceptible peer
if self.p.recovery_chance > random.random():
self.condition = 2 # Recover from infection
```

Next, we define our model `VirusModel`

by creating a subclass of `Model`

.
The four methods will be called automatically, as described in Running a simulation.

```
[3]:
```

```
class VirusModel(ap.Model):
def setup(self):
""" Initializes the agents and network of the model. """
self.p.population = p = int(self.p.population)
# Prepare a small-world network graph
graph = nx.watts_strogatz_graph(p,
self.p.number_of_neighbors,
self.p.network_randomness)
# Create agents and network
self.add_agents(p, Person)
self.add_network(graph=graph, agents=self.agents)
# Infect a random share of the population
I0 = int(self.p.initial_infections * self.p.population)
self.agents.random(I0).condition = 1
def update(self):
""" Records variables after setup and each step. """
# Record share of agents with each condition
for i, c in enumerate(('S', 'I', 'R')):
self[c] = (len(self.agents.select(self.agents.condition == i))
/ self.p.population)
self.record(c)
# Stop simulation if disease is gone
if self.I == 0:
self.stop()
def step(self):
""" Defines the models' events per simulation step. """
# Call 'being_sick' for infected agents
self.agents(self.agents.condition==1).being_sick()
def end(self):
""" Records evaluation measures at the end of the simulation. """
# Record final evaluation measures
self.measure('Total share infected', self.I + self.R)
self.measure('Peak share infected', max(self.log['I']))
```

## Running a simulation¶

To run our model, we define a dictionary with our parameters.
We then create a new instance of our model, passing the parameters as an argument,
and use the method `Model.run()`

to perform the simulation and return it’s output.

```
[4]:
```

```
parameters = {
'population': 1000,
'infection_chance': 0.3,
'recovery_chance': 0.1,
'initial_infections': 0.1,
'number_of_neighbors': 2,
'network_randomness': 0.5
}
model = VirusModel(parameters)
results = model.run()
```

```
Completed: 75 steps
Run time: 0:00:00.420014
Simulation finished
```

## Analyzing results¶

The simulation returns a `DataDict`

of recorded data with dataframes:

```
[5]:
```

```
results
```

```
[5]:
```

```
DataDict {
'log': Dictionary with 4 keys
'parameters': Dictionary with 6 keys
'measures': DataFrame with 2 variables and 1 row
'variables': DataFrame with 3 variables and 76 rows
}
```

To visualize the evolution of our variables over time, we create a plot function.

```
[6]:
```

```
def virus_stackplot(data, ax):
""" Stackplot of people's condition over time. """
x = data.index.get_level_values('t')
y = [data[var] for var in ['I', 'S', 'R']]
sns.set()
ax.stackplot(x, y, labels=['Infected', 'Susceptible', 'Recovered'],
colors = ['r', 'b', 'g'])
ax.legend()
ax.grid(False)
ax.set_xlim(0, max(1, len(x)-1))
ax.set_ylim(0, 1)
ax.set_xlabel("Time steps")
ax.set_ylabel("Percentage of population")
fig, ax = plt.subplots()
virus_stackplot(results.variables, ax)
```

## Creating an animation¶

We can also animate the model’s dynamics as follows.
The function `animation_plot()`

takes a model instance
and displays the previous stackplot together with a network graph.
The function `animate()`

will call this plot
function for every time-step and return an `matplotlib.animation.Animation`

.

```
[7]:
```

```
def animation_plot(m, axs):
ax1, ax2 = axs
ax1.set_title("Virus spread")
ax2.set_title(f"Share infected: {m.I}")
# Plot stackplot on first axis
virus_stackplot(m.output.variables, ax1)
# Plot network on second axis
color_dict = {0:'b', 1:'r', 2:'g'}
colors = [color_dict[c] for c in m.agents.condition]
nx.draw_circular(m.env.graph, node_color=colors,
node_size=50, ax=ax2)
fig, axs = plt.subplots(1, 2, figsize=(8, 4)) # Prepare figure
parameters['population'] = 50 # Lower population for better visibility
animation = ap.animate(VirusModel(parameters), fig, axs, animation_plot)
```

Using Jupyter, we can display this animation directly in our notebook.

```
[8]:
```

```
IPython.display.HTML(animation.to_jshtml())
```

```
[8]:
```