Skip to content

Add LPDiD non-absorbing treatment (entry-effect estimands), Phase C1#584

Merged
igerber merged 2 commits into
mainfrom
feature/lpdid-nonabsorbing
Jun 29, 2026
Merged

Add LPDiD non-absorbing treatment (entry-effect estimands), Phase C1#584
igerber merged 2 commits into
mainfrom
feature/lpdid-nonabsorbing

Conversation

@igerber

@igerber igerber commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implement Dube, Girardi, Jordà & Taylor (2025) Section 4.2 non-absorbing treatment for LPDiD via a new non_absorbing parameter: "first_entry" (Eq. 12 — the effect of entering treatment for the first time and staying treated) and "effect_stabilization" (Eq. 13, with stabilization_window=L — units whose treatment has been stable for at least L periods serve as clean controls, so estimation is feasible with few or no never-treated units). The default non_absorbing=None is unchanged: the absorbing path, which still rejects non-absorbing input and is bit-identical to before.
  • Mode-aware clean-sample masks evaluate the window conditions via cumulative treatment-change / level lookups, with a documented "untreated before the first observed period" boundary convention. Placebo (h<0) and pooled-pre windows reach back to the deepest horizon ([t-max(L,-h), t-1]) so pre-trends are uncontaminated. A per-horizon clean-treated indicator threads through the estimator / regression-adjustment / reweight / pooled paths so re-entry events (which are not first entries) are classified correctly. Non-absorbing modes require a gap-free panel within each unit's observed span.
  • non_absorbing / stabilization_window are validated (atomic set_params), propagated to get_params() and LPDiDResults (summary() / to_dict()).

