Local Projections Difference-in-Differences#
Local Projections DiD (LP-DiD) estimator for staggered, absorbing-treatment event studies, from Dube, Girardi, Jordà & Taylor (2025).
LP-DiD estimates a separate regression at each event-time horizon h of a
long difference of the outcome (y_{i,t+h} - y_{i,t-1}) on the
treatment-switch indicator, restricted to a flexible “clean control” sample of
newly-treated observations and not-yet-treated controls. Excluding
already-treated units from the control group removes the negative-weighting
bias of naive two-way fixed effects, so the default (variance-weighted)
estimand is a strictly non-negatively-weighted average of cohort effects.
Note
This release implements the absorbing-treatment main path: treatment is
binary and, once switched on, stays on. The estimator rejects panels where a
unit’s treatment turns off. Non-absorbing (switch on/off) treatment and
survey-design support are planned follow-ups. Covariates and absorbed fixed
effects are supported; under reweight=False they enter by direct
inclusion, which preserves the non-negative weighting result only under
homogeneous covariate effects (online Appendix B.2.2) — the
regression-adjustment path (reweight=True) is preferred for
covariate-adjusted designs (it does not auto-switch; the default remains
reweight=False, which emits the warning). The time column must be
numeric with integer-spaced periods (long differences use t-1 / t+h
arithmetic on the labels); map irregular or datetime periods to consecutive
integers first. See docs/methodology/REGISTRY.md for the full contract.
When to use LPDiD:
Staggered, absorbing adoption where you want a fast, transparent, regression-based event study free of negative weighting
You want both a dynamic event-study path and a single pooled pre/post ATT
You want to flexibly choose the pretreatment base period (first-lag or premean-differenced) or hold the post-treatment sample composition fixed across post horizons
You want an estimator that is numerically equivalent to Callaway-Sant’Anna (reweighted) or to a Cengiz et al. (2019)-style stacked regression (variance-weighted), but much faster
Reference: 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.
LPDiD#
Main estimator class for Local Projections Difference-in-Differences.
- class diff_diff.LPDiD[source]
Bases:
objectMethods
fit(data, outcome, unit, time, treatment[, ...])get_params()set_params(**params)- __init__(pre_window=2, post_window=0, control_group='clean', reweight=False, no_composition=False, pmd=None, alpha=0.05, cluster=None, rank_deficient_action='warn')[source]
- fit(data, outcome, unit, time, treatment, covariates=None, ylags=0, dylags=0, absorb=None, post_pooled=None, pre_pooled=None, only_event=False, only_pooled=False)[source]
LPDiDResults#
Results container for LP-DiD estimation (event-study and pooled tables).
- class diff_diff.lpdid_results.LPDiDResults[source]
Bases:
objectResults container for the
LPDiDestimator.Holds the per-horizon
event_studytable and thepooledpre/post table (each apandas.DataFramewithcoefficient,se,t_stat,p_value,conf_low,conf_high,n_obs,n_clusterscolumns). The headline ATT is the pooledpostrow.n_control_unitscounts never-treated units only (the library-wide field convention, surfaced as “Never-treated units” insummary()); undercontrol_group="clean"the realized control pool at each horizon also includes not-yet-treated cohorts, whose per-horizon counts live in then_obs/n_clusterscolumns of the tables.Methods
summary()print_summary()to_dataframe([level])to_dict()- n_obs: int
- n_treated_units: int
- n_control_units: int
- pre_window: int
- post_window: int
- control_group: str
- reweight: bool
- no_composition: bool
- alpha: float = 0.05
- vcov_type: str = 'hc1'
- rank_deficient_action: str = 'warn'
- ylags: int = 0
- dylags: int = 0
- property estimand: str
- property att: float
- property se: float
- property t_stat: float
- property p_value: float
- print_summary()[source]
- Return type:
None
- __init__(event_study, pooled, n_obs, n_treated_units, n_control_units, pre_window, post_window, control_group, reweight, no_composition, pmd, alpha=0.05, cluster_name=None, n_clusters=None, vcov_type='hc1', rank_deficient_action='warn', covariates=None, absorb=None, ylags=0, dylags=0)
- Parameters:
event_study (DataFrame | None)
pooled (DataFrame | None)
n_obs (int)
n_treated_units (int)
n_control_units (int)
pre_window (int)
post_window (int)
control_group (str)
reweight (bool)
no_composition (bool)
alpha (float)
cluster_name (str | None)
n_clusters (int | None)
vcov_type (str)
rank_deficient_action (str)
ylags (int)
dylags (int)
- Return type:
None
Example Usage#
Basic usage (LP-DiD takes a binary, absorbing treatment indicator):
from diff_diff import LPDiD, generate_staggered_data
data = generate_staggered_data(n_units=300, n_periods=12,
cohort_periods=[4, 7, 10], seed=42)
# Binary absorbing indicator: 1 from a unit's first treated period onward.
data["treated"] = (data["period"] >= data["first_treat"]).astype(int)
lp = LPDiD(pre_window=5, post_window=4)
results = lp.fit(data, outcome="outcome", unit="unit",
time="period", treatment="treated")
results.print_summary()
print(results.event_study) # per-horizon coefficients
print(results.pooled) # pooled pre (placebo) / post (ATT) rows
Equally-weighted ATT (numerically equivalent to Callaway-Sant’Anna):
lp_rw = LPDiD(pre_window=5, post_window=4, reweight=True)
results_rw = lp_rw.fit(data, outcome="outcome", unit="unit",
time="period", treatment="treated")
print(f"Variance-weighted ATT: {results.att:.4f} (SE={results.se:.4f})")
print(f"Equally-weighted ATT: {results_rw.att:.4f} (SE={results_rw.se:.4f})")
Premean-differenced base period and fixed-composition sample:
lp_pmd = LPDiD(pre_window=5, post_window=4, pmd="max", no_composition=True)
results_pmd = lp_pmd.fit(data, outcome="outcome", unit="unit",
time="period", treatment="treated")
Comparison with Other Staggered Estimators#
Feature |
LPDiD |
CallawaySantAnna |
ImputationDiD |
|---|---|---|---|
Approach |
Per-horizon long-difference LP regression on clean controls |
Separate 2x2 DiD aggregation |
Impute Y(0) via FE model |
Treatment |
Binary, absorbing (this release) |
Binary, absorbing |
Binary, absorbing |
Default estimand |
Variance-weighted ATT (non-negative weights) |
Equally-weighted ATT |
Equally-weighted ATT |
Equivalences |
Reweighted == CS; variance-weighted == Cengiz (2019)-style stacking (not |
Baseline |
== reweighted PMD LP-DiD (single cohort) |
Covariates |
Supported (regression adjustment preferred; direct inclusion under homogeneity) |
Supported (OR, IPW, DR) |
Supported |
Inference |
Cluster-robust at unit (default) |
Multiplier bootstrap |
Influence-function cluster variance |
Speed |
Very fast (stack of small OLS fits) |
Slower (pairwise group-time) |
Fast |