Skip to content

Commit c7bd27c

Browse files
authored
Merge pull request #156 from igerber/two-stage-did
Add Two-Stage DiD estimator (Gardner 2022)
2 parents 27eceee + e0286d2 commit c7bd27c

8 files changed

Lines changed: 3512 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ cross-platform compilation - no OpenBLAS or Intel MKL installation required.
111111
- Pre-trend test (Equation 9) via `results.pretrend_test()`
112112
- Proposition 5: NaN for unidentified long-run horizons without never-treated units
113113

114+
- **`diff_diff/two_stage.py`** - Gardner (2022) Two-Stage DiD estimator:
115+
- `TwoStageDiD` - Two-stage estimator: (1) estimate unit+time FE on untreated obs, (2) regress residualized outcomes on treatment indicators
116+
- `TwoStageDiDResults` - Results with overall ATT, event study, group effects, per-observation treatment effects
117+
- `TwoStageBootstrapResults` - Multiplier bootstrap inference on GMM influence function
118+
- `two_stage_did()` - Convenience function
119+
- Point estimates identical to ImputationDiD; different variance estimator (GMM sandwich vs. conservative)
120+
- Custom `_compute_gmm_variance()` — cannot reuse `compute_robust_vcov()` because correction term uses GLOBAL cross-moment
121+
- No finite-sample adjustments (raw asymptotic sandwich, matching R `did2s`)
122+
114123
- **`diff_diff/triple_diff.py`** - Triple Difference (DDD) estimator:
115124
- `TripleDifference` - Ortiz-Villavicencio & Sant'Anna (2025) estimator for DDD designs
116125
- `TripleDifferenceResults` - Results with ATT, SEs, cell means, diagnostics
@@ -270,6 +279,7 @@ cross-platform compilation - no OpenBLAS or Intel MKL installation required.
270279
├── CallawaySantAnna
271280
├── SunAbraham
272281
├── ImputationDiD
282+
├── TwoStageDiD
273283
├── TripleDifference
274284
├── TROP
275285
├── SyntheticDiD
@@ -384,6 +394,7 @@ Tests mirror the source modules:
384394
- `tests/test_staggered.py` - Tests for CallawaySantAnna
385395
- `tests/test_sun_abraham.py` - Tests for SunAbraham interaction-weighted estimator
386396
- `tests/test_imputation.py` - Tests for ImputationDiD (Borusyak et al. 2024) estimator
397+
- `tests/test_two_stage.py` - Tests for TwoStageDiD (Gardner 2022) estimator, including equivalence tests with ImputationDiD
387398
- `tests/test_triple_diff.py` - Tests for Triple Difference (DDD) estimator
388399
- `tests/test_trop.py` - Tests for Triply Robust Panel (TROP) estimator
389400
- `tests/test_bacon.py` - Tests for Goodman-Bacon decomposition

