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.
Needs the SocioVerse-ABM checkout
The study's adapter, artifacts and report ship in SocioVerse2; the legacy
Chicago model and its census data ship in the companion
SocioVerse-ABM repository.
Clone it next to SocioVerse2 and install the chicago extra
(pip install -e ".[dev,chicago,workbench]"), or point SV_ABM_ROOT /
SV_CHICAGO_LEGACY at it — see
companion repositories.
A no-token dry run uses the adapter's DeterministicLLMClient. The Chicago
data carry their own terms, including a non-commercial field; see the
SocioVerse-ABM data notice.
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— tract17031612000(New City, South Side),cta_stations += 2; - broadcast
policy_A_citywideonnews,audience: "all", ttl 3 — the station opening plus a citywide fair-housing / anti-displacement program; - broadcast
policy_B_tractonward_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 a later version can 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¶

| 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 change¶
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 — a new study, not an
edit of this one.
Every change here is a version, never a branch
A counterfactual branch replays the parent run's stored actions for the steps before the fork step, and the engine can only do that for a from-scratch study running one interaction round. This study wraps a legacy simulator, so it is not branch-eligible: each change above becomes a new version that re-runs cold from step 0, including the ones that only touch the environment. See Iterate: versions and branches for how a version is snapshotted, and Wrap a Legacy Simulator (Path C) for the seam-and-adapter mechanics.