Methodology references

  • Method name(s): LPDiD (Local Projections Difference-in-Differences) — non-absorbing extension (entry-effect estimands)
  • Paper / source link(s): Dube, A., Girardi, D., Jordà, Ò., & Taylor, A. M. (2025). A Local Projections Approach to Difference-in-Differences. Journal of Applied Econometrics, 40(5), 741-758. https://doi.org/10.1002/jae.70000 (Section 4.2 / online Appendix C: Eq. 12 first-time entry, Eq. 13 effect stabilization, Assumption 9).
  • Intentional deviations (documented in docs/methodology/REGISTRY.md ## LPDiD):
    • Scope (Deviation Claude/setup pip install k2 m4j #4): the entry-effect estimands only. Appendix-C exit-event dynamics (eta_h^{g,n}), R-package parity (PR-C2), and survey-design support are deferred follow-ups (tracked in TODO.md).
    • Boundary convention (extends Deviation Add comprehensive code review for diff-diff library #5): periods before a unit's first observed period are treated as untreated with no change; a unit genuinely treated before the panel starts could be misread as a fresh entry under effect_stabilization (an assumption to confirm against R in PR-C2).
    • Interior gaps: non-absorbing modes require gap-free panels within each unit's span and raise otherwise (the [t-L, t+h] window conditions cannot be verified across a gap); extending the absorbing path's interior-gap reindex to non-absorbing is a deferred follow-up.
    • Standard errors: the same cluster-robust family as the absorbing path (the paper specifies no SE formula); validation against the reference R packages is the planned PR-C2.

Validation

  • Tests added/updated: tests/test_lpdid.py — new TestLPDiDNonAbsorbing + TestLPDiDNonAbsorbingAPI (20 tests: absorbing reduction (bit-identical), single-cohort reduction, re-entry mechanism, boundary retention, negative-horizon placebos, non-negative weighting, stabilized-control admission, equal-weight recovery, DGP recovery, pooled-pre window, one-off/no-control/interior-gap edge cases, param validation), plus the updated absorbing-rejection test. The 70 absorbing tests and 8 R-parity goldens (tests/test_methodology_lpdid.py) are unchanged; all green on both the pure-Python and Rust backends.
  • Backtest / simulation / notebook evidence: N/A — validation is pure-Python (analytical DGP recovery + deterministic hand-built panels).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Generated with Claude Code

igerber and others added 2 commits June 29, 2026 14:21
Implement Dube, Girardi, Jorda & Taylor (2025) Section 4.2 non-absorbing
treatment for LPDiD via a new `non_absorbing` parameter:

- "first_entry" (Eq. 12): effect of entering treatment for the first time and
  staying treated; reuses the absorbing clean control, restricts only the
  treated set. Bit-identical to the absorbing path on absorbing panels.
- "effect_stabilization" (Eq. 13, `stabilization_window=L`): units whose
  treatment has been stable for >= L periods serve as clean controls, so
  estimation is feasible with few/no never-treated units.

Default `non_absorbing=None` is unchanged (absorbing path, still rejects
non-absorbing input). Mode-aware clean-sample masks evaluate window conditions
via cumulative treatment-change/level lookups with a documented "untreated
before the first observed period" boundary convention; placebo horizons use the
full pre-span window so pre-trends are uncontaminated; a per-horizon clean-
treated indicator threads through the estimator / RA / reweight / pooled paths
so re-entry events are classified correctly. Non-absorbing modes require a
gap-free panel within each unit's observed span.

Pure-Python validation (tests/test_lpdid.py::TestLPDiDNonAbsorbing): absorbing
reduction, single-cohort reduction, re-entry mechanism, boundary retention,
negative-horizon placebos, non-negative weighting, stabilized-control
admission, equal-weight recovery, and DGP recovery; absorbing tests + R-parity
goldens unchanged. Exit-event dynamics, R-package parity (PR-C2), and
survey-design support are tracked follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex P1: `_build_pooled_sample(kind="pre")` passed horizon=0 to the
non-absorbing masks, so the effect_stabilization clean window only covered
[t-L, t] instead of the pooled-pre reach-back to the most-negative horizon
([t - max(L, -h), t-1]). A unit with a prior treated spell at t-3 (clean at
t-1) leaked into a [-3, -2] pooled-pre placebo and biased it. Pre windows now
use min(horizons); the absorbing branch keeps horizon=0 (not-yet-treated at t
already implies a clean pre-span, so its R-parity goldens are unchanged).

Adds a deterministic regression test (spell entrants excluded from the
pooled-pre sample; verified to fail at 0.286 before the fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Overall Assessment

Looks good — no unmitigated P0/P1 findings.

Executive Summary

  • Affected method: LPDiD, specifically new non-absorbing modes first_entry and effect_stabilization.
  • The clean-sample masks in diff_diff/lpdid.py match the project registry’s Section 4.2 contracts and the public paper’s non-absorbing treatment restrictions. (nber.org)
  • New params are validated, propagated to get_params(), LPDiDResults, summaries, and serialization.
  • NaN-consistent inference uses the existing safe_inference() path; no new inline inference anti-patterns found.
  • Deferred exit-event dynamics, non-absorbing R parity, and interior-gap support are documented in REGISTRY.md / TODO.md, so they are P3 informational, not blockers.

Methodology

P3 informational — deferred non-absorbing scope is documented.
Impact: The PR implements entry-effect estimands only; exit-event dynamics and R-package parity remain follow-ups. This is aligned with docs/methodology/REGISTRY.md:L1853-L1862 and tracked in TODO.md:L97-L121, so it is not a defect.
Concrete fix: None required for this PR.

No P0/P1 methodology findings.
The implemented masks at diff_diff/lpdid.py:L267-L327 match the registry’s first-entry and effect-stabilization contracts at docs/methodology/REGISTRY.md:L1831-L1841.

Code Quality

No findings. Parameter validation and atomic rollback cover the new mode/window interaction at diff_diff/lpdid.py:L63-L87 and diff_diff/lpdid.py:L1350-L1364.

Performance

No findings. _nonabsorbing_masks() rebuilds indexed lookup Series per call at diff_diff/lpdid.py:L283-L284; this is acceptable for the current estimator structure and not a correctness issue.

Maintainability

No findings. The new per-horizon treated indicator is clearly threaded into horizon and pooled samples at diff_diff/lpdid.py:L441-L454 and diff_diff/lpdid.py:L971-L977.

Tech Debt

P3 informational — tracked deferred work.
Impact: Non-absorbing interior-gap support and R parity are deferred, but fail closed or are explicitly tracked.
Concrete fix: None required; follow the existing TODO entries at TODO.md:L98-L121.

Security

No findings. I saw no accidental secrets in the changed files.

Documentation/Tests

P3 informational — local test execution unavailable in this review environment.
Impact: I could not run the added tests because pytest and runtime dependencies such as pandas are not installed here. The PR does add focused coverage for API validation, absorbing reduction, re-entry, boundary clamping, pooled pre windows, NaN inference, and metadata at tests/test_lpdid.py:L1409-L1711.
Concrete fix: None required by the diff; CI should run tests/test_lpdid.py and methodology goldens.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jun 29, 2026
@igerber igerber merged commit 4f1a0a3 into main Jun 29, 2026
33 of 34 checks passed
@igerber igerber deleted the feature/lpdid-nonabsorbing branch June 29, 2026 20:26
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