Skip to content

test: anchor LPDiD non-absorbing SEs against the authors' Stata lpdid package - #785

Merged
igerber merged 2 commits into
mainfrom
stata-lpdid-nonabsorbing
Aug 20, 2026
Merged

test: anchor LPDiD non-absorbing SEs against the authors' Stata lpdid package#785
igerber merged 2 commits into
mainfrom
stata-lpdid-nonabsorbing

Conversation

@igerber

@igerber igerber commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • New committed-golden Stata arm (benchmarks/stata/generate_lpdid_nonabsorbing_golden.dobenchmarks/data/lpdid_nonabsorbing_stata_golden.jsontests/test_lpdid_nonabsorbing_stata_parity.py, 51 tests) anchoring the LPDiD non-absorbing modes against the authors' SSC lpdid package (v1.0.3) end-to-end — the package builds its own clean samples, unlike the hand-reconstructed teffects RA arm.
  • First external anchors for: the non-absorbing reweighted SE (previously pinned-only via RW_SE_PIN with a documented ~5e-5 fixest weighted-cluster convention gap), the non-absorbing pooled windows (points AND SEs), and the Eq. 12 reweighted point.
  • Option↔mode mapping pinned and gated: nonabsorbing(, firsttreat notyet) == first_entry (Eq. 12); nonabsorbing(L) == effect_stabilization (Eq. 13).
  • benchmarks/stata/requirements.do gains lpdid + its deps (boottest/egenmore/listreg); the generator fails closed on all 8 run-time dependencies and records each in meta.ssc_versions with an embedded ado checksum+length drift signal.
  • No library source change; no pin values change. Docs: REGISTRY LPDiD (SE block, Deviation 4 two-tier adjudication, boundary bullet, checklist), DEFERRED.md row resolved (SE half, scoped), both benchmark READMEs, docs/benchmarks.rst, docs/api/lpdid.rst, llms-full.txt scoping, paper-review annotation, doc-deps mappings, CHANGELOG.

Measured deviations (library recompute vs committed Stata golden, same machine)

Surface max |att| dev max SE dev
Eq. 12 vw — all ES horizons (incl. placebos) + pooled pre/post 1.0e-14 5.6e-17
Eq. 12 rw — same 1.3e-08 2.3e-09
Eq. 13 vw — post h=0..4 + pooled post (convention-neutral subsample) 1.1e-14 1.8e-16
Eq. 13 rw — same 1.9e-08 1.6e-09

All inside the module gates (att abs=1e-6, SE abs=1e-7). Sample sizes match exactly on every gated surface (per-horizon and pooled obs). Stata Eq. 12 vw also reproduces the committed R fixest::feols golden to ~1e-13 (gate 1).

Convention differences (measured, adjudicated, divergence-gated)

Three reference-package convention differences are documented in REGISTRY ## LPDiD Deviation 4, adjudicated against the paper's stated Eq. 13 restriction (treated: D=0 levels on [t-L, t-1]; control: ΔD=0 on [-h, L] lags):

  1. Exact-L re-entry — the package's switch-free lag window effectively requires L+1 untreated periods before re-entry; stricter than the paper, whose levels condition the library implements (3 rows on the committed panel).
  2. Pre-panel boundary — the package's missing-lag semantics admit always-treated units as early-t controls; paper-silent surface, both sides documented conventions (30 rows/horizon).
  3. Placebo windows — recursive lagged intersection (package) vs backward window (library); paper-silent.

The Eq. 13 full-panel and placebo/pooled-pre divergences these produce are recorded in the golden and locked by divergence-documentation gates (att floors + sample-size-mismatch asserts), so parity claims stay scoped; those surfaces remain outside the Stata anchor (tracked in DEFERRED.md along with the non-absorbing RA path).

