"""Stage-3 PAIRED BENCH — shipped-system scorecard over every paired dirty/clean set. Walks data/real/*/ for cell-aligned pairs and scores the SHIPPED deterministic pipeline (mock_plan — what the Space runs) with the same churn-neutral metric and variant-class breakdown as eval/generalization.py. Sources that fed the champion's training mix are INCLUDED but flagged (seen=True) — transparency over exclusion. uv run python -m eval.paired_bench Writes eval/results/paired_bench.json and docs/PAIRED_BENCH.md. """ from __future__ import annotations import argparse import json import time from pathlib import Path import pandas as pd from scrubdata.executor import apply_plan from scrubdata.planner import mock_plan from .generalization import TRAIN_SOURCES, variant_breakdown from .run_real_multi import _cell_only, score ROOT = Path(__file__).resolve().parent.parent REAL = ROOT / "data" / "real" N_CAP = 20000 # row cap for very large pairs (gidcl_imdb, tax100k) SEEN = TRAIN_SOURCES["v6"] | {"fodors_zagats", "cleanml_company", "cleanml_movie", "gidcl_imdb"} # gidcl trains from v9 (stage-3) def pairs() -> list[Path]: return sorted(p for p in REAL.iterdir() if (p / "dirty.csv").exists() and (p / "clean.csv").exists()) def _load(p: Path): kw = dict(dtype=str, keep_default_na=False, nrows=N_CAP, on_bad_lines="skip") d = pd.read_csv(p / "dirty.csv", encoding_errors="replace", **kw) c = pd.read_csv(p / "clean.csv", encoding_errors="replace", **kw) n = min(len(d), len(c)) return d.head(n).reset_index(drop=True), c.head(n).reset_index(drop=True) def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--only", default=None) ap.add_argument("--out", default="eval/results/paired_bench.json") args = ap.parse_args() rows = [] for p in pairs(): name = p.name if args.only and name != args.only: continue try: dirty, clean = _load(p) except Exception as e: # noqa: BLE001 print(f" {name}: LOAD FAILED {type(e).__name__}") continue t0 = time.perf_counter() cleaned, _ = apply_plan(dirty, _cell_only(mock_plan(dirty))) m = score(dirty, clean, cleaned) b = variant_breakdown(dirty, clean, cleaned) vr = b["variant_fixed"] / b["variant_errors"] if b["variant_errors"] else None row = {"name": name, "seen_in_training": name in SEEN, "rows": len(dirty), "cols": dirty.shape[1], "errors": m["_errors"], "variant_errors": b["variant_errors"], "f1": round(m["f1"], 3), "precision": round(m["precision"], 3), "recall": round(m["recall"], 3), "damage": round(m["damage"], 4), "variant_recall": round(vr, 3) if vr is not None else None, "sec": round(time.perf_counter() - t0, 1)} rows.append(row) print(f" {name:<42} F1={row['f1']:<6} VR={row['variant_recall']} " f"dmg={row['damage']} err={row['errors']} " f"{'[SEEN]' if row['seen_in_training'] else ''}", flush=True) json.dump(rows, open(args.out, "w"), indent=1) L = ["# Paired Bench — shipped system on every cell-aligned pair", "", "Churn-neutral repairs metric + variant-class recall; `seen` = source fed", "the champion's training mix (flagged, not hidden).", "", "| dataset | seen | rows×cols | errors | variant | F1 | precision | recall | VR | damage |", "|---|---|---|---|---|---|---|---|---|---|"] for r in sorted(rows, key=lambda x: x["f1"]): L.append(f"| {r['name']} | {'✓' if r['seen_in_training'] else ''} | " f"{r['rows']}×{r['cols']} | {r['errors']} | {r['variant_errors']} | " f"{r['f1']} | {r['precision']} | {r['recall']} | " f"{r['variant_recall']} | {r['damage']} |") (ROOT / "docs" / "PAIRED_BENCH.md").write_text("\n".join(L) + "\n") print(f"{len(rows)} pairs -> {args.out} + docs/PAIRED_BENCH.md") if __name__ == "__main__": main()