Skip to content

Chicago Schelling — a transit intervention that made segregation worse

studies/chicago_schelling/ · reference: true · Path C (wrapped legacy simulator)

Starting from the 2010 census, how does the Black–White dissimilarity index D_bw evolve over three steps when a CTA rail station opens in a South-Side tract at step 2, paired with a fair-housing policy broadcast? The hypothesis was that better transit access draws a more diverse inflow and lowers D_bw. It did not. This case is the pattern for wrapping a validated legacy simulator (a Mesa-style Chicago segregation model) behind the four core interfaces, driving it with real census data, and letting a scheduled intervention plus audience-scoped broadcasts play out on top — with the trajectory recording what happened rather than what was hoped for.

P / E / B

what it is where it comes from
P 570 persistent households, materialized from 2010 census tracts (chicago.census, scale: small). agent_id encodes race / income bracket / family type / neighborhood type plus the tract GEOID, so every panel row is geo-locatable. Interaction is spatial_adjacency over Queen contiguity; propagation independent. population/population.json + population/roster.jsonl
E Four layers on the two axes — see the table below. Provider chicago.env (scale: small, init_mode: census). environment/environment.json
B Decision chicago.schelling: each household emits an Action of kind move or stay, carrying ranked_targets. Moves are executed inside the environment's apply, so relocation is an environment mutation, not a bookkeeping trick. adapter/decision.py

The four environment layers:

layer modality scope dynamics content
city_race_share physical macro endogenous city-wide racial shares
tract_local physical local endogenous per-tract demographics + 1-hop Queen surroundings
news information macro scheduled citywide policy broadcasts
ward_notice information local scheduled tract-scoped notices

The intervention is three things firing together at step 2:

  • a scheduled event on tract_local — tract 17031612000 (New City, South Side), cta_stations += 2;
  • broadcast policy_A_citywide on news, audience: "all", ttl 3 — the station opening plus a citywide fair-housing / anti-displacement program;
  • broadcast policy_B_tract on ward_notice, audience = that one GEOID, ttl 3 — the same news addressed to the block.

That pairing is the point of the design: the same event reaches everyone as macro information and reaches the affected tract as local information, so you can later fork and vary each channel independently.

One step, and the horizon

One step is one relocation round for the whole city: every household sees its tract and its Queen neighbours, decides move or stay, and the moves settle before metrics are computed. n_steps: 3, interaction_rounds: 1, seed: 42 — four metric rows (t=0 baseline plus three decided steps) over 570 tracked households, 2280 panel rows in study.duckdb.

display_metrics are D_black_white, D_hispanic_white, D_asian_white, Isolation_black; the store carries more (isolation and exposure indices, mover diagnostics).

Results

The study's own metric timeline: the four segregation indices over 3 steps, with the step-2 intervention marked

step D_bw D_hw D_aw Isolation_black n_movers pct_pop_moved mover_alignment_rate
0 0.8160 0.5582 0.3282 0.6137
1 0.8068 0.5730 0.3364 0.6240 41 0.0349 0.220
2 (intervention) 0.8447 0.5891 0.3085 0.6083 50 0.0292 0.280
3 0.8464 0.6112 0.3237 0.6162 31 0.0123 0.387

The hypothesis was falsified. D_bw rose by +0.0304 over the horizon, with the jump concentrated exactly at the intervention step. The mechanism is visible in the panel: in the target tract, the White population went 13 → 0 while the Hispanic population went 0 → 241, with the Black stock nearly unchanged. The accessibility shock lowered the cost of moving, but the direction of moving stayed governed by same-group neighbourhood share — so the nearest majority group, not a returning White population, took the opportunity, and the tract became more non-White than before.

Two secondary readings survive the same data. mover_alignment_rate climbs monotonically (0.220 → 0.280 → 0.387): movers get more like-seeking over time, the classic self-reinforcing tipping signature. And n_movers peaks at the intervention step and then falls to 31, so the system is re-equilibrating after a one-off shock, not being permanently stirred.

This is the interesting result, not a failed run

A study whose numbers contradict its own hypothesis field is working as designed. The artifacts record the falsification, the report explains the mechanism, and the adjustable_params tell you which knob to turn next (the report's own next step: rerun on a tract that starts near the tipping band rather than one already ~100% segregated).

Grounding

grounding/grounding.json carries three sourced facts, three declared assumptions and two implementation references — and the split is exactly where you would want it: the magnitudes are sourced, the scenario choices are declared.

entry basis role
Chicago metro Black–White dissimilarity = 76 (0–100), 2010 census — Brown University US2010 (Logan/Stults) sourced sanity band for the t=0 D_bw = 0.816
CTA Red Line Extension — 5.5-mile South-Side rail expansion sourced the real shock the step-2 event is modeled on
Schelling tolerance threshold 30–50% same-group neighbours sourced bounds the decision model's satisfaction regime
intervention tract = 17031612000 assumed a plausible South-Side site; the real RLE terminates further south
cta_stations += 2 as the station opening assumed no public metric maps a station onto this accessibility field
the paired fair-housing / anti-displacement policy text assumed stylized, not a specific enacted ordinance

Implementation references: Schelling (1971), Dynamic Models of Segregation — the per-household relocation rule; and a 2025 JAPA quasi-experimental study of transit-induced gentrification — which is precisely why the environment pairs the station with an anti-displacement broadcast, since the literature says the ethnoracial effect can go either way.

resources.json names the two datasets the run actually consumed (chicago_tracts.geojson, cta_stations.geojson) without recording any endpoint or key — see External Capabilities.

What you can fork

adjustable_params — read each as "change this to ask that question":

param the question it opens
n_steps does D_bw stabilize at the higher level or slowly relax? Three steps cannot say.
seed how much of the +0.0304 is a single seed's mover draw?
intervention tract & step put the station in a tract inside the tipping band — does accessibility integrate a mixed neighbourhood even though it cannot integrate a segregated one?
policy broadcast text the current text promises anti-displacement but gives no one a reason to move in. Add a citywide targeted-attraction message and A/B it.
metrics subset swap Isolation_black for the exposure indices when the question is contact rather than concentration.

Forking is the reuse path, not editing: this study is reference: true, so sv-init routes a matching question to a fork of it. See Wrap a Legacy Simulator (Path C) for the seam-and-adapter mechanics, and Iterate, Versions & Reports for how a variant is snapshotted.