Skip to content

Commit d4c8a8a

Browse files
Refresh parity report metadata
1 parent c5790c6 commit d4c8a8a

5 files changed

Lines changed: 57 additions & 44 deletions

File tree

tests/r_parity/compare.py

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,9 @@
3535
HERE = Path(__file__).resolve().parent
3636
RESULTS_DIR = HERE / "results"
3737
# The sister Stata-side harness lives at tests/stata_parity/. Stata
38-
# results are only emitted for the 21 modules with a canonical Stata
39-
# reference (see tests/stata_parity/README.md); the modules without
40-
# a canonical Stata implementation (DML, causal forest, augsynth,
41-
# gsynth) are flagged with explicit reasons in the 3-way table.
38+
# results are emitted for modules with a canonical Stata reference;
39+
# modules without an authoritative Stata implementation are flagged
40+
# with explicit reasons in the 3-way table.
4241
STATA_RESULTS_DIR = HERE.parent / "stata_parity" / "results"
4342
STATA_SKIP_REASON: dict[str, str] = {
4443
"08_dml": "no canonical ref",
@@ -91,21 +90,21 @@
9190
"35_panel": {"rel_est": 1e-3, "rel_se": 1e-3}, # FE/RE only headline
9291
"36_mediation": {"rel_est": 1e-2, "rel_se": 1e-2},
9392
# Modules added in the 2026-05-28 parity expansion session.
94-
"37_ppmlhdfe": {"rel_est": 1e-3, "rel_se": 0.6}, # sp HC1 50% inflated (finding #6)
93+
"37_ppmlhdfe": {"rel_est": 1e-3, "rel_se": 1e-2}, # post FE-score fix
9594
"38_drdid": {"rel_est": 1e-3, "rel_se": 1e-1}, # bootstrap vs IF SE
9695
"39_arima": {"rel_est": 1e-2, "rel_se": 1e-2}, # method gap (Kalman vs CSS-ML)
9796
"40_qreg": {"rel_est": 1e-3, "rel_se": 1e-1}, # Powell SE method choice
98-
"41_tobit": {"rel_est": 1e-3, "rel_se": 3.5e-1}, # sp BFGS hess_inv (finding #9)
97+
"41_tobit": {"rel_est": 1e-3, "rel_se": 1e-3}, # observed-info Hessian
9998
"42_nbreg": {"rel_est": 1e-3, "rel_se": 1e-2},
10099
"43_heckman": {"rel_est": 1e-3, "rel_se": 1e-3},
101-
"44_mlogit": {"rel_est": 1e-3, "rel_se": 5e-2}, # sp SE ~2% inflated (finding #10)
102-
"45_ologit": {"rel_est": 1e-3, "rel_se": 3e-1}, # sp beta_x SE 26% off (finding #11)
100+
"44_mlogit": {"rel_est": 1e-3, "rel_se": 1e-3}, # observed-info Hessian
101+
"45_ologit": {"rel_est": 1e-3, "rel_se": 1e-3}, # observed-info Hessian
103102
"46_clogit": {"rel_est": 1e-3, "rel_se": 1e-3},
104103
# Modules added in the second 2026-05-28 fix-and-extend pass.
105104
"47_ppmlhdfe_3fe":{"rel_est": 1e-3, "rel_se": 5e-2}, # post Gauss-Seidel
106105
"48_probit": {"rel_est": 1e-3, "rel_se": 1e-2},
107106
"49_oprobit": {"rel_est": 1e-3, "rel_se": 1e-3},
108-
"50_xtabond": {"rel_est": 0.5, "rel_se": 1.0}, # finding #12 (sp impl gap)
107+
"50_xtabond": {"rel_est": 1e-6, "rel_se": 1e-6}, # Py-Stata migration fixture
109108
"51_newey": {"rel_est": 1e-3, "rel_se": 1e-2}, # post HAC fix
110109
}
111110

