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 with all nuisances
estimated nonparametrically: sieve-based propensity score ratios and a
sieve outcome regression (polynomial basis, AIC/BIC order selection),
plus the kernel-smoothed conditional covariance. The DR property ensures
consistency if either the outcome regression or the PS ratio is correctly
specified, and because the nuisances are growing sieves/kernels the
covariate path attains the semiparametric efficiency bound asymptotically
under the paper’s regularity conditions (degree 1 reproduces a linear
working model, and sieve_k_max=1 forces all covariate-path sieves to
degree 1). 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 and attains the efficiency bound under the paper’s regularity conditions, with all nuisances (sieve propensity ratio, sieve outcome regression, kernel covariance) estimated nonparametrically.
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), sieve outcome regressions (polynomial basis, AIC/BIC order selection), 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 outcome regression or the sieve propensity ratio is correctly specified; because all nuisances are sieves / kernel smoothers (the paper’s flexible-nuisance specification), the covariate path attains the semiparametric efficiency bound under the paper’s regularity conditions (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).vcov_type (str, default
"hc1") – Variance-estimator family. Permanently narrow to{"hc1"}per the Chen-Sant’Anna-Xie (2025) IF-based variance — analytical-sandwich families{classical, hc2, hc2_bm}andconleyare rejected at__init__/set_params. See REGISTRY.md for the methodology rationale (no single design matrix on which hat-matrix leverage or Bell-McCaffrey Satterthwaite DOF can be defined). Usecluster=<col>for Liang-Zeger CR1 on cluster-aggregated EIF; usesurvey_design=for Taylor Series Linearization on the combined IF.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 the covariate-path sieves — the propensity-ratio, inverse-propensity, AND outcome-regression fits all use it. None = auto (
floor(n_pos^{1/5})over each group’s positive-weight supportn_pos— the raw group size when unweighted — a growing sieve with no fixed ceiling, bounded byn_basis < n_pos; zero-weight survey rows do not affect order selection). Only used with covariates.sieve_k_max=1forces every covariate-path sieve (outcome regression and both propensity sieves) to degree 1: it recovers the pre-sieve linear-OLS outcome regression but also degree-1-constrains the propensity sieves, so it does not reproduce the exact pre-sieve estimator.sieve_criterion (str, default
"bic") – Information criterion ("aic"or"bic") for the order selection of all covariate-path sieves (propensity ratio, inverse propensity, and outcome regression).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, vcov_type='hc1', 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).
Atomic: snapshots the original attribute values before applying mutations, validates the new state via
_validate_params, and rolls every attribute back to its pre-call value if validation raises. Without this,set_params(vcov_type="classical", alpha=0.1)would leaveself.vcov_typepartially mutated even though the call raised, defeating the eager-validation contract for callers that catchValueErrorand keep using the estimator.- 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
- cluster_name
Cluster column used at fit time (None for unclustered fits; suppressed under any survey design). Populated when
cluster=is passed tofit().- Type:
str or None
- n_clusters
Number of clusters at fit time (None for unclustered or survey fits). Renders as
G=<n>in the variance-estimator summary line.- Type:
int or None
- vcov_type
Variance-estimator family. Permanently
"hc1"per the Chen-Sant’Anna-Xie (2025) IF-based variance; see REGISTRY.md.- Type:
- 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 the covariate-path sieves (propensity ratio, inverse propensity, and outcome regression);
1forces a linear outcome-regression working model.- Type:
int or None
- sieve_criterion
Information criterion used (
"aic"or"bic") for all covariate-path sieve order selection.- 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'
- vcov_type: str = 'hc1'
- bootstrap_results: EDiDBootstrapResults | None = None
- estimation_path: str = 'nocov'
- sieve_criterion: str = 'bic'
- ratio_clip: float = 20.0
- property att: float
- property se: float
- property p_value: 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_name=None, n_clusters=None, vcov_type='hc1', 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_name (str | None)
n_clusters (int | None)
vcov_type (str)
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 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.
- to_dict()[source]
Convert headline results to a flat dictionary.
Mirrors
TripleDifferenceResults.to_dict()andImputationDiDResults.to_dict()— surfaces variance metadata (vcov_type,cluster_name,n_clusters,n_bootstrap,inference_method) for external adapters that don’t render the full summary.
- 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 the semiparametric bound on the no-covariate path; the doubly-robust covariate path attains it too (under regularity conditions) via sieve nuisances |
Not efficient |
Efficient under homogeneity |
Covariates |
Supported (doubly robust, sieve-based PS ratio + sieve outcome regression) |
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 |