The probabilistic model
What the simulation is, as a joint distribution. Every definition names the Clojure namespace that computes it (city.sim.candidates, for example); the source is in this repository, and doc/architecture.md maps each section below to its namespace. Written 2026-09-15 against the Stuttgart world and kept current with the explorer.
What is measured, what is drawn, what is inferred
The model has three kinds of quantity. Measured inputs come from published statistics, surveys and registers and are never fitted. The world of people, diaries and firms is drawn from those inputs once and then held fixed. Nine kernel numbers, three per demand class, are the only quantities inferred, and they are inferred from the 69 published turnovers by district and class. A policy scenario changes the venue set and nothing else.
Index sets
| symbol | meaning | size (Stuttgart) |
|---|---|---|
i | person | 936,597 = 610,444 residents + 326,153 in-commuters |
j | firm | 24,849, of which 3,637 carry the retail flag |
c | grid cell, 100 m | 34,650 = 175 × 198 |
d | Stadtbezirk | 23 |
h | hour of day | 24 |
k | episode within a diary | 0 … K_D |
1. The world is drawn once
Germany publishes no microdata seed at city level, so persons are generated from marginals directly (city.synth.stuttgart). Per Zensus 2022 grid cell c the published population N_c is taken as given, and each resident's attributes are drawn from that cell's marginals:
This draw is not re-run per posterior sample. It is the world, fixed, and its uncertainty is not currently propagated. That is a known gap, and the honest way to close it is a second inference loop over synthesis seeds.
2. Workplaces, which are also the commute model
Each employed resident with home cell c draws a firm (city.sim.cityworld):
computed on a 300 m grid and sampled without replacement against cap_j. A full firm is retried four times; after that the worker draws a firm in proportion to its remaining capacity. The 326,153 in-commuters have no home, so they draw in proportion to remaining capacity alone and are then placed at the city edge on the corridor they arrive by. Out-commuters are placed symmetrically at their exit point.
3. Attractiveness is measured, not fitted
Shape from one source, level from another (city.synth.retail). About 89 % of firms match a building polygon; an unmatched firm takes its district's median. The floor areas come from the Unternehmenserhebung 2021 published in the city's Einzelhandels- und Zentrenkonzept 2024. This matters for the prior on α: because A is raked to a measured level, α = 1 preserves that level and α ≠ 1 destroys it.
4. Destination choice, the one kernel that carries parameters
For a person currently anchored in cell c, the probability of choosing retail venue j (city.sim.candidates):
This is Huff's gravity model of trade areas [1] in Fotheringham's competing-destinations form [2], read as a random-utility choice [3] over the whole venue set. It is a bounded-rationality model in Simon's sense [4]: a shopper does not optimise over the city, they weigh size against distance with a decay the data must fix, and the visitation law of human mobility finds that decay near \(\beta\approx 2\) [5]. The form belongs to Wilson's family of spatial interaction models [12].
d_cj is the haversine distance from cell centre to firm, precomputed for all 34,650 × 3,637 pairs as float32, about 0.5 GB. The sum runs over every candidate: the earlier top-K tables renormalised a minority of the kernel, since the top 64 venues hold only 30 % of its mass, and K then became a behavioural parameter by accident.
The other destination classes are not inferred; their kernels are fixed in city.sim.day/default-venue-kernels (the run config, city.sim.run, can override the store and restaurant entries), and each is a top-K table over its own candidates:
| class | kernel | size term \(A_j\) | K | cap |
|---|---|---|---|---|
| restaurant | power, α = 1, β = 1.6, d₀ = 500 m | retail floor area of the venue | 64 | |
| school, clinic, outdoors, sports, culture, worship | exponential, λ = 400 m | none (OSM point) | 16 | |
| another resident's home | exponential, λ = 1,500 m | residents per cell | 64 | 1,500 m |
| street (a walk) | exponential, λ = 1,500 m | a fixed random weight per street node | 16 | 500 m |
| business | exponential, λ = 1,500 m | employees | 16 |
These classes place legs and, for restaurants, visits; none of them carries money or enters the likelihood.
5. The diary process
Each person has a type t(i) = (activity, age band, sex) with activity in {worker, student, retired, home}. The diaries are urban weekday diaries from the Statistics Canada Time Use Survey 2022 public-use microdata, reweighted by exponential tilting to Stuttgart's published trip rate of 2.861 trips per person per day (MiD 2023) (city.synth.timeuse, city.synth.tripgen):
The diary supplies when and what kind of place. The kernel supplies which place. Nothing in the diary is German, only its trip rate is: daily rhythms transferred from urban Canada are the largest single transfer assumption in the model. No German diary microdata is used; the Zeitverwendungserhebung is available only through the research data centre.
6. The day
Randomness is counter-addressed, so a draw is a pure function of its address rather than of execution order. This is what makes paired scenario comparison and a GPU port both possible:
The stream key \(\kappa\) is hashed per person, so indices added to it cannot line up across seeds or persons, and draw indices are reserved by purpose (diary, venue of episode \(k\), demand class, leakage, workplace) so one person's draws never meet (city.sim.day/uniform).
Then for each person, with anchor a starting at the home cell (city.sim.day/step-day):
where \(F^{-1}_{\mathrm{class}}(a,u)\) is the inverse cdf of that class's kernel from anchor \(a\) (§4), so one uniform draw picks one venue.
The anchor is the entire state: a destination is chosen relative to where the person is now, which is home or work. Trips are then routed on the street graph only when traces are requested, never inside the likelihood.
Every store episode is decided once (city.sim.kernel/store-choices!): its demand class, whether the purchase leaves the city, and its venue under that class's kernel at the posterior mean (§12). The economic day adds up those decisions, and the untraced and traced days send the person to the venue decided, so the trips, the visits and the money on the map describe the same day. A purchase that leaves the city makes no store trip.
7. Money
Purchasing power, shape assumed and level measured (city.econ.household):
Age is the census age group of the synthetic person, so the thresholds are group boundaries. The ratios are assumptions; only the district level is measured.
Retail-relevant share, one measured scalar (city.econ.spending):
Citywide this gives 4,651 M € against the EZK's 4,650.9 M €, by construction. One scalar stands for something that varies: the district ratios run 0.194 to 0.296 with a mean of 0.240, and they fall with income the way Engel's law says they should. Making σ income-dependent would fit better and would also be fitted against the very series it predicts, so the residual is left to carry that signal as a finding.
8. What the likelihood actually sees
Predicted turnover is the expected allocation, integrated analytically over the day rather than sampled from it:
so the simulator in the inference loop is deterministic given θ, takes about three seconds for the three classes on an eight-core machine, and contributes no Monte Carlo noise to the likelihood. Against the 2024 retail concept's published turnover Y_d (city.infer):
σ_obs is recorded with each run (0.25 for the published posterior). It is not a taste parameter: the residual floor of the model on this data is RMS 0.24 in log terms and does not move along the β ridge, so a smaller σ would score every particle on noise the model cannot remove. Optionally a second observe on the population's expected straight-line shopping distance:
The detour factor is an assumption of the caller and is recorded with the run. Used as an observe it is fitted; left out it is a holdout. It cannot be both.
9. Priors and posterior
(\(\alpha=0\) is no size term, \(\alpha=1\) classic Huff; the visitation law [5] puts \(\beta\) near 2.) With the segment split of §12 there is one such triple per demand class \(s\) and one observe per (district, class), 69 on Stuttgart, so the posterior is
This is simulation-based inference in the sense of [8]: the likelihood is never written down as a function of the data alone, it is the simulator's expected allocation evaluated at each proposed \(\theta\). The particle methods are Chopin's iterated batch importance sampling [9] with the resample-move rejuvenation of Gilks and Berzuini [10], in the general SMC-sampler frame of [11]; the MH alternative is plain random-walk Metropolis over the program's trace.
Inference runs through spindel, one forked execution context per particle:
- prior kernel: importance sampling with a resampling barrier at each of the 23 observes. N evaluations, cheap, and degenerate: read
distinct-theta, not ESS, because resampling duplicates survivors and never proposes new θ. - random-walk MH: N independent chains, each perturbing one continuous site and re-executing, at N × (1 + iterations) evaluations. Chains are weighted uniformly; the diagnostic is the acceptance rate.
The published posterior is approximate. The explorer's scenarios are drawn under two independent runs of 48 random-walk Metropolis–Hastings chains, 200 single-site moves each, started from prior draws, with σ_obs = 0.25 and seeds 1 and 2, pooled into 96 equally weighted states (city.demo.stuttgart/fit-posterior, pool-posteriors; evaluated on a GPU, city.sim.device). The two runs agree: for every parameter the Gelman–Rubin statistic between them, each run's final states taken as one sample, is between 0.99 and 1.01. That compares end points, not histories, so it shows the runs forgot their starts without showing how well each chain mixed. The data identify the long-term class's size exponent (α ≈ 1.06 ± 0.17) and part of the medium class's; the short class's α, every β and every d₀ stay close to their priors, so the scenario bands are wide mostly because the published turnovers do not constrain distance decay. The model also carries no class-level offset, so non-resident money (clothing turnover in the centre that residents alone cannot account for) has to be absorbed by the shape of the medium-class kernel.
10. Interventions
An intervention changes the candidate set and nothing else (city.sim.scenario):
Setting the venue set is an intervention in Pearl's sense [6], \(\mathrm{do}(R:=R')\): the arrows into \(R\) are cut, everything downstream is recomputed, and nothing upstream (the population, its money, \(\theta\)) is touched. The graph below is the model's causal structure; the three rungs [6, 7] are read off it.
over the posterior branches (θ^(p), w^(p)). The published scenarios report turnover per venue and district and the expected straight-line shopping distance per class (city.sim.scenario). Because the outcome is an expectation rather than a sampled day, baseline and scenario differ by exactly the intervention, and the spread across branches is the model's uncertainty about the kernel, not sampling noise.
With the segment split (§12) the same intervention acts on three attractiveness vectors at once (city.sim.scenario): closing a building removes its clothing floor from the medium class and its supermarket from the short one, and the outcome is evaluated per class with that class's money and kernel, then summed. The report gives, for every district and venue, the weighted mean and the 5 / 50 / 95 % quantiles of Δ over the particles.
The three rungs the demo walks, in these terms:
| rung | in this model | what is computed |
|---|---|---|
| observe | \(p(\theta\mid Y)\), the particles | the posterior predictive of district turnover is the calibration |
| intervene | \(\mathrm{do}(R:=R')\) | outcome3 under each particle: same money, same residents, \(\theta\) held, giving a band on \(\Delta\) |
| paired day | same seed, \(R'\) | the economic day re-run under \(R'\) at the posterior mean with the same draws for every person: a paired simulation with common random numbers |
The second rung is exact and noise-free. The paired day is what the map's visit-change layer shows. It is not a minimal counterfactual: a venue is drawn by walking the cumulative weights, so removing one venue shifts the intervals of the venues after it, and a person whose venue did not close can also move. A per-venue Gumbel-max draw would keep every unaffected choice fixed; that is not implemented.
11. Measured, assumed, fitted
| quantity | status |
|---|---|
| cell populations, age and sex, employment rates | measured (Zensus 2022) |
| purchasing power per district | measured (IFH, via the city's statistical yearbook and the EZK 2024), raked |
| retail floor area per district and class | measured (Unternehmenserhebung 2021 in the EZK 2024), raked |
| turnover and retail potential per district and class | measured (EZK 2024), the observations |
| firm locations, categories and footprints | Overture Places and OSM buildings |
| purchasing-power ratios by age group and employment | assumed, declared in city.econ.household |
| retail share of purchasing power σ | measured as one scalar 0.2377, known to vary 0.194–0.296 |
| activity diaries | transferred from the Statistics Canada Time Use Survey 2022, reweighted to Stuttgart's trip rate |
| class shares of shopping trips π = (0.65, 0.20, 0.15) | assumed, a placeholder in city.econ.day |
| leakage and non-resident inflow per class | sizes measured (EZK class balances); attribution assumed: all inflow to in-commuters, all leakage to residents |
| share of person-days keeping the diary's travel modes, 0.5 | assumed |
| α, β, d₀ per class | fitted, the approximate posterior of §9 |
| commute λ_w, K, non-retail kernels | assumed, no prior, not inferred |
The rule the data plane exists to enforce is that a model parameter is traceable to a measurement and a measurement to a receipt. The assumed rows of that table are where the rule is currently weakest.
12. Three demand classes
One destination kernel cannot reproduce Stuttgart: a kernel flat enough to fill the centre with turnover makes shopping trips far longer than the mobility survey reports. The retail concept publishes the structure that resolves this, three demand classes per district that reconcile to its own totals:
| class | turnover M € | resident potential M € | city centrality | Mitte centrality |
|---|---|---|---|---|
| short (food, convenience) | 2,168.7 | 2,443.1 | 0.89 | 4.49 |
| medium (clothing, shoes) | 1,331.7 | 732.5 | 1.82 | 36.72 |
| long (furniture, electronics) | 979.8 | 1,356.8 | 0.72 | 8.17 |
For each class \(s\):
A venue selling across classes, a department store or Overture's generic shopping category, has its floor split across the three (city.synth.segments), and the class floor areas sum to 99.9 % of the district totals, so the raking partitions cleanly. candidates/reduce-dense evaluates a class without materialising its choice table, and city.infer/segmented-model is the nine-parameter program with one keyed observe per (district, class).
Money on the day. In the store decisions (city.sim.kernel/store-choices!) a store episode first draws its class with probability \(\pi_s\), then its venue from that class's kernel, and in the economic day (city.econ.day/money-from-choices) it carries the person's spend per class visit:
pooled over the household \(h(i)\), so that 365 days reproduce the class potential in expectation. The class balances of the retail concept size the money that crosses the city boundary: medium-term goods draw 599 M € more than residents can spend, while short-term goods leak 274 M € and long-term goods 377 M € out. In the day, a resident's class-\(s\) purchase leaves the city with the measured leakage share, and in-commuters carry the class's measured net inflow, spent from wherever their day anchors them: their entry point on the city boundary or their workplace. How that money is attributed between residents, in-commuters and visitors is assumed; visitors are not modelled.
References
- Huff, D. L. (1963). A probabilistic analysis of shopping center trade areas. Land Economics 39(1), 81–90.
- Fotheringham, A. S. (1983). A new set of spatial-interaction models: the theory of competing destinations. Environment and Planning A 15(1), 15–36.
- McFadden, D. (1974). Conditional logit analysis of qualitative choice behavior. In P. Zarembka (ed.), Frontiers in Econometrics, 105–142. Academic Press.
- Simon, H. A. (1955). A behavioral model of rational choice. Quarterly Journal of Economics 69(1), 99–118.
- Schläpfer, M., Dong, L., O'Keeffe, K., Santi, P., Szell, M., Salat, H., Anklesaria, S., Vazifeh, M., Ratti, C. & West, G. B. (2021). The universal visitation law of human mobility. Nature 593, 522–527.
- Pearl, J. (2009). Causality: Models, Reasoning, and Inference, 2nd ed. Cambridge University Press.
- Pearl, J. & Mackenzie, D. (2018). The Book of Why. Basic Books. The ladder of association, intervention and counterfactual.
- Cranmer, K., Brehmer, J. & Louppe, G. (2020). The frontier of simulation-based inference. PNAS 117(48), 30055–30062.
- Chopin, N. (2002). A sequential particle filter method for static models. Biometrika 89(3), 539–552.
- Gilks, W. R. & Berzuini, C. (2001). Following a moving target — Monte Carlo inference for dynamic Bayesian models. Journal of the Royal Statistical Society B 63(1), 127–146.
- Del Moral, P., Doucet, A. & Jasra, A. (2006). Sequential Monte Carlo samplers. Journal of the Royal Statistical Society B 68(3), 411–436.
- Wilson, A. G. (1971). A family of spatial interaction models, and associated developments. Environment and Planning 3(1), 1–32.
Data (none of it ships with the code; see DATA.md for access, licences and attribution):
- Statistisches Bundesamt (Destatis), Zensus 2022: 100 m grid and Gemeindetabellen (dl-de/by-2-0).
- Landeshauptstadt Stuttgart: Einzelhandels- und Zentrenkonzept 2024 (turnover, potential and floor area by district and class; figures cited); Kleinräumige Gliederung (district boundaries); Statistisches Jahrbuch (purchasing power by district, IFH Retail Consultants).
- Statistik der Bundesagentur für Arbeit: employment and commuter statistics.
- Statistics Canada: Time Use Survey 2022, public-use microdata file (Statistics Canada Open Licence).
- infas / BMDV: Mobilität in Deutschland 2017 and 2023 (trip rate, trip length, mode shares; figures cited).
- © OpenStreetMap contributors (ODbL 1.0): buildings, street network.
- Overture Maps Foundation: Places (CDLA-Permissive-2.0).
- NVBW: GTFS timetable for Baden-Württemberg.
What this model does not have
No prices, no wages paid to workers, no inventory, no capacity constraint on a shop, no congestion feeding back into travel cost, no transit timetable or line topology, no housing market, no day-to-day learning or habit, and no firm entry or exit: the firm register is fixed for the day. The destination kernel uses straight-line distance, not network impedance, so a road closure cannot currently change where anyone shops.
Rendered from doc/model.md in the repository; the chronological findings log is doc/simulator.md.