
stacked
Stacked difference-in-differences with corrective weights: for R and Stata
stacked implements the stacked difference-in-differences estimator of Wing, Hollingsworth & Freedman (2024).
Stacked DiD is designed for settings with staggered treatment adoption. It:
- builds one clean sub-experiment per adoption cohort
- aligns observations by event time
- applies corrective weights
This ensures that a simple weighted regression can recover the treatment-effect parameter you actually set out to estimate, the average treatment effect (ATE) for the average treated unit.
Both R and Stata packages are available.
Install
# From CRAN (coming soon)
# install.packages("stacked")
# Development version from GitHub
# install.packages("remotes")
remotes::install_github("hollina/stacked", subdir = "R/stacked")* From SSC (coming soon)
* ssc install stacked
* Development version from GitHub
net install stacked, ///
from("https://raw.githubusercontent.com/hollina/stacked/main/Stata") replaceWhy do we need corrective weights?
The problem in one example
Consider two cohorts that have different treatment effects and are wildly unequal in size.
- 10 groups adopt in year 11 with a large positive effect (300)
- 1,000 groups adopt in year 60 with a negative effect (−100)
The simple unit-weighted average should clearly be negative (-96 \(~ = \frac{300\times 10 -100\times 1000}{10 + 1000}\)).
But a plain stacked TWFE regression without-corrective weights returns 93. Not only is this the wrong answer it is the wrong sign.
Why does this happen?
stacked TWFE overweights the tiny early cohort, giving it a disproportionate share of the treatment variance. Our method makes simple corrective weights (Q-weights for short) that fix this problem. The corrective weights do this by load the mass in proportion to each cohort’s treated units, ensuring the correct answer is recovered.
The three panels below show the raw group means, the weights that decide the sign and the two event studies. These panels tell the whole story using simulated data.
Raw group means (presented without error simply for a legible picture):

Each cohort’s ATT scattered against its sub-experiment weight without and without corrective weighting. We call the stacked TWFE without corrective-weights, precision weighting.
- Here the precision weights split the mass 50%/50%,
- while corrective weighting correclty loads 99% on the large negative cohort


Event studies for both estimators. Only the event-studies from stacked TWFE using corrective weights track the true ATT.


Walk the whole argument step by step in the worked example, or move the pieces yourself in the interactive app.
Quick start
The workflow is four steps:
- preview the event-window trade-offs,
- build the stacked dataset,
- estimate the corrective-weighted event study and/or ATT
- plot it
This example uses the bundled medicaid panel (state uninsurance rates and ACA Medicaid-expansion adoption dates).
library(stacked)
data(medicaid)
# 1. Preview how event-window length (kappa) trades off against sample
to <- kappa_trade_offs(
medicaid, "year", "state", "adopt_year",
kappa_pre_range = 1:4, kappa_post_range = 1:4
)
to[kappa_pre <= 3 & kappa_post <= 3,
.(kappa_pre, kappa_post, n_sub_exp, n_obs)] kappa_pre kappa_post n_sub_exp n_obs
1: 1 1 5 354
2: 1 2 4 400
3: 1 3 3 425
4: 2 1 5 472
5: 2 2 4 500
6: 2 3 3 510
7: 3 1 5 590
8: 3 2 4 600
9: 3 3 3 595
# 2. Build the stacked dataset (3 pre periods, 2 post periods)
stack <- build_stack(
medicaid, "year", "state", "adopt_year",
kappa_pre = 3, kappa_post = 2
)
# 3. Fit the Q-weighted event-study regression, clustering on state
model <- stackreg(stack, "uninsured", cluster_var = "state")
# Average post-period ATT
attr(model, "avg_post_att")$estimate[1] -0.02187775
# 4. Plot the event study
p_med <- stack_plot(model, title = "Medicaid expansion and the uninsured rate")

stacked use medicaid, clear // load the bundled example panel
* 1. Preview how event-window length (kappa) trades off against sample
stacked kappa, time(year) unit(state) adopt(adopt_year) kpre(1/4) kpost(1/4)
* 2. Build the stacked dataset (3 pre periods, 2 post periods)
stacked build, time(year) unit(state) adopt(adopt_year) kpre(3) kpost(2)
* 3. Fit the Q-weighted event-study regression, clustering on state
stacked reg uninsured, cluster(state)
* 4. Plot the event study
stacked plotKey functions
| R | Stata | Description |
|---|---|---|
kappa_trade_offs() |
stacked kappa |
Preview stack composition (and design divergence D) across event windows |
build_stack() |
stacked build |
Create the stacked dataset with Q-weights |
stackreg() |
stacked reg |
Fit the Q-weighted event-study (or static ATT) regression |
stack_plot() |
stacked plot |
Visualize the event study |
stack_levels() |
stacked levels |
Q-weighted mean outcome levels for treated and control groups |
add_pscore_weights() |
stacked pscore |
Add propensity-score-adjusted weights |
See the full function reference for arguments and details.
Where to next
- A more detailed look into stacking, the aggregation issue, and corrective weights: how stacking works, why pooled stacked TWFE can still mislead, and what the corrective-weights fix.
- Compositional balance: showing how compositional balance can masquerade as dynamic treatment effects. Also introducing a
Dstatistic which can help diagnose when naive and corrected estimates might diverge. - Static example using simulated data: using a tiny dataset where naive stacked TWFE gets the sign wrong.
- Interactive app: move the pieces with sliders and watch the estimators diverge.
- Paper: the working paper.