Skip to content

feat: add LWDiD estimator (Lee & Wooldridge 2025, 2026)#588

Open
gorgeousfish wants to merge 1 commit into
igerber:mainfrom
gorgeousfish:feature/lwdid-estimator
Open

feat: add LWDiD estimator (Lee & Wooldridge 2025, 2026)#588
gorgeousfish wants to merge 1 commit into
igerber:mainfrom
gorgeousfish:feature/lwdid-estimator

Conversation

@gorgeousfish

@gorgeousfish gorgeousfish commented Jun 30, 2026

Copy link
Copy Markdown

This PR adds native support for the Lee & Wooldridge (2025, 2026) rolling-transformation difference-in-differences method. The approach works by applying unit-specific time-series transformations (demeaning or detrending) to panel outcomes before treatment, converting the panel DiD problem into a standard cross-sectional one. Once transformed, any treatment-effect estimator — regression adjustment, inverse probability weighting, doubly robust, or propensity-score matching — can be applied directly to the cross-section. The method handles both common-timing and staggered-adoption designs with flexible control-group selection.

The implementation lives entirely within diff_diff/lwdid*.py (9 modules) and introduces zero new runtime dependencies — it reuses the existing solve_ols, solve_logit, and safe_inference infrastructure. The IPW and IPWRA standard errors use the full semiparametric influence function with propensity-score and outcome-model correction terms, matching the variance formula in the authors' Stata package.

Numerical correctness has been validated against the reference lwdid Python package across all supported configurations. The RA path achieves machine-precision agreement (≤1e-10), and IPW/IPWRA paths agree to within 1%. The California Proposition 99 results from Table 3 of LW (2026) are reproduced exactly: demeaning ATT = −0.422, detrending ATT = −0.227.

Beyond core estimation, the PR includes wild cluster bootstrap inference (Rademacher/Mammen/Webb), Fisher randomization inference, parallel-trends pre-testing, sensitivity analysis, clustering-level diagnostics, and visualization methods. A tutorial notebook walks through the full workflow on the papers' empirical datasets (California smoking data, Castle Doctrine laws, Walmart county-level entry).

Methodology references (required if estimator / math changes)

  • Method name(s): LWDiD (Lee & Wooldridge rolling-transformation DiD)
  • Paper / source link(s):
  • Any intentional deviations from the source (and why): None. Implementation follows Procedures 2.1, 3.1, and 4.1 exactly. IPW/IPWRA SE uses the full semiparametric influence function matching the authors' Stata package (lwdid.ado).

Validation

  • Tests added/updated: 9 test files, 222 tests (unit tests, numerical precision, equivalence against lwdid-py, wild bootstrap, randomization, diagnostics, sensitivity, visualization)
  • Backtest / simulation / notebook evidence (if applicable): Tutorial notebook (docs/tutorials/26_lwdid.ipynb) reproduces Tables 3–4 from LW (2026) on California Proposition 99 data. ATT values match published results to 0.04% precision.

Security / privacy

  • Confirm no secrets/PII in this PR: Confirmed. No secrets, tokens, or PII. Datasets included (smoking.csv, walmart.csv, castle.csv) are publicly available research data from the referenced papers.

Native implementation of rolling-transformation DiD:
- Transformations: demean, detrend, demeanq, detrendq
- Estimation: RA, IPW, IPWRA, PSM
- Inference: classical, HC0-HC4, cluster-robust, t-distribution
- Designs: common timing + staggered adoption
- Advanced: wild cluster bootstrap, randomization inference
- Diagnostics: parallel trends, sensitivity, clustering
- Visualization: cohort trends, event study, sensitivity plots

Numerical equivalence validated against lwdid-py (tolerance ≤1e-10 for
RA paths) and paper Tables 3-4 (LW 2026) reproduced exactly.

Zero new runtime dependencies (uses existing numpy/pandas/scipy).

References:
- Lee, S.J. & Wooldridge, J.M. (2025). SSRN 4516518.
- Lee, S.J. & Wooldridge, J.M. (2026). SSRN 5325686.
@gorgeousfish gorgeousfish force-pushed the feature/lwdid-estimator branch from 9712477 to 8c5ccce Compare June 30, 2026 07:50
@igerber

