AgentPy - Agent-based modeling in Python
AgentPy is an open-source library for the development and analysis of agent-based models in Python. The framework integrates the tasks of model design, interactive simulations, numerical experiments, and data analysis within a single environment. The package is optimized for interactive computing with IPython, IPySimulate, and Jupyter. If you have questions or ideas for improvements, please visit the discussion forum.
Quick orientation
To get started, please take a look at Installation and Overview.
For a simple demonstration, check out the Wealth transfer tutorial in the Model Library.
For a detailled description of all classes and functions, refer to API Reference.
To learn how agentpy compares with other frameworks, take a look at Comparison.
If you are interested to contribute to the library, see Contribute.
Citation
Please cite this software as follows:
Foramitti, J., (2021). AgentPy: A package for agent-based modeling in Python.
Journal of Open Source Software, 6(62), 3065, https://doi.org/10.21105/joss.03065
Table of contents
Installation
To install the latest release of agentpy, run the following command on your console:
$ pip install agentpy
Dependencies
Agentpy supports Python 3.6 and higher. The installation includes the following packages:
matplotlib, for visualization
pandas, for data manipulation
networkx, for networks/graphs
SALib, for sensitivity analysis
joblib, for parallel processing
These optional packages can further be useful in combination with agentpy:
jupyter, for interactive computing
ipysimulate >= 0.2.0, for interactive simulations
ema_workbench, for exploratory modeling
seaborn, for statistical data visualization
Development
The most recent version of agentpy can be cloned from Github:
$ git clone https://github.com/JoelForamitti/agentpy.git
Once you have a copy of the source, you can install it with:
$ pip install -e
To include all necessary packages for development & testing, you can use:
$ pip install -e .['dev']
Overview
This section provides an overview over the main classes and functions of AgentPy and how they are meant to be used. For a more detailed description of each element, please refer to the User Guides and API Reference. Throughout this documentation, AgentPy is imported as follows:
import agentpy as ap
Structure
The basic structure of the AgentPy framework has four levels:
The
Agent
is the basic building block of a modelThe environment types
Grid
,Space
, andNetwork
contain agentsA
Model
contains agents, environments, parameters, and simulation proceduresAn
Experiment
can run a model multiple times with different parameter combinations
All of these classes are templates that can be customized through the creation of sub-classes with their own variables and methods.
Creating models
A custom agent type can be defined as follows:
class MyAgent(ap.Agent):
def setup(self):
# Initialize an attribute with a parameter
self.my_attribute = self.p.my_parameter
def agent_method(self):
# Define custom actions here
pass
The method Agent.setup()
is meant to be overwritten
and will be called automatically after an agent’s creation.
All variables of an agents should be initialized within this method.
Other methods can represent actions that the agent will be able to take during a simulation.
All model objects (including agents, environments, and the model itself) are equipped with the following default attributes:
model
the model instanceid
a unique identifier number for each objectp
the model’s parameterslog
the object’s recorded variables
Using the new agent type defined above, here is how a basic model could look like:
class MyModel(ap.Model):
def setup(self):
""" Initiate a list of new agents. """
self.agents = ap.AgentList(self, self.p.agents, MyAgent)
def step(self):
""" Call a method for every agent. """
self.agents.agent_method()
def update(self):
""" Record a dynamic variable. """
self.agents.record('my_attribute')
def end(self):
""" Repord an evaluation measure. """
self.report('my_measure', 1)
The simulation procedures of a model are defined by four special methods that will be used automatically during different parts of a simulation.
Model.setup
is called at the start of the simulation (t==0).Model.step
is called during every time-step (excluding t==0).Model.update
is called after every time-step (including t==0).Model.end
is called at the end of the simulation.
If you want to see a basic model like this in action, take a look at the Wealth transfer demonstration in the Model Library.
Agent sequences
The Sequences module provides containers for groups of agents.
The main classes are AgentList
, AgentDList
, and AgentSet
,
which come with special methods to access and manipulate whole groups of agents.
For example, when the model defined above calls self.agents.agent_method()
,
it will call the method MyAgentType.agent_method()
for every agent in the model.
Similar commands can be used to set and access variables, or select subsets
of agents with boolean operators.
The following command, for example, selects all agents with an id above one:
agents.select(agents.id > 1)
Further examples can be found in Sequences and the Virus spread demonstration model.
Environments
Environments are objects in which agents can inhabit a specific position. A model can contain zero, one or multiple environments which agents can enter and leave. The connection between positions is defined by the environment’s topology. There are currently three types:
Grid
n-dimensional spatial topology with discrete positions.Space
n-dimensional spatial topology with continuous positions.
Applications of networks can be found in the demonstration models Virus spread and Button network; spatial grids in Forest fire and Segregation; and continuous spaces in Flocking behavior. Note that there can also be models without environments like in Wealth transfer.
Recording data
There are two ways to document data from the simulation for later analysis.
The first way is to record dynamic variables,
which can be recorded for each object (agent, environment, or model) and time-step.
They are useful to look at the dynamics of individual or aggregate objects over time
and can be documented by calling the method record()
for the respective object.
Recorded variables can at run-time with the object’s log attribute.
The second way is to document reporters,
which represent summary statistics or evaluation measures of a simulation.
In contrast to variables, reporters can be stored only for the model as a whole and only once per run.
They will be stored in a separate dataframe for easy comparison over multiple runs,
and can be documented with the method Model.report()
.
Reporters can be accessed at run-time via Model.reporters
.
Running a simulation
To perform a simulation, we initialize a new instance of our model type
with a dictionary of parameters, and then use the function Model.run()
.
This will return a DataDict
with recorded data from the simulation.
A simple run can be prepared and executed as follows:
parameters = {
'my_parameter':42,
'agents':10,
'steps':10
}
model = MyModel(parameters)
results = model.run()
A simulation proceeds as follows (see also Figure 1 below):
The model initializes with the time-step
Model.t = 0
.Model.setup()
andModel.update()
are called.The model’s time-step is increased by 1.
Model.step()
andModel.update()
are called.Step 2 and 3 are repeated until the simulation is stopped.
Model.end()
is called.
The simulation of a model can be stopped by one of the following two ways:
Calling the
Model.stop()
during the simulation.Reaching the time-limit, which be defined as follows:
Defining
steps
in the paramater dictionary.Passing
steps
as an argument toModel.run()
.
Interactive simulations
Within a Jupyter Notebook, AgentPy models can be explored as an interactive simulation (similar to the traditional NetLogo interface) using ipysimulate and d3.js. For more information on this, please refer to Interactive simulations.
Multi-run experiments
The Parameter samples module provides tools to create a Sample
with multiple parameter combinations from a dictionary of ranges.
Here is an example using IntRange
integer ranges:
parameters = {
'my_parameter': 42,
'agents': ap.IntRange(10, 20),
'steps': ap.IntRange(10, 20)
}
sample = ap.Sample(parameters, n=5)
The class Experiment
can be used to run a model multiple times.
As shown in Figure 1, it will start with the first parameter combination
in the sample and repeat the simulation for the amount of defined iterations.
After, that the same cycle is repeated for the next parameter combination.

