"""Phase C — compare primary (data/extracted/json) and independent duplicate (data/validated/json) extractions on the validation sample. Reports study-level eligibility agreement, coefficient recall/precision (matched on model + taxonomy_id + printed variable name, fuzzy), numerical agreement of beta and se (relative error ≤ 1 %), and writes reports/extraction_audit.md tables + data/validated/validation_results.csv.""" import json, re from difflib import SequenceMatcher from pathlib import Path import numpy as np, pandas as pd ROOT = Path(__file__).resolve().parents[2]; P = ROOT / "data" / "extracted" / "json"; V = ROOT / "data" / "validated" / "json" def norm(s): return re.sub(r"[^a-z0-9]", "", (s or "").lower()) def load(d): out = {} for f in d.glob("*.json"): try: out[f.stem] = json.loads(f.read_text()) except Exception as e: out[f.stem] = {"error": repr(e)} return out def match_coefs(a, b): """Greedy matching of coefficient rows between two extractions of the same paper.""" ca = [c for c in a.get("coefficients", []) if c.get("confidence") != "low"]; cb = [c for c in b.get("coefficients", []) if c.get("confidence") != "low"] used = set(); pairs = [] for i, x in enumerate(ca): best = None; bs = 0 for j, y in enumerate(cb): if j in used: continue s = (x.get("taxonomy_id") == y.get("taxonomy_id")) * 2 + SequenceMatcher(None, norm(x.get("var_name_printed")), norm(y.get("var_name_printed"))).ratio() + (norm(x.get("model_id")) == norm(y.get("model_id"))) if s > bs: bs, best = s, j if best is not None and bs >= 2.5: used.add(best); pairs.append((x, cb[best])) return ca, cb, pairs def main(): prim, dup = load(P), load(V); rows = [] for rid, b in dup.items(): a = prim.get(rid) if a is None: continue ea, eb = a.get("eligible"), b.get("eligible") ca, cb, pairs = match_coefs(a, b) beta_ok = sum(1 for x, y in pairs if x.get("beta") is not None and y.get("beta") is not None and abs(x["beta"] - y["beta"]) <= 0.01 * max(abs(x["beta"]), 1e-9) + 1e-9) se_pairs = [(x, y) for x, y in pairs if x.get("se") is not None and y.get("se") is not None] se_ok = sum(1 for x, y in se_pairs if abs(x["se"] - y["se"]) <= 0.01 * max(abs(x["se"]), 1e-9) + 1e-9) sign_ok = sum(1 for x, y in pairs if x.get("beta") is not None and y.get("beta") is not None and np.sign(x["beta"]) == np.sign(y["beta"])) tax_ok = sum(1 for x, y in pairs if x.get("taxonomy_id") == y.get("taxonomy_id")) rows.append({"record_id": rid, "eligible_primary": ea, "eligible_duplicate": eb, "eligibility_agree": ea == eb, "n_coef_primary": len(ca), "n_coef_duplicate": len(cb), "n_matched": len(pairs), "recall_vs_duplicate": len(pairs) / len(cb) if cb else np.nan, "precision_vs_duplicate": len(pairs) / len(ca) if ca else np.nan, "beta_agree_1pct": beta_ok / len(pairs) if pairs else np.nan, "se_agree_1pct": se_ok / len(se_pairs) if se_pairs else np.nan, "sign_agree": sign_ok / len(pairs) if pairs else np.nan, "taxonomy_agree": tax_ok / len(pairs) if pairs else np.nan}) df = pd.DataFrame(rows); (ROOT / "data" / "validated").mkdir(exist_ok=True); df.to_csv(ROOT / "data" / "validated" / "validation_results.csv", index=False) if len(df): summ = {"papers_double_extracted": len(df), "eligibility_agreement": df.eligibility_agree.mean(), "coef_recall": df.recall_vs_duplicate.mean(), "coef_precision": df.precision_vs_duplicate.mean(), "beta_agreement_1pct": df.beta_agree_1pct.mean(), "se_agreement_1pct": df.se_agree_1pct.mean(), "sign_agreement": df.sign_agree.mean(), "taxonomy_agreement": df.taxonomy_agree.mean(), "coefficients_compared": int(df.n_matched.sum())} (ROOT / "data" / "validated" / "validation_summary.json").write_text(json.dumps(summ, indent=2, default=float)); print(json.dumps(summ, indent=2, default=float)) if __name__ == "__main__": main()