Source document

pipeline_miami/verify/cross_publisher_absenteeism.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.

"""(iii-b) Cross-publisher check for the outcomes track: FLDOE "absent 10% or
more" (chronic_absenteeism_rate × its denominator = a count) vs the federal
copy of the same submission in EDFacts FS195/DG814 (EdDataExpress), school by
school, 2022-23.

Population difference fixed BEFORE the comparison was run (documented in
docs/cities/miami/findings.md, F.14): EDFacts DG814 covers K-12 (+ ungraded)
while the FLDOE file counts every student enrolled ≥10 days including PK.
So the exact comparison is made on schools with no October PK membership;
schools with PK are reported as a regression of the gap on PK membership
(a descriptive slope, not a prevalence estimate). A suppressed PK cell means 1–9 PK
students (not none): those schools are kept out of the exact comparison and
out of the regression (their PK count is unknown), and counted. EDFacts
perturbs cells ≤4 by ±1, so the pass rule is within ±1 for ≥90% of no-PK
schools.

Run:  python -m pipeline_miami.verify.cross_publisher_absenteeism
"""
from __future__ import annotations

import csv
import io
import json
import statistics
import zipfile

from pipeline_miami.verify import common
from pipeline_miami.verify.connectors import served_db

SOURCE_ID = "eddataexpress_chronic_absenteeism_2022-23"
YEAR = "2022-23"
DADE_LEAID = "1200390"


def edfacts_allsch() -> dict[str, float]:
    """NCES_SCH → chronically absent count (ALLSCH) for Miami-Dade."""
    path = common.latest_source_path(SOURCE_ID)
    out: dict[str, float] = {}
    with zipfile.ZipFile(path) as z:
        name = next(n for n in z.namelist() if "_SCH_" in n and n.endswith(".csv"))
        with z.open(name) as fh:
            for r in csv.DictReader(io.TextIOWrapper(fh, encoding="utf-8-sig")):
                if r["LEAID"] != DADE_LEAID or r["SUBGROUP"] != "ALLSCH":
                    continue
                try:
                    out[r["NCES_SCH"]] = float(r["NUMERIC_VALUE"])
                except (TypeError, ValueError):
                    continue
    return out


def nces_map() -> dict[str, str]:
    import psycopg

    out: dict[str, str] = {}
    with psycopg.connect(common.served_db_dsn()) as conn:
        conn.read_only = True
        with conn.cursor() as cur:
            cur.execute("SELECT nces_id, school_number FROM miami_schools WHERE nces_id IS NOT NULL")
            for nces, num in cur:
                out[str(nces)] = str(num)
    return out


def main() -> None:
    common.ensure_qa_dir()
    ede = edfacts_allsch()
    nces = nces_map()
    fl = served_db.fetch("chronic_absenteeism_rate")
    pk = served_db.fetch("enrollment_oct_gPK")
    rows = []
    unmatched = 0
    for nces_id, count in ede.items():
        num = nces.get(nces_id)
        if not num:
            unmatched += 1
            continue
        f = fl.get((num, YEAR))
        if not f or f["value"] is None or not f["n"]:
            continue
        fl_count = round(f["value"] / 100 * f["n"])
        p = pk.get((num, YEAR))
        pk_status = "suppressed" if (p and p["suppressed"]) else ("with_pk" if (p and p["value"]) else "no_pk")
        rows.append({"school": num, "edfacts": count, "fldoe": fl_count, "delta": count - fl_count,
                     "pk": (p["value"] or 0.0) if p and p["value"] is not None else 0.0, "pk_status": pk_status, "denominator": f["n"]})
    no_pk = [r for r in rows if r["pk_status"] == "no_pk"]
    with_pk = [r for r in rows if r["pk_status"] == "with_pk"]
    pk_suppressed = [r for r in rows if r["pk_status"] == "suppressed"]
    within1 = sum(1 for r in no_pk if abs(r["delta"]) <= 1)
    exact = sum(1 for r in no_pk if r["delta"] == 0)
    share = within1 / len(no_pk) if no_pk else 0.0
    verdict = "PASS" if no_pk and share >= 0.90 else "REVIEW"
    # Gap on PK membership, least squares (different annual and October denominators; no prevalence interpretation).
    slope = intercept = corr = None
    if len(with_pk) >= 10:
        xs = [r["pk"] for r in with_pk]
        ys = [-r["delta"] for r in with_pk]
        mx, my = statistics.fmean(xs), statistics.fmean(ys)
        sxx = sum((x - mx) ** 2 for x in xs)
        sxy = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
        syy = sum((y - my) ** 2 for y in ys)
        slope = sxy / sxx if sxx else None
        intercept = my - slope * mx if slope is not None else None
        corr = sxy / (sxx * syy) ** 0.5 if sxx and syy else None
    summary = {
        "summary": {
            "year": YEAR, "verdict": verdict, "n_edfacts_dade": len(ede), "n_unmatched_nces": unmatched, "n_pairs": len(rows),
            "no_pk": {"n": len(no_pk), "exact": exact, "within_1": within1, "within_1_share": share,
                      "median_delta": statistics.median(r["delta"] for r in no_pk) if no_pk else None},
            "pk_suppressed": {"n": len(pk_suppressed), "median_delta": statistics.median(r["delta"] for r in pk_suppressed) if pk_suppressed else None},
            "with_pk": {"n": len(with_pk), "median_delta": statistics.median(r["delta"] for r in with_pk) if with_pk else None,
                        "gap_on_pk": {"slope": slope, "intercept": intercept, "r": corr}},
            "verdict_note": "EDFacts DG814 is K-12 (+ungraded); FLDOE's absent-10%+ count includes PK, so exact agreement is expected only where October PK membership is zero. Pass rule: within ±1 (EDFacts perturbation) for ≥90% of no-PK schools. For PK schools the gap is reported as a regression on PK membership; schools whose PK cell is suppressed (1–9 students) are in neither group.",
        },
        "largest_no_pk_deltas": sorted(no_pk, key=lambda r: -abs(r["delta"]))[:15],
    }
    (common.QA_REPORTS_DIR / "__cross_publisher_absenteeism.json").write_text(json.dumps(summary, indent=2, default=str))
    s = summary["summary"]
    out = [
        "# Cross-publisher (iii-b) — FLDOE absent-10%+ count vs EDFacts DG814 (2022-23)\n",
        f"Verdict: **{verdict}** — {s['n_pairs']} schools matched by NCES id ({s['n_unmatched_nces']} EDFacts rows without a directory match).\n",
        "| population | schools | exact | within ±1 | median Δ (EDFacts − FLDOE) |", "|---|---:|---:|---:|---:|",
        f"| no October PK membership | {s['no_pk']['n']} | {s['no_pk']['exact']} | {s['no_pk']['within_1']} ({share * 100:.1f}%) | {s['no_pk']['median_delta']} |",
        f"| with PK membership | {s['with_pk']['n']} | — | — | {s['with_pk']['median_delta']} |",
        f"| PK cell suppressed (1–9) | {s['pk_suppressed']['n']} | — | — | {s['pk_suppressed']['median_delta']} |",
        "",
        (f"Schools with PK: EDFacts undercount = {slope:.3f} × PK membership {intercept:+.2f} (r = {corr:.3f}); the annual-count gap and October membership use different denominators, so this slope is not a direct estimate of PK chronic-absence prevalence."
         if slope is not None else "Too few PK schools for the regression."),
        "", s["verdict_note"], "", "## Largest differences among no-PK schools (school, EDFacts, FLDOE)",
    ]
    out += [f"- {r['school']}: {int(r['edfacts'])} vs {int(r['fldoe'])}" for r in summary["largest_no_pk_deltas"]]
    (common.QA_REPORTS_DIR / "__cross_publisher_absenteeism.md").write_text("\n".join(out))
    print(json.dumps(s, indent=1, default=str))


if __name__ == "__main__":
    main()