igerber commented Jul 4, 2026

Copy link
Copy Markdown
Owner

@gorgeousfish Thanks for this - it's a serious, well-prepared contribution, and the method is a great
fit for the library. I'd like to move it forward.

That said, before I do a full review, one thing to clear first: licensing. This implementation
appears closely related to your lwdid-py package, which is AGPL-3.0, while diff-diff
is MIT. Could you confirm:

  1. You hold the copyright to all code in this PR (i.e., it's your own work, not derived
    from third-party code such as the authors' Stata lwdid package), and you're
    contributing it under diff-diff's MIT license.
  2. The bundled datasets (smoking.csv, castle.csv, walmart.csv, .dta files) are
    redistributable - a pointer to their original source/terms would help.

@gorgeousfish

Copy link
Copy Markdown
Author

Thanks for the quick response and the positive signal.

On licensing:

Yes, I hold the copyright to all code in this PR. It's my own independent implementation, not derived from Lee & Wooldridge's Stata lwdid package or any third-party source. I'm contributing it under diff-diff's MIT license.

On datasets:

smoking.csv: Abadie, Diamond & Hainmueller (2010), California tobacco control program. Publicly available, widely redistributed in academic packages.

castle.csv: Cheng & Hoekstra (2013) / Cunningham (2021), Castle Doctrine laws. Publicly available.

walmart.csv: county-level panel from Brown & Butts (2025, Journal of Econometrics), constructed from County Business Patterns (CBP) data. Publicly available government statistical data.

@igerber

igerber commented Jul 12, 2026

Copy link
Copy Markdown
Owner

@gorgeousfish Thanks for the licensing confirmation - that closes the question. Since then we've completed a full evaluation on our side: fresh reviews of both papers against their current SSRN revisions, an independent replication of your headline results, and a code review against the papers and this library's conventions.

The short version: this is a strong contribution and we want to merge it. We reproduced your Prop 99 numbers ourselves (demeaning ATT -0.4222, SE 0.1208; detrending -0.2270, SE 0.0941, with the exact-inference p-value matching the paper), and the core is exactly right: the transformation reproduces our DifferenceInDifferences estimator to machine precision where the theory says it must, and the control-group logic matches the current paper revision precisely.

Getting it merged takes real work on both sides, so below is the full plan - split, sequenced, and complete (no surprise rounds later). None of it starts until you confirm you're on board. If any part looks wrong to you, push back and we'll discuss.

First, one decision from you: the event study

The current revision's Appendix D event study (placebo/dynamic WATT(r)) plus the Algorithm 1 influence-function multiplier bootstrap (simultaneous sup-t bands) is the one substantial piece of the papers not yet implemented - and it's also this estimator's proper pre-trends diagnostic. Your call:

  • Include it in this PR -> LWDiD ships as a standard estimator.
  • Defer it to a follow-up PR -> we merge this PR as an experimental preview (as BR/DR are today), promoted to standard when the event study lands.

Both options are genuinely fine with us.

The plan

Step 0 - you, now. Reply confirming (a) you're good with this plan and (b) your event-study choice above. Nothing below starts until then.

Step 1 - us. We open a maintainer PR to main with (a) our methodology notes for both papers, written against the current SSRN revisions (June 8 and February 3, 2026 - both papers were revised after you wrote your implementation notes, and the June revision reworked the event-study and inference appendices), and (b) diff_diff.datasets loaders for the smoking and Walmart data (the library's existing download+cache mechanism). Those notes become the canonical spec this estimator is validated against. We then push directly to your branch (maintainer-edit is enabled): the rebase onto current main (~85 PRs of drift - version strings, CHANGELOG, tutorial renumbering to 27), removal of the committed data files in favor of the loaders (castle.csv and both .dta files are referenced by nothing and just go - this also moots any dataset redistribution questions), and a suite of independent validation tests: paper-table goldens (Prop 99 Table 3, Tables 4/A1, the castle-laws staggered targets), from-scratch reference implementations of the core procedures, cross-estimator equivalence pins, and property tests. Tests the current code doesn't pass will be xfail-marked so CI stays green - they are your acceptance criteria. Alongside this we'll leave a detailed review on this PR with file:line comments for every item in step 2, so each ask is anchored in code, not prose.

Step 2 - you. Work the fix list below, flipping xfails as you go. One consequence of step 1 to fold into your work: our methodology notes supersede the two review docs currently in this PR - please drop those during the rebase and treat the maintainer versions as canonical (the file paths collide anyway).

Step 3 - us. Wild-bootstrap dedupe into the existing diff_diff.utils machinery (one public bootstrap, not two), the REGISTRY entry and remaining docs surfaces, final review, merge.

The step 2 fix list

  1. Fix the IPW standard error (must-fix bug). In the IPW variance path (lwdid.py, roughly lines 2061-2088 on your current branch) the influence-function terms enter un-centered - raw weighted outcomes where the formula needs deviations from their weighted means. The observable symptom: adding a constant to all post-period outcomes leaves the ATT unchanged (as it must) but shifts the IPW SE. Your IPWRA path (roughly lines 2404-2466) centers correctly and is the internal template for the fix. Our xfail'd invariance test is both the repro and the acceptance criterion, and the review will mark the exact lines. (Worth checking whether lwdid-py inherits the same issue.)
  2. Remove silent failures. Dropped units/periods, PSM-unmatched treated units, and discarded failed bootstrap replicates all need explicit warnings, and lwdid() must not silently ignore unrecognized kwargs. We found seven sites; the review will list each one.
  3. Fold the custom exceptions into house conventions (ValueError/ImportError plus standard warning categories). As written, validation errors don't derive from ValueError, so downstream except ValueError handlers miss them.
  4. Trim top-level exports to LWDiD, LWDiDResults, and the LW alias; everything else stays importable at module level but leaves diff_diff.__init__. test_parallel_trends needs a rename regardless - it's pytest-collectable in downstream codebases.
  5. Adopt the house test conventions: ci_params for iteration scaling, assert_nan_inference, @pytest.mark.slow on the heavy bootstrap/RI tests. Keep the lwdid-py equivalence suite as an optional upstream-drift check - but drop the lwdid entry from the dev extra in pyproject.toml. Reference implementations aren't dependencies here (R isn't one either): the tests stay importorskip-gated and run only where lwdid is manually installed, with a note in the test-file docstring on how to enable them.
  6. Source or fold the diagnostics modules. Keep the paper-sourced sensitivity checks (varying-T0 robustness per Sec 8.1; no-anticipation) with citations. lwdid_clustering.py becomes docstring/tutorial guidance - Sec 8.2 is advice, not an algorithm. lwdid_trend_diagnostics.py is superseded by the Appendix D event study, and its generic slope test duplicates the existing check_parallel_trends.
  7. mypy to zero for the new modules (currently +43 errors against a zero baseline for comparable estimators).

Thanks again - the core here is exactly right, which is the hard part. The rest is the normal cost of moving a standalone package into a library with strong invariants, and we're glad to carry our share of it.

@gorgeousfish

Copy link
Copy Markdown
Author

Thanks for the thorough plan - I'm on board.

On the event study: Option A - I'd like to include the Appendix D event study (WATT(r) + Algorithm 1 multiplier bootstrap) in this PR so LWDiD ships as a standard estimator.

Ready for Step 1 whenever you are. I'll start looking into the IPW centering issue in the meantime.

igerber added a commit that referenced this pull request Jul 13, 2026
…on rubric)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FZK3FD9jxWGxBrPDw5APSg
igerber added a commit that referenced this pull request Jul 13, 2026
…D REGISTRY entry + references (precursor to #588)

Loaders download the MIT-licensed ancillary datasets of the authors' Stata
lwdid package (SSC): pinned SHA-256 verification of every byte-load (HTTP-only
host), stale-cache re-download, structural validation against source
invariants, loud UserWarning + df.attrs['source'] marker on synthetic
fallback, seeded local-RNG fallback constructors. REGISTRY.md gains the
maintainer-authored LWDiD section (E.1 contributing-unit WATT weights;
provenance pinned via reviewed-PDF SHA-256 + live-verified SSRN metadata).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FZK3FD9jxWGxBrPDw5APSg
igerber added a commit that referenced this pull request Jul 13, 2026
…il PR #588 lands)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FZK3FD9jxWGxBrPDw5APSg
igerber added a commit that referenced this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants