From cec7941b447df62299c0301c1910f9bd39e38aa2 Mon Sep 17 00:00:00 2001 From: Tudor Date: Mon, 13 Jul 2026 18:45:30 +0100 Subject: [PATCH] feat(api): computed state-school benchmarks Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB --- backend/data_loader.py | 66 +++++++++++++++++++++++++++ backend/tests/test_benchmarks.py | 78 ++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 backend/tests/test_benchmarks.py diff --git a/backend/data_loader.py b/backend/data_loader.py index 4305e42..7d73205 100644 --- a/backend/data_loader.py +++ b/backend/data_loader.py @@ -525,6 +525,72 @@ def get_data_info(db: Session = None) -> dict: # SUPPLEMENTARY DATA — per-school detail page # ============================================================================= +def compute_benchmarks(df: pd.DataFrame) -> dict: + """State-school benchmarks computed from our dataset (spec §5/§8.6). + + NOT official DfE figures — consumers must label them + "state-school average (computed from our dataset)". The disadvantaged + attainment average is weighted by cohort size (eligible_pupils) so + small schools don't dominate; context measures are medians. + """ + if df.empty or "year" not in df.columns: + return {} + latest_year = df["year"].max() + if pd.isna(latest_year): + return {} + d = df[df["year"] == latest_year] + if d.empty: + return {} + is_secondary = ( + d["attainment_8_score"].notna() + if "attainment_8_score" in d.columns + else pd.Series(False, index=d.index) + ) + prim, sec = d[~is_secondary], d[is_secondary] + + def _median(sub, col): + if col not in sub.columns: + return None + v = sub[col].median() + return round(float(v), 1) if pd.notna(v) else None + + def _weighted_disadvantaged(sub): + needed = {"rwm_expected_disadvantaged_pct", "eligible_pupils"} + if not needed <= set(sub.columns): + return None + s = sub.dropna(subset=list(needed)) + if s.empty or s["eligible_pupils"].sum() == 0: + return None + w = ( + (s["rwm_expected_disadvantaged_pct"] * s["eligible_pupils"]).sum() + / s["eligible_pupils"].sum() + ) + return round(float(w), 1) + + def _block(sub, with_disadvantaged): + median_pupils = None + if "total_pupils" in sub.columns: + mp = sub["total_pupils"].median() + if pd.notna(mp): + median_pupils = int(mp) + block = { + "eal_pct": _median(sub, "eal_pct"), + "sen_support_pct": _median(sub, "sen_support_pct"), + "disadvantaged_pct": _median(sub, "disadvantaged_pct"), + "median_pupils": median_pupils, + } + if with_disadvantaged: + block["disadvantaged_rwm_expected_pct"] = _weighted_disadvantaged(sub) + return block + + return { + "source": "state-school average (computed from our dataset)", + "year": int(latest_year), + "primary": _block(prim, with_disadvantaged=True), + "secondary": _block(sec, with_disadvantaged=False), + } + + def _ofsted_block(o, urn: int) -> dict: """Serialize the latest Ofsted inspection row for API responses. diff --git a/backend/tests/test_benchmarks.py b/backend/tests/test_benchmarks.py new file mode 100644 index 0000000..01d8893 --- /dev/null +++ b/backend/tests/test_benchmarks.py @@ -0,0 +1,78 @@ +"""compute_benchmarks: state-school benchmarks computed from our dataset +(spec §5/§8.6). The disadvantaged average must be weighted by cohort size, +medians must ignore NaN, and only the latest year counts.""" + +import numpy as np +import pandas as pd + +from backend.data_loader import compute_benchmarks + +LATEST = 202425 + + +def _df(): + rows = [ + # Six primary schools, latest year. Disadvantaged RWM chosen so the + # weighted average differs clearly from the unweighted mean: + # weighted = (40*100 + 60*300) / 400 = 55.0 ; unweighted mean = 50.0 + dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=100, + rwm_expected_disadvantaged_pct=40.0, eal_pct=10.0, + sen_support_pct=10.0, disadvantaged_pct=20.0, total_pupils=200), + dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=300, + rwm_expected_disadvantaged_pct=60.0, eal_pct=20.0, + sen_support_pct=14.0, disadvantaged_pct=24.0, total_pupils=280), + dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=np.nan, + rwm_expected_disadvantaged_pct=99.0, eal_pct=30.0, + sen_support_pct=18.0, disadvantaged_pct=30.0, total_pupils=300), + dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=50, + rwm_expected_disadvantaged_pct=np.nan, eal_pct=np.nan, + sen_support_pct=np.nan, disadvantaged_pct=np.nan, total_pupils=np.nan), + dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=40, + rwm_expected_disadvantaged_pct=np.nan, eal_pct=40.0, + sen_support_pct=20.0, disadvantaged_pct=40.0, total_pupils=350), + dict(year=LATEST, attainment_8_score=np.nan, eligible_pupils=60, + rwm_expected_disadvantaged_pct=np.nan, eal_pct=50.0, + sen_support_pct=22.0, disadvantaged_pct=44.0, total_pupils=400), + # Two secondary schools (attainment_8 non-null) + dict(year=LATEST, attainment_8_score=45.0, eligible_pupils=180, + rwm_expected_disadvantaged_pct=np.nan, eal_pct=15.0, + sen_support_pct=12.0, disadvantaged_pct=22.0, total_pupils=1000), + dict(year=LATEST, attainment_8_score=50.0, eligible_pupils=200, + rwm_expected_disadvantaged_pct=np.nan, eal_pct=25.0, + sen_support_pct=16.0, disadvantaged_pct=26.0, total_pupils=1200), + # An older-year primary row that must NOT influence anything + dict(year=202324, attainment_8_score=np.nan, eligible_pupils=500, + rwm_expected_disadvantaged_pct=1.0, eal_pct=99.0, + sen_support_pct=99.0, disadvantaged_pct=99.0, total_pupils=9999), + ] + return pd.DataFrame(rows) + + +def test_weighted_disadvantaged_average(): + b = compute_benchmarks(_df()) + # Row 3 has NaN eligible_pupils and must be excluded from the weighting. + assert b["primary"]["disadvantaged_rwm_expected_pct"] == 55.0 + + +def test_medians_ignore_nan_and_older_years(): + b = compute_benchmarks(_df()) + assert b["year"] == LATEST + # eal medians over [10,20,30,40,50] = 30 + assert b["primary"]["eal_pct"] == 30.0 + # median pupils over [200,280,300,350,400] = 300 + assert b["primary"]["median_pupils"] == 300 + + +def test_secondary_block_has_no_disadvantaged_rwm(): + b = compute_benchmarks(_df()) + assert "disadvantaged_rwm_expected_pct" not in b["secondary"] + assert b["secondary"]["median_pupils"] == 1100 + + +def test_provenance_string(): + b = compute_benchmarks(_df()) + assert b["source"] == "state-school average (computed from our dataset)" + + +def test_empty_df(): + assert compute_benchmarks(pd.DataFrame()) == {}