Introduction
Mesa is an Agent-based Modeling module in Python and an alternative to NetLogo or other applications for agent-based modeling Agent-based modeling has been extensively used in domains such as social, biology, and network sciences. As mentioned in the previous post agent-based modeling relies on simulating autonomous agents and their interactions with and within the environment to evaluate emergent phenomena and projections within complex system. Therefore, the aim of ABM is to obtain explanatory insight on how the agents will behave given a particular set of rules. This article covers the first steps to start an agent-based modeling project using an open-source python module called Mesa.
Schelling Segregation Model
This tutorial first will focus the Schelling Segregation model, a use-case of agent-based modeling to explain why racial segregation issue is difficult to be eradicated. Although the actual model is quite simple, it provides explanatory insights at how individuals might self-segregate even though when they have no explicit desire to do so. Let’s have a look at the explanation for this model provided by the Mesa official github page:
The Schelling segregation model is a classic agent-based model, demonstrating how even a mild preference for similar neighbors can lead to a much higher degree of segregation than we would intuitively expect. The model consists of agents on a square grid, where each grid cell can contain at most one agent. Agents come in two colors: red and blue. They are happy if a certain number of their eight possible neighbors are of the same color, and unhappy otherwise. Unhappy agents will pick a random empty cell to move to each step, until they are happy. The model keeps running until there are no unhappy agents. By default, the number of similar neighbors the agents need to be happy is set to 3. That means the agents would be perfectly happy with a majority of their neighbors being of a different color (e.g. a Blue agent would be happy with five Red neighbors and three Blue ones). Despite this, the model consistently leads to a high degree of segregation, with most agents ending up with no neighbors of a different color.
The code for this model is simple we will go through each part one-by-one to have a clearer understanding of how agent-based modeling works. The procedure includes steps such as Initializing the agent class, creating a step function, extracting the number of similar neighbors and moving the agent to an empty location if the agent is unhappy.
First we upload the necessary modules
from mesa import Model, Agent
from mesa.time import RandomActivation
from mesa.space import SingleGrid
from mesa.datacollection import DataCollector
import matplotlib.pyplot as plt
%matplotlib inline
The system will consists of at least a basic agent class and a model class.
Let’s start by writing the agent class first. We will need to define 2 main variables of position and agent type and a dependency variable to model to get access to the variables specified in the model.
class SchellingAgent(Agent):
"""
Schelling segregation agent
"""
def __init__(self, pos, model, agent_type):
"""
Create a new Schelling agent.
Args:
unique_id: Unique identifier for the agent.
x, y: Agent initial location.
agent_type: Indicator for the agent's type (minority=1, majority=0)
"""
super().__init__(pos, model)
self.pos = pos
self.type = agent_type
def step(self):
similar = 0
for neighbor in self.model.grid.neighbor_iter(self.pos):
if neighbor.type == self.type:
similar += 1
# If unhappy, move:
if similar < self.model.homophily:
self.model.grid.move_to_empty(self)
else:
self.model.happy += 1
Then we develop model class, here we will need to define 5 main variables:
Width: Horizontal axis of the grid which is used together with Height to define the total number of agents in the system.
Height: Vertical axis of the grid which is used together with Width to define the total number of agents in the system.
Density: Define the population density of agent in the system. Floating value from 0 to 1.
Fraction minority: The ratio between blue and red. Blue is represented as the minority while red is represented as the majority. Floating value from 0 to 1. If the value is higher than 0.5, blue will become the majority instead.
Homophily: Define the number of similar neighbors required for the agents to be happy. Integer value range from 0 to 8 since you can only be surrounded by 8 neighbors.
class Schelling(Model):
"""
Model class for the Schelling segregation model.
"""
def __init__(self, height=20, width=20, density=0.8, minority_pc=0.2, homophily=3):
""" """
self.height = height
self.width = width
self.density = density
self.minority_pc = minority_pc
self.homophily = homophily
self.schedule = RandomActivation(self)
self.grid = SingleGrid(width, height, torus=True)
self.happy = 0
self.datacollector = DataCollector(
{"happy": "happy"}, # Model-level count of happy agents
# For testing purposes, agent's individual x and y
{"x": lambda a: a.pos[0], "y": lambda a: a.pos[1], "type": lambda a: a.type , "happ": lambda a: a.model.happy},
)
# Set up agents
# We use a grid iterator that returns
# the coordinates of a cell as well as
# its contents. (coord_iter)
for cell in self.grid.coord_iter():
x = cell[1]
y = cell[2]
if self.random.random() < self.density:
if self.random.random() < self.minority_pc:
agent_type = 1
else:
agent_type = 0
agent = SchellingAgent((x, y), self, agent_type)
self.grid.position_agent(agent, (x, y))
self.schedule.add(agent)
self.running = True
self.datacollector.collect(self)
def step(self):
"""
Run one step of the model. If All agents are happy, halt the model.
"""
self.happy = 0 # Reset counter of happy agents
self.schedule.step()
# collect data
self.datacollector.collect(self)
if self.happy == self.schedule.get_agent_count():
self.running = False
Thereafter we will need to set the model specification, this can be only for one model or a batch of models.
model = Schelling(10, 10, 0.8, 0.2, 3)
Scheduler: Next up, we will need to have a scheduler. The scheduler is a special model component which controls the order in which agents are activated. The most common scheduler is the random activation which activates all the agents once per step, in random order. There is also another type called Simultaneous activation. Check out the API reference to find out more.
while model.running and model.schedule.steps < 100:
model.step()
print(model.schedule.steps) # Show how many steps have actually run
Data Collection: Data collection is essential to ensure that we obtained the necessary data after each step of the simulation. You can use the built-in data collection module. In this case, we only need to know whether the agent is happy or not.
model_out = model.datacollector.get_model_vars_dataframe()
model_out.head()
This option is also available for agents.
agent_df = model.datacollector.get_agent_vars_dataframe()
agent_df.head()
Infection model
Mesa Infection model provides the basic building blocks for an Agent Based free movement, contagion, and epidemic model. Three version of this model are up on my github and hope is others will improve upon it aid in understanding and decision making for any kind of movement/connection/contagion. For more advanced COVID related module you can also see Mesa SIR module.
Here we present the most simple model including free movement and interaction, as above first we load required packages
%matplotlib notebook
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from mesa import Agent, Model
from mesa.time import RandomActivation
from mesa.space import ContinuousSpace
import mesa
from mesa.datacollection import DataCollector
import numpy as np
from statistics import mean
import pandas as pd
import collections
This is function to be used by the agents.
def flatten(x):
if isinstance(x, collections.Iterable):
return [a for i in x for a in flatten(i)]
else:
return [x]
Again we start by writing the agent class first. We will need to define main variables including name, position, and neighbors at each step as well as a dependency variable to model to get access to the variables specified in the model.
class MyAgent(Agent):
def __init__(self, name, model,pos,dest=None, choi= None):
super().__init__(name, model)
self.name = name
self.pos= pos
self.neigh=None
self.neigh_names=None
self.heading=None
self.dest=dest
self.choices = choi
self.posl1=None
def neigh_namer(self, neigh):
me = self.name
them = (n.name for n in neigh)
Names = []
for other in them:
Names.append(other)
return Names
def dest_adjuster(self):
m1 = self.heading
if m1==(0,0):
kk=np.array([[10,90],[55,55],[90,10],[10,10]])
jj=kk[np.random.choice(len(kk),1)]
m2=np.array((jj[0][0],jj[0][1]))
else:
m2=self.dest
return m2
def step(self):
neigh = self.model.space.get_neighbors(self.pos, 3, False)
self.neigh=neigh
self.neigh_names=self.neigh_namer(neigh)
self.heading=self.model.space.get_heading(self.pos, self.dest)
self.posl1=self.pos
self.dest=self.dest_adjuster()
new_step =[round(x/y) for x, y in zip([ x for x in self.heading], [ abs(x)+0.000001 for x in self.heading])]
new_pos = (self.pos[0]+new_step[0],self.pos[1]+new_step[1])
print("{} activated".format(self.name))
self.model.space.move_agent(self, new_pos)
Then in the model class we specify the grid size, agent headings, and he information to be collected in data collectors.
class MyModel(Model):
def __init__(self, n_agents,xylim=200, prob=0.05,):
super().__init__()
self.schedule = RandomActivation(self)
self.space = ContinuousSpace(xylim, xylim, torus=False)
for i in range(n_agents):
pos = (self.random.randrange(0, 100), self.random.randrange(0, 100))
choices=np.array([[90,90],[55,55],[90,10],[10,10]])
choi=choices[np.random.choice(len(choices),1)]
dest=np.array((choi[0][0],choi[0][1]))
a = MyAgent(i, self,pos, dest,choi)
self.schedule.add(a)
self.space.place_agent(a, pos)
self.dest=dest
self.choices=choi
self.dc = DataCollector(model_reporters={"agent_count":
lambda m: m.schedule.get_agent_count()},
#agent_reporters={"neigh": lambda a: [a.neigh, a.name] }
agent_reporters={"neigh": lambda a: a.neigh_names,
"name": lambda a: a.name,
"pos": lambda a: a.pos,
"posl1": lambda a: a.posl1,
"heading": lambda a: a.heading,
"choice": lambda a: a.choices}
)
def step(self):
self.schedule.step()
self.dc.collect(self)
Here we specify the number of agents and steps.
nagent=30
nstep=200
Here instead of using scheduler with random activation which activates all the agents once per step, in random order we just call the step function in a loop.
model = MyModel(nagent)
for t in range(nstep):
model.step()
model_df = model.dc.get_model_vars_dataframe()
agent_df = model.dc.get_agent_vars_dataframe()
model_df
Similar to above you can use the built-in data collection module to get access to model level or agent level data.
model_df = model.dc.get_model_vars_dataframe()
agent_df = model.dc.get_agent_vars_dataframe()
model_df