Figure 1: Chain of events in Model
and Experiment
.
Here is an example of an experiment with the model defined above. In this experiment, we use a sample where one parameter is kept fixed while the other two are varied 5 times from 10 to 20 and rounded to integer. Every possible combination is repeated 2 times, which results in 50 runs:
exp = ap.Experiment(MyModel, sample, iterations=2, record=True)
results = exp.run()
For more applied examples of experiments, check out the demonstration models Virus spread, Button network, and Forest fire. An alternative to the built-in experiment class is to use AgentPy models with the EMA workbench (see Exploratory modelling and analysis (EMA)).
Random numbers
Model
contains two random number generators:
Model.random
is an instance ofrandom.Random
Model.nprandom
is an instance ofnumpy.random.Generator
The random seed for these generators can be set by defining a parameter seed.
The Sample
class has an argument randomize
to control whether vary seeds over different parameter combinations.
Similarly, Experiment
also has an argument randomize
to control whether to vary seeds over different iterations.
More on this can be found in Randomness and reproducibility.
Data analysis
Both Model
and Experiment
can be used to run a simulation,
which will return a DataDict
with output data.
The output from the experiment defined above looks as follows:
>>> results
DataDict {
'info': Dictionary with 5 keys
'parameters':
'constants': Dictionary with 1 key
'sample': DataFrame with 2 variables and 25 rows
'variables':
'MyAgent': DataFrame with 1 variable and 10500 rows
'reporters': DataFrame with 1 variable and 50 rows
}
All data is given in a pandas.DataFrame
and
formatted as long-form data
that can easily be used with statistical packages like seaborn.
The output can contain the following categories of data:
info
holds meta-data about the model and simulation performance.parameters
holds the parameter values that have been used for the experiment.variables
holds dynamic variables, which can be recorded at multiple time-steps.reporters
holds evaluation measures that are documented only once per simulation.sensitivity
holds calculated sensitivity measures.
The DataDict
provides the following main methods to handle data:
DataDict.save()
andDataDict.load()
can be used to store results.DataDict.arrange()
generates custom combined dataframes.DataDict.calc_sobol()
performs a Sobol sensitivity analysis.
Visualization
In addition to the Interactive simulations, AgentPy provides the following functions for visualization:
animate()
generates an animation that can display output over time.gridplot()
visualizes agent positions on a spatialGrid
.
To see applied examples of these functions, please check out the Model Library.
User Guides
This section contains interactive notebooks with common applications of the agentpy framework. If you are interested to add a new article to this guide, please visit Contribute. If you are looking for examples of complete models, take a look at Model Library. To learn how agentpy compares with other frameworks, take a look at Comparison.
Note
You can download this demonstration as a Jupyter Notebook
here
Interactive simulations
The exploration of agent-based models can often be guided through an interactive simulation interface that allows users to visualize the models dynamics and adjust parameter values while a simulation is running. Examples are the traditional interface of NetLogo, or the browser-based visualization module of Mesa.
This guide shows how to create such interactive interfaces for agentpy models within a Jupyter Notebook by using the libraries IPySimulate, ipywidgets and d3.js. This approach is still in an early stage of development, and more features will follow in the future. Contributions are very welcome :)
[1]:
import agentpy as ap
import ipysimulate as ips
from ipywidgets import AppLayout
from agentpy.examples import WealthModel, SegregationModel
Lineplot
To begin we create an instance of the wealth transfer model (without parameters).
[2]:
model = WealthModel()
Parameters that are given as ranges will appear as interactive slider widgets. The parameter fps
(frames per second) will be used automatically to indicate the speed of the simulation. The third value in the range defines the default position of the slider.
[3]:
parameters = {
'agents': 1000,
'steps': 100,
'fps': ap.IntRange(1, 20, 5),
}
We then create an ipysimulate control panel with the model and our set of parameters. We further pass two variables t
(time-steps) and gini
to be displayed live during the simulation.
[4]:
control = ips.Control(model, parameters, variables=('t', 'gini'))
Next, we create a lineplot of the variable gini
:
[5]:
lineplot = ips.Lineplot(control, 'gini')
Finally, we want to display our two widgets control
and lineplot
next to each other. For this, we can use the layout templates from ipywidgets.
[6]:
AppLayout(
left_sidebar=control,
center=lineplot,
pane_widths=['125px', 1, 1],
height='400px'
)
Note that this widget is not displayed interactively if viewed in the docs. To view the widget, please download the Jupyter Notebook at the top of this page or launch this notebook as a binder. Here is a screenshot of an interactive simulation:
Scatterplot
In this second demonstration, we create an instance of the segregation model:
[7]:
model = SegregationModel()
[8]:
parameters = {
'fps': ap.IntRange(1, 10, 5),
'want_similar': ap.Range(0, 1, 0.3),
'n_groups': ap.Values(2, 3, 4),
'density': ap.Range(0, 1, 0.95),
'size': 50,
}
[9]:
control = ips.Control(model, parameters, ('t'))
scatterplot = ips.Scatterplot(
control,
xy=lambda m: m.grid.positions.values(),
c=lambda m: m.agents.group
)
[10]:
AppLayout(left_sidebar=control,
center=scatterplot,
pane_widths=['125px', 1, 1],
height='400px')
Note that this widget is not displayed interactively if viewed in the docs. To view the widget, please download the Jupyter Notebook at the top of this page or launch this notebook as a binder. Here is a screenshot of an interactive simulation:
Note
You can download this demonstration as a Jupyter Notebook
here
Randomness and reproducibility
Random numbers and stochastic processes are essential to most agent-based models. Pseudo-random number generators can be used to create numbers in a sequence that appears random but is actually a deterministic sequence based on an initial seed value. In other words, the generator will produce the same pseudo-random sequence over multiple runs if it is given the same seed at the beginning. Note that is possible that the generators will draw the same number repeatedly, as illustrated in this comic strip from Scott Adams:
[1]:
import agentpy as ap
import numpy as np
import random
Random number generators
Agentpy models contain two internal pseudo-random number generators with different features:
Model.random
is an instance ofrandom.Random
(more info here)Model.nprandom
is an instance ofnumpy.random.Generator
(more info here)
To illustrate, let us define a model that uses both generators to draw a random integer:
[2]:
class RandomModel(ap.Model):
def setup(self):
self.x = self.random.randint(0, 99)
self.y = self.nprandom.integers(99)
self.report(['x', 'y'])
self.stop()
If we run this model multiple times, we will likely get a different series of numbers in each iteration:
[3]:
exp = ap.Experiment(RandomModel, iterations=5)
results = exp.run()
Scheduled runs: 5
Completed: 5, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.027836
[4]:
results.reporters
[4]:
seed | x | y | |
---|---|---|---|
iteration | |||
0 | 163546198553218547629179155646693947592 | 75 | 1 |
1 | 248413101981860191382115517400004092470 | 57 | 61 |
2 | 71182126006424514048330534400698800795 | 96 | 37 |
3 | 319505356893330694850769146666666339584 | 89 | 95 |
4 | 64281825103124977892605409325092957646 | 37 | 84 |
Defining custom seeds
If we want the results to be reproducible, we can define a parameter seed
that will be used automatically at the beginning of a simulation to initialize both generators.
[5]:
parameters = {'seed': 42}
exp = ap.Experiment(RandomModel, parameters, iterations=5)
results = exp.run()
Scheduled runs: 5
Completed: 5, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.039785
By default, the experiment will use this seed to generate different random seeds for each iteration:
[6]:
results.reporters
[6]:
seed | x | y | |
---|---|---|---|
iteration | |||
0 | 252336560693540533935881068298825202077 | 26 | 68 |
1 | 47482295457342411543800303662309855831 | 70 | 9 |
2 | 252036172554514852379917073716435574953 | 58 | 66 |
3 | 200934189435493509245876840523779924304 | 48 | 77 |
4 | 31882839497307630496007576300860674457 | 94 | 65 |
Repeating this experiment will yield the same results:
[7]:
exp2 = ap.Experiment(RandomModel, parameters, iterations=5)
results2 = exp2.run()
Scheduled runs: 5
Completed: 5, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.047647
[8]:
results2.reporters
[8]:
seed | x | y | |
---|---|---|---|
iteration | |||
0 | 252336560693540533935881068298825202077 | 26 | 68 |
1 | 47482295457342411543800303662309855831 | 70 | 9 |
2 | 252036172554514852379917073716435574953 | 58 | 66 |
3 | 200934189435493509245876840523779924304 | 48 | 77 |
4 | 31882839497307630496007576300860674457 | 94 | 65 |
Alternatively, we can set the argument randomize=False
so that the experiment will use the same seed for each iteration:
[9]:
exp3 = ap.Experiment(RandomModel, parameters, iterations=5, randomize=False)
results3 = exp3.run()
Scheduled runs: 5
Completed: 5, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.021621
Now, each iteration yields the same results:
[10]:
results3.reporters
[10]:
seed | x | y | |
---|---|---|---|
iteration | |||
0 | 42 | 35 | 39 |
1 | 42 | 35 | 39 |
2 | 42 | 35 | 39 |
3 | 42 | 35 | 39 |
4 | 42 | 35 | 39 |
Sampling seeds
For a sample with multiple parameter combinations, we can treat the seed like any other parameter. The following example will use the same seed for each parameter combination:
[11]:
parameters = {'p': ap.Values(0, 1), 'seed': 0}
sample1 = ap.Sample(parameters, randomize=False)
list(sample1)
[11]:
[{'p': 0, 'seed': 0}, {'p': 1, 'seed': 0}]
If we run an experiment with this sample, the same iteration of each parameter combination will have the same seed (remember that the experiment will generate different seeds for each iteration by default):
[12]:
exp = ap.Experiment(RandomModel, sample1, iterations=2)
results = exp.run()
Scheduled runs: 4
Completed: 4, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.052923
[13]:
results.reporters
[13]:
seed | x | y | ||
---|---|---|---|---|
sample_id | iteration | |||
0 | 0 | 302934307671667531413257853548643485645 | 68 | 31 |
1 | 328530677494498397859470651507255972949 | 55 | 30 | |
1 | 0 | 302934307671667531413257853548643485645 | 68 | 31 |
1 | 328530677494498397859470651507255972949 | 55 | 30 |
Alternatively, we can use Sample
with randomize=True
(default) to generate random seeds for each parameter combination in the sample.
[14]:
sample3 = ap.Sample(parameters, randomize=True)
list(sample3)
[14]:
[{'p': 0, 'seed': 302934307671667531413257853548643485645},
{'p': 1, 'seed': 328530677494498397859470651507255972949}]
This will always generate the same set of random seeds:
[15]:
sample3 = ap.Sample(parameters)
list(sample3)
[15]:
[{'p': 0, 'seed': 302934307671667531413257853548643485645},
{'p': 1, 'seed': 328530677494498397859470651507255972949}]
An experiment will now have different results for every parameter combination and iteration:
[16]:
exp = ap.Experiment(RandomModel, sample3, iterations=2)
results = exp.run()
Scheduled runs: 4
Completed: 4, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.050806
[17]:
results.reporters
[17]:
seed | x | y | ||
---|---|---|---|---|
sample_id | iteration | |||
0 | 0 | 189926022767640608296581374469671322148 | 53 | 18 |
1 | 179917731653904247792112551705722901296 | 3 | 60 | |
1 | 0 | 255437819654147499963378822313666594855 | 83 | 62 |
1 | 68871684356256783618296489618877951982 | 80 | 68 |
Repeating this experiment will yield the same results:
[18]:
exp = ap.Experiment(RandomModel, sample3, iterations=2)
results = exp.run()
Scheduled runs: 4
Completed: 4, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.037482
[19]:
results.reporters
[19]:
seed | x | y | ||
---|---|---|---|---|
sample_id | iteration | |||
0 | 0 | 189926022767640608296581374469671322148 | 53 | 18 |
1 | 179917731653904247792112551705722901296 | 3 | 60 | |
1 | 0 | 255437819654147499963378822313666594855 | 83 | 62 |
1 | 68871684356256783618296489618877951982 | 80 | 68 |
Stochastic methods of AgentList
Let us now look at some stochastic operations that are often used in agent-based models. To start, we create a list of five agents:
[20]:
model = ap.Model()
agents = ap.AgentList(model, 5)
[21]:
agents
[21]:
AgentList (5 objects)
If we look at the agent’s ids, we see that they have been created in order:
[22]:
agents.id
[22]:
[1, 2, 3, 4, 5]
To shuffle this list, we can use AgentList.shuffle
:
[23]:
agents.shuffle().id
[23]:
[3, 2, 1, 4, 5]
To create a random subset, we can use AgentList.random
:
[24]:
agents.random(3).id
[24]:
[2, 1, 4]
And if we want it to be possible to select the same agent more than once:
[25]:
agents.random(6, replace=True).id
[25]:
[5, 3, 2, 5, 2, 3]
Agent-specific generators
For more advanced applications, we can create separate generators for each object. We can ensure that the seeds of each object follow a controlled pseudo-random sequence by using the models’ main generator to generate the seeds.
[26]:
class RandomAgent(ap.Agent):
def setup(self):
seed = self.model.random.getrandbits(128) # Seed from model
self.random = random.Random(seed) # Create agent generator
self.x = self.random.random() # Create a random number
class MultiRandomModel(ap.Model):
def setup(self):
self.agents = ap.AgentList(self, 2, RandomAgent)
self.agents.record('x')
self.stop()
[27]:
parameters = {'seed': 42}
exp = ap.Experiment(
MultiRandomModel, parameters, iterations=2,
record=True, randomize=False)
results = exp.run()
Scheduled runs: 2
Completed: 2, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.033219
[28]:
results.variables.RandomAgent
[28]:
x | |||
---|---|---|---|
iteration | obj_id | t | |
0 | 1 | 0 | 0.414688 |
2 | 0 | 0.591608 | |
1 | 1 | 0 | 0.414688 |
2 | 0 | 0.591608 |
Alternatively, we can also have each agent start from the same seed:
[29]:
class RandomAgent2(ap.Agent):
def setup(self):
self.random = random.Random(self.p.agent_seed) # Create agent generator
self.x = self.random.random() # Create a random number
class MultiRandomModel2(ap.Model):
def setup(self):
self.agents = ap.AgentList(self, 2, RandomAgent2)
self.agents.record('x')
self.stop()
[30]:
parameters = {'agent_seed': 42}
exp = ap.Experiment(
MultiRandomModel2, parameters, iterations=2,
record=True, randomize=False)
results = exp.run()
Scheduled runs: 2
Completed: 2, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:00:00.033855
[31]:
results.variables.RandomAgent2
[31]:
x | |||
---|---|---|---|
iteration | obj_id | t | |
0 | 1 | 0 | 0.639427 |
2 | 0 | 0.639427 | |
1 | 1 | 0 | 0.639427 |
2 | 0 | 0.639427 |
Note
You can download this demonstration as a Jupyter Notebook
here
Exploratory modelling and analysis (EMA)
This guide shows how to use agentpy models together with the EMA Workbench. Similar to the agentpy Experiment
class, this library can be used to perform experiments over different parameter combinations and multiple runs, but offers more advanced tools for parameter sampling and analysis with the aim to support decision making under deep uncertainty.
Converting an agentpy model to a function
Let us start by defining an agent-based model. Here, we use the wealth transfer model from the model library.
[1]:
import agentpy as ap
from agentpy.examples import WealthModel
To use the EMA Workbench, we need to convert our model to a function that takes each parameter as a keyword argument and returns a dictionary of the recorded evaluation measures.
[2]:
wealth_model = WealthModel.as_function()
[3]:
help(wealth_model)
Help on function agentpy_model_as_function in module agentpy.model:
agentpy_model_as_function(**kwargs)
Performs a simulation of the model 'WealthModel'.
Arguments:
**kwargs: Keyword arguments with parameter values.
Returns:
dict: Reporters of the model.
Let us test out this function:
[4]:
wealth_model(agents=5, steps=5)
[4]:
{'gini': 0.32}
Using the EMA Workbench
Here is an example on how to set up an experiment with the EMA Workbench. For more information, please visit the documentation of EMA Workbench.
[9]:
from ema_workbench import (IntegerParameter, Constant, ScalarOutcome,
Model, perform_experiments, ema_logging)
[6]:
if __name__ == '__main__':
ema_logging.LOG_FORMAT = '%(message)s'
ema_logging.log_to_stderr(ema_logging.INFO)
model = Model('WealthModel', function=wealth_model)
model.uncertainties = [IntegerParameter('agents', 10, 100)]
model.constants = [Constant('steps', 100)]
model.outcomes = [ScalarOutcome('gini')]
results = perform_experiments(model, 100)
performing 100 scenarios * 1 policies * 1 model(s) = 100 experiments
performing experiments sequentially
10 cases completed
20 cases completed
30 cases completed
40 cases completed
50 cases completed
60 cases completed
70 cases completed
80 cases completed
90 cases completed
100 cases completed
experiments finished
[7]:
results[0]
[7]:
agents | scenario | policy | model | |
---|---|---|---|---|
0 | 70.0 | 0 | None | WealthModel |
1 | 44.0 | 1 | None | WealthModel |
2 | 77.0 | 2 | None | WealthModel |
3 | 87.0 | 3 | None | WealthModel |
4 | 51.0 | 4 | None | WealthModel |
... | ... | ... | ... | ... |
95 | 38.0 | 95 | None | WealthModel |
96 | 26.0 | 96 | None | WealthModel |
97 | 59.0 | 97 | None | WealthModel |
98 | 94.0 | 98 | None | WealthModel |
99 | 75.0 | 99 | None | WealthModel |
100 rows × 4 columns
[10]:
results[1]
[10]:
{'gini': array([0.67877551, 0.61880165, 0.6392309 , 0.62491743, 0.65820838,
0.62191358, 0.61176471, 0.66986492, 0.6134068 , 0.63538062,
0.69958848, 0.63777778, 0.61862004, 0.6786 , 0.6184424 ,
0.61928474, 0.6446281 , 0.6358 , 0.7283737 , 0.60225922,
0.6404321 , 0.59729448, 0.63516068, 0.515 , 0.58301785,
0.66780045, 0.6321607 , 0.58131488, 0.6201873 , 0.70083247,
0.7 , 0.58666667, 0.58131382, 0.5964497 , 0.56014692,
0.6446281 , 0.59146814, 0.70919067, 0.61592693, 0.59736561,
0.52623457, 0.64604402, 0.56790123, 0.65675193, 0.49905482,
0.55250979, 0.62606626, 0.49864792, 0.63802469, 0.62722222,
0.65500945, 0.69010417, 0.64160156, 0.67950052, 0.60207612,
0.63115111, 0.64246914, 0.65162722, 0.65759637, 0.66392948,
0.63971072, 0.57375 , 0.55310287, 0.58692476, 0.59410431,
0.61950413, 0.6228125 , 0.52444444, 0.59119898, 0.63180975,
0.6592 , 0.6540149 , 0.60133914, 0.67884977, 0.57852447,
0.58739596, 0.52040816, 0.52077562, 0.66304709, 0.59750567,
0.57692308, 0.65189289, 0.64697266, 0.68507561, 0.66874582,
0.67857143, 0.59410431, 0.55953251, 0.63651717, 0.62809917,
0.61111111, 0.6328 , 0.64003673, 0.65140479, 0.65972222,
0.62465374, 0.65384615, 0.64464234, 0.61588954, 0.63111111])}
Model Library
Welcome to the agentpy model library. Below you can find a set of demonstrations on how the package can be used. All of the models are provided as interactive Jupyter Notebooks that can be downloaded and experimented with.
Note
You can download this demonstration as a Jupyter Notebook
here
Wealth transfer
This notebook presents a tutorial for beginners on how to create a simple agent-based model with the agentpy package. It demonstrates how to create a basic model with a custom agent type, run a simulation, record data, and visualize results.
[1]:
# Model design
import agentpy as ap
import numpy as np
# Visualization
import seaborn as sns
About the model
The model explores the distribution of wealth under a trading population of agents. Each agent starts with one unit of wealth. During each time-step, each agents with positive wealth randomly selects a trading partner and gives them one unit of their wealth. We will see that this random interaction will create an inequality of wealth that follows a Boltzmann distribution. The original version of this model been written in MESA and can be found here.
Model definition
We start by defining a new type of Agent
with the following methods:
setup()
is called automatically when a new agent is created and initializes a variablewealth
.wealth_transfer()
describes the agent’s behavior at every time-step and will be called by the model.
[2]:
class WealthAgent(ap.Agent):
""" An agent with wealth """
def setup(self):
self.wealth = 1
def wealth_transfer(self):
if self.wealth > 0:
partner = self.model.agents.random()
partner.wealth += 1
self.wealth -= 1
Next, we define a method to calculate the Gini Coefficient, which will measure the inequality among our agents.
[3]:
def gini(x):
""" Calculate Gini Coefficient """
# By Warren Weckesser https://stackoverflow.com/a/39513799
x = np.array(x)
mad = np.abs(np.subtract.outer(x, x)).mean() # Mean absolute difference
rmad = mad / np.mean(x) # Relative mean absolute difference
return 0.5 * rmad
Finally, we define our `Model
<https://agentpy.readthedocs.io/en/stable/reference_models.html>`__ with the following methods:
setup
defines how many agents should be created at the beginning of the simulation.step
calls all agents during each time-step to perform theirwealth_transfer
method.update
calculates and record the current Gini coefficient after each time-step.end
, which is called at the end of the simulation, we record the wealth of each agent.
[4]:
class WealthModel(ap.Model):
""" A simple model of random wealth transfers """
def setup(self):
self.agents = ap.AgentList(self, self.p.agents, WealthAgent)
def step(self):
self.agents.wealth_transfer()
def update(self):
self.record('Gini Coefficient', gini(self.agents.wealth))
def end(self):
self.agents.record('wealth')
Simulation run
To prepare, we define parameter dictionary with a random seed, the number of agents, and the number of time-steps.
[5]:
parameters = {
'agents': 100,
'steps': 100,
'seed': 42,
}
To perform a simulation, we initialize our model with a given set of parameters and call `Model.run()
<https://agentpy.readthedocs.io/en/stable/reference_models.html>`__.
[6]:
model = WealthModel(parameters)
results = model.run()
Completed: 100 steps
Run time: 0:00:00.124199
Simulation finished
Output analysis
The simulation returns a `DataDict
<https://agentpy.readthedocs.io/en/stable/reference_output.html>`__ with our recorded variables.
[7]:
results
[7]:
DataDict {
'info': Dictionary with 9 keys
'parameters':
'constants': Dictionary with 3 keys
'variables':
'WealthModel': DataFrame with 1 variable and 101 rows
'WealthAgent': DataFrame with 1 variable and 100 rows
}
The output’s info
provides general information about the simulation.
[8]:
results.info
[8]:
{'model_type': 'WealthModel',
'time_stamp': '2021-05-28 09:33:50',
'agentpy_version': '0.0.8.dev0',
'python_version': '3.8.5',
'experiment': False,
'completed': True,
'created_objects': 100,
'completed_steps': 100,
'run_time': '0:00:00.124199'}
To explore the evolution of inequality, we look at the recorded `DataFrame
<https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html>`__ of the model’s variables.
[9]:
results.variables.WealthModel.head()
[9]:
Gini Coefficient | |
---|---|
t | |
0 | 0.0000 |
1 | 0.5370 |
2 | 0.5690 |
3 | 0.5614 |
4 | 0.5794 |
To visualize this data, we can use `DataFrame.plot
<https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.plot.html>`__.
[10]:
data = results.variables.WealthModel
ax = data.plot()

To look at the distribution at the end of the simulation, we visualize the recorded agent variables with seaborn.
[11]:
sns.histplot(data=results.variables.WealthAgent, binwidth=1);

The result resembles a Boltzmann distribution.
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 modelAgent.neighbors()
returns a list of the agents’ peers in the networkrandom.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. """
rng = self.model.random
for n in self.network.neighbors(self):
if n.condition == 0 and self.p.infection_chance > rng.random():
n.condition = 1 # Infect susceptible peer
if self.p.recovery_chance > rng.random():
self.condition = 2 # Recover from infection
Next, we define our model VirusModel
by creating a subclass of Model
.
The four methods of this class will be called automatically at different steps of the simulation,
as described in Running a simulation.
[3]:
class VirusModel(ap.Model):
def setup(self):
""" Initialize the agents and network of the model. """
# Prepare a small-world network
graph = nx.watts_strogatz_graph(
self.p.population,
self.p.number_of_neighbors,
self.p.network_randomness)
# Create agents and network
self.agents = ap.AgentList(self, self.p.population, Person)
self.network = self.agents.network = ap.Network(self, graph)
self.network.add_agents(self.agents, self.network.nodes)
# Infect a random share of the population
I0 = int(self.p.initial_infection_share * self.p.population)
self.agents.random(I0).condition = 1
def update(self):
""" Record variables after setup and each step. """
# Record share of agents with each condition
for i, c in enumerate(('S', 'I', 'R')):
n_agents = len(self.agents.select(self.agents.condition == i))
self[c] = n_agents / self.p.population
self.record(c)
# Stop simulation if disease is gone
if self.I == 0:
self.stop()
def step(self):
""" Define the models' events per simulation step. """
# Call 'being_sick' for infected agents
self.agents.select(self.agents.condition == 1).being_sick()
def end(self):
""" Record evaluation measures at the end of the simulation. """
# Record final evaluation measures
self.report('Total share infected', self.I + self.R)
self.report('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_infection_share': 0.1,
'number_of_neighbors': 2,
'network_randomness': 0.5
}
model = VirusModel(parameters)
results = model.run()
Completed: 77 steps
Run time: 0:00:00.152576
Simulation finished
Analyzing results
The simulation returns a DataDict
of recorded data with dataframes:
[5]:
results
[5]:
DataDict {
'info': Dictionary with 9 keys
'parameters':
'constants': Dictionary with 6 keys
'variables':
'VirusModel': DataFrame with 3 variables and 78 rows
'reporters': DataFrame with 2 variables and 1 row
}
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.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.VirusModel, 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.VirusModel, 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.network.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]:
Multi-run experiment
To explore the effect of different parameter values,
we use the classes Sample
, Range
, and IntRange
to create a sample of different parameter combinations.
[9]:
parameters = {
'population': ap.IntRange(100, 1000),
'infection_chance': ap.Range(0.1, 1.),
'recovery_chance': ap.Range(0.1, 1.),
'initial_infection_share': 0.1,
'number_of_neighbors': 2,
'network_randomness': ap.Range(0., 1.)
}
sample = ap.Sample(
parameters,
n=128,
method='saltelli',
calc_second_order=False
)
We then create an Experiment
that takes a model and sample as input.
Experiment.run()
runs our model repeatedly over the whole sample
with ten random iterations per parameter combination.
[10]:
exp = ap.Experiment(VirusModel, sample, iterations=10)
results = exp.run()
Scheduled runs: 7680
Completed: 7680, estimated time remaining: 0:00:00
Experiment finished
Run time: 0:04:55.800449
Optionally, we can save and load our results as follows:
[11]:
results.save()
Data saved to ap_output/VirusModel_1
[12]:
results = ap.DataDict.load('VirusModel')
Loading from directory ap_output/VirusModel_1/
Loading parameters_constants.json - Successful
Loading parameters_sample.csv - Successful
Loading parameters_log.json - Successful
Loading reporters.csv - Successful
Loading info.json - Successful
The measures in our DataDict
now hold one row for each simulation run.
[13]:
results
[13]:
DataDict {
'parameters':
'constants': Dictionary with 2 keys
'sample': DataFrame with 4 variables and 768 rows
'log': Dictionary with 5 keys
'reporters': DataFrame with 2 variables and 7680 rows
'info': Dictionary with 12 keys
}
We can use standard functions of the pandas library like
pandas.DataFrame.hist()
to look at summary statistics.
[14]:
results.reporters.hist();

