Mild preferences, strong sorting
Thomas Schelling's point in 1971 was about the gap between what individuals want and what the group gets. In the default run every agent is content as long as at least 30% of its neighbors are like itself: each would happily live as a minority, a preference compatible with a thoroughly mixed grid. The random starting grid is already close to that: most agents are content from the start. Yet the few discontent agents move, their departure and arrival tip other agents' neighborhoods over the threshold, and the process cascades until the grid is made of large single-color patches.
- Watch the pale cells. They mark agents that are discontent right now. They are scattered at first, then concentrate along the borders between patches until none remain.
- Compare the two numbers. The threshold is what each agent asks for; the like-neighbor share is what the population ends up with. The second is typically well above the first.
- Sweep the threshold. Very low thresholds change almost nothing. Moderate ones produce the striking sorting. Near the top of the range almost nobody can be content, agents keep moving every round, and the grid never settles — high demands produce churn, not tidy segregation.
The exact update rule
The grid is L × L with edges that wrap around. Each cell is empty or holds one agent of group A or B. An agent's neighbors are the eight surrounding cells (the Moore neighborhood); empty cells do not count. With s like neighbors among o occupied neighbor cells and threshold θ, the agent is content when
- Place the agents at random: shuffle all cells, fill the first ones with group A, the next with group B, and leave the rest empty.
- Each round, visit every agent exactly once in a new random order. This is a sequential (asynchronous) update: each agent is judged against the grid as it is at the moment of its visit, including moves made earlier in the same round.
- A discontent agent moves to an empty cell chosen uniformly at random — not necessarily one where it will be content — and its old cell becomes empty.
- Stop when a whole round passes with no moves (every agent is then content and nothing can change) or when the round limit is reached.
The segregation measure is the average, over agents with at least one neighbor, of the share of their neighbors that are like them. Under random placement it is close to a² + b², where a and b are the group shares — one half for equal groups.
Schelling's 1971 paper moved a discontent agent to the nearest location where it would be content. Random relocation, used here and in many teaching versions, reaches different numbers but shows the same mechanism. Synchronous updating, where all agents decide at once, is another variant with different dynamics.
What the model does and does not show
The model is an existence proof for a mechanism: strong aggregate segregation does not require strong individual preferences, or any central plan. It does not show that preferences are the main cause of segregation in any real city. Residential segregation has well-documented causes the model leaves out entirely — discrimination in housing, lending and employment, zoning and public housing policy, highways and school boundaries, and differences in income and wealth that restrict where people can afford to live. A run of this model says nothing about how much each of those contributes.
It also teaches a modeling lesson that carries over to flocking and cellular automata: local rules plus interaction produce patterns that no single agent's rule describes, so the aggregate outcome has to be simulated rather than read off the rule.
Example result
With the defaults — a 50 × 50 grid, 10% empty, equal groups, and agents content with at least 30% like neighbors — the random start has 50.0% like neighbors on average and 17.0% of agents discontent. After 920 moves nobody wanted to move in round 17, and the average share of like neighbors had risen to 73.4%.
| Like neighbors, start | 50.0% |
|---|---|
| Like neighbors, end | 73.4% |
| Discontent agents, start | 17.0% |
| Discontent agents, end | 0.0% |
| Total moves | 920 |
| Rounds run | 17 |
| Nobody moved in round | 17 |
Implementation
The page runs simulation.js from the package below; the Python file is a line-by-line port with the same random-number generator, so a given seed produces the same result in both languages. The excerpts show the core of each.
// An agent is content when at least a share θ of its occupied neighbor cells
// hold its own group (an agent with no neighbors is content).
export function content(st, c, threshold) {
const [like, occupied] = neighbors(st, c);
return occupied === 0 || like >= threshold * occupied;
}
// One round: visit every agent once, in a fresh random order. A discontent agent
// (judged at the moment it is visited) moves to an empty cell chosen uniformly at
// random; the cell it leaves becomes empty. Returns the number of moves.
export function round(st, threshold, rng) {
const order = shuffle(Array.from({ length: st.pos.length }, (_, i) => i), rng);
let moves = 0;
for (const id of order) {
const from = st.pos[id];
if (content(st, from, threshold)) continue;
const k = rng.int(st.empties.length);
const to = st.empties[k];
st.grid[to] = st.grid[from];
st.grid[from] = 0;
st.empties[k] = from;
st.pos[id] = to;
moves++;
}
st.round++;
st.moves += moves;
if (moves === 0 && st.settled === null) st.settled = st.round;
return moves;
}def content(st, c, threshold):
"""Content if at least a share `threshold` of occupied neighbor cells hold the same group."""
like, occupied = neighbors(st, c)
return occupied == 0 or like >= threshold * occupied
def play_round(st, threshold, rng):
"""Visit agents once in random order; a discontent agent moves to a random empty cell."""
order = shuffle(list(range(len(st["pos"]))), rng)
grid, pos, empties = st["grid"], st["pos"], st["empties"]
moves = 0
for i in order:
frm = pos[i]
if content(st, frm, threshold):
continue
k = rng.int(len(empties))
to = empties[k]
grid[to], grid[frm] = grid[frm], EMPTY
empties[k] = frm
pos[i] = to
moves += 1
st["round"] += 1
st["moves"] += moves
if moves == 0 and st["settled"] is None:
st["settled"] = st["round"]
return movesRun the full model from a terminal:
node simulation.js --threshold 0.3 --size 50 --seed 42
python simulation.py --threshold 0.3 --size 50 --seed 42Download the model
| File | Contents | Size |
|---|---|---|
README.md | How to run it, parameters, example result | 2.5 KB |
model.json | Parameters with ranges and defaults, outputs, the seed-42 example | 2.6 KB |
simulation.js | The model used on this page plus a command-line runner (Node 18+) | 9.4 KB |
simulation.py | Python 3.9+ port, standard library only | 8.1 KB |
example.csv | Result table from the example run | 783 B |
LICENSE.txt | MIT License | 1.0 KB |
Assumptions & limitations
- Two groups, one trait, one preference. All agents share the same threshold and care only about the group of their immediate neighbors — not prices, schools, jobs or distance.
- Moving is free and always possible. Any empty cell is available to anyone; there is no housing market, no cost and no discrimination by others.
- Random relocation. Movers pick a random empty cell rather than searching for an acceptable one; the sequential random order is one of several update schemes, and each changes the numbers.
- Torus grid. Edges wrap around, so no agent sits on a border.
- A mechanism, not an explanation. The model shows that mild preferences can be sufficient for segregation; it cannot tell how much of real segregation they cause.
References
- Schelling, T. C. (1971). Dynamic models of segregation. Journal of Mathematical Sociology, 1(2), 143–186. doi.org/10.1080/0022250X.1971.9989794
- Wilensky, U. & Rand, W. (2015). An Introduction to Agent-Based Modeling. MIT Press.