Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e7f9626
feat(lpdid): LP-DiD estimator source + tests (absorbing path, B1)
igerber Jun 28, 2026
cd6a574
fix(lpdid): compute reweight/RA from realized sample; window validati…
igerber Jun 28, 2026
3676dab
fix(lpdid): PMD baseline-column requirement + pooled pre-window valid…
igerber Jun 28, 2026
19d16af
fix(lpdid): no_composition post-only common sample; propagate rank_de…
igerber Jun 28, 2026
e49b37f
fix(lpdid): label RA-path SEs as influence-function cluster variance
igerber Jun 28, 2026
46b4d00
fix(lpdid): pmd="max" excludes present-but-NaN pretreatment; lag cont…
igerber Jun 28, 2026
3f70ecc
fix(lpdid): value-based no_composition availability; pooled id check;…
igerber Jun 28, 2026
68fcf80
fix(lpdid): validate alpha + numeric time; headline n_clusters; remov…
igerber Jun 28, 2026
7adc934
fix(lpdid): enforce integer-spaced time; reject string covariates + N…
igerber Jun 28, 2026
fcfa3c7
fix(lpdid): calendar-correct per-unit features via interior-grid reindex
igerber Jun 28, 2026
14bb9c5
fix(lpdid): pmd="max" premean must not depend on the base row's own y_t
igerber Jun 28, 2026
a683a88
fix(lpdid): RA absorbed-factor overlap check; document pooled estimand
igerber Jun 28, 2026
196b2b7
fix(lpdid): reserved-name guard; reorder arg validation; clarify cont…
igerber Jun 28, 2026
a5b40fb
docs(lpdid): serialize covariates/absorb in to_dict; document n_contr…
igerber Jun 28, 2026
f0af256
fix(lpdid): enforce per-event-time clean-control support in default O…
igerber Jun 28, 2026
f484f87
docs(lpdid): show ylags/dylags in summary; qualify SE + no_compositio…
igerber Jun 28, 2026
c43e7d9
docs(lpdid): correct RA SE-convention validation wording (overclaim)
igerber Jun 28, 2026
e43b31e
docs(changelog): add LPDiD estimator to Unreleased
igerber Jun 28, 2026
f9adb99
fix(lpdid): RA path must not propagate NaN from dropped nuisance coef…
igerber Jun 28, 2026
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **`LPDiD` (Local Projections Difference-in-Differences; Dube, Girardi, Jordà & Taylor 2025,
*J. Applied Econometrics* 40(5):741-758).** Per-horizon long-difference OLS
(`y_{i,t+h} − y_{i,t−1}`) on a clean-control sample (newly-treated + not-yet-treated) with
calendar-time fixed effects and no unit FE, so the default variance-weighted estimand has
strictly non-negative weights (no TWFE negative-weighting). Options: `reweight=True`
(equally-weighted ATT, numerically equivalent to Callaway-Sant'Anna), premean-differenced base
period (`pmd`), `no_composition` (fixed post-treatment composition), pooled pre/post estimands,
outcome/first-difference lag controls (`ylags`/`dylags`), and a regression-adjustment covariate
path (ImputationDiD/BJS-family influence-function cluster variance). Cluster-robust SEs at the
unit level by default. This release implements the **absorbing-treatment** path; per-unit
interior time gaps are handled by calendar-correct feature construction. Non-absorbing
treatment, survey-design support, and external R-package parity are tracked follow-ups.
- **`placebo_group_test` gained an optional `treatment` parameter.** When supplied, units
that are ever real-treated are dropped before the placebo so it runs on never-treated units
only (the uncontaminated design); without it, behavior is unchanged and the caller must pass
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
- [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html) - Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
- [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference) - Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT
- [WooldridgeDiD](https://diff-diff.readthedocs.io/en/stable/api/wooldridge_etwfe.html) - Wooldridge (2023, 2025) ETWFE: saturated OLS, logit/Poisson QMLE (ASF-based ATT). Alias `ETWFE`.
- [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html) - Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting), variance- or equally-weighted ATT, for absorbing treatment
- [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html) - Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings

## Diagnostics & Sensitivity
Expand Down
5 changes: 5 additions & 0 deletions diff_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@
from diff_diff.synthetic_control_results import SyntheticControlResults
from diff_diff.wooldridge import WooldridgeDiD
from diff_diff.wooldridge_results import WooldridgeDiDResults
from diff_diff.lpdid import LPDiD
from diff_diff.lpdid_results import LPDiDResults
from diff_diff.utils import (
WildBootstrapResults,
check_parallel_trends,
Expand Down Expand Up @@ -381,6 +383,9 @@
"WooldridgeDiD",
"WooldridgeDiDResults",
"ETWFE",
# LPDiD (Local Projections DiD)
"LPDiD",
"LPDiDResults",
# Visualization
"plot_bacon",
"plot_event_study",
Expand Down
50 changes: 50 additions & 0 deletions diff_diff/guides/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,56 @@ results = edid.fit(data, outcome='y', unit='id', time='t',
results.print_summary()
```

### LPDiD

Local Projections DiD (Dube, Girardi, Jorda & Taylor 2025). Estimates a separate OLS at each event-time horizon of a long difference (`y_{i,t+h} - y_{i,t-1}`) on the treatment-switch indicator plus calendar-time fixed effects (no unit FE), restricted to a flexible "clean control" sample of newly-treated and not-yet-treated units. Excluding already-treated units from the control group removes the negative-weighting bias of naive TWFE, so the default (variance-weighted) estimand has strictly non-negative weights. `reweight=True` yields the equally-weighted ATT (numerically equivalent to Callaway-Sant'Anna); covariates then enter via regression adjustment. Standard errors on the default/weighted path are cluster-robust at the unit level (the paper specifies no SE; matches Stata `lpdid` `vce(cluster unit)`); the regression-adjustment covariate path (`reweight=True`) instead reports an influence-function cluster variance (ImputationDiD/BJS family). Scope: binary, absorbing treatment (rejects panels where treatment turns off).

```python
LPDiD(
pre_window: int = 2, # Number of pre-treatment horizons (placebos)
post_window: int = 0, # Number of post-treatment horizons
control_group: str = "clean", # "clean" (not-yet-treated) or "never_treated"
reweight: bool = False, # True -> equally-weighted ATT (== Callaway-Sant'Anna); False -> variance-weighted
no_composition: bool = False, # Hold the post-treatment composition fixed across post horizons
pmd: str | int | None = None, # Base period: None=first-lag (t-1), "max"=premean over all pretreatment periods, int=last-k premean
alpha: float = 0.05,
cluster: str | None = None, # Cluster column for cluster-robust SEs; defaults to the unit identifier
rank_deficient_action: str = "warn", # "warn", "error", or "silent"
)
```

**fit() parameters:**

```python
lpdid.fit(
data: pd.DataFrame,
outcome: str,
unit: str,
time: str,
treatment: str, # Binary, absorbing treatment indicator (0/1)
covariates: list[str] = None, # Direct inclusion (reweight=False) or regression adjustment (reweight=True)
ylags: int = 0, # Lagged-outcome controls
dylags: int = 0, # Lagged first-difference controls
absorb: list[str] = None, # Additional absorbed fixed-effect columns
post_pooled: int | tuple = None, # Pooled post-window horizons (int or (start, end))
pre_pooled: int | tuple = None, # Pooled pre-window horizons (int or (start, end))
only_event: bool = False, # Compute only the event-study table
only_pooled: bool = False, # Compute only the pooled pre/post table
) -> LPDiDResults
```

**Usage:**

```python
from diff_diff import LPDiD

lp = LPDiD(pre_window=5, post_window=10)
results = lp.fit(data, outcome='y', unit='id', time='t', treatment='treated')
results.print_summary()
print(results.event_study) # per-horizon coefficients
print(results.pooled) # pooled pre (placebo) / post (ATT) rows
```

### TROP

Triply Robust Panel estimator (Athey, Imbens, Qu & Viviano 2025). Combines nuclear norm regularization, distance-based unit weights, and time decay weights.
Expand Down
3 changes: 2 additions & 1 deletion diff_diff/guides/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> A Python library for Difference-in-Differences (DiD) causal inference analysis. Provides sklearn-like estimators with statsmodels-style summary output for econometric analysis.

diff-diff offers 18 estimators covering basic 2x2 DiD, modern staggered adoption methods, reversible (non-absorbing) treatments, advanced panel estimators, nonlinear models, and diagnostic tools. It supports robust and cluster-robust standard errors, wild cluster bootstrap, formula and column-name interfaces, fixed effects (dummy and absorbed), complex survey designs (strata/PSU/FPC, replicate weights, design-based variance), and publication-ready output. The optional Rust backend accelerates compute-intensive estimators like Synthetic DiD and TROP.
diff-diff offers 19 estimators covering basic 2x2 DiD, modern staggered adoption methods, reversible (non-absorbing) treatments, advanced panel estimators, nonlinear models, and diagnostic tools. It supports robust and cluster-robust standard errors, wild cluster bootstrap, formula and column-name interfaces, fixed effects (dummy and absorbed), complex survey designs (strata/PSU/FPC, replicate weights, design-based variance), and publication-ready output. The optional Rust backend accelerates compute-intensive estimators like Synthetic DiD and TROP.

- Install: `pip install diff-diff`
- License: MIT
Expand Down Expand Up @@ -69,6 +69,7 @@ Full practitioner guide: call `diff_diff.get_llm_guide("practitioner")`
- [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html): Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
- [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference): Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT
- [WooldridgeDiD](https://diff-diff.readthedocs.io/en/stable/api/wooldridge_etwfe.html): Wooldridge (2023, 2025) ETWFE — saturated OLS, logit/Poisson QMLE (ASF-based ATT). Alias: ETWFE
- [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html): Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting); variance- or equally-weighted ATT, premean differencing, pooled pre/post, fast. Absorbing treatment.
- [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html): Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings

## Diagnostics and Sensitivity Analysis
Expand Down
Loading
Loading