This commit is contained in:
Andras Schmelczer 2026-07-03 18:47:28 +01:00
parent ab688243d7
commit 463bd4c647
54 changed files with 13239 additions and 625 deletions

View file

@ -5,7 +5,7 @@
"id": "46a28f40",
"metadata": {},
"source": [
"# School catchment model the working\n",
"# School catchment model: the working\n",
"\n",
"The postcode features **\"Good+/Outstanding primary/secondary school catchments\"** count the\n",
"rated state schools whose modelled *admission cutoff radius* covers a postcode. This notebook\n",
@ -17,7 +17,7 @@
"Pupil Database. What *is* public: where every school is and how many pupils it has (GIAS), how\n",
"many children live where (Census 2021), and the fact that most English admissions are run as\n",
"**deferred acceptance with distance tie-breaks**. That is enough to *solve for* each school's\n",
"cutoff distance — the \"last distance offered\" that councils publish each offer day — and those\n",
"cutoff distance (the \"last distance offered\" that councils publish each offer day) and those\n",
"published figures give us ground truth to calibrate against.\n",
"\n",
"The production code is `pipeline/transform/school_catchments.py`; the calibration harness is\n",
@ -81,14 +81,14 @@
"id": "e13f2bc4",
"metadata": {},
"source": [
"## 1. Supply schools and their phase fill targets\n",
"## 1. Supply: schools and their phase fill targets\n",
"\n",
"Every open, **non-selective** state school (academies, LA-maintained, free schools) takes part.\n",
"Grammar schools are excluded outright: their intakes are test-based and region-wide, so any\n",
"distance-based catchment would be fabricated. Independent, special and Welsh schools don't\n",
"admit by distance either.\n",
"\n",
"A school's *fill target* is `max(capacity, headcount)` an over-full school keeps its\n",
"A school's *fill target* is `max(capacity, headcount)`: an over-full school keeps its\n",
"demonstrated size, an under-full one can admit up to capacity (the feature asks \"would you get\n",
"a place?\", not \"does a pupil already live there?\"). The target is prorated over the cohort ages\n",
"the school teaches, parsed from its age range: nursery years weigh 0.5 and sixth-form years 0.6,\n",
@ -182,16 +182,16 @@
"id": "2905514f",
"metadata": {},
"source": [
"## 2. Demand children per postcode\n",
"## 2. Demand: children per postcode\n",
"\n",
"Census 2021 (TS007A) gives children by five-year band per LSOA. Bands don't align with school\n",
"phases, so phases take fractional shares primary (ages 410) = ⅕·(04) + (59) + ⅕·(1014);\n",
"secondary (1115) = ⅘·(1014) + ⅕·(1519) and each LSOA's total is split evenly across its\n",
"phases, so phases take fractional shares: primary (ages 410) = ⅕·(04) + (59) + ⅕·(1014);\n",
"secondary (1115) = ⅘·(1014) + ⅕·(1519), and each LSOA's total is split evenly across its\n",
"live postcodes (LSOAs hold ~40 postcodes, small enough at catchment scale).\n",
"\n",
"Not all of those children compete for state places: births fell ~10% between 2016 and 2021\n",
"(exactly the gap between the census stock and the cohorts reaching Reception by mid-decade) and\n",
"~7% attend independent schools or are home-educated. `DEMAND_SCALE = 0.8` absorbs both without\n",
"~7% attend independent schools or are home-educated. `DEMAND_SCALE = 0.8` absorbs both, without\n",
"it, modelled cutoffs run systematically tight and half the genuinely undersubscribed schools\n",
"look full (this was the single biggest correction the ground truth forced; see §7).\n"
]
@ -241,18 +241,18 @@
"id": "20d44b21",
"metadata": {},
"source": [
"## 3. Preferences grade bonuses and logit choice\n",
"## 3. Preferences: grade bonuses and logit choice\n",
"\n",
"Families don't just pick the nearest school. Two ingredients:\n",
"\n",
"- **Grade bonus** a school's *effective distance* is its real distance minus an Ofsted-grade\n",
"- **Grade bonus**: a school's *effective distance* is its real distance minus an Ofsted-grade\n",
" bonus (+0.6 km Outstanding, +0.3 km Good, 0.3/0.6 km for grade 3/4). A family accepts that\n",
" much extra travel for a better school.\n",
"- **Logit smearing** even so, not everyone at a postcode ranks the same school first. Each\n",
"- **Logit smearing**: even so, not everyone at a postcode ranks the same school first. Each\n",
" postcode's children split across the nearby feasible schools with weights\n",
" `softmax(effective_distance / τ)`, τ = 0.3 km. This matters more than it looks: with\n",
" deterministic choice a popular school fills entirely from its nearest band, putting its\n",
" marginal admitted child — and therefore its cutoff — unrealistically close (about 2× too\n",
" marginal admitted child (and therefore its cutoff) unrealistically close (about 2× too\n",
" tight against published cutoffs).\n",
"\n",
"Below: the share of applications a Good school captures against an unrated neighbour 1 km away.\n"
@ -306,7 +306,7 @@
"id": "04bcfbcf",
"metadata": {},
"source": [
"## 4. The equilibrium cutoff dynamics\n",
"## 4. The equilibrium: cutoff dynamics\n",
"\n",
"English admissions run deferred acceptance with distance priority; in a continuum economy that\n",
"is equivalent to finding **market-clearing cutoff distances** (Azevedo & Leshno 2016). The solver:\n",
@ -314,7 +314,7 @@
"1. start every school's cutoff at ∞;\n",
"2. every child unit applies to its preferred school(s) among those whose cutoff still covers it;\n",
"3. every oversubscribed school tightens its cutoff to the distance of its **marginal admitted\n",
" child** exactly the published \"last distance offered\";\n",
" child**, exactly the published \"last distance offered\";\n",
"4. repeat. Cutoffs only ever tighten, so the iteration converges to the deferred-acceptance\n",
" outcome. Schools that never fill keep no binding cutoff; their radius falls back to the\n",
" distance within which the local child population would cover their fill target.\n",
@ -533,7 +533,7 @@
"metadata": {},
"source": [
"The bimodal logic is visible: oversubscribed urban schools cluster well under 1 km while schools\n",
"with spare places reach further. A concrete slice Cambridge and its villages. Circles are the\n",
"with spare places reach further. A concrete slice: Cambridge and its villages. Circles are the\n",
"calibrated catchment radii of Good+ primary schools: tight in town, wide in the villages.\n"
]
},
@ -600,11 +600,11 @@
"id": "25770af8",
"metadata": {},
"source": [
"## 6. Calibration modelled vs published cutoffs\n",
"## 6. Calibration: modelled vs published cutoffs\n",
"\n",
"Councils publish each school's **last distance offered** in their allocation reports. We scraped\n",
"783 rows from nine authorities (Hertfordshire, Surrey, Stockport, Manchester, Bristol, Barnet,\n",
"Redbridge, Ealing, Lambeth `property-data/ground_truth/`), matched them to GIAS URNs, and\n",
"Redbridge, Ealing, Lambeth: `property-data/ground_truth/`), matched them to GIAS URNs, and\n",
"compare against the modelled radii. Faith schools are reported separately: their published\n",
"cutoff applies *within* faith priority, which a postcode model cannot see. \"All applicants\n",
"offered\" schools test whether the model agrees there was no binding cutoff at all.\n"
@ -823,11 +823,11 @@
"source": [
"## 8. Limitations\n",
"\n",
"- **Faith admissions are not modelled** whether a faith school's catchment is open to a given\n",
"- **Faith admissions are not modelled**: whether a faith school's catchment is open to a given\n",
" family depends on the family. Their fit is accordingly worse (the orange triangles above).\n",
"- **Cutoffs are single-year snapshots**; real ones move with each cohort. The model is a\n",
" steady-state estimate, not this September's number.\n",
"- **Straight-line distance** is used throughout — it is the modal LA tie-break, but some\n",
"- **Straight-line distance** is used throughout. It is the modal LA tie-break, but some\n",
" authorities measure walking routes, and none of sibling priority, feeder schools or\n",
" designated catchment polygons are visible to the model.\n",
"- Census 2021 child counts age; `DEMAND_SCALE` should drift upward as the birth-rate dip works\n",