Source document

docs/cities/miami/probe/race_imputation_prior.py

Served verbatim from the project repository. Internal working document conventions apply: documents may reference file paths, branch names, and findings-ledger anchors from the repo.

"""Does an adjacent-year prior beat the equal split for hidden race cells?

Follow-up to race_imputation.py after the 2026-09-16 review found the 2022-23
file marks zeros with '*' (17,464 tokens against ~5,900 in other years), so
the equal split over hidden cells inflates the small groups that year
(American Indian median share 0.000 → 0.958 → 0.000 across 2021-22 → 2023-24).

Diagnostic, derived from the file rather than chosen: tokens can only include
zeros when a school shows more hidden cells S than hidden students
U = total − visible (each genuinely hidden cell holds ≥ 1). Year-level S > U
(2022-23: 17,345 cells vs 17,077 students) says the file's '*' includes zeros;
school-level S > U picks the schools where that is certain.

Rules compared against the unsuppressed NCES CCD race subtotals:
  hybrid        : the loader's rule (fully-visible grade → zeros; equal split of U over the rest)
  prior_school  : hybrid, but where the school has S > U, split U over hidden groups in
                  proportion to the school's adjacent-year hybrid shares (years with S ≤ U only)
  prior_year    : the same prior for every school in a year whose file has S > U
  prior_all     : the prior wherever an adjacent trustworthy year exists (all years)
  prior_year_cap9: prior_year with no group receiving more than 9 students per hidden cell (the publisher's own
                  floor); the overflow is re-spread over the other hidden groups by prior, then equally
Fallback when no adjacent trustworthy year exists: hybrid. Error = rule − CCD, pp.

Held-out selection (added after the 2026-09-16b review, N9): the rule served
for 2022-23 (prior_year_cap9) was chosen on the same CCD file the harness now
reports it against. To measure the optimism of that in-sample choice, the
schools are split into two halves by the parity of their school number; the
best rule on each half (lowest mean |error| over the Black, Hispanic and
White shares) is applied to the other half and its held-out MAE reported
beside the in-sample one.

Run: pipeline_miami/.venv/bin/python docs/cities/miami/probe/race_imputation_prior.py [years]
"""
from __future__ import annotations

import sys
from pathlib import Path

import numpy as np
import pandas as pd
import psycopg

sys.path.insert(0, str(Path(__file__).parent))
import race_imputation as base  # noqa: E402

RACES = list(base.RACES)
ALL_YEARS = ["2018-19", "2021-22", "2022-23", "2023-24", "2024-25", "2025-26"]


def year_shares(year: str, con) -> tuple[pd.DataFrame, dict[str, float], bool]:
    """Per school×race: visible n, hidden cells, S; the equal-split (hybrid) share; and whether the year is trustworthy (S ≤ U)."""
    hy = base.fldoe_hybrid(year, con)
    enr = dict(con.execute("select school_number, value::float from miami_school_year_metrics where metric_key='enrollment_oct' and subgroup='ALL' and year=%s and value is not null", (year,)).fetchall())
    hy["total"] = hy.school.map(enr)
    hy = hy[hy.total.notna() & (hy.total > 0)].copy()
    vis = hy.groupby("school").n.sum()
    hy["U"] = (hy.total - hy.school.map(vis)).clip(lower=0)
    hy["hybrid"] = (hy.n + np.where(hy.S > 0, hy.U * hy.hid / hy.S.replace(0, np.nan), 0.0).astype(float)) / hy.total * 100
    hy["hybrid"] = hy.hybrid.fillna(hy.n / hy.total * 100)
    per = hy.groupby("school").agg(S=("S", "first"), U=("U", "first"))
    return hy, enr, int((per.S > per.U).sum()) == 0   # trustworthy = no school has more hidden cells than hidden students


def prior_alloc(hy: pd.DataFrame, prior: pd.DataFrame, apply_mask: pd.Series, cap: float | None = None) -> pd.Series:
    """Replace the equal split with a prior-proportional split for schools in apply_mask. With cap=9, no group
    receives more than 9 students per hidden cell (the publisher's own rule: a hidden cell holds 1–9); the excess
    is re-spread over the uncapped groups in proportion to their prior, and anything still left is spread
    equally over every hidden cell (the trustworthy-year rule)."""
    out = hy.hybrid.copy()
    p = prior.rename("prior")
    df = hy.join(p, on=["school", "race"])
    for school, g in df[apply_mask].groupby("school"):
        hidden = g[g.hid > 0]
        if not len(hidden) or g.U.iloc[0] <= 0:
            continue
        w = hidden.prior.fillna(np.nan)
        if w.isna().all():
            continue  # no adjacent trustworthy year → keep hybrid
        w = w.fillna(0.0)
        if w.sum() <= 0:
            continue  # every hidden group absent in adjacent years → keep hybrid (cannot all be zero when U > 0)
        U = float(g.U.iloc[0])
        if cap is None:
            alloc = U * w / w.sum()
        else:
            alloc = pd.Series(0.0, index=hidden.index)
            limit = cap * hidden.hid.astype(float)
            remaining, active = U, w.copy()
            for _ in range(len(hidden) + 1):
                if remaining <= 1e-9 or active.sum() <= 0:
                    break
                share = remaining * active / active.sum()
                room = (limit - alloc).clip(lower=0)
                take = np.minimum(share, room)
                alloc = alloc + take
                remaining -= float(take.sum())
                active = active.where(alloc < limit - 1e-9, 0.0)
            if remaining > 1e-9:   # every hidden cell already at 9: spread the rest equally over the hidden cells
                alloc = alloc + remaining * hidden.hid / hidden.hid.sum()
        out.loc[hidden.index] = (hidden.n + alloc) / hidden.total * 100
        zero_hidden = g[(g.hid > 0) & ~g.index.isin(hidden.index)]
        out.loc[zero_hidden.index] = zero_hidden.n / zero_hidden.total * 100
    return out


