Overview of Schelling’s Model

The Nobel prize winning economist Thomas Schelling developed a model that shows how global patterns of spatial segregation can arise from the effect of homophily operating at a local level. Homophily is the principle that we tend to be similar to our friends and/or neighbors.

T. Schelling. Dynamic models of segregation. The Journal of Mathematical Sociology, 1(2), 143-186, 1971.

See also,

  • J. M. Sakoda, The checkerboard model of social interaction. The Journalof Mathematical Sociology 1(1), 119-132, 1971.

  • R. Hegselmann, Thomas C. Schelling and James M. Sakoda: The intellectual, technical, and social history of a model. Journal of Artificial Societies and Social Simulation 20 (3), 2017.

There are two components of the Schelling model:

  1. A grid representing different locations for the agents

  2. A set of agents with two properties:

    1. The agent type (e.g., ‘red’ or ‘blue’)

    2. The current position of the agent in the grid

An example of a grid with two types of agents:

grid.jpg

At each round of the simulation:

  1. For each agent \(a\), determine the number of neighbors that are of the same type.

  2. Agent \(a\) is happy if the number of similar agents is above a fixed threshold.

  3. If \(a\) is not happy, then \(a\) moves to an empty location.

Continue that process for a fixed number of rounds or until every agent is happy.

Implementing Schelling’s Model using Mesa

from mesa import Model, Agent
from mesa.time import RandomActivation
from mesa.space import SingleGrid
from mesa.datacollection import DataCollector
class SchellingAgent(Agent):
    '''
    Schelling segregation agent
    '''
    def __init__(self, unique_id, pos, model, agent_type):
        '''
         Create a new Schelling agent.
         Args:
            pos: Agent initial location.
            agent_type: Indicator for the agent's type (minority=1, majority=0)
        '''
        super().__init__(unique_id, model)
        self.pos = pos
        self.type = agent_type

    def step(self):
        similar = 0
        neighbors = self.model.grid.neighbor_iter(self.pos)
        for neighbor in neighbors:
            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
class SchellingModel(Model):
    '''
    Model class for the Schelling segregation model.
    '''
    def __init__(self, height, width, density, minority_percent, homophily, seed = None):
        
        self.height = height
        self.width = width
        self.density = density
        self.minority_percent = minority_percent
        self.homophily = homophily

        self.schedule = RandomActivation(self)
        self.grid = SingleGrid(height, width, torus=True)

        self.happy = 0
        self.datacollector = DataCollector(
            {"happy": lambda m: m.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]})
        self.running = True
        # Set up agents
        agent_id = 0
        for cell in self.grid.coord_iter():
            #print(cell)
            _,x,y = cell
            if self.random.random() < self.density:
                if self.random.random() < self.minority_percent:
                    agent_type = 1
                else:
                    agent_type = 0
                agent = SchellingAgent(agent_id, (x, y), self, agent_type)
                agent_id += 1
                self.grid.position_agent(agent, x=x, y=y)
                self.schedule.add(agent)
        
    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()
        self.datacollector.collect(self)
        if self.happy == self.schedule.get_agent_count():
            self.running = False

Create a model instance: a 10x10 grid, a 10% chance of an agent being placed in each cell, approximately 20% of agents set as minorities, and each agent wants at least 3 similar neighbors.

height, width = 10, 10
density = 0.1
minority_percent = 0.2
homophily = 4
model = SchellingModel(height, width, density, minority_percent, homophily)
print("Display the first 5 agents:\n")
for a in model.schedule.agents[0:5]: 
    print(a)
    print("type is ", a.type)
    print(f"pos is {a.pos}")
    print(f"unique id is {a.unique_id}\n")
Display the first 5 agents:

<__main__.SchellingAgent object at 0x151fbf4c0>
type is  0
pos is (1, 6)
unique id is 0

<__main__.SchellingAgent object at 0x151fbf460>
type is  0
pos is (5, 0)
unique id is 1

<__main__.SchellingAgent object at 0x151fbf550>
type is  0
pos is (8, 3)
unique id is 2

<__main__.SchellingAgent object at 0x151fbf100>
type is  1
pos is (9, 8)
unique id is 3
# execute one round of the models
model.step()

# some positions should change
for a in model.schedule.agents[0:5]: 
    print(a)
    print("type is ", a.type)
    print(f"pos is {a.pos}")
    print(f"unique id is {a.unique_id}")
    print(f"model.happy = {model.happy}\n")
<__main__.SchellingAgent object at 0x151fbf4c0>
type is  0
pos is (1, 7)
unique id is 0
model.happy = 0

<__main__.SchellingAgent object at 0x151fbf460>
type is  0
pos is (1, 9)
unique id is 1
model.happy = 0

<__main__.SchellingAgent object at 0x151fbf550>
type is  0
pos is (5, 1)
unique id is 2
model.happy = 0

<__main__.SchellingAgent object at 0x151fbf100>
type is  1
pos is (1, 4)
unique id is 3
model.happy = 0

Note that on line 17 of the definition of SchellingModel we have the following code:

self.schedule = RandomActivation(self)

This activates the agents one at a time in random order with the order reshuffled every step of the model.

See the source code for details.

# To illustrate the RandomAcitivation scheduler, note that running this
# multiple times will produce different orders of the agents

print([a.unique_id for a in model.schedule.agent_buffer(shuffled=True)])
[3, 1, 2, 0]

Note that line 18 of the definition of the SchellingModel has the following code:

self.grid = SingleGrid(height, width, torus=True)

This defines a grid to place the agents.

See the course code for details.

# create a simple Schelling model with a 3x3 grid
model2 = SchellingModel(3, 3, 0, 0.2, 4)

# each cell is a tuple where the first component is the agent, 
# the second component is the x position and 
# the 3rd component is the y position
for cell in model2.grid.coord_iter():
    print(cell)
(None, 0, 0)
(None, 0, 1)
(None, 0, 2)
(None, 1, 0)
(None, 1, 1)
(None, 1, 2)
(None, 2, 0)
(None, 2, 1)
(None, 2, 2)
a1 = SchellingAgent(0, (1, 1), model2, 0)

# initially position the agent at 1,1
model2.grid.position_agent(a1, x=1, y=1)

for cell in model2.grid.coord_iter():
    print(cell)
print(f"\na1 pos is {a1.pos}")
(None, 0, 0)
(None, 0, 1)
(None, 0, 2)
(None, 1, 0)
(<__main__.SchellingAgent object at 0x151fc77c0>, 1, 1)
(None, 1, 2)
(None, 2, 0)
(None, 2, 1)
(None, 2, 2)

a1 pos is (1, 1)
# now move a1 to an empty location
model2.grid.move_to_empty(a1)

for cell in model2.grid.coord_iter():
    print(cell)
print(f"\na1 pos is {a1.pos}")
(<__main__.SchellingAgent object at 0x151fc77c0>, 0, 0)
(None, 0, 1)
(None, 0, 2)
(None, 1, 0)
(None, 1, 1)
(None, 1, 2)
(None, 2, 0)
(None, 2, 1)
(None, 2, 2)

a1 pos is (0, 0)
model2 = SchellingModel(3, 3, 0, 0.2, 4)
model2.grid = SingleGrid(3, 3, torus=True)

a1 = SchellingAgent(1, (1, 1), model2, 0)
a2 = SchellingAgent(2, (1, 0), model2, 0)
a3 = SchellingAgent(3, (0, 0), model2, 0)
a4 = SchellingAgent(4, (2, 2), model2, 0)

model2.grid.position_agent(a1, x=1, y=1)
model2.grid.position_agent(a2, x=1, y=0)
model2.grid.position_agent(a3, x=0, y=0)
model2.grid.position_agent(a4, x=2, y=2)

print("The neighbors of a1 are: ")
for n in model2.grid.neighbor_iter(a1.pos):
    print(f"a{n.unique_id} at {n.pos}")
    
print("The neighbors of a3 are: ")
for n in model2.grid.neighbor_iter(a3.pos):
    print(f"a{n.unique_id} at {n.pos}")
The neighbors of a1 are: 
a3 at (0, 0)
a2 at (1, 0)
a4 at (2, 2)
The neighbors of a3 are: 
a4 at (2, 2)
a2 at (1, 0)
a1 at (1, 1)

