Efficient Difference-in-Differences#
Semiparametrically efficient ATT estimator for staggered adoption designs from Chen, Sant’Anna & Xie (2025).
This module implements the efficiency-bound-attaining estimator that:
Achieves the semiparametric efficiency bound for ATT(g,t) estimation on the no-covariate path
Optimally weights across comparison groups and baselines via the inverse covariance matrix Ω*
Supports two PT assumptions: PT-All (overidentified, tighter SEs) and PT-Post (just-identified, matches CS for post-treatment effects)
Uses EIF-based inference for analytical standard errors and multiplier bootstrap
Note
EfficientDiD supports a doubly-robust covariate path: sieve-based
propensity score ratios combined with a linear OLS outcome regression.
The DR property ensures consistency if either the OR or the PS ratio is
correctly specified, but the linear OLS working model for the outcome
regression does not generically attain the semiparametric efficiency
bound unless the conditional mean is linear in the covariates. The
unqualified efficiency-bound claim applies to the no-covariate path
only. Pass column names to the covariates parameter on fit().
See docs/methodology/REGISTRY.md for the full contract.
When to use EfficientDiD:
Staggered adoption design where you want maximum efficiency on the no-covariate path
You believe parallel trends holds across all pre-treatment periods (PT-All)
You want tighter confidence intervals than Callaway-Sant’Anna
You need a formal efficiency benchmark for comparing estimators
For covariate-adjusted designs, the doubly-robust path is consistent under either outcome-regression or propensity-ratio correctness but does not generically attain the efficiency bound under the shipped linear OLS outcome regression.
Reference: Chen, X., Sant’Anna, P. H. C., & Xie, H. (2025). Efficient Difference-in-Differences and Event Study Estimators.
EfficientDiD#
Main estimator class for Efficient Difference-in-Differences.
- class diff_diff.EfficientDiD[source]
Bases:
EfficientDiDBootstrapMixinEfficient DiD estimator (Chen, Sant’Anna & Xie 2025).
Without covariates, achieves the semiparametric efficiency bound for ATT(g,t) using a closed-form estimator based on within-group sample means and covariances.
With covariates, uses a doubly robust path: sieve-based propensity score ratios (Eq 4.1-4.2), OLS outcome regression, sieve-estimated inverse propensities (algorithm step 4), and kernel-smoothed conditional Omega*(X) with per-unit efficient weights (Eq 3.12). The DR property ensures consistency if either the OLS outcome model or the sieve propensity ratio is correctly specified. The OLS working model for outcome regressions does not generically guarantee the semiparametric efficiency bound (see REGISTRY.md).
- Parameters:
pt_assumption (str, default
"all") – Parallel trends variant:"all"(overidentified, uses all pre-treatment periods and comparison groups) or"post"(just-identified, single baseline, equivalent to CS).alpha (float, default 0.05) – Significance level.
cluster (str or None) – Column name for cluster-robust SEs. When set, analytical SEs use the Liang-Zeger clustered sandwich estimator on EIF values. With
n_bootstrap > 0, bootstrap weights are generated at the cluster level (all units in a cluster share the same weight).control_group (str, default
"never_treated") – Which units serve as the comparison group:"never_treated"requires a never-treated cohort (raises if none exist);"last_cohort"reclassifies the latest treatment cohort as pseudo-never-treated and drops periods att >= last_g - anticipationso the pseudo-control’s pre-treatment window excludes anticipation-contaminated periods. Distinct from CallawaySantAnna’s"not_yet_treated"— see REGISTRY.md for details.n_bootstrap (int, default 0) – Number of multiplier bootstrap iterations (0 = analytical only).
bootstrap_weights (str, default
"rademacher") – Bootstrap weight distribution.seed (int or None) – Random seed for reproducibility.
anticipation (int, default 0) – Number of anticipation periods (shifts the effective treatment boundary forward by this amount). When combined with
control_group="last_cohort", also trims the pseudo-control period set att >= last_g - anticipation(see REGISTRY.md).sieve_k_max (int or None) – Maximum polynomial degree for sieve ratio estimation. None = auto (
min(floor(n_gp^{1/5}), 5)). Only used with covariates.sieve_criterion (str, default
"bic") – Information criterion for sieve degree selection:"aic"or"bic".ratio_clip (float, default 20.0) – Clip sieve propensity ratios to
[1/ratio_clip, ratio_clip].kernel_bandwidth (float or None) – Bandwidth for Gaussian kernel in conditional Omega* estimation. None = Silverman’s rule-of-thumb (automatic).
Examples
>>> from diff_diff import EfficientDiD >>> edid = EfficientDiD(pt_assumption="all") >>> results = edid.fit(data, outcome="y", unit="id", time="t", ... first_treat="first_treat", aggregate="all") >>> results.print_summary()
Methods
fit(data, outcome, unit, time, first_treat)Fit the Efficient DiD estimator.
get_params()Get estimator parameters (sklearn-compatible).
set_params(**params)Set estimator parameters (sklearn-compatible).
- __init__(pt_assumption='all', alpha=0.05, cluster=None, control_group='never_treated', n_bootstrap=0, bootstrap_weights='rademacher', seed=None, anticipation=0, sieve_k_max=None, sieve_criterion='bic', ratio_clip=20.0, kernel_bandwidth=None)[source]
- alpha: float
- n_bootstrap: int
- bootstrap_weights: str
- anticipation: int
- results_: EfficientDiDResults | None
- set_params(**params)[source]
Set estimator parameters (sklearn-compatible).
- Parameters:
params (Any)
- Return type:
- fit(data, outcome, unit, time, first_treat, covariates=None, aggregate=None, balance_e=None, survey_design=None, store_eif=False)[source]
Fit the Efficient DiD estimator.
- Parameters:
data (DataFrame) – Balanced panel data.
outcome (str) – Outcome variable column name.
unit (str) – Unit identifier column name.
time (str) – Time period column name.
first_treat (str) – Column indicating first treatment period. Use 0 or
np.inffor never-treated units.covariates (list of str, optional) – Column names for time-invariant unit-level covariates. When provided, uses the doubly robust path (outcome regression + propensity score ratios).
aggregate (str, optional) –
None,"simple","event_study","group", or"all".balance_e (int, optional) – Balance event study at this relative period.
survey_design (SurveyDesign, optional) – Survey design specification for design-based inference. Applies survey weights to all means, covariances, and cohort fractions, and uses Taylor Series Linearization for SE estimation. Cannot be combined with
cluster.store_eif (bool, default False) – Store per-(g,t) EIF vectors in the results object. Used internally by
hausman_pretest(); not needed for normal usage.
- Return type:
- Raises:
ValueError – Missing columns, unbalanced panel, non-absorbing treatment, or PT-Post without a never-treated group.
- print_summary()[source]
Print summary to stdout.
- Return type:
None
- classmethod hausman_pretest(data, outcome, unit, time, first_treat, covariates=None, cluster=None, anticipation=0, control_group='never_treated', alpha=0.05, **nuisance_kwargs)[source]
Hausman pretest for PT-All vs PT-Post (Theorem A.1).
Fits the estimator under both parallel trends assumptions and compares the results. Under H0 (PT-All holds), both are consistent but PT-All is more efficient. Rejection suggests PT-All is too strong; use PT-Post instead.
- Parameters:
data (DataFrame) – Same as
fit().outcome (str) – Same as
fit().unit (str) – Same as
fit().time (str) – Same as
fit().first_treat (str) – Same as
fit().cluster (str, optional) – Cluster column for cluster-robust covariance.
anticipation (int) – Anticipation periods.
control_group (str) –
"never_treated"or"last_cohort".alpha (float) – Significance level for the test.
**nuisance_kwargs – Passed to both fits (e.g.
sieve_k_max,ratio_clip).
- Return type:
HausmanPretestResult
EfficientDiDResults#
Results container for Efficient DiD estimation.
- class diff_diff.efficient_did_results.EfficientDiDResults[source]
Bases:
objectResults from Efficient DiD (Chen, Sant’Anna & Xie 2025) estimation.
Stores group-time ATT(g,t) estimates with efficient weights, plus optional aggregations (overall ATT, event study, group effects).
- group_time_effects
{(g, t): {'effect', 'se', 't_stat', 'p_value', 'conf_int', 'n_treated', 'n_control'}}- Type:
- overall_att
Overall ATT (cohort-size weighted average of post-treatment group-time effects, matching CallawaySantAnna convention).
- Type:
- overall_se
Standard error of overall ATT.
- Type:
- overall_t_stat
t-statistic for overall ATT.
- Type:
- overall_p_value
p-value for overall ATT.
- Type:
- overall_conf_int
Confidence interval for overall ATT.
- Type:
- groups
Treatment cohort identifiers.
- Type:
- time_periods
All time periods.
- Type:
- n_obs
Total observations (units x periods).
- Type:
- n_treated_units
Number of ever-treated units.
- Type:
- n_control_units
Number of never-treated units.
- Type:
- alpha
Significance level.
- Type:
- pt_assumption
"all"or"post".- Type:
- anticipation
Number of anticipation periods used.
- Type:
- n_bootstrap
Number of bootstrap iterations (0 = analytical only).
- Type:
- bootstrap_weights
Bootstrap weight distribution (
"rademacher","mammen","webb").- Type:
- seed
Random seed used for bootstrap.
- Type:
int or None
- event_study_effects
{relative_time: effect_dict}- Type:
dict, optional
- group_effects
{group: effect_dict}- Type:
dict, optional
- efficient_weights
{(g, t): ndarray}— diagnostic: weight vector per target.- Type:
dict, optional
- omega_condition_numbers
{(g, t): float}— diagnostic: Omega* condition numbers.- Type:
dict, optional
- influence_functions
{(g, t): ndarray(n_units,)}— per-unit EIF values for each group-time cell. Only populated whenstore_eif=Trueinfit()(used internally byhausman_pretest).- Type:
dict, optional
- bootstrap_results
Bootstrap inference results.
- Type:
EDiDBootstrapResults, optional
- estimation_path
"nocov"or"dr"— which estimation path was used.- Type:
- sieve_k_max
Maximum polynomial degree for sieve ratio estimation.
- Type:
int or None
- sieve_criterion
Information criterion used (
"aic"or"bic").- Type:
- ratio_clip
Clipping bound for sieve propensity ratios.
- Type:
- kernel_bandwidth
Bandwidth used for kernel-smoothed conditional Omega*.
- Type:
float or None
Methods
summary([alpha])Generate formatted summary of estimation results.
print_summary([alpha])Print summary to stdout.
to_dataframe([level])Convert results to DataFrame.
- overall_att: float
- overall_se: float
- overall_t_stat: float
- overall_p_value: float
- n_obs: int
- n_treated_units: int
- n_control_units: int
- alpha: float = 0.05
- pt_assumption: str = 'all'
- anticipation: int = 0
- n_bootstrap: int = 0
- bootstrap_weights: str = 'rademacher'
- control_group: str = 'never_treated'
- bootstrap_results: EDiDBootstrapResults | None = None
- estimation_path: str = 'nocov'
- sieve_criterion: str = 'bic'
- ratio_clip: float = 20.0
- property att: float
- property se: float
- __init__(group_time_effects, overall_att, overall_se, overall_t_stat, overall_p_value, overall_conf_int, groups, time_periods, n_obs, n_treated_units, n_control_units, alpha=0.05, pt_assumption='all', anticipation=0, n_bootstrap=0, bootstrap_weights='rademacher', seed=None, event_study_effects=None, group_effects=None, efficient_weights=None, omega_condition_numbers=None, control_group='never_treated', cluster=None, influence_functions=None, bootstrap_results=None, estimation_path='nocov', sieve_k_max=None, sieve_criterion='bic', ratio_clip=20.0, kernel_bandwidth=None, survey_metadata=None)
- Parameters:
overall_att (float)
overall_se (float)
overall_t_stat (float)
overall_p_value (float)
n_obs (int)
n_treated_units (int)
n_control_units (int)
alpha (float)
pt_assumption (str)
anticipation (int)
n_bootstrap (int)
bootstrap_weights (str)
seed (int | None)
efficient_weights (Dict[Tuple[Any, Any], np.ndarray] | None)
omega_condition_numbers (Dict[Tuple[Any, Any], float] | None)
control_group (str)
cluster (str | None)
influence_functions (Dict[Tuple[Any, Any], np.ndarray] | None)
bootstrap_results (EDiDBootstrapResults | None)
estimation_path (str)
sieve_k_max (int | None)
sieve_criterion (str)
ratio_clip (float)
kernel_bandwidth (float | None)
survey_metadata (Any | None)
- Return type:
None
- property p_value: float
- property t_stat: float
- property coef_var: float
SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite.
- Type:
Coefficient of variation
- summary(alpha=None)[source]
Generate formatted summary of estimation results.
- print_summary(alpha=None)[source]
Print summary to stdout.
- Parameters:
alpha (float | None)
- Return type:
None
- to_dataframe(level='group_time')[source]
Convert results to DataFrame.
- property is_significant: bool
Check if overall ATT is significant.
- property significance_stars: str
Significance stars for overall ATT.
EDiDBootstrapResults#
Bootstrap inference results for Efficient DiD.
- class diff_diff.efficient_did_bootstrap.EDiDBootstrapResults[source]
Bases:
objectBootstrap inference results for EfficientDiD.
- n_bootstrap: int
- weight_type: str
- alpha: float
- overall_att_se: float
- overall_att_p_value: float
- __init__(n_bootstrap, weight_type, alpha, overall_att_se, overall_att_ci, overall_att_p_value, group_time_ses, group_time_cis, group_time_p_values, event_study_ses=None, event_study_cis=None, event_study_p_values=None, group_effect_ses=None, group_effect_cis=None, group_effect_p_values=None, bootstrap_distribution=None)
Example Usage#
Basic usage:
from diff_diff import EfficientDiD, generate_staggered_data
data = generate_staggered_data(n_units=300, n_periods=10,
cohort_periods=[4, 6, 8], seed=42)
edid = EfficientDiD(pt_assumption="all")
results = edid.fit(data, outcome='outcome', unit='unit',
time='period', first_treat='first_treat',
aggregate='all')
results.print_summary()
PT-Post mode (matches CS for post-treatment ATT):
edid_post = EfficientDiD(pt_assumption="post")
results_post = edid_post.fit(data, outcome='outcome', unit='unit',
time='period', first_treat='first_treat',
aggregate='all')
print(f"PT-All ATT: {results.overall_att:.4f} (SE={results.overall_se:.4f})")
print(f"PT-Post ATT: {results_post.overall_att:.4f} (SE={results_post.overall_se:.4f})")
Bootstrap inference:
edid_boot = EfficientDiD(pt_assumption="all", n_bootstrap=999, seed=42)
results_boot = edid_boot.fit(data, outcome='outcome', unit='unit',
time='period', first_treat='first_treat',
aggregate='all')
print(f"Bootstrap SE: {results_boot.overall_se:.4f}")
print(f"Bootstrap CI: [{results_boot.overall_conf_int[0]:.4f}, "
f"{results_boot.overall_conf_int[1]:.4f}]")
Comparison with Other Staggered Estimators#
Feature |
EfficientDiD |
CallawaySantAnna |
ImputationDiD |
|---|---|---|---|
Approach |
Optimal EIF-based weighting |
Separate 2x2 DiD aggregation |
Impute Y(0) via FE model |
PT assumption |
PT-All (stronger) or PT-Post |
Conditional PT |
Strict exogeneity |
Efficiency |
Achieves semiparametric bound on the no-covariate path; DR covariate path is consistent but does not generically attain the bound under a linear OLS outcome regression |
Not efficient |
Efficient under homogeneity |
Covariates |
Supported (doubly robust, sieve-based PS + linear OLS OR) |
Supported (OR, IPW, DR) |
Supported |
Bootstrap |
Multiplier bootstrap (EIF) |
Multiplier bootstrap |
Multiplier bootstrap |
PT-Post equivalence |
Matches CS post-treatment ATT(g,t) |
Baseline |
Different framework |