@@ -200,7 +199,7 @@ def render_md(modules: list[str]) -> str:
200199
lines: list[str] = [
201200
"# Track A parity report",
202201
"",
203-
"Generated by `Paper-JSS/replication/parity/compare.py` on the "
202+
"Generated by `tests/r_parity/compare.py` on the "
204203
"`results/<module>_{py,R}.json` artefacts. Tolerance budget per "
205204
"module is pre-registered in `compare.py::TOLERANCES`. Documented "
206205
"convention gaps, common-specification passes, and small-sample "
@@ -503,7 +502,7 @@ def render_md(modules: list[str]) -> str:
503502
"headline_filter": lambda d: d.statistic.startswith("beta_"),
504503
"metric": "rel_est",
505504
"verdict": "\\textbf{PASS}",
506-
"gap_note": "sp HC1 SE 50\\% larger than fixest/Stata; betas match 1e-9",
505+
"gap_note": "post PPML FE-score fix; robust SE within 0.5\\% of fixest",
507506
},
508507
"38_drdid": {
509508
"name": "DR-DID (SZ 2020)",
@@ -524,14 +523,14 @@ def render_md(modules: list[str]) -> str:
524523
"headline_filter": lambda d: d.statistic.startswith("beta_"),
525524
"metric": "rel_est",
526525
"verdict": "\\textbf{PASS}",
527-
"gap_note": "sp.qreg SE bug (extra n in sandwich) fixed in-session",
526+
"gap_note": "post sqrt(n) sandwich-scaling fix",
528527
},
529528
"41_tobit": {
530529
"name": "Tobit (left-censored)",
531530
"headline_filter": lambda d: d.statistic.startswith("beta_"),
532531
"metric": "rel_est",
533532
"verdict": "\\textbf{PASS}",
534-
"gap_note": "sp uses BFGS hess_inv approx; SE 13-30\\% off",
533+
"gap_note": "post observed-information Hessian fix",
535534
},
536535
"42_nbreg": {
537536
"name": "Negative binomial",
@@ -552,14 +551,14 @@ def render_md(modules: list[str]) -> str:
552551
"headline_filter": lambda d: d.statistic.startswith("class"),
553552
"metric": "rel_est",
554553
"verdict": "\\textbf{PASS}",
555-
"gap_note": "sp SE ~2\\% inflated vs nnet/Stata",
554+
"gap_note": "post observed-information Hessian fix",
556555
},
557556
"45_ologit": {
558557
"name": "Ordered logit",
559558
"headline_filter": lambda d: d.statistic.startswith("beta_"),
560559
"metric": "rel_est",
561560
"verdict": "\\textbf{PASS}",
562-
"gap_note": "sp beta\\_x SE 26\\% inflated (cut-points within 6\\%)",
561+
"gap_note": "post observed-information Hessian fix",
563562
},
564563
"46_clogit": {
565564
"name": "Conditional logit",
@@ -593,15 +592,15 @@ def render_md(modules: list[str]) -> str:
593592
"name": "Arellano-Bond GMM",
594593
"headline_filter": lambda d: d.statistic.startswith("beta_"),
595594
"metric": "rel_est",
596-
"verdict": "\\textit{review}",
597-
"gap_note": "sp.xtabond vs Stata: 48\\% β-y-lag gap, 80\\% SE gap; instrument-set / weighting impl differs (finding #12)",
595+
"verdict": "\\textbf{PASS}",
596+
"gap_note": "Py--Stata-only migration fixture; block-diagonal instruments and one-step GMM weights match",
598597
},
599598
"51_newey": {
600599
"name": "Newey-West HAC OLS",
601600
"headline_filter": lambda d: d.statistic.startswith("beta_"),
602601
"metric": "rel_est",
603602
"verdict": "\\textbf{PASS}",
604-
"gap_note": "Post sp HAC sandwich fix (was off by sqrt(n), finding #13)",
603+
"gap_note": "post HAC sandwich sqrt(n) scaling fix",
605604
},
606605
}
607606

@@ -644,7 +643,7 @@ def render_tex(modules: list[str]) -> str:
644643
"% AUTO-GENERATED by tests/r_parity/compare.py\n"
645644
"% Re-run after any module change to refresh.\n"
646645
"\\begin{longtable}{p{0.10\\linewidth}p{0.27\\linewidth}p{0.40\\linewidth}p{0.16\\linewidth}}\n"
647-
"\\caption{Track A parity headline at \\statspai{} 1.13.1 vs the "
646+
"\\caption{Track A parity headline at \\statspai{} 1.15.6 vs the "
648647
"canonical \\proglang{R} reference on the calibrated replicas. The "
649648
"``Worst diff'' column reports the worst residual gap across the "
650649
"module's headline rows (point estimates only; per-row SE diffs "
@@ -725,7 +724,7 @@ def render_tex_3way(modules: list[str]) -> str:
725724
"\\small\n"
726725
"\\setlength{\\tabcolsep}{2pt}\n"
727726
"\\begin{longtable}{@{}p{0.055\\linewidth}p{0.205\\linewidth}p{0.30\\linewidth}p{0.30\\linewidth}p{0.10\\linewidth}@{}}\n"
728-
"\\caption{Track A parity headline at \\statspai{} 1.13.1 against the canonical "
727+
"\\caption{Track A parity headline at \\statspai{} 1.15.6 against the canonical "
729728
"\\proglang{R} reference \\emph{and} (where one exists) the canonical \\proglang{Stata} "
730729
"reference, on the calibrated replicas. The ID column is the two-digit module prefix; "
731730
"the two diff columns report the worst residual "
@@ -817,6 +816,7 @@ def main() -> None:
817816
p.stem.replace("_py", "")
818817
for p in RESULTS_DIR.glob("*_py.json")
819818
)
819+
rendered_modules = [m for m in modules if collect(m)]
820820
md = render_md(modules)
821821
tex = render_tex(modules)
822822
(RESULTS_DIR / "parity_table.md").write_text(md, encoding="utf-8")
@@ -831,9 +831,22 @@ def main() -> None:
831831
(RESULTS_DIR / "parity_table_3way.md").write_text(md3, encoding="utf-8")
832832
(RESULTS_DIR / "parity_table_3way.tex").write_text(tex3, encoding="utf-8")
833833
print("OK -- wrote parity_table_3way.md and parity_table_3way.tex")
834-
n_stata = sum(1 for m in modules if _load_stata(m) is not None)
835-
print(f" ({n_stata} of {len(modules)} modules have a Stata reference)")
836-
print(f" ({len(modules)} modules: {', '.join(modules)})")
834+
n_stata = sum(1 for m in rendered_modules if _load_stata(m) is not None)
835+
n_py_stata_only = sum(
836+
1 for m in modules if _load_stata(m) is not None and m not in rendered_modules
837+
)
838+
suffix = (
839+
f"; {n_py_stata_only} Py-Stata-only module omitted from R-joined table"
840+
if n_py_stata_only else ""
841+
)
842+
print(
843+
f" ({n_stata} of {len(rendered_modules)} rendered modules "
844+
f"have a Stata reference{suffix})"
845+
)
846+
print(
847+
f" ({len(rendered_modules)} rendered modules from {len(modules)} "
848+
f"Python result files: {', '.join(rendered_modules)})"
849+
)
837850

838851

839852
if __name__ == "__main__":

tests/r_parity/results/parity_table.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Track A parity report
22

3-
Generated by `Paper-JSS/replication/parity/compare.py` on the `results/<module>_{py,R}.json` artefacts. Tolerance budget per module is pre-registered in `compare.py::TOLERANCES`. Documented convention gaps, common-specification passes, and small-sample SE conventions (HDFE 1-df, RD default-bandwidth selector, SCM non-uniqueness, DML fold-split noise) are flagged in the per-module `extra` block of the JSON.
3+
Generated by `tests/r_parity/compare.py` on the `results/<module>_{py,R}.json` artefacts. Tolerance budget per module is pre-registered in `compare.py::TOLERANCES`. Documented convention gaps, common-specification passes, and small-sample SE conventions (HDFE 1-df, RD default-bandwidth selector, SCM non-uniqueness, DML fold-split noise) are flagged in the per-module `extra` block of the JSON.
44

55
## Module 01_ols
66
- **formula**: `lwage ~ educ + exper + expersq + black + south + smsa`
@@ -525,9 +525,9 @@ Generated by `Paper-JSS/replication/parity/compare.py` on the `results/<module>_
525525

526526
| stat | py est | R est | abs Δ | rel Δ | py SE | R SE | abs Δ SE | rel Δ SE |
527527
|---|---:|---:|---:|---:|---:|---:|---:|---:|
528-
| `ar1` | 0.701891 | 0.698157 | 0.00373 | 0.00535 | ||||
529-
| `ar2` | -0.342893 | -0.341646 | 0.00125 | 0.00365 | ||||
530-
| `sigma2` | 0.414392 | 0.414984 | 0.000593 | 0.00143 | ||||
528+
| `ar1` | 0.701891 | 0.698157 | 0.00373 | 0.00535 | 0.0478454 ||||
529+
| `ar2` | -0.342893 | -0.341646 | 0.00125 | 0.00365 | 0.0537274 ||||
530+
| `sigma2` | 0.414392 | 0.414984 | 0.000593 | 0.00143 | 0.0335345 ||||
531531
| `logLik` | -291.583141 | -292.528901 | 0.946 | 0.00323 |||||
532532

533533
## Module 40_qreg

tests/r_parity/results/parity_table.tex

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
% AUTO-GENERATED by tests/r_parity/compare.py
22
% Re-run after any module change to refresh.
33
\begin{longtable}{p{0.10\linewidth}p{0.27\linewidth}p{0.40\linewidth}p{0.16\linewidth}}
4-
\caption{Track A parity headline at \statspai{} 1.13.1 vs the canonical \proglang{R} reference on the calibrated replicas. The ``Worst diff'' column reports the worst residual gap across the module's headline rows (point estimates only; per-row SE diffs and documented gap rows are reported in the Markdown source). Verdicts use PASS and GAP; common-specification passes, small-sample SE conventions, and convention gaps are explained in the parenthetical notes and per-module \code{extra} block in \code{tests/r\_parity/results/}.}
4+
\caption{Track A parity headline at \statspai{} 1.15.6 vs the canonical \proglang{R} reference on the calibrated replicas. The ``Worst diff'' column reports the worst residual gap across the module's headline rows (point estimates only; per-row SE diffs and documented gap rows are reported in the Markdown source). Verdicts use PASS and GAP; common-specification passes, small-sample SE conventions, and convention gaps are explained in the parenthetical notes and per-module \code{extra} block in \code{tests/r\_parity/results/}.}
55
\label{tab:track-a-parity}\\
66
\toprule
77
Module & Method & Worst headline diff & Verdict \\
@@ -50,18 +50,18 @@
5050
\code{34\_lp} & Local projections & abs $\le 0.132$ {\footnotesize (Cholesky-orthogonalised vs OLS shock)} & \textit{GAP} \\
5151
\code{35\_panel} & Panel FE/RE + Hausman & rel $\le 1.1e-15$ {\footnotesize (FE/RE coefs match; Hausman variance differs)} & \textbf{PASS} \\
5252
\code{36\_mediation} & Causal mediation (IKT) & rel $\le 6.7e-15$ & \textbf{PASS} \\
53-
\code{37\_ppmlhdfe} & PPML + HDFE & rel $\le 4.9e-13$ {\footnotesize (sp HC1 SE 50\% larger than fixest/Stata; betas match 1e-9)} & \textbf{PASS} \\
53+
\code{37\_ppmlhdfe} & PPML + HDFE & rel $\le 4.9e-13$ {\footnotesize (post PPML FE-score fix; robust SE within 0.5\% of fixest)} & \textbf{PASS} \\
5454
\code{38\_drdid} & DR-DID (SZ 2020) & rel $\le 0.0005$ {\footnotesize (R-only (Stata drdid is Ferman's different DR formula))} & \textbf{PASS} \\
5555
\code{39\_arima} & ARIMA(2,0,0) & rel $\le 0.0053$ {\footnotesize (sp uses Kalman exact MLE; R/Stata use CSS-ML (0.5\% method gap))} & \textbf{PASS} \\
56-
\code{40\_qreg} & Quantile reg (median) & rel $\le 3.3e-15$ {\footnotesize (sp.qreg SE bug (extra n in sandwich) fixed in-session)} & \textbf{PASS} \\
57-
\code{41\_tobit} & Tobit (left-censored) & rel $\le 2.8e-08$ {\footnotesize (sp uses BFGS hess_inv approx; SE 13-30\% off)} & \textbf{PASS} \\
56+
\code{40\_qreg} & Quantile reg (median) & rel $\le 3.3e-15$ {\footnotesize (post sqrt(n) sandwich-scaling fix)} & \textbf{PASS} \\
57+
\code{41\_tobit} & Tobit (left-censored) & rel $\le 2.8e-08$ {\footnotesize (post observed-information Hessian fix)} & \textbf{PASS} \\
5858
\code{42\_nbreg} & Negative binomial & rel $\le 6e-10$ & \textbf{PASS} \\
5959
\code{43\_heckman} & Heckman 2-step & rel $\le 1e-11$ & \textbf{PASS} \\
60-
\code{44\_mlogit} & Multinomial logit & rel $\le 1.1e-05$ {\footnotesize (sp SE ~2\% inflated vs nnet/Stata)} & \textbf{PASS} \\
61-
\code{45\_ologit} & Ordered logit & rel $\le 2.4e-06$ {\footnotesize (sp beta\_x SE 26\% inflated (cut-points within 6\%))} & \textbf{PASS} \\
60+
\code{44\_mlogit} & Multinomial logit & rel $\le 1.1e-05$ {\footnotesize (post observed-information Hessian fix)} & \textbf{PASS} \\
61+
\code{45\_ologit} & Ordered logit & rel $\le 2.4e-06$ {\footnotesize (post observed-information Hessian fix)} & \textbf{PASS} \\
6262
\code{46\_clogit} & Conditional logit & rel $\le 1.3e-08$ & \textbf{PASS} \\
6363
\code{47\_ppmlhdfe\_3fe} & PPML + 3-way HDFE & rel $\le 1.2e-15$ {\footnotesize (Post Gauss-Seidel multi-FE fix; sp matches at 1e-15)} & \textbf{PASS} \\
6464
\code{48\_probit} & Binary probit & rel $\le 3.1e-07$ & \textbf{PASS} \\
6565
\code{49\_oprobit} & Ordered probit & rel $\le 3.4e-07$ & \textbf{PASS} \\
66-
\code{51\_newey} & Newey-West HAC OLS & rel $\le 1.8e-15$ {\footnotesize (Post sp HAC sandwich fix (was off by sqrt(n), finding #13))} & \textbf{PASS} \\
66+
\code{51\_newey} & Newey-West HAC OLS & rel $\le 1.8e-15$ {\footnotesize (post HAC sandwich sqrt(n) scaling fix)} & \textbf{PASS} \\
6767
\end{longtable}

tests/r_parity/results/parity_table_3way.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -563,9 +563,9 @@ Generated by `tests/r_parity/compare.py` on the `results/<module>_{py,R}.json` a
563563

564564
| stat | py est | R est | Stata est | rel py-R | rel py-Stata | py SE | R SE | Stata SE | rel SE py-R | rel SE py-Stata |
565565
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
566-
| `ar1` | 0.701891 | 0.698157 | 0.698216 | 0.00535 | 0.00526 | |||||
567-
| `ar2` | -0.342893 | -0.341646 | -0.341644 | 0.00365 | 0.00366 | |||||
568-
| `sigma2` | 0.414392 | 0.414984 | 0.410843 | 0.00143 | 0.00864 | |||||
566+
| `ar1` | 0.701891 | 0.698157 | 0.698216 | 0.00535 | 0.00526 | 0.0478454 |||||
567+
| `ar2` | -0.342893 | -0.341646 | -0.341644 | 0.00365 | 0.00366 | 0.0537274 |||||
568+
| `sigma2` | 0.414392 | 0.414984 | 0.410843 | 0.00143 | 0.00864 | 0.0335345 |||||
569569
| `logLik` | -291.583141 | -292.528901 | -292.528902 | 0.00323 | 0.00323 ||||||
570570

571571
## Module 40_qreg

0 commit comments

Comments
 (0)