Methodology references (required if estimator / math changes)

  • Method name(s): LPDiD non-absorbing entry-effect estimands (first_entry Eq. 12 / effect_stabilization Eq. 13) — validation only, no estimator change
  • Paper / source link(s): Dube, Girardi, Jordà & Taylor (2025), A Local Projections Approach to Difference-in-Differences, JAE 40(7), https://doi.org/10.1002/jae.70000 (§4.2; Eq. 13 text verified against the open-access WP, whose Eq. 18 is the published Eq. 13); authors' Stata package SSC s459273 v1.0.3
  • Any intentional deviations from the source (and why): none new in the library; three reference-package convention differences documented under REGISTRY ## LPDiD Deviation 4 (see above)

Validation

  • Tests added/updated: tests/test_lpdid_nonabsorbing_stata_parity.py (new, 51 tests, gates 1–8); tests/test_methodology_lpdid.py (comment/docstring scoping only, no pin values changed); full targeted runs pass (test_lpdid_nonabsorbing_stata_parity 51, test_lpdid_ra_stata_parity + test_doc_deps_integrity 269 combined, test_methodology_lpdid + test_lpdid 132)
  • Backtest / simulation / notebook evidence (if applicable): N/A (committed-golden external-parity arm; generator log verified clean via grep -E '^r\([0-9]+\);')

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

@github-actions

Copy link
Copy Markdown

Overall assessment: ✅ Looks good

Executive summary

  • No estimator or statistical behavior changed.
  • LPDiD Eq. 12/13 parity claims are appropriately scoped.
  • Three Stata convention differences are documented and therefore non-defects.
  • Two P2 documentation/provenance issues remain; neither affects statistical output.
  • Runtime tests were unavailable because this environment lacks pytest, NumPy, and Stata; static Python/JSON/YAML checks passed.

Methodology

  • P2 — Conflicting pooled-inference documentation

    • Impact: The Registry now correctly distinguishes pooled regressions from suest joint tests, but the paper review still says pooled SEs use stacking, leaving contradictory methodology guidance.
    • Locations: docs/methodology/REGISTRY.md:L2471; docs/methodology/papers/dube-2025-review.md:L97-L101, L197-L204
    • Concrete fix: Update the paper review to state that suest applies to joint cross-horizon tests, while pooled pre/post inference uses a separate pooled long-difference regression.
  • P3 — Documented implementation conventions

    • Impact: Eq. 13 exact-L re-entry, pre-panel boundary, and placebo-window differences deliberately limit Stata parity scope. They are documented under a labeled Registry deviation and are not defects.
    • Location: docs/methodology/REGISTRY.md:L2488
    • Concrete fix: None required.

Code Quality

No findings.

Performance

No findings. The 51 tests share five module-scoped estimator fits.

Maintainability

  • P2 — Provenance omits one guarded ado file
    • Impact: _gclsst is fail-closed as a separate dependency, but only _gfilter.ado is checksummed for egenmore. A change to _gclsst alone would not alter meta.ssc_versions, contrary to the “every fail-closed dependency” contract.
    • Locations: benchmarks/stata/generate_lpdid_nonabsorbing_golden.do:L163-L185; tests/test_lpdid_nonabsorbing_stata_parity.py:L472-L490
    • Concrete fix: Record _gclsst.ado’s checksum/length alongside _gfilter.ado, then require both in the provenance test.

Tech Debt

  • P3 — Properly tracked
    • Impact: Non-absorbing RA and Eq. 13 placebo/pooled-pre inference remain deferred without blocking this PR.
    • Location: DEFERRED.md:L68
    • Concrete fix: None required for this PR.

Security

No findings or apparent secrets.

Documentation/Tests

Apart from the P2 documentation conflict above, coverage and parity scoping are strong. Fixture hashes and schemas validate successfully; full tests could not run because the review environment lacks project dependencies and Stata.

@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: a5527a4bedcbfbb90dca63af54023a93ccfdefb7


Overall assessment: ✅ Looks good