def main() -> None:
    con = psycopg.connect(base.DSN)
    years = sys.argv[1:] or ["2018-19", "2022-23", "2023-24"]
    shares: dict[str, tuple[pd.DataFrame, dict, bool]] = {}
    for y in ALL_YEARS:
        try:
            shares[y] = year_shares(y, con)
            print(f"[prior] {y}: tokens consistent with hidden-only (no school with S>U) = {shares[y][2]}; schools with S>U = {int((shares[y][0].groupby('school').S.first() > shares[y][0].groupby('school').U.first()).sum())}")
        except IndexError:
            pass
    for year in years:
        hy, enr, trusted = shares[year]
        i = ALL_YEARS.index(year)
        adj = [ALL_YEARS[j] for j in (i - 1, i + 1) if 0 <= j < len(ALL_YEARS) and ALL_YEARS[j] in shares and shares[ALL_YEARS[j]][2]]
        prior = pd.concat([shares[a][0].set_index(["school", "race"]).hybrid for a in adj], axis=1).mean(axis=1) if adj else pd.Series(dtype=float)
        S_school, U_school = hy.school.map(hy.groupby("school").S.first()), hy.school.map(hy.groupby("school").U.first())
        rules = {
            "hybrid": hy.hybrid,
            "prior_school": prior_alloc(hy, prior, S_school > U_school) if len(prior) else hy.hybrid,
            "prior_year": prior_alloc(hy, prior, pd.Series(True, index=hy.index)) if (len(prior) and not trusted) else hy.hybrid,
            "prior_all": prior_alloc(hy, prior, pd.Series(True, index=hy.index)) if len(prior) else hy.hybrid,
            "prior_year_cap9": prior_alloc(hy, prior, pd.Series(True, index=hy.index), cap=9.0) if (len(prior) and not trusted) else hy.hybrid,
        }
        ref = base.ccd(year)
        key = hy.set_index(["school", "race"]).index
        print(f"\n=== {year}: adjacent trustworthy years for the prior: {adj or 'none'}; year trustworthy = {trusted}")
        for race in RACES:
            m = (hy.race == race) & hy.school.isin(ref.index)
            truth = hy[m].school.map(ref[race])
            line = f"  {race:9s}"
            for name, col in rules.items():
                e = (col[m] - truth)
                line += f" | {name:12s} MAE {e.abs().mean():5.2f} bias {e.mean():+5.2f} p90 {e.abs().quantile(0.9):5.2f}"
            print(line)
        # process-bound test as the harness runs it: |rule − CCD| ≤ min(9·hid_r, U)/total·100 + 0.5
        for name, col in rules.items():
            m = hy.school.isin(ref.index)
            bound = (np.minimum(9.0 * hy.hid[m], hy.U[m]) / hy.total[m] * 100 + 0.5)
            truth = pd.Series([ref.loc[s_, r_] for s_, r_ in zip(hy.school[m], hy.race[m])], index=hy.index[m])
            ok = ((col[m] - truth).abs() <= bound)
            big3 = hy.race[m].isin(["black", "hispanic", "white"])
            print(f"  {name:14s} within process bound: all races {ok.mean() * 100:5.1f}%  black/hispanic/white {ok[big3].mean() * 100:5.1f}%")
        # held-out selection: choose on one half of the schools, score on the other
        big3 = ["black", "hispanic", "white"]
        m = hy.school.isin(ref.index) & hy.race.isin(big3)
        truth = pd.Series([ref.loc[s_, r_] for s_, r_ in zip(hy.school[m], hy.race[m])], index=hy.index[m])
        half = (hy.school[m].astype(int) % 2).rename("half")
        errs = pd.DataFrame({name: (col[m] - truth).abs() for name, col in rules.items()}).join(half)
        mae_by_half = errs.groupby("half").mean()
        for h in (0, 1):
            chosen = mae_by_half.loc[h].idxmin()
            other = 1 - h
            print(f"  held-out: chosen on half {h} (schools {'even' if h == 0 else 'odd'}) = {chosen:15s} in-sample MAE {mae_by_half.loc[h, chosen]:.2f}; on half {other} MAE {mae_by_half.loc[other, chosen]:.2f} (best rule there {mae_by_half.loc[other].idxmin()} {mae_by_half.loc[other].min():.2f})")
        # uniform-split school-years (every cell hidden) and the small-group medians
        allhid = hy.groupby("school").apply(lambda g: (g.n.sum() == 0) and (g.hid.sum() > 0))
        for name, col in rules.items():
            med = {r: float(np.median(col[hy.race == r])) for r in ("amerind", "hawaiian", "multi")}
            uni = int(sum(1 for s in allhid[allhid].index if np.allclose(col[hy.school == s].to_numpy(), 100 / 7, atol=1e-6)))
            print(f"  {name:12s} medians amerind {med['amerind']:.3f} hawaiian {med['hawaiian']:.3f} multi {med['multi']:.3f}; uniform-14.29% school-years {uni}")


if __name__ == "__main__":
    main()