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:

  1. Achieves the semiparametric efficiency bound for ATT(g,t) estimation on the no-covariate path

  2. Optimally weights across comparison groups and baselines via the inverse covariance matrix Ω*

  3. Supports two PT assumptions: PT-All (overidentified, tighter SEs) and PT-Post (just-identified, matches CS for post-treatment effects)

  4. 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: EfficientDiDBootstrapMixin

Efficient 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} and conley are 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). Use cluster=<col> for Liang-Zeger CR1 on cluster-aggregated EIF; use survey_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 at t >= last_g - anticipation so 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 at t >= 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 support n_pos — the raw group size when unweighted — a growing sieve with no fixed ceiling, bounded by n_basis < n_pos; zero-weight survey rows do not affect order selection). Only used with covariates. sieve_k_max=1 forces 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]
Parameters:
  • pt_assumption (str)

  • alpha (float)

  • cluster (str | None)

  • vcov_type (str)

  • control_group (str)

  • n_bootstrap (int)

  • bootstrap_weights (str)

  • seed (int | None)

  • anticipation (int)

  • sieve_k_max (int | None)

  • sieve_criterion (str)

  • ratio_clip (float)

  • kernel_bandwidth (float | None)

alpha: float
n_bootstrap: int
bootstrap_weights: str
seed: int | None
anticipation: int
results_: EfficientDiDResults | None
get_params()[source]

Get estimator parameters (sklearn-compatible).

Return type:

Dict[str, Any]

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 leave self.vcov_type partially mutated even though the call raised, defeating the eager-validation contract for callers that catch ValueError and keep using the estimator.

Parameters:

params (Any)

Return type:

EfficientDiD

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.inf for 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:

EfficientDiDResults

Raises:

ValueError – Missing columns, unbalanced panel, non-absorbing treatment, or PT-Post without a never-treated group.

summary()[source]

Get summary of estimation results.

Return type:

str

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().

  • covariates (List[str] | None) – 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: object

Results 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:

dict

overall_att

Overall ATT (cohort-size weighted average of post-treatment group-time effects, matching CallawaySantAnna convention).

Type:

float

overall_se

Standard error of overall ATT.

Type:

float

overall_t_stat

t-statistic for overall ATT.

Type:

float

overall_p_value

p-value for overall ATT.

Type:

float

overall_conf_int

Confidence interval for overall ATT.

Type:

tuple

groups

Treatment cohort identifiers.

Type:

list

time_periods

All time periods.

Type:

list

n_obs

Total observations (units x periods).

Type:

int

n_treated_units

Number of ever-treated units.

Type:

int

n_control_units

Number of never-treated units.

Type:

int

alpha

Significance level.

Type:

float

pt_assumption

"all" or "post".

Type:

str

anticipation

Number of anticipation periods used.

Type:

int

n_bootstrap

Number of bootstrap iterations (0 = analytical only).

Type:

int

bootstrap_weights

Bootstrap weight distribution ("rademacher", "mammen", "webb").

Type:

str

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 to fit().

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:

str

influence_functions

{(g, t): ndarray(n_units,)} — per-unit EIF values for each group-time cell. Only populated when store_eif=True in fit() (used internally by hausman_pretest).

Type:

dict, optional

bootstrap_results

Bootstrap inference results.

Type:

EDiDBootstrapResults, optional

estimation_path

"nocov" or "dr" — which estimation path was used.

Type:

str

sieve_k_max

Maximum polynomial degree for the covariate-path sieves (propensity ratio, inverse propensity, and outcome regression); 1 forces 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:

str

ratio_clip

Clipping bound for sieve propensity ratios.

Type:

float

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.

group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]]
overall_att: float
overall_se: float
overall_t_stat: float
overall_p_value: float
overall_conf_int: Tuple[float, float]
groups: List[Any]
time_periods: List[Any]
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'
seed: int | None = None
event_study_effects: Dict[int, Dict[str, Any]] | None = None
group_effects: Dict[Any, Dict[str, Any]] | None = None
efficient_weights: Dict[Tuple[Any, Any], np.ndarray] | None = None
omega_condition_numbers: Dict[Tuple[Any, Any], float] | None = None
control_group: str = 'never_treated'
cluster_name: str | None = None
n_clusters: int | None = None
vcov_type: str = 'hc1'
influence_functions: Dict[Tuple[Any, Any], np.ndarray] | None = None
bootstrap_results: EDiDBootstrapResults | None = None
estimation_path: str = 'nocov'
sieve_k_max: int | None = None
sieve_criterion: str = 'bic'
ratio_clip: float = 20.0
kernel_bandwidth: float | None = None
survey_metadata: Any | None = None
property att: float
property se: float
property conf_int: Tuple[float, 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:
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.

Parameters:

alpha (float | None)

Return type:

str

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.

Parameters:

level (str) – "group_time", "event_study", or "group".

Return type:

DataFrame

to_dict()[source]

Convert headline results to a flat dictionary.

Mirrors TripleDifferenceResults.to_dict() and ImputationDiDResults.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.

Return type:

Dict[str, Any]

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: object

Bootstrap inference results for EfficientDiD.

n_bootstrap: int
weight_type: str
alpha: float
overall_att_se: float
overall_att_ci: Tuple[float, float]
overall_att_p_value: float
group_time_ses: Dict[Tuple[Any, Any], float]
group_time_cis: Dict[Tuple[Any, Any], Tuple[float, float]]
group_time_p_values: Dict[Tuple[Any, Any], float]
event_study_ses: Dict[int, float] | None = None
event_study_cis: Dict[int, Tuple[float, float]] | None = None
event_study_p_values: Dict[int, float] | None = None
group_effect_ses: Dict[Any, float] | None = None
group_effect_cis: Dict[Any, Tuple[float, float]] | None = None
group_effect_p_values: Dict[Any, float] | None = None
bootstrap_distribution: ndarray | None = None
__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)
Parameters:
Return type:

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