Instatiate a model instance: a 10x10 grid, with an 80% chance of an agent being placed in each cell, approximately 20% of agents set as minorities, and agents wanting at least 3 similar neighbors. Run the model at most 100 times.

height, width = 50, 50
density = 0.8
minority_percent = 0.3
homophily = 4
model = SchellingModel(height, width, density, minority_percent, homophily)

while model.running and model.schedule.steps < 1000:
    model.step()
print(f"The model ran for {model.schedule.steps} steps") # Show how many steps have actually run
The model ran for 1000 steps

Lines 21 - 24 of the definition of the SchellingModel has the following code:

self.datacollector = DataCollector(
{"happy": lambda m: m.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]})

This code is called in the step() method on line 47:

self.datacollector.collect(self)

The DataCollector is a simple, standard way to collect data generated by a Mesa model. It collects three types of data: model-level data, agent-level data, and tables.

See the documentation for details.

Use the method get_model_vars_dataframe to get the model-level data after running the model.

Use the method get_agent_vars_dataframe to get the agent-level data after running the model.

Both returna a Pandas dataframe. Pandas is a popular tool for data analysis and manipulation.

import pandas as pd 

data = {
    "var1": [1, 2, 3, 4, 5], 
    "var2": ["a", "b", "c", "d", "e"],
    "var3": [1, None, "a", 2.0, "c"]
}

df = pd.DataFrame(data)

df
var1 var2 var3
0 1 a 1
1 2 b None
2 3 c a
3 4 d 2.0
4 5 e c

We will discuss Pandas in more detail later in the text. For now, see the 10-minute introduction to Pandas.

model_out = model.datacollector.get_model_vars_dataframe()
model_out
happy
0 1194
1 1459
2 1593
3 1685
4 1725
... ...
995 1993
996 1993
997 1993
998 1993
999 1993

1000 rows × 1 columns

# use describe() to get basic statistics about the data
model_out.describe()
happy
count 1000.000000
mean 1979.783000
std 39.494122
min 1194.000000
25% 1984.000000
50% 1987.000000
75% 1991.000000
max 1994.000000
import seaborn as sns
sns.set()

model_out.happy.plot();
../_images/01-schelling_27_0.png
agent_out = model.datacollector.get_agent_vars_dataframe()
agent_out.head()
x y
Step AgentID
1 0 0 0
1 0 1
2 0 2
3 0 3
4 0 4

Exploring the Parameter Space

from mesa.batchrunner import BatchRunner

def get_segregation(model):
    '''
    Find the % of agents that only have neighbors of their same type.
    '''
    segregated_agents = 0
    for agent in model.schedule.agents:
        segregated = True
        for neighbor in model.grid.neighbor_iter(agent.pos):
            if neighbor.type != agent.type:
                segregated = False
                break
        if segregated:
            segregated_agents += 1
    return segregated_agents / model.schedule.get_agent_count()
variable_params = {"homophily": range(1,9)}
fixed_params =  {"height": 10, "width": 10, "density": 0.8, "minority_percent": 0.2} 
model_reporters = {"Segregated_Agents": get_segregation}
param_sweep = BatchRunner(SchellingModel, 
                          variable_params, 
                          fixed_params, 
                          iterations=10, 
                          max_steps=200, 
                          model_reporters=model_reporters, 
                          display_progress=False)
param_sweep.run_all()
df = param_sweep.get_model_vars_dataframe()
df
homophily Run Segregated_Agents height width density minority_percent
0 1 0 0.118421 10 10 0.8 0.2
1 1 1 0.470588 10 10 0.8 0.2
2 1 2 0.192308 10 10 0.8 0.2
3 1 3 0.373494 10 10 0.8 0.2
4 1 4 0.443038 10 10 0.8 0.2
... ... ... ... ... ... ... ...
75 8 75 0.220779 10 10 0.8 0.2
76 8 76 0.123457 10 10 0.8 0.2
77 8 77 0.263158 10 10 0.8 0.2
78 8 78 0.340909 10 10 0.8 0.2
79 8 79 0.243902 10 10 0.8 0.2

80 rows × 7 columns

import matplotlib.pyplot as plt

plt.scatter(df.homophily, df.Segregated_Agents)
plt.grid(True)
../_images/01-schelling_34_0.png

Additional Reading