README.md

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Signif. codes: '***' 0.001, '**' 0.01, '*' 0.05, '.' 0.1
7070
- **Wild cluster bootstrap**: Valid inference with few clusters (<50) using Rademacher, Webb, or Mammen weights
7171
- **Panel data support**: Two-way fixed effects estimator for panel designs
7272
- **Multi-period analysis**: Event-study style DiD with period-specific treatment effects
73-
- **Staggered adoption**: Callaway-Sant'Anna (2021), Sun-Abraham (2021), and Borusyak-Jaravel-Spiess (2024) imputation estimators for heterogeneous treatment timing
73+
- **Staggered adoption**: Callaway-Sant'Anna (2021), Sun-Abraham (2021), Borusyak-Jaravel-Spiess (2024) imputation, and Two-Stage DiD (Gardner 2022) estimators for heterogeneous treatment timing
7474
- **Triple Difference (DDD)**: Ortiz-Villavicencio & Sant'Anna (2025) estimators with proper covariate handling
7575
- **Synthetic DiD**: Combined DiD with synthetic control for improved robustness
7676
- **Triply Robust Panel (TROP)**: Factor-adjusted DiD with synthetic weights (Athey et al. 2025)
@@ -927,6 +927,53 @@ ImputationDiD(
927927
| Inference | Conservative variance (Theorem 3) | Multiplier bootstrap |
928928
| Pre-trends | Built-in F-test (Equation 9) | Separate testing |
929929

930+
### Two-Stage DiD (Gardner 2022)
931+
932+
Two-Stage DiD addresses TWFE bias in staggered adoption designs by estimating unit and time fixed effects on untreated observations only, then regressing the residualized outcomes on treatment indicators. Point estimates match the Imputation DiD estimator (Borusyak et al. 2024); the key difference is that Two-Stage DiD uses a GMM sandwich variance estimator that accounts for first-stage estimation error, while Imputation DiD uses a conservative variance (Theorem 3).
933+
934+
```python
935+
from diff_diff import TwoStageDiD
936+
937+
# Basic usage
938+
est = TwoStageDiD()
939+
results = est.fit(data, outcome='outcome', unit='unit', time='period', first_treat='first_treat')
940+
results.print_summary()
941+
```
942+
943+
**Event study:**
944+
945+
```python
946+
# Event study aggregation with visualization
947+
results = est.fit(data, outcome='outcome', unit='unit', time='period',
948+
first_treat='first_treat', aggregate='event_study')
949+
plot_event_study(results)
950+
```
951+
952+
**Parameters:**
953+
954+
```python
955+
TwoStageDiD(
956+
anticipation=0, # Periods of anticipation effects
957+
alpha=0.05, # Significance level for CIs
958+
cluster=None, # Column for cluster-robust SEs (defaults to unit)
959+
n_bootstrap=0, # Bootstrap iterations (0 = analytical GMM SEs)
960+
seed=None, # Random seed
961+
rank_deficient_action='warn', # 'warn', 'error', or 'silent'
962+
horizon_max=None, # Max event-study horizon
963+
)
964+
```
965+
966+
**When to use Two-Stage DiD vs Imputation DiD:**
967+
968+
| Aspect | Two-Stage DiD | Imputation DiD |
969+
|--------|--------------|---------------|
970+
| Point estimates | Identical | Identical |
971+
| Variance | GMM sandwich (accounts for first-stage error) | Conservative (Theorem 3, may overcover) |
972+
| Intuition | Residualize then regress | Impute counterfactuals then aggregate |
973+
| Reference impl. | R `did2s` package | R `didimputation` package |
974+
975+
Both estimators are the efficient estimator under homogeneous treatment effects, producing shorter confidence intervals than Callaway-Sant'Anna or Sun-Abraham.
976+
930977
### Triple Difference (DDD)
931978

932979
Triple Difference (DDD) is used when treatment requires satisfying two criteria: belonging to a treated **group** AND being in an eligible **partition**. The `TripleDifference` class implements the methodology from Ortiz-Villavicencio & Sant'Anna (2025), which correctly handles covariate adjustment (unlike naive implementations).
@@ -2104,6 +2151,58 @@ ImputationDiD(
21042151
| `to_dataframe(level)` | Convert to DataFrame ('observation', 'event_study', 'group') |
21052152
| `pretrend_test(n_leads)` | Run pre-trend F-test (Equation 9) |
21062153

2154+
### TwoStageDiD
2155+
2156+
```python
2157+
TwoStageDiD(
2158+
anticipation=0, # Periods of anticipation effects
2159+
alpha=0.05, # Significance level for CIs
2160+
cluster=None, # Column for cluster-robust SEs (defaults to unit)
2161+
n_bootstrap=0, # Bootstrap iterations (0 = analytical GMM SEs)
2162+
seed=None, # Random seed
2163+
rank_deficient_action='warn', # 'warn', 'error', or 'silent'
2164+
horizon_max=None, # Max event-study horizon
2165+
)
2166+
```
2167+
2168+
**fit() Parameters:**
2169+
2170+
| Parameter | Type | Description |
2171+
|-----------|------|-------------|
2172+
| `data` | DataFrame | Panel data |
2173+
| `outcome` | str | Outcome variable column name |
2174+
| `unit` | str | Unit identifier column |
2175+
| `time` | str | Time period column |
2176+
| `first_treat` | str | First treatment period column (0 for never-treated) |
2177+
| `covariates` | list | Covariate column names |
2178+
| `aggregate` | str | Aggregation: None, "event_study", "group", "all" |
2179+
| `balance_e` | int | Balance event study to this many pre-treatment periods |
2180+
2181+
### TwoStageDiDResults
2182+
2183+
**Attributes:**
2184+
2185+
| Attribute | Description |
2186+
|-----------|-------------|
2187+
| `overall_att` | Overall average treatment effect on the treated |
2188+
| `overall_se` | Standard error (GMM sandwich variance) |
2189+
| `overall_t_stat` | T-statistic |
2190+
| `overall_p_value` | P-value for H0: ATT = 0 |
2191+
| `overall_conf_int` | Confidence interval |
2192+
| `event_study_effects` | Dict of relative time -> effect dict (if `aggregate='event_study'` or `'all'`) |
2193+
| `group_effects` | Dict of cohort -> effect dict (if `aggregate='group'` or `'all'`) |
2194+
| `treatment_effects` | DataFrame of unit-level treatment effects |
2195+
| `n_treated_obs` | Number of treated observations |
2196+
| `n_untreated_obs` | Number of untreated observations |
2197+
2198+
**Methods:**
2199+
2200+
| Method | Description |
2201+
|--------|-------------|
2202+
| `summary(alpha)` | Get formatted summary string |
2203+
| `print_summary(alpha)` | Print summary to stdout |
2204+
| `to_dataframe(level)` | Convert to DataFrame ('observation', 'event_study', 'group') |
2205+
21072206
### TripleDifference
21082207

21092208
```python
@@ -2582,6 +2681,10 @@ The `HonestDiD` module implements sensitivity analysis methods for relaxing the
25822681

25832682
- **Sun, L., & Abraham, S. (2021).** "Estimating Dynamic Treatment Effects in Event Studies with Heterogeneous Treatment Effects." *Journal of Econometrics*, 225(2), 175-199. [https://doi.org/10.1016/j.jeconom.2020.09.006](https://doi.org/10.1016/j.jeconom.2020.09.006)
25842683

2684+
- **Gardner, J. (2022).** "Two-stage differences in differences." *arXiv preprint arXiv:2207.05943*. [https://arxiv.org/abs/2207.05943](https://arxiv.org/abs/2207.05943)
2685+
2686+
- **Butts, K., & Gardner, J. (2022).** "did2s: Two-Stage Difference-in-Differences." *The R Journal*, 14(1), 162-173. [https://doi.org/10.32614/RJ-2022-048](https://doi.org/10.32614/RJ-2022-048)
2687+
25852688
- **de Chaisemartin, C., & D'Haultfœuille, X. (2020).** "Two-Way Fixed Effects Estimators with Heterogeneous Treatment Effects." *American Economic Review*, 110(9), 2964-2996. [https://doi.org/10.1257/aer.20181169](https://doi.org/10.1257/aer.20181169)
25862689

25872690
- **Goodman-Bacon, A. (2021).** "Difference-in-Differences with Variation in Treatment Timing." *Journal of Econometrics*, 225(2), 254-277. [https://doi.org/10.1016/j.jeconom.2021.03.014](https://doi.org/10.1016/j.jeconom.2021.03.014)

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ For past changes and release history, see [CHANGELOG.md](CHANGELOG.md).
1010

1111
diff-diff v2.3.0 is a **production-ready** DiD library with feature parity with R's `did` + `HonestDiD` + `synthdid` ecosystem for core DiD analysis:
1212

13-
- **Core estimators**: Basic DiD, TWFE, MultiPeriod, Callaway-Sant'Anna, Sun-Abraham, Borusyak-Jaravel-Spiess Imputation, Synthetic DiD, Triple Difference (DDD), TROP
13+
- **Core estimators**: Basic DiD, TWFE, MultiPeriod, Callaway-Sant'Anna, Sun-Abraham, Borusyak-Jaravel-Spiess Imputation, Synthetic DiD, Triple Difference (DDD), TROP, Two-Stage DiD (Gardner 2022)
1414
- **Valid inference**: Robust SEs, cluster SEs, wild bootstrap, multiplier bootstrap, placebo-based variance
1515
- **Assumption diagnostics**: Parallel trends tests, placebo tests, Goodman-Bacon decomposition
1616
- **Sensitivity analysis**: Honest DiD (Rambachan-Roth), Pre-trends power analysis (Roth 2022)
@@ -24,7 +24,7 @@ diff-diff v2.3.0 is a **production-ready** DiD library with feature parity with
2424

2525
High-value additions building on our existing foundation.
2626

27-
### Gardner's Two-Stage DiD (did2s)
27+
### Gardner's Two-Stage DiD (did2s) -- IMPLEMENTED (v2.4)
2828

2929
Two-stage approach gaining traction in applied work. First residualizes outcomes, then estimates effects.
3030

diff_diff/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@
101101
ImputationDiDResults,
102102
imputation_did,
103103
)
104+
from diff_diff.two_stage import (
105+
TwoStageBootstrapResults,
106+
TwoStageDiD,
107+
TwoStageDiDResults,
108+
two_stage_did,
109+
)
104110
from diff_diff.sun_abraham import (
105111
SABootstrapResults,
106112
SunAbraham,
@@ -152,6 +158,7 @@
152158
"CallawaySantAnna",
153159
"SunAbraham",
154160
"ImputationDiD",
161+
"TwoStageDiD",
155162
"TripleDifference",
156163
"TROP",
157164
# Bacon Decomposition
@@ -173,6 +180,9 @@
173180
"ImputationDiDResults",
174181
"ImputationBootstrapResults",
175182
"imputation_did",
183+
"TwoStageDiDResults",
184+
"TwoStageBootstrapResults",
185+
"two_stage_did",
176186
"TripleDifferenceResults",
177187
"triple_difference",
178188
"TROPResults",

0 commit comments

Comments
 (0)