Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **`LPDiD` R-parity validation (absorbing).** `tests/test_methodology_lpdid.py` pins the
estimator against the method authors' own R recipes (`danielegirardi/lpdid` event-study /
reweight / premean / pooled `fixest::feols` specifications) with an `alexCardazzi/lpdid`
cross-check gate on the event-study variants, generated by
`benchmarks/R/generate_lpdid_golden.R`. Variance-weighted, reweighted, premean,
direct-covariate, pooled, and regression-adjustment-point estimates match to ~1e-12
(pooled is anchored to the authors' fixed-composition recipe only — `alexCardazzi`'s pooled
uses a laxer clean-control window and is recorded for transparency, not gated). The
regression-adjustment standard error (canonically Stata `teffects ra` only — no R-package
analogue) is pinned and its calibration validated by an ungated Monte-Carlo coverage study
(`benchmarks/python/coverage_lpdid_ra.py`, ≈0.95 coverage across cluster counts). Confirms
the two previously-provisional REGISTRY notes (RA influence-function variance convention;
pooled fixed-composition estimand) resolve in the library's favour with no estimator change.

## [3.6.0] - 2026-06-29

### Added
Expand Down
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ exists but parity can't be verified without a local toolchain.
| `CallawaySantAnna` bootstrap: align p-value computation with R `did`'s symmetric-percentile method (former "CallawaySantAnna Bootstrap Improvements" section). | `staggered.py` | — | Low |
| **`bias_corrected_local_linear` (lprobust) Phase-1c follow-ups:** extend golden parity to `kernel ∈ {triangular, uniform}` (epa-only today); expose `vce ∈ {hc0,hc1,hc2,hc3}` on the public wrapper once R goldens exist (port supports all four; needs a per-mode generator + a hc2/hc3 q-fit-leverage decision); clustered-DGP auto-bandwidth parity is **blocked upstream** on an nprobust singleton-cluster bug in `lpbwselect.mse.dpi` (Phase-1c DGP 4 uses manual `h=b=0.3`). | `_nprobust_port.py`, `local_linear.py`, `generate_nprobust_lprobust_golden.R` | Phase 1c | Low-Med |
| `HeterogeneousAdoptionDiD` Stute-family Stata-bridge parity: no public R `Stutetest` package exists; would add `benchmarks/stata/generate_stute_golden.do` + a Stata dependency. | `benchmarks/stata/`, `tests/test_stute_test_parity.py` | follow-up | Low |
| **`LPDiD` regression-adjustment SE — no runnable R reference.** The RA influence-function cluster SE is canonically Stata `teffects ra ... atet vce(cluster)` only; no R package computes it (`alexCardazzi/lpdid` does direct covariate inclusion, not RA). Today the RA *point* is R-anchored (~1e-12), the SE is pinned + MC-coverage-validated (`coverage_lpdid_ra.py`). Follow-up: contribute the RA path to `alexCardazzi/lpdid` so a runnable R RA reference exists — only a *trusted* anchor once cross-checked vs Stata `teffects` (else circular). | `tests/test_methodology_lpdid.py`, `benchmarks/python/coverage_lpdid_ra.py` | #B2 follow-up | Low |
| `HeterogeneousAdoptionDiD` Phase-3 R-parity: ships coverage-rate validation on synthetic DGPs, not tight point parity vs `chaisemartin::stute_test` / `yatchew_test` (needs bootstrap-seed-semantics + `B` alignment across numpy/R). | `tests/test_had_pretests.py` | Phase 3 | Low |

### Parked — pending user demand / out of scope
Expand Down
314 changes: 314 additions & 0 deletions benchmarks/R/generate_lpdid_golden.R

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions benchmarks/data/lpdid_golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"meta": {
"estimator": "LPDiD (Dube, Girardi, Jorda & Taylor 2025) - absorbing",
"r_version": "R version 4.5.2 (2025-10-31)",
"fixest_version": "0.13.2",
"lpdid_alexcardazzi_version": "0.6.1",
"lpdid_alexcardazzi_commit": "ba64563983861be5e616f6020842c1a1cdf17a27",
"seed": 20260629,
"pre_window": 3,
"post_window": 4,
"se_convention": "Default/weighted/direct/pmd/pooled SEs: feols cluster-robust at unit with the reghdfe-style finite-sample factor (G/(G-1))*((n-1)/(n-k)) via setFixest_ssc(adj=TRUE, cluster.adj=TRUE) - the authors' convention.",
"ra_se_note": "ra_cov records the RA POINT estimate (full-interaction == teffects ra atet). The canonical RA SE is Stata teffects (unconditional IF, NO finite-sample factor) - no R package computes it; the library influence-function SE is pinned on the Python side and calibration-validated by benchmarks/python/coverage_lpdid_ra.py. ra_cov[h] = c(att, conditional_CR0_se_for_reference_only).",
"pooled_note": "Pooled uses the authors' fixed-composition window-mean recipe (mean(y over [t,t+POST]) - y_{t-1}, clean through F.POST). alexCardazzi's pooled uses a laxer clean-control window, so its pooled value is recorded here for transparency but NOT used as the cross-check gate.",
"nocomp_note": "no_composition is NOT a golden parity variant: the library fixes the realized sample across all post horizons (paper Section 3.6) and excludes cohorts p_g > T-H, more faithful to the paper than alexCardazzi's looser per-horizon version (recorded below). The library path is validated by the pure-Python B1 tests.",
"alex_pooled_post": [3.487463576882, 0.3495904202394],
"alex_nocomp_looser": {
"-3": [-0.7532927692809, 0.5003193796022],
"-2": [-0.250198606317, 0.6160670067758],
"-1": [0, 0],
"0": [1.46387943226, 0.545454853302],
"1": [2.335727820716, 0.5511865769279],
"2": [2.865671038762, 0.5566693486612],
"3": [3.245223911323, 0.4728816808867],
"4": [4.100734546643, 0.4370902112454]
}
},
"vw_es": {
"0": [2.891454184758, 0.4972838592697],
"1": [3.215287698688, 0.4242105863868],
"2": [3.570283061763, 0.4502488079078],
"3": [3.968564586288, 0.3904079437881],
"4": [4.269176444588, 0.396524220757],
"-2": [0.1183626967809, 0.4413440724331],
"-3": [-0.1254433057345, 0.4292726043484]
},
"ew_es": {
"0": [2.903628272087, 0.4973282590126],
"1": [3.221973963653, 0.4235473199649],
"2": [3.57527972142, 0.4499773773075],
"3": [3.971884855509, 0.390089371568],
"4": [4.265997983221, 0.3934421813065],
"-2": [0.1195811966608, 0.4402860720602],
"-3": [-0.1219233071656, 0.4298940947632]
},
"pmd_es": {
"0": [2.832272836367, 0.4337549369753],
"1": [3.156106350297, 0.3522180898809],
"2": [3.511101713373, 0.394076563941],
"3": [3.902081106111, 0.3654320104818],
"4": [4.269986848274, 0.3537196151693],
"-2": [0.05918134839047, 0.2206720362165],
"-3": [-0.184624654125, 0.3713701578016]
},
"direct_es": {
"0": [2.876086144279, 0.4110153204559],
"1": [3.217671119032, 0.433940624349],
"2": [3.571778263524, 0.4571207641365],
"3": [3.969558831006, 0.3912243590417],
"4": [4.268878161094, 0.3995261701574],
"-2": [0.1185563879358, 0.442300060411],
"-3": [-0.1239894706087, 0.4328879644821]
},
"ra_cov": {
"0": [2.888871838355, 0.2774049784919, null],
"1": [3.22526891068, 0.3671167840304, null],
"2": [3.577950221628, 0.3920693801281, null],
"3": [3.973278128237, 0.363703531293, null],
"4": [4.265103781445, 0.3614251956151, null],
"-2": [0.1204444745696, 0.4103497220604, null],
"-3": [-0.1197046094519, 0.4026535247538, null]
},
"pooled": {
"post": [3.532265685012, 0.3521082333958],
"pre": [-0.003540304476795, 0.377308359478]
}
}
76 changes: 76 additions & 0 deletions benchmarks/data/lpdid_ra_coverage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"study": "LPDiD RA influence-function SE coverage",
"nominal": 0.95,
"tau": {
"0": 1.0,
"1": 1.5,
"2": 2.0,
"3": 2.5
},
"note": "RA IF variance is asymptotic (teffects convention, no finite-sample factor); empirical coverage of the true effect holds near nominal across G for both the event-study horizons and the headline pooled-row RA CI - the t(G-1) reference keeps it well-calibrated even at modest G. Underwrites REGISTRY ## LPDiD Deviation 2.",
"sweep": [
{
"n_units": 30,
"reps": 500,
"per_horizon": {
"0": 0.954,
"1": 0.952,
"2": 0.94,
"3": 0.968
},
"mean_event_coverage": 0.9535,
"pooled_att_coverage": 0.968,
"n_valid": {
"0": 500,
"1": 500,
"2": 500,
"3": 500
},
"n_pooled_valid": 500,
"n_fit_errors": 0,
"min_valid_share": 1.0
},
{
"n_units": 100,
"reps": 500,
"per_horizon": {
"0": 0.942,
"1": 0.932,
"2": 0.944,
"3": 0.942
},
"mean_event_coverage": 0.94,
"pooled_att_coverage": 0.946,
"n_valid": {
"0": 500,
"1": 500,
"2": 500,
"3": 500
},
"n_pooled_valid": 500,
"n_fit_errors": 0,
"min_valid_share": 1.0
},
{
"n_units": 300,
"reps": 500,
"per_horizon": {
"0": 0.95,
"1": 0.948,
"2": 0.946,
"3": 0.936
},
"mean_event_coverage": 0.945,
"pooled_att_coverage": 0.958,
"n_valid": {
"0": 500,
"1": 500,
"2": 500,
"3": 500
},
"n_pooled_valid": 500,
"n_fit_errors": 0,
"min_valid_share": 1.0
}
]
}
Loading
Loading