Sensitivity analysis
The function DataDict.calc_sobol()
calculates Sobol sensitivity
indices
for the passed results and parameter ranges, using the
SAlib package.
[15]:
results.calc_sobol()
[15]:
DataDict {
'parameters':
'constants': Dictionary with 2 keys
'sample': DataFrame with 4 variables and 768 rows
'log': Dictionary with 5 keys
'reporters': DataFrame with 2 variables and 7680 rows
'info': Dictionary with 12 keys
'sensitivity':
'sobol': DataFrame with 2 variables and 8 rows
'sobol_conf': DataFrame with 2 variables and 8 rows
}
This adds a new category sensitivity to our results, which includes:
sobol
returns first-order sobol sensitivity indicessobol_conf
returns confidence ranges for the above indices
We can use pandas to create a bar plot that visualizes these sensitivity indices.
[16]:
def plot_sobol(results):
""" Bar plot of Sobol sensitivity indices. """
sns.set()
fig, axs = plt.subplots(1, 2, figsize=(8, 4))
si_list = results.sensitivity.sobol.groupby(by='reporter')
si_conf_list = results.sensitivity.sobol_conf.groupby(by='reporter')
for (key, si), (_, err), ax in zip(si_list, si_conf_list, axs):
si = si.droplevel('reporter')
err = err.droplevel('reporter')
si.plot.barh(xerr=err, title=key, ax=ax, capsize = 3)
ax.set_xlim(0)
axs[0].get_legend().remove()
axs[1].set(ylabel=None, yticklabels=[])
axs[1].tick_params(left=False)
plt.tight_layout()
plot_sobol(results)

