ANALYSIS TOOL BOX

Simulations

SimulateSchellingSegregation

Simulate Thomas Schelling's model of residential segregation across several tolerance levels.

agent-based-modelsimulationemergencesegregationschelling

Introduction

When you see groups sharply clustered — in neighborhoods, offices, markets, or networks — does that mean individuals strongly prefer to be apart, or that someone coordinated it? Schelling's agent-based model tests whether weak, local preferences alone are enough to produce the pattern. Agents from two or more factions move whenever too many of their neighbors are unlike them, and running the same starting population under several tolerance levels side by side shows where mild preferences tip into strong segregation. An optional tolerance sweep maps the full preference-to-outcome curve, so you can find the tipping points a single scenario would hide.

Teaching Note

Schelling's central insight is that you cannot read individual motives off aggregate patterns. In his model, no agent wants a segregated neighborhood — an agent with a tolerance of 0.5 is perfectly content being in the minority half the time — yet the population as a whole sorts itself into sharply divided clusters. Small, reasonable-seeming preferences, applied locally and repeatedly, compound through feedback: when one agent leaves, the faction balance of the neighborhood it left shifts, making the remaining agents more likely to leave as well. This is emergence: a collective outcome that no individual chose and that is far more extreme than any individual's preference.

The model also shows that the relationship between preference and outcome is non-linear. At very high tolerance nobody moves and the grid stays mixed. At very low tolerance nobody can ever be satisfied, so agents churn endlessly without settling into stable clusters. The strongest segregation often appears at moderate-to-low tolerances, where enough agents settle to anchor clusters while the rest keep moving toward them. Comparing several tolerances side by side — or sweeping across the full range — reveals tipping points that any single scenario would hide.

For analysts, the lesson is a caution against the "ecological fallacy" in reverse: observing a highly clustered pattern (of people, organizations, or activity) is not evidence of strong preferences or deliberate coordination. Weak local rules can be sufficient. Agent-based models like this one are a way to test whether a proposed micro-level mechanism is capable of producing the macro-level pattern you observe.

Parameters

ParameterTypeDefaultDescription
tolerancessequence of float(0.0, 0.25, 0.5, 0.75, 1.0)The tolerance levels to compare, each between 0 and 1. Tolerance is the largest share of unlike neighbors an agent will accept before moving: 0.0 means a single unlike neighbor is too many, 1.0 means any mix is fine (nobody ever moves). Schelling's "similarity threshold" is 1 - tolerance. Each tolerance gets its own column in the plot.
grid_shapetuple of int(25, 25)The shape of the grid: (length,) for a 1-D row or (rows, columns) for a 2-D grid. Ignored if starting_grid is provided. Defaults to (25, 25).
empty_fractionfloat0.1The share of cells left empty (0 to less than 1). Agents can only move if empty cells exist. Ignored if starting_grid is provided. Defaults to 0.1.
number_of_factionsint2The number of distinct factions (groups) of agents. Must be at least 2 and no more than 50% of the smaller grid dimension (or of the length of a 1-D row). Defaults to 2.
faction_fractionssequence of floatNoneThe share of occupied cells assigned to each faction. Must have one entry per faction and sum to 1. If None, occupied cells are split evenly. Ignored if starting_grid is provided. Defaults to None.
starting_gridarray-likeNoneA hand-built 1-D or 2-D array of integers to start from, where 0 is an empty cell and 1 to number_of_factions identify each faction. Overrides grid_shape, empty_fraction, and faction_fractions. Defaults to None.
neighborhoodstr'moore'Which surrounding cells count as neighbors on a 2-D grid: 'moore' (the 8 surrounding cells, including diagonals) or 'von_neumann' (the 4 cells sharing an edge). On a 1-D row both options use the 2 adjacent cells. Defaults to 'moore'.
wrap_edgesboolFalseWhether the grid wraps around (a torus), so cells on one edge neighbor cells on the opposite edge. Requires every grid dimension to be at least 3. Defaults to False.
max_stepsint300The maximum number of steps to run each simulation before stopping. Defaults to 300.
random_seedint412The seed for the random number generator to ensure replicability. Defaults to 412.
include_tolerance_sweepboolFalseWhether to also run the model across a fine range of tolerances (over several seeds) and add a panel showing final similarity versus tolerance. This runs many extra simulations. Defaults to False.
sweep_tolerancessequence of floatNoneThe tolerance levels used in the sweep. If None, uses 21 evenly spaced values from 0 to 1. Defaults to None.
sweep_number_of_seedsint3The number of random seeds averaged at each sweep tolerance. Defaults to 3.
return_formatstr'dataframe'The format of the returned data: 'dataframe' (summary table only) or 'dict' (summary table plus grids, per-step histories, and sweep results). Defaults to 'dataframe'.
plot_simulation_resultsboolTrueWhether to display the combined figure of grids and histories. Defaults to True.
faction_labelssequence of strNoneDisplay names for each faction, used in the legend. If None, uses 'Faction 1', 'Faction 2', etc. Defaults to None.
faction_colorssequence of strNoneHex color codes for each faction. If None, a colorblind-friendly palette is used for up to 10 factions and an evenly spaced hue palette beyond that. Defaults to None.
empty_colorstr'#FFFFFF'The hex color code for empty cells. Defaults to '#FFFFFF'.
show_cell_bordersboolTrueWhether to draw faint lines between grid cells. Defaults to True.
figure_sizetupleNoneThe size of the figure in inches (width, height). If None, it is sized automatically from the number of tolerances. Defaults to None.
title_for_plotstr'Schelling Segregation Model'The main title for the figure. Defaults to 'Schelling Segregation Model'.
subtitle_for_plotstr'How individual tolerance for unlike neighbors shapes neighborhood segregation'The descriptive subtitle for the figure.
caption_for_plotstrNoneOptional caption text displayed at the bottom of the figure. Defaults to None.
data_source_for_plotstrNoneOptional data source identification text. Defaults to None.

Returns

With return_format='dataframe', a summary table with one row per tolerance and the columns Tolerance, Similarity Threshold, Converged, Steps, Initial Similarity, Final Similarity, and Final Unhappy Agents (similarity is the average share of an agent's neighbors from its own faction). With return_format='dict', a dictionary holding that summary, per-tolerance results (the initial and final grids plus similarity_history and unhappy_history), and a sweep DataFrame (or None if no sweep was run).

Example

python
from analysistoolbox.simulations import SimulateSchellingSegregation

# Urban planning: compare five tolerance levels on a 25 x 25 neighborhood
summary = SimulateSchellingSegregation(
    tolerances=(0.0, 0.25, 0.5, 0.75, 1.0),
    grid_shape=(25, 25),
    empty_fraction=0.1
)
print(summary[['Tolerance', 'Initial Similarity', 'Final Similarity', 'Converged']])

# Sociology: three groups of unequal size on a wrapped grid, with a tolerance sweep
results = SimulateSchellingSegregation(
    tolerances=(0.3, 0.5, 0.7),
    grid_shape=(40, 40),
    number_of_factions=3,
    faction_fractions=(0.5, 0.3, 0.2),
    faction_labels=('Majority', 'Minority 1', 'Minority 2'),
    wrap_edges=True,
    include_tolerance_sweep=True,
    return_format='dict'
)