Source document

pipeline_philly/verify/reconcile_spree.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-a) Spot check — served DB vs SDP SPREE composite source.

SPREE = SDP's School Progress Report on Education and Equity. It republishes
the same PSSA/Keystone/grad/attendance numbers families see in the official
report card, on a per-school per-metric grain (long-format CSV).

Per O.3: SPREE is both a published-rank baseline (for the site's
cross-referencing) AND the (iii-a) spot-check oracle. This script does the
spot-check half.

Same-publisher caveat applies (per 05_validation_spec.md): SPREE comes from
SDP's own pipeline, so it's a presentation cross-reference, not an
independent oracle. The independent (iii-b) check uses Future Ready PA Index
(separate script).

Output: docs/qa_reports/philly/__spree_spot_check.md (+ .json).

Run: python pipeline_philly/verify/reconcile_spree.py
"""
from __future__ import annotations

import json
import pathlib
import statistics
import sys

import pandas as pd

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))

from pipeline_philly.verify import common  # noqa: E402

SPREE_SOURCE = "sdp_spree_2023-24"
TOLERANCE = 1.5  # ±pp, per KX's NYC convention

# Map SPREE metric-string prefix → our metric_key.
# Pattern: a SPREE metric string starts with a known prefix and may end with
# an optional subgroup suffix like ", Black/African American".
METRIC_PREFIX_MAP: dict[str, str] = {
    "PSSA ELA: % of Students Proficient or Advanced, Grades 3-8": "pssa_all_ela_proficiency",
    "PSSA Math: % of Students Proficient or Advanced, Grades 3-8": "pssa_all_math_proficiency",
    "PSSA ELA: % of Students Proficient or Advanced, Grade 3": "pssa_grade3_ela_proficiency",
    "PSSA Math: % of Students Proficient or Advanced, Grade 3": "pssa_grade3_math_proficiency",
    "PSSA ELA: % of Students Proficient or Advanced, Grade 4": "pssa_grade4_ela_proficiency",
    "PSSA Math: % of Students Proficient or Advanced, Grade 4": "pssa_grade4_math_proficiency",
    "PSSA ELA: % of Students Proficient or Advanced, Grade 5": "pssa_grade5_ela_proficiency",
    "PSSA Math: % of Students Proficient or Advanced, Grade 5": "pssa_grade5_math_proficiency",
    "PSSA ELA: % of Students Proficient or Advanced, Grade 6": "pssa_grade6_ela_proficiency",
    "PSSA Math: % of Students Proficient or Advanced, Grade 6": "pssa_grade6_math_proficiency",
    "PSSA ELA: % of Students Proficient or Advanced, Grade 7": "pssa_grade7_ela_proficiency",
    "PSSA Math: % of Students Proficient or Advanced, Grade 7": "pssa_grade7_math_proficiency",
    "PSSA ELA: % of Students Proficient or Advanced, Grade 8": "pssa_grade8_ela_proficiency",
    "PSSA Math: % of Students Proficient or Advanced, Grade 8": "pssa_grade8_math_proficiency",
    "Keystone Algebra I: % of Students Proficient or Advanced": "keystone_algebra_proficiency",
    "Keystone Literature: % of Students Proficient or Advanced": "keystone_literature_proficiency",
    "Keystone Biology: % of Students Proficient or Advanced": "keystone_biology_proficiency",
    "4-Year Cohort Graduation Rate": "graduation_rate_4yr",
}

# Suffix after ", " → our PhillySubgroup
SUBGROUP_SUFFIX_MAP: dict[str, str] = {
    "": "ALL",                                  # bare prefix = All Students
    "American Indian/Alaskan Native": "AMER_INDIAN_AK_NATIVE",
    "Asian": "ASIAN",
    "Black/African American": "BLACK",
    "Hispanic/Latino": "HISPANIC",
    "Multiracial": "TWO_OR_MORE_RACES",
    "Native Hawaiian/Pacific Islander": "HAWAIIAN_PAC_ISL",
    "White": "WHITE",
    "Economically Disadvantaged": "ECON_DISADV",
    "English Learners": "ELL",
    "Special Education": "IEP",
}


def parse_spree_metric(s: str) -> tuple[str, str] | None:
    """Map SPREE metric text → (metric_key, subgroup). None if unrecognized."""
    s = s.strip()
    for prefix, metric_key in METRIC_PREFIX_MAP.items():
        if s == prefix:
            return metric_key, "ALL"
        if s.startswith(prefix + ","):
            suffix = s[len(prefix) + 1:].strip()
            sg = SUBGROUP_SUFFIX_MAP.get(suffix)
            if sg:
                return metric_key, sg
    return None


def main() -> None:
    common.ensure_qa_dir()
    spree_path = common.latest_source_path(SPREE_SOURCE)
    df = pd.read_csv(spree_path, low_memory=False)
    print(f"[spree] loaded {len(df):,} SPREE rows")

    # SPREE year is "2023-2024"; normalize to "2023-24" to match our DB
    df["year_canon"] = df["accountability_year"].str.replace(r"^(\d{4})-\d{2}(\d{2})$", r"\1-\2", regex=True)

    # Parse metrics
    df["parsed"] = df["metric"].map(lambda m: parse_spree_metric(str(m)) if m else None)
    df_mapped = df[df["parsed"].notna()].copy()
    print(f"[spree] mapped rows (recognized metric × subgroup): {len(df_mapped):,}")
    df_mapped[["metric_key", "subgroup"]] = pd.DataFrame(df_mapped["parsed"].tolist(), index=df_mapped.index)

    # Now query DB for the same (ulcs, year, metric_key, subgroup) tuples.
    # For PSSA + Keystone we have two population_cuts (acct, actual). The (iii-a)
    # check uses acct (canonical per O.8).
    dsn = common.served_db_dsn()
    try:
        import psycopg
    except ImportError as e:
        raise SystemExit("psycopg not installed") from e

    # SPREE's score values are stored as percent (0-100), so direct compare works.
    diffs: list[dict] = []
    found = 0
    not_in_db = 0
    null_in_spree = 0
    pairs_count = 0
    matches = 0
    mismatches = 0

    with psycopg.connect(dsn) as conn:
        conn.read_only = True
        with conn.cursor() as cur:
            # Build a temp lookup of (ulcs, year, metric_key, subgroup, cut='acct') → value
            keys = [
                (str(r["school_id"]).strip(), str(r["year_canon"]), r["metric_key"], r["subgroup"])
                for _, r in df_mapped.iterrows()
            ]
            # Query the relevant slice
            cur.execute(
                """
                SELECT school_ulcs, year, metric_key, subgroup::text AS subgroup, value
                  FROM philly_school_year_metrics
                 WHERE population_cut IN ('acct', 'n/a')
                   AND value IS NOT NULL
                """,
            )
            db_map: dict[tuple, float] = {}
            for ulcs, year, mk, sg, val in cur:
                db_map[(str(ulcs), str(year), mk, sg)] = float(val)

    for _, r in df_mapped.iterrows():
        pairs_count += 1
        ulcs = str(r["school_id"]).strip()
        year = str(r["year_canon"])
        mk = r["metric_key"]
        sg = r["subgroup"]
        spree_v = pd.to_numeric(r["metric_score"], errors="coerce")
        if pd.isna(spree_v):
            null_in_spree += 1
            continue
        db_v = db_map.get((ulcs, year, mk, sg))
        if db_v is None:
            not_in_db += 1
            continue
        found += 1
        delta = float(db_v) - float(spree_v)
        if abs(delta) <= TOLERANCE:
            matches += 1
        else:
            mismatches += 1
        diffs.append({
            "school_ulcs": ulcs,
            "year": year,
            "metric_key": mk,
            "subgroup": sg,
            "served": round(float(db_v), 2),
            "spree": round(float(spree_v), 2),
            "delta": round(delta, 2),
        })

    deltas = [d["delta"] for d in diffs]
    summary = {
        "spree_source": SPREE_SOURCE,
        "tolerance_abs": TOLERANCE,
        "spree_rows_total": len(df),
        "spree_rows_mapped": len(df_mapped),
        "pairs_attempted": pairs_count,
        "null_in_spree": null_in_spree,
        "not_in_db": not_in_db,
        "pairs_compared": found,
        "matches_within_tol": matches,
        "mismatches": mismatches,
        "agreement_rate": round(matches / found, 4) if found > 0 else None,
        "mean_delta": round(statistics.mean(deltas), 3) if deltas else None,
        "median_delta": round(statistics.median(deltas), 3) if deltas else None,
        "max_abs_delta": round(max(abs(d) for d in deltas), 3) if deltas else None,
    }

    # Worst 30 by abs(delta)
    worst = sorted(diffs, key=lambda r: abs(r["delta"]), reverse=True)[:30]

    out: list[str] = []
    out.append("# (iii-a) SPREE spot check — same-publisher presentation cross-reference\n")
    out.append("Compares served DB values against SDP SPREE composite. Same-publisher\n")
    out.append("(both feed from SDP's data pipeline) — this is a presentation check, NOT\n")
    out.append("an independent oracle. For independent cross-publisher check, see\n")
    out.append("__cross_publisher_pssa.md.\n")
    out.append(f"\n## Summary\n```json\n{json.dumps(summary, indent=2)}\n```\n")
    if worst:
        out.append("\n## Worst 30 mismatches (by absolute delta)\n")
        for r in worst:
            out.append(
                f"- ULCS {r['school_ulcs']} {r['year']} {r['metric_key']} {r['subgroup']}: "
                f"served={r['served']:.2f} spree={r['spree']:.2f} Δ={r['delta']:+.2f}",
            )

    (common.QA_REPORTS_DIR / "__spree_spot_check.md").write_text("\n".join(out))
    (common.QA_REPORTS_DIR / "__spree_spot_check.json").write_text(
        json.dumps({"summary": summary, "worst_30": worst}, indent=2),
    )
    print(f"\n[spree] {json.dumps(summary)}")
    print(f"[spree] report -> docs/qa_reports/philly/__spree_spot_check.md")


if __name__ == "__main__":
    main()