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.
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
| Parameter | Type | Default | Description |
|---|---|---|---|
tolerances | sequence 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_shape | tuple 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_fraction | float | 0.1 | The 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_factions | int | 2 | The 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_fractions | sequence of float | None | The 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_grid | array-like | None | A 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. |
neighborhood | str | '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_edges | bool | False | Whether 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_steps | int | 300 | The maximum number of steps to run each simulation before stopping. Defaults to 300. |
random_seed | int | 412 | The seed for the random number generator to ensure replicability. Defaults to 412. |
include_tolerance_sweep | bool | False | Whether 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_tolerances | sequence of float | None | The tolerance levels used in the sweep. If None, uses 21 evenly spaced values from 0 to 1. Defaults to None. |
sweep_number_of_seeds | int | 3 | The number of random seeds averaged at each sweep tolerance. Defaults to 3. |
return_format | str | '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_results | bool | True | Whether to display the combined figure of grids and histories. Defaults to True. |
faction_labels | sequence of str | None | Display names for each faction, used in the legend. If None, uses 'Faction 1', 'Faction 2', etc. Defaults to None. |
faction_colors | sequence of str | None | Hex 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_color | str | '#FFFFFF' | The hex color code for empty cells. Defaults to '#FFFFFF'. |
show_cell_borders | bool | True | Whether to draw faint lines between grid cells. Defaults to True. |
figure_size | tuple | None | The size of the figure in inches (width, height). If None, it is sized automatically from the number of tolerances. Defaults to None. |
title_for_plot | str | 'Schelling Segregation Model' | The main title for the figure. Defaults to 'Schelling Segregation Model'. |
subtitle_for_plot | str | 'How individual tolerance for unlike neighbors shapes neighborhood segregation' | The descriptive subtitle for the figure. |
caption_for_plot | str | None | Optional caption text displayed at the bottom of the figure. Defaults to None. |
data_source_for_plot | str | None | Optional 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
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'
)