Executive summary

  • No estimator or statistical behavior changed.
  • LPDiD Eq. 12/13 validation aligns with the Registry and cited paper review.
  • All Stata convention differences are explicitly documented and parity-scoped.
  • Both previous P2 findings are resolved.
  • No unmitigated P0/P1 findings.

Methodology

  • Severity: P3 — Documented implementation conventions
    • Impact: Exact-L re-entry, pre-panel boundaries, and placebo windows limit Eq. 13 Stata parity scope without changing library behavior.
    • Locations: docs/methodology/REGISTRY.md:L2457-L2471, L2488; tests/test_lpdid_nonabsorbing_stata_parity.py:L413-L460
    • Concrete fix: None required. The deviations are labeled, source-adjudicated, and divergence-gated.

The paper specifies the Eq. 12/13 clean-sample restrictions but no SE formula. Unit-clustered inference is therefore correctly presented as an implementation choice validated against Stata.

Code Quality

No findings. Empty/missing values fail closed, and parity comparisons cover points, SEs, and sample sizes.

Performance

No findings. The 51 parametrized tests reuse five module-scoped fits.

Maintainability

No findings. The previous provenance issue is resolved: both _gfilter.ado and _gclsst.ado now receive independent checksum/length drift signals.

Locations: benchmarks/stata/generate_lpdid_nonabsorbing_golden.do:L163-L189; tests/test_lpdid_nonabsorbing_stata_parity.py:L475-L493

Tech Debt

  • Severity: P3 — Properly tracked
    • Impact: Non-absorbing RA and Eq. 13 placebo/pooled-pre inference remain outside this anchor.
    • Location: DEFERRED.md:L68
    • Concrete fix: None required for this PR.

Security

No findings or apparent secrets.

Documentation/Tests

No findings. The prior pooled-inference documentation conflict is resolved by distinguishing suest joint tests from pooled long-difference regressions.

Static Python, JSON, YAML, fixture-hash, and sample-rule checks passed. Runtime tests could not run because pytest, NumPy, pandas, and Stata are unavailable in the review environment.

@igerber igerber added ready-for-ci Triggers CI test workflows and removed ready-for-ci Triggers CI test workflows labels Aug 20, 2026
… package

New committed-golden Stata arm running SSC lpdid v1.0.3 end-to-end (the
package builds its own clean samples, unlike the hand-reconstructed
teffects RA arm). First external anchors for the non-absorbing reweighted
SE (previously pinned-only via RW_SE_PIN), the non-absorbing pooled
windows (points and SEs), and the Eq. 12 reweighted point.

Mapping: nonabsorbing(, firsttreat notyet) == first_entry (Eq. 12);
nonabsorbing(L) == effect_stabilization (Eq. 13).