Alternatively, we can also display sensitivities by plotting average evaluation measures over our parameter variations.
[17]:
def plot_sensitivity(results):
""" Show average simulation results for different parameter values. """
sns.set()
fig, axs = plt.subplots(2, 2, figsize=(8, 8))
axs = [i for j in axs for i in j] # Flatten list
data = results.arrange_reporters().astype('float')
params = results.parameters.sample.keys()
for x, ax in zip(params, axs):
for y in results.reporters.columns:
sns.regplot(x=x, y=y, data=data, ax=ax, ci=99,
x_bins=15, fit_reg=False, label=y)
ax.set_ylim(0,1)
ax.set_ylabel('')
ax.legend()
plt.tight_layout()
plot_sensitivity(results)

Note
You can download this demonstration as a Jupyter Notebook
here
Flocking behavior
This notebook presents an agent-based model that simulates the flocking behavior of animals. It demonstrates how to use the agentpy package for models with a continuous space with two or three dimensions.
[3]:
# Model design
import agentpy as ap
import numpy as np
# Visualization
import matplotlib.pyplot as plt
import IPython
About the model
The boids model was invented by Craig Reynolds, who describes it as follows:
In 1986 I made a computer model of coordinated animal motion such as bird flocks and fish schools. It was based on three dimensional computational geometry of the sort normally used in computer animation or computer aided design. I called the generic simulated flocking creatures boids. The basic flocking model consists of three simple steering behaviors which describe how an individual boid maneuvers based on the positions and velocities its nearby flockmates: - Separation: steer to avoid crowding local flockmates - Alignment: steer towards the average heading of local flockmates - Cohesion: steer to move toward the average position of local flockmates
The model presented here is a simplified implementation of this algorithm, following the Boids Pseudocode written by Conrad Parker.
If you want to see a real-world example of flocking behavior, check out this fascinating video of Starling murmurations from National Geographic:
[4]:
IPython.display.YouTubeVideo('V4f_1_r80RY', width=600, height=350)
[4]:
Model definition
The Boids model is based on two classes, one for the agents, and one for the overall model. For more information about this structure, take a look at the creating models.
Each agent starts with a random position and velocity, which are implemented as numpy arrays. The position is defined through the space environment, which the agent can access via Agent.position()
and Agent.neighbors()
.
The methods update_velocity()
and update_position()
are separated so that all agents can update their velocity before the actual movement takes place. For more information about the algorithm in update_velocity()
, check out the Boids Pseudocode.
[5]:
def normalize(v):
""" Normalize a vector to length 1. """
norm = np.linalg.norm(v)
if norm == 0:
return v
return v / norm
[6]:
class Boid(ap.Agent):
""" An agent with a position and velocity in a continuous space,
who follows Craig Reynolds three rules of flocking behavior;
plus a fourth rule to avoid the edges of the simulation space. """
def setup(self):
self.velocity = normalize(
self.model.nprandom.random(self.p.ndim) - 0.5)
def setup_pos(self, space):
self.space = space
self.neighbors = space.neighbors
self.pos = space.positions[self]
def update_velocity(self):
pos = self.pos
ndim = self.p.ndim
# Rule 1 - Cohesion
nbs = self.neighbors(self, distance=self.p.outer_radius)
nbs_len = len(nbs)
nbs_pos_array = np.array(nbs.pos)
nbs_vec_array = np.array(nbs.velocity)
if nbs_len > 0:
center = np.sum(nbs_pos_array, 0) / nbs_len
v1 = (center - pos) * self.p.cohesion_strength
else:
v1 = np.zeros(ndim)
# Rule 2 - Seperation
v2 = np.zeros(ndim)
for nb in self.neighbors(self, distance=self.p.inner_radius):
v2 -= nb.pos - pos
v2 *= self.p.seperation_strength
# Rule 3 - Alignment
if nbs_len > 0:
average_v = np.sum(nbs_vec_array, 0) / nbs_len
v3 = (average_v - self.velocity) * self.p.alignment_strength
else:
v3 = np.zeros(ndim)
# Rule 4 - Borders
v4 = np.zeros(ndim)
d = self.p.border_distance
s = self.p.border_strength
for i in range(ndim):
if pos[i] < d:
v4[i] += s
elif pos[i] > self.space.shape[i] - d:
v4[i] -= s
# Update velocity
self.velocity += v1 + v2 + v3 + v4
self.velocity = normalize(self.velocity)
def update_position(self):
self.space.move_by(self, self.velocity)
[7]:
class BoidsModel(ap.Model):
"""
An agent-based model of animals' flocking behavior,
based on Craig Reynolds' Boids Model [1]
and Conrad Parkers' Boids Pseudocode [2].
[1] http://www.red3d.com/cwr/boids/
[2] http://www.vergenet.net/~conrad/boids/pseudocode.html
"""
def setup(self):
""" Initializes the agents and network of the model. """
self.space = ap.Space(self, shape=[self.p.size]*self.p.ndim)
self.agents = ap.AgentList(self, self.p.population, Boid)
self.space.add_agents(self.agents, random=True)
self.agents.setup_pos(self.space)
def step(self):
""" Defines the models' events per simulation step. """
self.agents.update_velocity() # Adjust direction
self.agents.update_position() # Move into new direction
Visualization functions
Next, we define a plot function that can take our model and parameters as an input and creates an animated plot with animate()
:
[8]:
def animation_plot_single(m, ax):
ndim = m.p.ndim
ax.set_title(f"Boids Flocking Model {ndim}D t={m.t}")
pos = m.space.positions.values()
pos = np.array(list(pos)).T # Transform
ax.scatter(*pos, s=1, c='black')
ax.set_xlim(0, m.p.size)
ax.set_ylim(0, m.p.size)
if ndim == 3:
ax.set_zlim(0, m.p.size)
ax.set_axis_off()
def animation_plot(m, p):
projection = '3d' if p['ndim'] == 3 else None
fig = plt.figure(figsize=(7,7))
ax = fig.add_subplot(111, projection=projection)
animation = ap.animate(m(p), fig, ax, animation_plot_single)
return IPython.display.HTML(animation.to_jshtml(fps=20))
Simulation (2D)
To run a simulation, we define a dictionary with our parameters:
[9]:
parameters2D = {
'size': 50,
'seed': 123,
'steps': 200,
'ndim': 2,
'population': 200,
'inner_radius': 3,
'outer_radius': 10,
'border_distance': 10,
'cohesion_strength': 0.005,
'seperation_strength': 0.1,
'alignment_strength': 0.3,
'border_strength': 0.5
}
We can now display our first animation with two dimensions:
[10]:
animation_plot(BoidsModel, parameters2D)
[10]:
Simulation (3D)
Finally, we can do the same with three dimensions, a larger number of agents, and a bit more space:
[11]:
new_parameters = {
'ndim': 3,
'population': 1000,
}
parameters3D = dict(parameters2D)
parameters3D.update(new_parameters)
animation_plot(BoidsModel, parameters3D)
[11]: