A recent article in the 1 November 2024, Daily Skimm discussed an article in the New York Times which analysed the movement of voters in the United States. The article found that voters are increasingly moving to neighbourhoods where the majority of residents share their political views.
This change leading to increased polarisation (not to mention that some do not think being an adjudicated rapist is a barrier to being elected) are in the United States.
There are a number of models to look at how segregation may appear with only mild preferences in the individuals in the system. One of these is Schelling Model which is an agent-based model where:
By default, the number of similar neighbours the agents need to be happy is set to 3 and they have up to 8 possible neighbours (Moore neighbourhood). That means the agents would be perfectly happy with a majority of their neighbours being of a different color (e.g. a Blue agent would be happy with five Red neighbours and three Blue ones). Despite this, the model consistently leads to a high degree of segregation, with most agents ending up with no neighbours of a different color.
Before we start implementing the model, you should seriously have a look at the blog entry created by Vi Hart and Nicky Case. Vi Hart produces amazing content and well worth following
Finally, if you are interested in reading the original paper on this see Schelling, Thomas C. Dynamic Models of Segregation. Journal of Mathematical Sociology. 1971, Vol. 1, pp 143-186.
The Schelling model traditionally talked about agents having different colours. I switched to state so my implementation is more similar to the Game of Life and Fire model. The Agent
class has a state
attribute that is either JETS
or SHARKS
. (I did now want to use RED
and BLUE
.)
There are two differences between this model and the previous models:
To move an agent to a random empty location we can use the following code:
1 |
|
to move an agent to a random location with at least one neighbour of the same state we could do something like the following:
1 2 3 4 |
|
Agent
and Model
Agent
classstate
which is JETS
, or SHARKS
. This corresponds to the colour of the agent in the traditional Schelling model.get_neighbors
to return a list of all neighbours.is_happy
to check if the agent is happy.step
to compute its behaviour based on its state
and the state
of its neighbours.1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Model
classThe Model
class stores the model parameters,
the model geometry (space/grid),
and the agents in the model.
The basic structure of this class is
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Agent
classThe get_neighbors
method:
The is_happy
method:
model.pr_same
.The step
method:
self.model.grid.move_to_empty
method.Model
classThe Model
class has parameters width
, height
, density
, and pr_same
. The density
parameter is the proportion of cells that are occupied by agents.
The pr_minority
is the size of the minority class, JETS
,
and the pr_same
parameter is the proportion of neighbours that need to have the same state as the agent for the agent to be happy.
The model space is a SingleGrid
with toroidal boundary conditions. This the same as that used in the Game of Life.
Creating the agents is done using a loop similar to the fire model by iterating over the grid locations and create an agent (with probability density
). When creating agent we set its state
to JETS
based on pr_minority
.
We will add stuff here later, when we add data collection facilities.
The visualisation and animation is practically the same as in the Fire Model, the only addition is the letter X
for agents that are not happy.
The mesa library has a DataCollector
class that can be used to collect data during the simulation. We will add a data collector to collect the number of happy agents at each step. This will allow us to plot the number of happy agents over time.
running
flag to Model
classIn Model.__init__
, in "# start simulation" section we set running
flag to True
, to indicate that the model is still running. This flag will be set to False
when there are no unhappy agents.
In Model.step
we first check to see if the model is still running, if not we return immediately. Otherwise we perform the model step. Then check if there are any unhappy agents, if not we set the running
flag to False
.
1 2 3 4 5 6 |
|
Now, with running
flag implemented we can run a simulation as follows
1 2 3 |
|
move
counter variable to Model
classInstead of (or in addition to) counting the number of unhappy agents at each step, we can add a counter variable to the Model
class that is incremented each time an agent is moves. This counter is reset at start of each Model.step
call. So:
In Model.__init__
, in "# start simulation" section we set moved
counter to zero.
In Model.step
we first reset the moved
counter to zero.
Agent.step
method.running
flag to False
, when moved
is zero at end of the step. So simulation stops when no agents are moved instead of when no agents are unhappy. So we use1 |
|
To use the DataCollector
class, we create an instance of it in the Model.__init__
method. Where we list the information we want to collect. We can use the DataCollector
to collect the number of steps, the number of agents that have moved, and the number of unhappy agents at each step.
1 2 3 4 5 6 7 |
|
Then in Model.__init__
we add the following line to collect the data:
1 |
|
Finally, in the Model.step
method we add the following line to collect the data at each step:
1 |
|
The DataCollector
class collects the data in a pandas
DataFrame. We can access the data using the get_model_vars_dataframe
method. This method returns a DataFrame with the data collected.
1 2 |
|
steps | moved | unhappy | |
---|---|---|---|
0 | 0 | 0 | 48 |
1 | 1 | 43 | 31 |
2 | 2 | 31 | 23 |
3 | 3 | 22 | 14 |
4 | 4 | 17 | 13 |
We can plot the data using the matplotlib
library. For example, we can plot the number of unhappy agents and the number moved over time using the following code:
1 2 3 4 5 |
|
This will produce a plot similar to the following:
The mesa library also has a BatchRunner
class that can be used to run multiple simulations with different parameters.
For example, we can use the BatchRunner
to run the Schelling model with different values of the density
parameter. So we could write
1 |
|
and then run the simulations using the BatchRunner
class using:
1 2 3 4 5 6 7 8 |
|
This code creates a DataFrame with the results of the simulations. The DataFrame has a row for each simulation and a column for each parameter and model variable that was collected.
1 |
|
RunId | iteration | Step | width | height | density | steps | moved | unhappy | |
---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 10 | 10 | 0.1 | 1 | 3 | 0 |
1 | 1 | 0 | 1 | 10 | 10 | 0.15 | 1 | 1 | 0 |
2 | 2 | 0 | 3 | 10 | 10 | 0.2 | 3 | 1 | 0 |
3 | 3 | 0 | 6 | 10 | 10 | 0.25 | 6 | 1 | 0 |
4 | 4 | 0 | 4 | 10 | 10 | 0.3 | 4 | 2 | 0 |
We could generate a boxplot of the number of steps in the simulation by density using the following code:
1 2 3 4 5 6 7 |
|
This will produce a plot similar to the following:
As the population density increases the number of steps in the simulation needed before agents are all happy increases.