Parity scoping: Eq. 12 agrees on every surface of the committed panel
(all ES horizons incl. placebos + both pooled windows; att ~1e-14 vw /
~1.3e-8 rw, SE ~6e-17 vw / ~2.3e-9 rw). Eq. 13 agrees at post horizons +
pooled post on a convention-neutral 47-unit subsample (att ~1.1e-14 /
~1.9e-8, SE ~1.8e-16 / ~1.6e-9). Three package convention differences
measured, adjudicated against the paper's Eq. 13 text, and locked by
divergence-documentation gates (att floors + sample-size mismatch): the
package requires L+1 untreated periods before re-entry (stricter than
Eq. 13's levels condition, which the library implements), admits
always-treated units as early controls via missing-lag boundary
semantics (paper-silent), and builds placebo samples by recursive lagged
intersection (paper-silent).

- benchmarks/stata/generate_lpdid_nonabsorbing_golden.do: arms A (Eq. 12
  full panel, vw+rw), B (Eq. 13 subsample, vw+rw, preserve/restore-
  wrapped), C (Eq. 13 full panel, divergence documentation); fail-closed
  8-dep guard; _jnum fails closed on missing scalars; version capture
  widened for headerless packages and ALWAYS embeds the ado checksum+len
  (fails closed if checksum unavailable)
- benchmarks/data/lpdid_nonabsorbing_stata_golden.json: committed golden
  (source_sha256-pinned to the committed panel; ssc_versions records
  every guard dependency with embedded checksums)
- tests/test_lpdid_nonabsorbing_stata_parity.py: 51 tests, gates 1-8;
  split fixture guards (the R golden gates only gate 1)
- benchmarks/stata/requirements.do: adds lpdid + boottest/egenmore/
  listreg (egenmore probed via _gfilter AND _gclsst so an incomplete
  install is repaired on rerun)
- REGISTRY LPDiD: SE block + Deviation 4 rewritten with the measured
  anchors and the two-tier paper adjudication; pooled-inference
  description corrected (package fits pooled windows as separate
  unit-clustered regressions, not suest stacking); boundary bullet
  extended; checklist split; Note 1 scoped
- DEFERRED.md non-absorbing row: Resolved (SE half, scoped); residual
  narrowed to RA + Eq. 13 placebo/pooled-pre inference
- Docs: benchmarks/stata/README.md (new arm section + stale five-arm/
  native-claim fixes), benchmarks/README.md, docs/benchmarks.rst,
  docs/api/lpdid.rst, llms-full.txt scoping, dube-2025-review.md
  (this-repo annotation), doc-deps.yaml (benchmarks.rst + paper review
  mapped under lpdid.py), CHANGELOG
- tests/test_methodology_lpdid.py: RW_SE_PIN comment + class docstring
  scoped to the new anchor; tier-2 wording sweep (comments only; R
  goldens/panels NOT regenerated)

No library source change; no pin values change.
…ope the paper-review suest bullets to joint tests with the measured pooled construction
@igerber
igerber force-pushed the stata-lpdid-nonabsorbing branch from a5527a4 to 9dfd151 Compare August 20, 2026 21:46
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 9dfd151a975e5167ba04e4cd9dc6f4e3b80e5128


Overall assessment: ✅ Looks good

Executive summary

  • No estimator, default, weighting, or inference code changed.
  • Eq. 12/13 claims align with the Methodology Registry and paper review.
  • Eq. 13 convention differences are documented and excluded from parity gates.
  • Both prior review findings are resolved.
  • No unmitigated P0/P1 findings.

Methodology

  • Severity: P3 — Documented convention differences
    • Impact: Exact-(L) re-entry, pre-panel boundaries, and placebo windows limit Eq. 13 Stata parity without changing library behavior.
    • Locations: docs/methodology/REGISTRY.md:L2488; tests/test_lpdid_nonabsorbing_stata_parity.py:L413-L460
    • Concrete fix: None required; differences are labeled and divergence-gated.

The paper specifies no SE formula. Unit-clustered inference is appropriately documented as an implementation choice validated against Stata.

Code Quality

No findings. Missing/non-finite golden values fail closed, and no inline inference anti-pattern was introduced.

Performance

No findings. The 51 parametrized checks reuse five module-scoped fits.

Maintainability

No findings. The prior provenance gap is resolved: _gfilter.ado and _gclsst.ado have independent checksum/length signals.

Location: benchmarks/stata/generate_lpdid_nonabsorbing_golden.do:L169-L189

Tech Debt

  • Severity: P3 — Properly tracked
    • Impact: Non-absorbing RA and Eq. 13 placebo/pooled-pre inference remain outside this anchor.
    • Location: DEFERRED.md:L68
    • Concrete fix: None required for this PR.

Security

No findings or apparent secrets.

Documentation/Tests

No findings. The prior suest conflict is resolved by distinguishing joint tests from pooled long-difference regressions.

Static AST, JSON, source-hash, provenance, drop-rule, and doc-dependency checks passed. Runtime pytest was unavailable because pytest and NumPy are not installed.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 20, 2026
@igerber
igerber merged commit 49c682e into main Aug 20, 2026
39 of 40 checks passed
@igerber
igerber deleted the stata-lpdid-nonabsorbing branch August 20, 2026 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant