Local-authority business survival — a monthly sector×age Leslie register under support policy

Methodology card. This is the primary human- and agent-legible description of the model. The runnable stub beside it (stub.go) is the type-checked generative demonstration; this card carries the structure, assumptions, and validity regime that the Go code does not spell out.

System

The business demography of a single UK local authority: the standing register of active businesses, stratified by sector and by age in months, evolving under monthly formation (new businesses arriving), exit hazards (businesses failing), macroeconomic covariates, and support-policy multipliers. Businesses are born into an age-0 bucket per sector, age one month per step, and exit at a monthly hazard derived from an ONS-style cumulative survival curve; a 60th “top” bucket aggregates all ages ≥59 months. The quantities of interest are the standing register stock (how many businesses are alive) and five-year cohort survival (what fraction of a birth cohort is still active after 60 months), and how each responds to support interventions and to the economic weather.

The generative core is a single partition — the whole demographic engine lives in one bespoke iteration:

Partition Iteration State Role
population SingleLAPopulationIteration [count per (sector, age-month)], width nSectors×60 Monthly Leslie register: per-sector Poisson formation, age progression, ONS-derived exit hazards, macro + policy multipliers

Formation. Each sector draws new businesses from a Poisson process at a base monthly rate, scaled by an economic birth multiplier (log-linear in Bank Rate, claimant count and optional GDP growth) and by policy birth multipliers (a global policy_birth_scale and a per-sector policy_sector_birth_scale).

Exit hazards. A cumulative survival curve at years 1–5 (ONS business demography) is converted to a piecewise-constant monthly hazard (hazard.go), applied per sector via a binomial thinning of each age bucket. The effective hazard is scaled by a per-sector structural multiplier (sector_hazard_scales), an economic death multiplier (log-linear in Bank Rate), a distress boost, and policy hazard multipliers — a global policy_death_hazard_scale, a per-sector policy_sector_hazard_scale, and a first-year policy_infant_hazard_scale acting only on the age 0→1 transition.

The swept driver. BuildStub exposes one knob, hazardScale, which sets policy_death_hazard_scale: the intensity of a support package that lowers business exits (< 1) or an adverse shock that raises them (> 1). Every other input is a literal Default* constant. The register starts empty and fills from formation, approaching a quasi-steady stock of formation × mean-lifetime — which is exactly what the hazard multiplier moves.

Partition wiring

The partition dependency graph, derived statically from the stub’s BuildStub wiring by pkg/graph. Solid arrows are within-step params_from_upstream wiring (which imposes a computation order); dashed arrows leaving a shaded past-copy node are lag reads of a partition’s committed state from an earlier step — drawn as separate source nodes so the graph stays a DAG.

flowchart TB
  n0["population"]

Ingests (in the stub: nothing)

The stub is data-free — every input is a literal constant in stub.go, with hazardScale exposed as the one swept driver. In the downstream application the baseline survival curve is taken from ONS business demography (by LA / sector / cohort); per-LA monthly formation is built from the Companies House live register joined to ONS NSPL postcode geography; the macroeconomic covariates are Bank of England Bank Rate and NOMIS claimant counts; and the birth / hazard elasticities are estimated from a panel first-difference regression and SMC calibration. All of that ingestion and calibration stays downstream — the stub generates formation and survival structurally from constants where the downstream fits them from data.

Assumptions

Validity regime

Failure modes

Question answered

For a single local authority, in which direction — and roughly how much — do the standing business register and five-year cohort survival move as a support package changes the exit hazard (and as formation support, first-year support, sector targeting, and the economic weather change), so that support portfolios can be ranked?

Generative behaviour under test

stub_test.go asserts, beyond “it runs”: 1. Harness — no NaNs, correct state width, no params mutation, no statefulness residue across a repeated run (simulator.RunWithHarnesses). 2. Structural invariants — every sector×age bucket is a non-negative business count of the correct width at every step, and (from an empty register) formation leaves a strictly positive standing stock. 3. Correct direction of parameter response — a supportive hazard multiplier (hazardScale = 0.85) yields a higher back-half register stock than an adverse one (1.15), averaged over an 8-member stochastic ensemble. The baseline isolated five-year cohort survival reproduces the ONS ≈0.384 benchmark by construction.

The expected-behaviour suite (behaviour_test.go) makes the decision-readiness explicit — each subtest is a named, plain-language response claim, run in deterministic mean-field mode so each signed effect is exact. The full set of claims, with the exact numbers each run produces, is emitted by the suite itself and rendered in the Observed behaviour table below — none of those numbers are hand-typed, so a claim’s result can never drift from the assertion that enforces it. The claims split into:

Observed behaviour

Every row below is one bound object: a plain-language response claim, the test subtest that enforces it, and the number that test produced (ensemble values rounded to 2 dp). Nothing here is hand-written — the claims and their numbers are emitted by TestBusinessSurvivalExpectedBehaviour (via go run ./cmd/model-graphs), so a claim cannot drift from its test or its result. If the model’s behaviour changes, either the binding test fails (a claim’s assertion broke) or TestCardsUpToDate fails (a number moved) — a broken claim cannot reach the card silently.

Response claim Enforced by Observed
A lower support-policy exit-hazard scale raises five-year cohort survival TestBusinessSurvivalExpectedBehaviour/lower_death_hazard_scale_raises_five_year_cohort_survival five-year cohort survival fraction — adverse (scale=1.15) 0.33 · supported (scale=0.85) 0.44
Formation support (higher birth scale) raises register stock TestBusinessSurvivalExpectedBehaviour/higher_formation_support_raises_register_stock deterministic back-half register stock — base 1348.24 · policy_birth_scale=1.2 1617.88
First-year (infant) hazard relief raises cohort survival TestBusinessSurvivalExpectedBehaviour/lower_infant_hazard_support_raises_cohort_survival five-year cohort survival fraction — infant scale=1.7 0.38 · infant scale=0.3 0.39
A sector-targeted formation subsidy raises that sector’s stock (Technology) TestBusinessSurvivalExpectedBehaviour/targeted_sector_formation_support_raises_that_sector_stock deterministic back-half sector stock — base 149.80 · Technology birth scale=1.5 224.71
Sector-targeted hazard relief raises that sector’s stock (Hospitality) TestBusinessSurvivalExpectedBehaviour/targeted_sector_hazard_relief_raises_that_sector_stock deterministic back-half sector stock — base 199.74 · Hospitality hazard scale=0.8 221.25
A worse baseline ONS survival curve lowers register stock TestBusinessSurvivalExpectedBehaviour/worse_baseline_survival_curve_lowers_stock deterministic back-half register stock — base curve 1348.24 · survival ×0.9 1230.40
A higher Bank Rate (negative birth elasticity) suppresses formation TestBusinessSurvivalExpectedBehaviour/higher_bank_rate_suppresses_formation deterministic back-half register stock — Bank Rate 0.5% 1348.24 · Bank Rate 3.0% 386.28
A higher claimant count (negative birth elasticity) suppresses formation TestBusinessSurvivalExpectedBehaviour/higher_claimant_count_suppresses_formation deterministic back-half register stock — claimants 12k 1348.24 · claimants 24k 1021.77
A higher Bank Rate (positive death elasticity) raises exit hazards and lowers cohort survival TestBusinessSurvivalExpectedBehaviour/higher_bank_rate_raises_exit_hazard_and_lowers_survival five-year cohort survival fraction — Bank Rate 0.5% 0.38 · Bank Rate 3.0% 0.03
A positive distress-hazard boost lowers cohort survival TestBusinessSurvivalExpectedBehaviour/distress_signal_lowers_cohort_survival five-year cohort survival fraction — calm 0.38 · distress boost +0.3 0.29
A higher structural sector baseline hazard lowers that sector’s stock (Retail) TestBusinessSurvivalExpectedBehaviour/higher_sector_baseline_hazard_lowers_that_sector_stock deterministic back-half sector stock — base 249.67 · Retail hazard scale=1.5 199.46

Bespoke extensions (staged beside the stub)

SingleLAPopulationIteration (single_la_population.go) and its survival→hazard helper (hazard.go) are custom simulator.Iteration / generative code lifted verbatim from the downstream repo’s pkg/population. They live here rather than in engine core because the catalogue is the staging ground for the “should this be promoted into core?” question — a generic aged-cohort / Leslie primitive (age-bucket progression with per-bucket hazards and a boundary aggregating bucket) recurring across other models would be the signal to promote, but that waits for the recurrence.

The data-fitting helpers that accompany this iteration downstream — the ONS survival-curve loader, the panel first-difference regression and elasticity mapping, and the SMC forward-statistic iterations (ScaledCohortSurvivalIteration, PopulationSurvivalBirthMomentsIteration) — are inference / ingestion concerns and were left downstream. The scenario overlays and support portfolios are a decision layer and likewise stay downstream; here they appear only as raw param multipliers the behaviour suite sweeps.

Downstream

Data ingestion (ONS / Companies House / NSPL / NOMIS / Bank of England), panel and SMC calibration, the support-portfolio and macro-scenario decision layer, and the Monte-Carlo policy evaluator live in the project repo:

https://github.com/umbralcalc/business-survival