This commit is contained in:
Andras Schmelczer 2026-07-12 15:03:33 +01:00
parent 982e0cc89c
commit cfaf58dfba
44 changed files with 793 additions and 201 deletions

View file

@ -5,7 +5,7 @@ are set per admission authority, only a handful of councils publish polygons,
and the pupil-residence data behind commercial "heatmap" catchments lives in
the restricted National Pupil Database. This module therefore COMPILES one
from open data, estimating each school's admission cutoff distance ("last
distance offered") the radius within which an applicant would plausibly be
distance offered"): the radius within which an applicant would plausibly be
offered a place.
Model: English state admissions are run as deferred acceptance with distance
@ -13,30 +13,30 @@ tie-breaks, which in a continuum economy is equivalent to finding
market-clearing cutoff distances (Azevedo & Leshno 2016). Per phase
(primary/secondary):
1. Demand Census 2021 children per LSOA (TS007A age bands, prorated to the
1. Demand: Census 2021 children per LSOA (TS007A age bands, prorated to the
phase's cohort ages) split evenly across the LSOA's live postcodes.
2. Supply every open, non-selective state-funded school (GIAS), with a fill
2. Supply: every open, non-selective state-funded school (GIAS), with a fill
target of max(capacity, headcount) prorated to the phase's cohorts
(sixth-form and nursery years carry reduced weight, since their class
sizes differ and they are not allocated by the same admissions round).
3. Preferences children prefer nearby schools, trading distance against
3. Preferences: children prefer nearby schools, trading distance against
Ofsted grade: a school's effective distance is its real distance minus a
grade bonus (Outstanding > Good > ungraded > below-Good). Because real
first preferences are heterogeneous, each postcode's children split
across nearby feasible schools with logit weights over effective
distance rather than all picking the same one.
4. Equilibrium cutoffs start unbounded and tighten monotonically: each
4. Equilibrium: cutoffs start unbounded and tighten monotonically. Each
round, children apply to their preferred feasible school(s), and
oversubscribed schools tighten their cutoff to the distance of their
marginal admitted child. Converges to the deferred-acceptance outcome.
5. Schools that never fill have no binding cutoff anyone who applies gets
in so their feasibility radius is the distance within which the local
5. Schools that never fill have no binding cutoff (anyone who applies gets
in), so their feasibility radius is the distance within which the local
child population would cover their fill target, capped.
The free parameters (preference bonuses, demand scale, choice temperature,
residual calibration factors) are CALIBRATED against published "last
distance offered" figures scraped from nine local authorities' allocation
reports see check_school_cutoffs.py and the constants below.
reports. See check_school_cutoffs.py and the constants below.
A postcode is "inside the catchment" of every school whose cutoff radius
covers it. The output counts those schools per postcode for the four
@ -71,7 +71,7 @@ SCHOOL_GROUPS = {
# PRIMARY-age children if its statutory lowest age is <= 10, and SECONDARY-age
# children if its statutory highest age is >= 12. All-through (e.g. 3-18) and
# middle-deemed-secondary (e.g. 9-13) schools satisfy BOTH and so are counted in
# both the primary and the secondary metrics Ofsted's coarse "Ofsted phase"
# both the primary and the secondary metrics. Ofsted's coarse "Ofsted phase"
# labels such schools as just "Secondary", which previously hid them from every
# postcode's primary-school count.
PRIMARY_MAX_AGE = 10
@ -133,7 +133,7 @@ CHOICE_TEMPERATURE_KM = 0.3
# Residual calibration from the same ground truth: after the equilibrium
# solve, modelled cutoffs still ran systematically tight (median log2 bias
# -0.53 primary / -0.36 secondary at the settings above published "last
# -0.53 primary / -0.36 secondary at the settings above; published "last
# distance offered" reflects offer-day frictions, waiting-list churn and
# furthest-applicant noise that no clean equilibrium reproduces). Radii are
# multiplied by 2^-bias so the modelled median matches the published median;
@ -166,7 +166,7 @@ def classify_good_plus_schools(
Framework). A large and growing share of schools were last inspected under an
UNGRADED (Section 8) inspection or the post-2024 report-card framework, so
that column is null/"Not judged" for them even when they are demonstrably
good their status lives in "Ungraded inspection overall outcome" ("School
good. Their status lives in "Ungraded inspection overall outcome" ("School
remains Good"/"School remains Outstanding"). Filtering on the graded column
alone dropped ~7,000 genuinely good/outstanding schools. We fall back to the
ungraded outcome, but ONLY when there is no usable graded result
@ -296,15 +296,15 @@ def phase_intakes(gias: pl.DataFrame) -> pl.DataFrame:
Returns one row per open, non-selective state-funded school with valid
coordinates: ``(urn, lat, lng, primary_intake, secondary_intake)``. The
fill target max(capacity, headcount), so over-full schools keep their
demonstrated size and under-full schools can admit up to capacity is
fill target (max(capacity, headcount), so over-full schools keep their
demonstrated size and under-full schools can admit up to capacity) is
spread over the cohort ages the school teaches (parsed from ``age_range``,
e.g. "311" = ages 3..10) with nursery and sixth-form ages down-weighted,
and each phase receives the share of cohort weight in its age band.
"""
# gias._format_age_range emits three shapes: "{low}{high}", "up to {high}"
# (StatutoryLowAge missing) and "{low}+" (StatutoryHighAge missing). Parse
# all three — the one-sided shapes previously fell through the two-number
# all three. The one-sided shapes previously fell through the two-number
# parse and silently dropped the school from the catchment supply.
age = pl.col("age_range")
leading = age.str.extract(r"^\s*(\d+)", 1).cast(pl.Int64, strict=False)
@ -426,10 +426,10 @@ def equilibrium_cutoffs(
Deferred acceptance with distance priority, solved as cutoff dynamics
(Azevedo & Leshno): cutoffs start unbounded; each round every child unit
applies to its preferred feasible school(s) a logit split over
applies to its preferred feasible school(s): a logit split over
effective distance (distance - school bonus) among schools whose cutoff
covers it, collapsing to the single best school when ``tau_km`` is 0
and each oversubscribed school tightens its cutoff to its marginal
covers it, collapsing to the single best school when ``tau_km`` is 0.
Each oversubscribed school then tightens its cutoff to its marginal
admitted child's distance. Cutoffs only ever tighten, so the iteration
converges.