Interactive notebook

This tutorial is a Jupyter notebook. You can view it on GitHub or download it to run locally.

Imputation DiD (Borusyak, Jaravel & Spiess 2024)#

This tutorial demonstrates the ImputationDiD estimator, which implements the efficient imputation approach from Borusyak, Jaravel & Spiess (2024), “Revisiting Event-Study Designs: Robust and Efficient Estimation”, Review of Economic Studies.

When to use ImputationDiD:

  • Staggered adoption settings where treatment effects may be homogeneous across cohorts and time — produces ~50% shorter CIs than Callaway-Sant’Anna

  • When you want to use all untreated observations (never-treated + not-yet-treated) for maximum efficiency

  • As a complement to Callaway-Sant’Anna or Sun-Abraham: if all three agree, results are robust; if they disagree, investigate heterogeneity

[ ]:
import numpy as np
import warnings
warnings.filterwarnings('ignore')

from diff_diff import (
    ImputationDiD, CallawaySantAnna, SunAbraham,
    generate_staggered_data, plot_event_study
)

# For nicer plots (optional)
try:
    import matplotlib.pyplot as plt
    plt.style.use('seaborn-v0_8-whitegrid')
    HAS_MATPLOTLIB = True
except ImportError:
    HAS_MATPLOTLIB = False
    print("matplotlib not installed - visualization examples will be skipped")

Basic Usage#

The imputation estimator follows a simple three-step process:

  1. Estimate unit and time fixed effects using only untreated observations

  2. Impute counterfactual Y(0) for treated observations

  3. Aggregate imputed treatment effects with researcher-chosen weights

[ ]:
# Generate staggered adoption data with known treatment effect
data = generate_staggered_data(n_units=300, n_periods=10, treatment_effect=2.0, seed=42)

# Fit the imputation estimator
est = ImputationDiD()
results = est.fit(data, outcome='outcome', unit='unit', time='period', first_treat='first_treat')
results.print_summary()

Event Study with Pre-Trend Diagnostics#

Event study aggregation estimates treatment effects at each relative time horizon. Setting pretrends=True adds pre-period coefficients (negative horizons) to the event study, enabling a diagnostic check of the parallel trends assumption.

Under parallel trends, pre-period coefficients should cluster around zero — indicating no differential trends before treatment. The reference period (h = -1) is normalized to zero by construction.

[ ]:
# Fit with event study aggregation and pre-period coefficients
est = ImputationDiD(pretrends=True)
results_es = est.fit(data, outcome='outcome', unit='unit', time='period',
                     first_treat='first_treat', aggregate='event_study')

# Plot event study — pre-period region is automatically shaded
if HAS_MATPLOTLIB:
    plot_event_study(results_es, title='Imputation DiD Event Study (with Pre-Trends)')
else:
    print("Install matplotlib to see visualizations: pip install matplotlib")
[ ]:
# View event study effects as a table
results_es.to_dataframe(level='event_study')

Formal Pre-Trend Test#

The event study plot above gives a visual diagnostic — do pre-period coefficients look close to zero? For a statistical check, pretrend_test() runs a Wald F-test on whether all pre-treatment leads are jointly zero (Equation 9 in the paper). This complements the plot: the eye spots patterns, the F-test quantifies evidence consistent with parallel trends.

Note: pretrend_test() does not require pretrends=True — it runs its own internal lead regression on untreated observations, independent of the treatment effect estimator (Proposition 9). This avoids the pre-testing problem identified by Roth (2022).

[ ]:
# Run pre-trend test
pt = results.pretrend_test(n_leads=3)
print(f"F-statistic: {pt['f_stat']:.3f}")
print(f"P-value:     {pt['p_value']:.4f}")
print(f"Leads tested: {pt['n_leads']}")
print(f"\nConclusion: {'Fail to reject' if pt['p_value'] > 0.05 else 'Reject'} parallel trends at 5% level")

Comparison with Other Estimators#

Under homogeneous treatment effects, ImputationDiD, Callaway-Sant’Anna, and Sun-Abraham should produce similar point estimates. The key difference is efficiency — ImputationDiD produces shorter confidence intervals.

[ ]:
# Fit all three estimators on the same data
imp = ImputationDiD().fit(data, outcome='outcome', unit='unit',
                          time='period', first_treat='first_treat')
cs = CallawaySantAnna().fit(data, outcome='outcome', unit='unit',
                            time='period', first_treat='first_treat')
sa = SunAbraham().fit(data, outcome='outcome', unit='unit',
                      time='period', first_treat='first_treat')

print("Estimator Comparison (True effect = 2.0)")
print("=" * 55)
print(f"{'Estimator':<25} {'ATT':>8} {'SE':>8} {'CI Width':>10}")
print("-" * 55)

for name, r in [("ImputationDiD", imp), ("CallawaySantAnna", cs), ("SunAbraham", sa)]:
    ci_width = r.overall_conf_int[1] - r.overall_conf_int[0]
    print(f"{name:<25} {r.overall_att:>8.3f} {r.overall_se:>8.3f} {ci_width:>10.3f}")

Group Aggregation#

Group aggregation estimates average treatment effects by treatment cohort (groups defined by first treatment period).

[ ]:
# Fit with group aggregation
results_grp = ImputationDiD().fit(data, outcome='outcome', unit='unit',
                                   time='period', first_treat='first_treat',
                                   aggregate='group')
results_grp.to_dataframe(level='group')

Advanced Features#

Anticipation#

If treatment effects begin before the official treatment date, use the anticipation parameter to account for this.

[ ]:
# Account for 1 period of anticipation
est_antic = ImputationDiD(anticipation=1)
results_antic = est_antic.fit(data, outcome='outcome', unit='unit',
                               time='period', first_treat='first_treat')
print(f"ATT (no anticipation):    {results.overall_att:.3f}")
print(f"ATT (1-period anticipation): {results_antic.overall_att:.3f}")

Auxiliary Model Partition#

The aux_partition parameter controls the auxiliary model partition for the conservative variance estimator (Theorem 3). Finer partitions give tighter SEs but may overfit with few observations per group.

[ ]:
# Compare different partition choices
for partition in ['cohort_horizon', 'cohort', 'horizon']:
    r = ImputationDiD(aux_partition=partition).fit(
        data, outcome='outcome', unit='unit',
        time='period', first_treat='first_treat')
    print(f"aux_partition='{partition}': ATT={r.overall_att:.3f}, SE={r.overall_se:.3f}")

Summary#

Feature

ImputationDiD

CallawaySantAnna

SunAbraham

Approach

Impute Y(0) via FE model

Group-time ATT(g,t)

Saturated regression

Efficiency

Most efficient under homogeneity

Less efficient

Least efficient

Robustness

Requires homogeneity for efficiency

Fully robust to heterogeneity

Robust to heterogeneity

Control group

All untreated (always)

Never-treated or not-yet-treated

Never-treated

Best for

Homogeneous effects, maximum power

Heterogeneous effects, flexible

Robustness check

Reference: Borusyak, K., Jaravel, X., & Spiess, J. (2024). Revisiting Event-Study Designs: Robust and Efficient Estimation. Review of Economic Studies, 91(6), 3253-3285.