|
| 1 | +"""Executable bounds for documented parity/convention gaps. |
| 2 | +
|
| 3 | +These tests intentionally read committed parity artifacts only. They make the |
| 4 | +"known gap" language auditable without requiring R or Stata at test time. |
| 5 | +""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +import json |
| 10 | +import math |
| 11 | +from pathlib import Path |
| 12 | +from typing import Any |
| 13 | + |
| 14 | +import statspai as sp |
| 15 | + |
| 16 | + |
| 17 | +ROOT = Path(__file__).resolve().parents[1] |
| 18 | +R_RESULTS = ROOT / "tests" / "r_parity" / "results" |
| 19 | +STATA_RESULTS = ROOT / "tests" / "stata_parity" / "results" |
| 20 | + |
| 21 | + |
| 22 | +def _payload(module: str, side: str) -> dict[str, Any]: |
| 23 | + if side == "Stata": |
| 24 | + path = STATA_RESULTS / f"{module}_Stata.json" |
| 25 | + else: |
| 26 | + path = R_RESULTS / f"{module}_{side}.json" |
| 27 | + return json.loads(path.read_text(encoding="utf-8")) |
| 28 | + |
| 29 | + |
| 30 | +def _row(module: str, side: str, statistic: str) -> dict[str, Any]: |
| 31 | + for row in _payload(module, side)["rows"]: |
| 32 | + if row["statistic"] == statistic: |
| 33 | + return row |
| 34 | + raise AssertionError(f"{module}_{side}.json has no statistic {statistic!r}") |
| 35 | + |
| 36 | + |
| 37 | +def _estimate(module: str, side: str, statistic: str) -> float: |
| 38 | + value = _row(module, side, statistic)["estimate"] |
| 39 | + assert isinstance(value, (int, float)) and math.isfinite(value) |
| 40 | + return float(value) |
| 41 | + |
| 42 | + |
| 43 | +def _rel_gap(a: float, b: float) -> float: |
| 44 | + denom = max(abs(a), abs(b), 1e-12) |
| 45 | + return abs(a - b) / denom |
| 46 | + |
| 47 | + |
| 48 | +def _extra(module: str, side: str = "py") -> dict[str, Any]: |
| 49 | + extra = _payload(module, side).get("extra", {}) |
| 50 | + assert isinstance(extra, dict) |
| 51 | + return extra |
| 52 | + |
| 53 | + |
| 54 | +def _gap_description(module: str, gap: str) -> str: |
| 55 | + for row in sp.parity_gap_report(repo_root=ROOT, fmt="records"): |
| 56 | + if row["module_id"] == module and row["gap"] == gap: |
| 57 | + return row["description"] |
| 58 | + raise AssertionError(f"parity_gap_report has no {module}/{gap} row") |
| 59 | + |
| 60 | + |
| 61 | +def test_parity_gap_report_has_actionable_stable_rows(): |
| 62 | + rows = sp.parity_gap_report(repo_root=ROOT, fmt="records") |
| 63 | + assert len(rows) >= 30 |
| 64 | + |
| 65 | + expected = { |
| 66 | + ("06_rd", "bandwidth_selector_gap"), |
| 67 | + ("08_dml", "fold_split_note"), |
| 68 | + ("16_bjs", "stata_gap_note"), |
| 69 | + ("17_etwfe", "aggregation_note"), |
| 70 | + ("52_scm_unique", "note"), |
| 71 | + } |
| 72 | + observed = {(row["module_id"], row["gap"]) for row in rows} |
| 73 | + assert expected <= observed |
| 74 | + |
| 75 | + for row in rows: |
| 76 | + assert row["module_id"] |
| 77 | + assert row["description"].strip() |
| 78 | + assert row["priority"] in {"high", "medium", "low"} |
| 79 | + assert row["next_action"].strip() |
| 80 | + |
| 81 | + |
| 82 | +def test_rd_bandwidth_gap_is_documented_and_common_bandwidth_passes(): |
| 83 | + extra = _extra("06_rd") |
| 84 | + assert "bandwidth_selector_gap" in extra |
| 85 | + assert "forced to the same bandwidth" in extra["bandwidth_selector_gap"] |
| 86 | + |
| 87 | + py_h = _estimate("06_rd", "py", "default_bandwidth_h") |
| 88 | + r_h = _estimate("06_rd", "R", "default_bandwidth_h") |
| 89 | + assert 0.70 <= _rel_gap(py_h, r_h) <= 0.80 |
| 90 | + |
| 91 | + for stat in ( |
| 92 | + "forced_h0.042287_conventional_est", |
| 93 | + "forced_h0.042287_robust_est", |
| 94 | + ): |
| 95 | + assert _rel_gap(_estimate("06_rd", "py", stat), _estimate("06_rd", "R", stat)) < 1e-10 |
| 96 | + |
| 97 | + se_rel = _rel_gap( |
| 98 | + _row("06_rd", "py", "forced_h0.042287_conventional_est")["se"], |
| 99 | + _row("06_rd", "R", "forced_h0.042287_conventional_est")["se"], |
| 100 | + ) |
| 101 | + assert se_rel <= 0.07 |
| 102 | + |
| 103 | + |
| 104 | +def test_dml_fold_split_gap_stays_small_and_explained(): |
| 105 | + note = _extra("08_dml")["fold_split_note"] |
| 106 | + assert "fold-split" in note |
| 107 | + assert "machine precision" in note |
| 108 | + |
| 109 | + py_theta = _estimate("08_dml", "py", "theta_DML_PLR") |
| 110 | + r_theta = _estimate("08_dml", "R", "theta_DML_PLR") |
| 111 | + assert abs(py_theta - r_theta) <= 3e-4 |
| 112 | + assert _rel_gap(py_theta, r_theta) <= 0.003 |
| 113 | + |
| 114 | + |
| 115 | +def test_scm_nonunique_row_is_bounded_by_unique_solution_counterpart(): |
| 116 | + nonunique = _extra("07_scm") |
| 117 | + assert nonunique["validation_tier"] == "identification_dependent_native" |
| 118 | + assert nonunique["tier"] == "T4" |
| 119 | + assert "not unique" in nonunique["native_note"] |
| 120 | + |
| 121 | + py_gap = _estimate("07_scm", "py", "avg_post_gap") |
| 122 | + r_gap = _estimate("07_scm", "R", "avg_post_gap") |
| 123 | + stata_gap = _estimate("07_scm", "Stata", "avg_post_gap") |
| 124 | + assert _rel_gap(py_gap, r_gap) <= 0.025 |
| 125 | + assert _rel_gap(py_gap, stata_gap) <= 0.001 |
| 126 | + |
| 127 | + unique = _extra("52_scm_unique") |
| 128 | + assert unique["true_gap"] == 2.0 |
| 129 | + assert "Unique-solution counterpart" in unique["note"] |
| 130 | + assert abs(_estimate("52_scm_unique", "py", "avg_post_gap") - 2.0) <= 1e-5 |
| 131 | + assert _estimate("52_scm_unique", "py", "pre_treatment_rmse") == 0.0 |
| 132 | + assert _rel_gap( |
| 133 | + _estimate("52_scm_unique", "py", "avg_post_gap"), |
| 134 | + _estimate("52_scm_unique", "R", "avg_post_gap"), |
| 135 | + ) <= 0.01 |
| 136 | + |
| 137 | + |
| 138 | +def test_did_aggregation_convention_rows_keep_point_parity(): |
| 139 | + bjs_note = _extra("16_bjs") |
| 140 | + assert "parity_note" in bjs_note |
| 141 | + assert ( |
| 142 | + "different autosample/aggregation convention" |
| 143 | + in _gap_description("16_bjs", "stata_gap_note") |
| 144 | + ) |
| 145 | + assert _rel_gap( |
| 146 | + _estimate("16_bjs", "py", "att_bjs"), |
| 147 | + _estimate("16_bjs", "R", "att_bjs"), |
| 148 | + ) < 1e-7 |
| 149 | + |
| 150 | + etwfe_note = _extra("17_etwfe")["aggregation_note"] |
| 151 | + assert "weighting='treated'" in etwfe_note |
| 152 | + assert "Point estimates match to machine precision" in etwfe_note |
| 153 | + assert _rel_gap( |
| 154 | + _estimate("17_etwfe", "py", "att_etwfe"), |
| 155 | + _estimate("17_etwfe", "R", "att_etwfe"), |
| 156 | + ) < 1e-12 |
| 157 | + |
| 158 | + |
| 159 | +def test_causal_forest_gap_is_within_combined_monte_carlo_error(): |
| 160 | + extra = _extra("13_causal_forest") |
| 161 | + assert "clean overlap" in extra["dgp"] |
| 162 | + assert "AIPW doubly-robust" in extra["note"] |
| 163 | + |
| 164 | + for stat in ("ate_causal_forest", "att_causal_forest"): |
| 165 | + py = _row("13_causal_forest", "py", stat) |
| 166 | + r = _row("13_causal_forest", "R", stat) |
| 167 | + combined_se = math.sqrt(float(py["se"]) ** 2 + float(r["se"]) ** 2) |
| 168 | + assert abs(float(py["estimate"]) - float(r["estimate"])) / combined_se <= 1.25 |
| 169 | + assert _rel_gap(float(py["estimate"]), float(r["estimate"])) <= 0.07 |
0 commit comments