Stacked DiD estimator for staggered adoption designs with corrective Q-weights.
This module implements the methodology from Wing, Freedman & Hollingsworth (2024), which addresses bias in naive stacked DiD regressions by:
- Constructing sub-experiments: One per adoption cohort with clean controls
- Applying corrective Q-weights: Ensures proper weighting of treatment and control group trends across sub-experiments
- Running weighted event-study regression: WLS with Q-weights identifies the "trimmed aggregate ATT"
When to use Stacked DiD:
- Staggered adoption design with multiple treatment cohorts
- Want an intuitive sub-experiment-based approach (vs. aggregation methods)
- Desire compositional balance: treatment group composition fixed across event times
- Need direct access to the stacked dataset for custom analysis
Reference: Wing, C., Freedman, S. M., & Hollingsworth, A. (2024). Stacked Difference-in-Differences. NBER Working Paper 32054. http://www.nber.org/papers/w32054
Main estimator class for Stacked Difference-in-Differences.
.. autoclass:: diff_diff.StackedDiD
:no-index:
:members:
:undoc-members:
:show-inheritance:
:inherited-members:
.. rubric:: Methods
.. autosummary::
~StackedDiD.fit
~StackedDiD.get_params
~StackedDiD.set_params
Results container for Stacked DiD estimation.
.. autoclass:: diff_diff.StackedDiDResults
:no-index:
:members:
:undoc-members:
:show-inheritance:
.. rubric:: Methods
.. autosummary::
~StackedDiDResults.summary
~StackedDiDResults.print_summary
~StackedDiDResults.to_dataframe
.. autofunction:: diff_diff.stacked_did
Basic usage:
from diff_diff import StackedDiD, generate_staggered_data
data = generate_staggered_data(n_units=200, n_periods=12,
cohort_periods=[4, 6, 8], seed=42)
est = StackedDiD(kappa_pre=2, kappa_post=2)
results = est.fit(data, outcome='outcome', unit='unit',
time='period', first_treat='first_treat',
aggregate='event_study')
results.print_summary()
Accessing the stacked dataset:
# The stacked data is available for custom analysis stacked = results.stacked_data print(stacked[['unit', 'period', '_sub_exp', '_event_time', '_D_sa', '_Q_weight']].head())
Different weighting schemes:
# Population-weighted ATT (requires population column)
est = StackedDiD(kappa_pre=2, kappa_post=2, weighting='population')
results = est.fit(data, outcome='outcome', unit='unit',
time='period', first_treat='first_treat',
population='pop_size')
# Sample-share weighted ATT
est = StackedDiD(kappa_pre=2, kappa_post=2, weighting='sample_share')
results = est.fit(data, outcome='outcome', unit='unit',
time='period', first_treat='first_treat')
| Feature | Stacked DiD | Callaway-Sant'Anna |
|---|---|---|
| Approach | Pooled WLS on stacked sub-experiments | Separate group-time regressions |
| Compositional balance | Enforced by IC1/IC2 trimming | Via balanced event study aggregation |
| Target parameter | Trimmed aggregate ATT | Weighted average of ATT(g,t) |
| Custom analysis | Full stacked dataset accessible | Group-time effects accessible |
| Covariates | Entropy balancing via balance="entropy" + fit(covariates=...) (CBWSDID, Ustyuzhanin 2026); requires weighting="aggregate" + balanced windows, no survey_design |
Supported (OR, IPW, DR) |