Estimators#
Core estimator classes for Difference-in-Differences analysis.
The main estimators module (diff_diff.estimators) contains the base classes
DifferenceInDifferences and MultiPeriodDiD. Additional estimators are
organized in separate modules for maintainability:
diff_diff.twfe-TwoWayFixedEffectsestimatordiff_diff.synthetic_did-SyntheticDiDestimator
All estimators are re-exported from diff_diff.estimators and diff_diff
for backward compatibility, so you can import any of them using:
from diff_diff import DifferenceInDifferences, TwoWayFixedEffects, MultiPeriodDiD, SyntheticDiD
Most estimators have short aliases (TROP already uses its short canonical name):
from diff_diff import DiD, TWFE, EventStudy, SDiD, CS, CDiD, SA, BJS, Gardner, DDD, Stacked, Bacon
DifferenceInDifferences (alias: DiD)#
Basic 2x2 DiD estimator.
- class diff_diff.DifferenceInDifferences[source]
Bases:
objectDifference-in-Differences estimator with sklearn-like interface.
Estimates the Average Treatment effect on the Treated (ATT) using the canonical 2x2 DiD design or panel data with two-way fixed effects.
- Parameters:
formula (str, optional) – R-style formula for the model (e.g., “outcome ~ treated * post”). If provided, overrides column name parameters.
robust (bool, default=True) – Legacy alias for
vcov_type.robust=Truemaps tovcov_type="hc1";robust=Falsemaps tovcov_type="classical". Explicitvcov_typeoverridesrobustunless the pair is contradictory (e.g.robust=False, vcov_type="hc2"raises).cluster (str, optional) – Column name for cluster-robust standard errors. Combined with
vcov_type: with"hc1"dispatches to CR1 (Liang-Zeger); with"hc2_bm"dispatches to CR2 Bell-McCaffrey (Pustejovsky-Tipton 2018 symmetric-sqrt + Satterthwaite DOF).vcov_type ({"classical", "hc1", "hc2", "hc2_bm", "conley"}, optional) –
Variance-covariance family. Defaults to the
robustalias."classical": non-robust OLS SEs,sigma_hat^2 * (X'X)^{-1}."hc1": heteroskedasticity-robust HC1 withn/(n-k)adjustment (library default). Withcluster=, uses CR1 (Liang-Zeger)."hc2": leverage-corrected meat (one-way only). Errors withcluster=; use"hc2_bm"for clustered Bell-McCaffrey."hc2_bm": one-way HC2 + Imbens-Kolesar (2016) Satterthwaite DOF; withcluster=, Pustejovsky-Tipton (2018) CR2 cluster-robust.MultiPeriodDiD(cluster=..., vcov_type="hc2_bm")is supported and uses a cluster-aware Bell-McCaffrey contrast DOF for the post-period-average ATT (see_compute_cr2_bm_contrast_dofinlinalg.pyand the REGISTRY.md note). Weighted CR2-BM (survey_design=paths) is a separate gate."conley": Conley 1999 spatial-HAC sandwich. Passconley_coords=(lat_col, lon_col),conley_cutoff_km=<float>, andconley_lag_cutoff=<int>on the constructor; passunit=<col>as a fit-time kwarg tofit()(NOT on__init__; unused unless Conley is set; not part ofget_params()/set_params()). The block-decomposed panel sandwich (matches Rconleyregwithlag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded). Explicitcluster=<col>enables the combined spatial + cluster product kernel;survey_design=andinference='wild_bootstrap'both raiseNotImplementedError.
alpha (float, default=0.05) – Significance level for confidence intervals.
inference (str, default="analytical") – Inference method: “analytical” for standard asymptotic inference, or “wild_bootstrap” for wild cluster bootstrap (recommended when number of clusters is small, <50).
n_bootstrap (int, default=999) – Number of bootstrap replications when inference=”wild_bootstrap”.
bootstrap_weights (str, default="rademacher") – Type of bootstrap weights: “rademacher” (standard), “webb” (recommended for <10 clusters), or “mammen” (skewness correction).
seed (int, optional) – Random seed for reproducibility when using bootstrap inference. If None (default), results will vary between runs.
rank_deficient_action (str, default "warn") – Action when design matrix is rank-deficient (linearly dependent columns): - “warn”: Issue warning and drop linearly dependent columns (default) - “error”: Raise ValueError - “silent”: Drop columns silently without warning
conley_coords – Conley (1999) spatial-HAC variance configuration. Pass
conley_coords=(lat_col, lon_col),conley_cutoff_km=<float>, andconley_lag_cutoff=<int>on the constructor; theunitidentifier is passed as a fit-time arg tofit(...)(NOT on__init__) — it is unused unlessvcov_type="conley"and is therefore not part ofget_params()/set_params()(which return constructor-arg dicts). The block-decomposed panel sandwich (matching Rconleyregwithlag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicitcluster=<col>+ Conley enables the combined spatial + cluster product kernel; the cluster must be constant within each unit across periods (validator-enforced). DiD has no auto-cluster, so cluster is fully opt-in on the Conley path — absentcluster=, pure Conley spatial HAC applies.survey_design=+ Conley andinference='wild_bootstrap'+ Conley both raiseNotImplementedError.conley_cutoff_km – Conley (1999) spatial-HAC variance configuration. Pass
conley_coords=(lat_col, lon_col),conley_cutoff_km=<float>, andconley_lag_cutoff=<int>on the constructor; theunitidentifier is passed as a fit-time arg tofit(...)(NOT on__init__) — it is unused unlessvcov_type="conley"and is therefore not part ofget_params()/set_params()(which return constructor-arg dicts). The block-decomposed panel sandwich (matching Rconleyregwithlag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicitcluster=<col>+ Conley enables the combined spatial + cluster product kernel; the cluster must be constant within each unit across periods (validator-enforced). DiD has no auto-cluster, so cluster is fully opt-in on the Conley path — absentcluster=, pure Conley spatial HAC applies.survey_design=+ Conley andinference='wild_bootstrap'+ Conley both raiseNotImplementedError.conley_metric – Conley (1999) spatial-HAC variance configuration. Pass
conley_coords=(lat_col, lon_col),conley_cutoff_km=<float>, andconley_lag_cutoff=<int>on the constructor; theunitidentifier is passed as a fit-time arg tofit(...)(NOT on__init__) — it is unused unlessvcov_type="conley"and is therefore not part ofget_params()/set_params()(which return constructor-arg dicts). The block-decomposed panel sandwich (matching Rconleyregwithlag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicitcluster=<col>+ Conley enables the combined spatial + cluster product kernel; the cluster must be constant within each unit across periods (validator-enforced). DiD has no auto-cluster, so cluster is fully opt-in on the Conley path — absentcluster=, pure Conley spatial HAC applies.survey_design=+ Conley andinference='wild_bootstrap'+ Conley both raiseNotImplementedError.conley_kernel – Conley (1999) spatial-HAC variance configuration. Pass
conley_coords=(lat_col, lon_col),conley_cutoff_km=<float>, andconley_lag_cutoff=<int>on the constructor; theunitidentifier is passed as a fit-time arg tofit(...)(NOT on__init__) — it is unused unlessvcov_type="conley"and is therefore not part ofget_params()/set_params()(which return constructor-arg dicts). The block-decomposed panel sandwich (matching Rconleyregwithlag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicitcluster=<col>+ Conley enables the combined spatial + cluster product kernel; the cluster must be constant within each unit across periods (validator-enforced). DiD has no auto-cluster, so cluster is fully opt-in on the Conley path — absentcluster=, pure Conley spatial HAC applies.survey_design=+ Conley andinference='wild_bootstrap'+ Conley both raiseNotImplementedError.conley_lag_cutoff – Conley (1999) spatial-HAC variance configuration. Pass
conley_coords=(lat_col, lon_col),conley_cutoff_km=<float>, andconley_lag_cutoff=<int>on the constructor; theunitidentifier is passed as a fit-time arg tofit(...)(NOT on__init__) — it is unused unlessvcov_type="conley"and is therefore not part ofget_params()/set_params()(which return constructor-arg dicts). The block-decomposed panel sandwich (matching Rconleyregwithlag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicitcluster=<col>+ Conley enables the combined spatial + cluster product kernel; the cluster must be constant within each unit across periods (validator-enforced). DiD has no auto-cluster, so cluster is fully opt-in on the Conley path — absentcluster=, pure Conley spatial HAC applies.survey_design=+ Conley andinference='wild_bootstrap'+ Conley both raiseNotImplementedError.
- results_
Estimation results after calling fit().
- Type:
- is_fitted_
Whether the model has been fitted.
- Type:
Examples
Basic usage with a DataFrame:
>>> import pandas as pd >>> from diff_diff import DifferenceInDifferences >>> >>> # Create sample data >>> data = pd.DataFrame({ ... 'outcome': [10, 11, 15, 18, 9, 10, 12, 13], ... 'treated': [1, 1, 1, 1, 0, 0, 0, 0], ... 'post': [0, 0, 1, 1, 0, 0, 1, 1] ... }) >>> >>> # Fit the model >>> did = DifferenceInDifferences() >>> results = did.fit(data, outcome='outcome', treatment='treated', time='post') >>> >>> # View results >>> print(results.att) # ATT estimate >>> results.print_summary() # Full summary table
Using formula interface:
>>> did = DifferenceInDifferences() >>> results = did.fit(data, formula='outcome ~ treated * post')
Notes
The ATT is computed using the standard DiD formula:
ATT = (E[Y|D=1,T=1] - E[Y|D=1,T=0]) - (E[Y|D=0,T=1] - E[Y|D=0,T=0])
Or equivalently via OLS regression:
Y = α + β₁*D + β₂*T + β₃*(D×T) + ε
Where β₃ is the ATT.
Methods
fit(data[, outcome, treatment, time, ...])Fit the Difference-in-Differences model.
get_params()Get estimator parameters (sklearn-compatible).
set_params(**params)Set estimator parameters (sklearn-compatible).
- __init__(robust=True, cluster=None, vcov_type=None, alpha=0.05, inference='analytical', n_bootstrap=999, bootstrap_weights='rademacher', seed=None, rank_deficient_action='warn', conley_coords=None, conley_cutoff_km=None, conley_metric='haversine', conley_kernel='bartlett', conley_lag_cutoff=None)[source]
- fit(data, outcome=None, treatment=None, time=None, formula=None, covariates=None, fixed_effects=None, absorb=None, survey_design=None, unit=None)[source]
Fit the Difference-in-Differences model.
- Parameters:
data (pd.DataFrame) – DataFrame containing the outcome, treatment, and time variables.
outcome (str) – Name of the outcome variable column.
treatment (str) – Name of the treatment group indicator column (0/1).
time (str) – Name of the post-treatment period indicator column (0/1).
formula (str, optional) – R-style formula (e.g., “outcome ~ treated * post”). If provided, overrides outcome, treatment, and time parameters.
covariates (list, optional) – List of covariate column names to include as linear controls.
fixed_effects (list, optional) – List of categorical column names to include as fixed effects. Creates dummy variables for each category (drops first level). Use for low-dimensional fixed effects (e.g., industry, region).
absorb (list, optional) – List of categorical column names for high-dimensional fixed effects. Uses within-transformation (demeaning) instead of dummy variables. More efficient for large numbers of categories (e.g., firm, individual).
survey_design (SurveyDesign, optional) – Survey design specification for design-based inference. When provided, uses Taylor Series Linearization for variance estimation and applies sampling weights to the regression.
unit (str, optional) – Name of the unit identifier column. Required ONLY when
vcov_type="conley"— the panel block-decomposed Conley sandwich (matching Rconleyregwithlag_cutoff > 0) needs the unit identifier to compute the per-unit serial sum. MirrorsMultiPeriodDiD.fit(unit=...)()andTwoWayFixedEffects.fit(unit=...)(). Fit-time only — NOT a constructor kwarg, so it is not part ofget_params()/set_params()(which return constructor-arg dicts). Ignored whenvcov_typeis not"conley".
- Returns:
Object containing estimation results.
- Return type:
- Raises:
ValueError – If required parameters are missing or data validation fails.
Examples
Using fixed effects (dummy variables):
>>> did.fit(data, outcome='sales', treatment='treated', time='post', ... fixed_effects=['state', 'industry'])
Using absorbed fixed effects (within-transformation):
>>> did.fit(data, outcome='sales', treatment='treated', time='post', ... absorb=['firm_id'])
- predict(data)[source]
Predict outcomes using fitted model.
- Parameters:
data (pd.DataFrame) – DataFrame with same structure as training data.
- Returns:
Predicted values.
- Return type:
np.ndarray
- get_params()[source]
Get estimator parameters (sklearn-compatible).
Returns the raw user input for
vcov_type(Nonewhen the value was alias-derived fromrobust). This preserves the backward-compat remap semantics across clones: a clone ofDifferenceInDifferences(robust=False, cluster="unit")must behave the same as the original on a clustered fit, which requires the clone’s__init__to seevcov_type=None(so it flags_vcov_type_explicit=False) rather than the alias-resolved"classical"(which would mark it explicit and skip the CR1 remap).- Returns:
Estimator parameters suitable for passing to
__init__.- Return type:
Dict[str, Any]
- set_params(**params)[source]
Set estimator parameters (sklearn-compatible).
After assignment, the
robust/vcov_typepair is re-validated via the samediff_diff.linalg.resolve_vcov_type()helper used by__init__. Invalid combinations (e.g.robust=Falsewithvcov_type="hc2") raiseValueErrorinstead of leaving the object in an inconsistent state.- Parameters:
**params – Estimator parameters.
- Return type:
self
- print_summary()[source]
Print summary to stdout.
- Return type:
None
MultiPeriodDiD (alias: EventStudy)#
Event study estimator with period-specific treatment effects.
- class diff_diff.MultiPeriodDiD[source]
Bases:
DifferenceInDifferencesMulti-Period Difference-in-Differences estimator.
Extends the standard DiD to handle multiple pre-treatment and post-treatment time periods, providing period-specific treatment effects as well as an aggregate average treatment effect.
- Parameters:
robust (bool, default=True) – Legacy alias for
vcov_type.robust=Truemaps tovcov_type="hc1";robust=Falsemaps tovcov_type="classical". Explicitvcov_typeoverridesrobustunless the pair is contradictory (e.g.robust=False, vcov_type="hc2"raises).cluster (str, optional) – Column name for cluster-robust standard errors. With
vcov_type="hc1"dispatches to CR1 (Liang-Zeger). Withvcov_type="hc2_bm"dispatches to CR2 cluster-robust SEs with Bell-McCaffrey Satterthwaite DOF on both per-period coefficients and the post-period-average ATT contrast (the latter via the new_compute_cr2_bm_contrast_dofhelper inlinalg.py; matches clubSandwich’sWald_test(test="HTZ")$df_denomat atol=1e-10). Weighted CR2-BM (survey_design=) is a separate, still-gated path.vcov_type ({"classical", "hc1", "hc2", "hc2_bm", "conley"}, optional) –
Variance-covariance family. Defaults to the
robustalias."classical": non-robust OLS SEs,sigma_hat^2 * (X'X)^{-1}."hc1": heteroskedasticity-robust HC1 withn/(n-k)adjustment (library default). Withcluster=, uses CR1 (Liang-Zeger)."hc2": leverage-corrected meat (one-way only). Errors withcluster=; use"hc2_bm"without cluster for Bell-McCaffrey."hc2_bm": one-way HC2 + Imbens-Kolesar (2016) Satterthwaite DOF per coefficient plus a contrast-aware DOF for the post-period-average ATT. Withcluster=, dispatches to Pustejovsky-Tipton (2018) CR2 cluster-robust with a Bell-McCaffrey Satterthwaite contrast DOF on the post-period average (seeclusterabove for parity details). Weighted CR2-BM (survey_design=) is still gated."conley": Conley 1999 spatial-HAC sandwich via the panel block-decomposed form (matches Rconleyregwithlag_cutoff > 0). Passconley_coords=(lat_col, lon_col),conley_cutoff_km=<float>, andconley_lag_cutoff=<int>on the constructor;unit=must be supplied at fit-time. The sandwich sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting); this is NOT a multiplicative product kernel.conley_timeis auto-derived from thetimecolumn at fit-time and normalized to dense panel-period codes0..T-1soconley_lag_cutoffalways counts panel periods (works for int / datetime64 /pd.Period/ string encodings). Explicitcluster=<col>enables the combined spatial + cluster product kernel (Wave A #119; cluster must be constant within each unit across periods). Restrictions:survey_design=andinference="wild_bootstrap"raise on this path (Phase 5 / follow-up).
alpha (float, default=0.05) – Significance level for confidence intervals.
conley_coords – Constructor kwargs that take effect when
vcov_type="conley".conley_coordsis a(lat_col, lon_col)tuple of column names ondata.conley_lag_cutoffis the within-unit Bartlett lag (non-negative int; 0 means within-period spatial only, no serial component).conley_cutoff_km – Constructor kwargs that take effect when
vcov_type="conley".conley_coordsis a(lat_col, lon_col)tuple of column names ondata.conley_lag_cutoffis the within-unit Bartlett lag (non-negative int; 0 means within-period spatial only, no serial component).conley_metric – Constructor kwargs that take effect when
vcov_type="conley".conley_coordsis a(lat_col, lon_col)tuple of column names ondata.conley_lag_cutoffis the within-unit Bartlett lag (non-negative int; 0 means within-period spatial only, no serial component).conley_kernel – Constructor kwargs that take effect when
vcov_type="conley".conley_coordsis a(lat_col, lon_col)tuple of column names ondata.conley_lag_cutoffis the within-unit Bartlett lag (non-negative int; 0 means within-period spatial only, no serial component).conley_lag_cutoff – Constructor kwargs that take effect when
vcov_type="conley".conley_coordsis a(lat_col, lon_col)tuple of column names ondata.conley_lag_cutoffis the within-unit Bartlett lag (non-negative int; 0 means within-period spatial only, no serial component).
- results_
Estimation results after calling fit().
- Type:
- is_fitted_
Whether the model has been fitted.
- Type:
Examples
Basic usage with multiple time periods:
>>> import pandas as pd >>> from diff_diff import MultiPeriodDiD >>> >>> # Create sample panel data with 6 time periods >>> # Periods 0-2 are pre-treatment, periods 3-5 are post-treatment >>> data = create_panel_data() # Your data >>> >>> # Fit the model >>> did = MultiPeriodDiD() >>> results = did.fit( ... data, ... outcome='sales', ... treatment='treated', ... time='period', ... post_periods=[3, 4, 5] # Specify which periods are post-treatment ... ) >>> >>> # View period-specific effects >>> for period, effect in results.period_effects.items(): ... print(f"Period {period}: {effect.effect:.3f} (SE: {effect.se:.3f})") >>> >>> # View average treatment effect >>> print(f"Average ATT: {results.avg_att:.3f}")
Notes
The model estimates:
Y_it = α + β*D_i + Σ_t γ_t*Period_t + Σ_{t≠ref} δ_t*(D_i × 1{t}) + ε_it
Where: - D_i is the treatment indicator - Period_t are time period dummies (all non-reference periods) - D_i × 1{t} are treatment-by-period interactions (all non-reference) - δ_t are the period-specific treatment effects - The reference period (default: last pre-period) has δ_ref = 0 by construction
Pre-treatment δ_t test the parallel trends assumption (should be ≈ 0). Post-treatment δ_t estimate dynamic treatment effects. The average ATT is computed from post-treatment δ_t only.
- fit(data, outcome, treatment, time, post_periods=None, covariates=None, fixed_effects=None, absorb=None, reference_period=None, unit=None, survey_design=None)[source]
Fit the Multi-Period Difference-in-Differences model.
- Parameters:
data (pd.DataFrame) – DataFrame containing the outcome, treatment, and time variables.
outcome (str) – Name of the outcome variable column.
treatment (str) – Name of the treatment group indicator column (0/1). Should be a time-invariant ever-treated indicator (D_i = 1 for all periods of treated units). If treatment is time-varying (D_it), pre-period interaction coefficients will be unidentified.
time (str) – Name of the time period column (can have multiple values).
post_periods (list) – List of time period values that are post-treatment. All other periods are treated as pre-treatment.
covariates (list, optional) – List of covariate column names to include as linear controls.
fixed_effects (list, optional) – List of categorical column names to include as fixed effects.
absorb (list, optional) – List of categorical column names for high-dimensional fixed effects.
reference_period (any, optional) – The reference (omitted) time period for the period dummies. Defaults to the last pre-treatment period (e=-1 convention).
unit (str, optional) – Name of the unit identifier column. When provided, checks whether treatment timing varies across units and warns if staggered adoption is detected (suggests CallawaySantAnna instead). Required when
vcov_type="conley"(the panel block-decomposed sandwich computes a per-unit serial sum). For othervcov_typevalues, use theclusterparameter for cluster-robust SEs.survey_design (SurveyDesign, optional) – Survey design specification for design-based inference. When provided, uses Taylor Series Linearization for variance estimation and applies sampling weights to the regression.
- Returns:
Object containing period-specific and average treatment effects.
- Return type:
- Raises:
ValueError – If required parameters are missing or data validation fails.
- __init__(robust=True, cluster=None, vcov_type=None, alpha=0.05, inference='analytical', n_bootstrap=999, bootstrap_weights='rademacher', seed=None, rank_deficient_action='warn', conley_coords=None, conley_cutoff_km=None, conley_metric='haversine', conley_kernel='bartlett', conley_lag_cutoff=None)
- get_params()
Get estimator parameters (sklearn-compatible).
Returns the raw user input for
vcov_type(Nonewhen the value was alias-derived fromrobust). This preserves the backward-compat remap semantics across clones: a clone ofDifferenceInDifferences(robust=False, cluster="unit")must behave the same as the original on a clustered fit, which requires the clone’s__init__to seevcov_type=None(so it flags_vcov_type_explicit=False) rather than the alias-resolved"classical"(which would mark it explicit and skip the CR1 remap).- Returns:
Estimator parameters suitable for passing to
__init__.- Return type:
Dict[str, Any]
- predict(data)
Predict outcomes using fitted model.
- Parameters:
data (pd.DataFrame) – DataFrame with same structure as training data.
- Returns:
Predicted values.
- Return type:
np.ndarray
- print_summary()
Print summary to stdout.
- Return type:
None
- set_params(**params)
Set estimator parameters (sklearn-compatible).
After assignment, the
robust/vcov_typepair is re-validated via the samediff_diff.linalg.resolve_vcov_type()helper used by__init__. Invalid combinations (e.g.robust=Falsewithvcov_type="hc2") raiseValueErrorinstead of leaving the object in an inconsistent state.- Parameters:
**params – Estimator parameters.
- Return type:
self
TwoWayFixedEffects (alias: TWFE)#
Panel DiD with unit and time fixed effects.
- class diff_diff.TwoWayFixedEffects[source]
Bases:
DifferenceInDifferencesTwo-Way Fixed Effects (TWFE) estimator for panel DiD.
Extends DifferenceInDifferences to handle panel data with unit and time fixed effects.
- Parameters:
robust (bool, default=True) – Whether to use heteroskedasticity-robust standard errors.
cluster (str, optional) –
Column name for cluster-robust standard errors. If None, automatically clusters at the unit level (the unit parameter passed to fit()). This differs from DifferenceInDifferences where cluster=None means no clustering.
Exception: when
vcov_type="classical"andinference="analytical", the unit auto-cluster is dropped because the classical family is by construction one-way only and the validator rejectscluster_ids + classical. The user’s explicit choice of the classical family wins over the TWFE default in that narrow analytical-inference case. Underinference="wild_bootstrap"the auto-cluster is preserved (the bootstrap uses the cluster structure to resample residuals).alpha (float, default=0.05) – Significance level for confidence intervals.
Notes
This estimator uses the regression:
Y_it = α_i + γ_t + β*(D_i × Post_t) + X_it’δ + ε_it
where α_i are unit fixed effects and γ_t are time fixed effects.
HC2 / Bell-McCaffrey are not available on TWFE. Because TWFE uses within-transformation (demeaning) to absorb the fixed effects, the reduced design’s hat matrix is not the full FE projection; HC2 leverage and CR2 Bell-McCaffrey corrections on the demeaned design would produce silently-wrong small-sample SEs (FWL preserves coefficients, not the hat matrix).
vcov_type in {"hc2","hc2_bm"}therefore raisesNotImplementedErrorwith workarounds: usevcov_type="hc1"(HC1/ CR1 survive FWL), or switch toDifferenceInDifferences(fixed_effects= [...])where the dummies appear in the full design. Tracked inTODO.mdunder Methodology/Correctness; also documented indocs/methodology/REGISTRY.md.Conley spatial-HAC (``vcov_type=”conley”``) is supported via the block-decomposed panel sandwich (matches R ``conleyreg`` with ``lag_cutoff > 0``). Pass
conley_coords=(lat_col, lon_col),conley_cutoff_km=<float>, andconley_lag_cutoff=<int>on the constructor; thetime/unitarrays are auto-derived from the estimator’stimeandunitcolumn-name arguments at fit-time. The sandwich sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting); this is NOT a multiplicative product kernel. FWL composability: the within-transformed scoresS = X_demeaned * residuals_demeanedform the same meat as the full-dummy-expansion design. The temporal kernel is hardcoded Bartlett regardless ofconley_kernel(matchesconleyreg::time_dist). Explicitcluster=<col>+ Conley enables the combined spatial + cluster product kernelK_total[i, j] = K_space(d_ij/h) · 1{cluster_i = cluster_j}; cluster membership must be constant within each unit across periods (validator-enforced on the panel block-decomposed path). Whencluster=is unset, TWFE’s default auto-cluster at the unit level is silently dropped on the Conley path — Conley spatial HAC alone is applied, not the combined kernel. Restrictions:inference="wild_bootstrap"+ Conley raises (incompatible inference modes);survey_design=+ Conley raises (Phase 5 follow-up).Warning: TWFE can be biased with staggered treatment timing and heterogeneous treatment effects. Consider using more robust estimators (e.g., Callaway-Sant’Anna) for staggered designs.
- fit(data, outcome, treatment, time, unit, covariates=None, survey_design=None)[source]
Fit Two-Way Fixed Effects model.
- Parameters:
data (pd.DataFrame) – Panel data.
outcome (str) – Name of outcome variable column.
treatment (str) – Name of treatment indicator column.
time (str) – Name of time period column.
unit (str) – Name of unit identifier column.
covariates (list, optional) – List of covariate column names.
survey_design (SurveyDesign, optional) – Survey design specification for design-based inference. When provided, uses Taylor Series Linearization for variance estimation and applies sampling weights to the regression.
- Returns:
Estimation results.
- Return type:
- decompose(data, outcome, unit, time, first_treat, weights='exact')[source]
Perform Goodman-Bacon decomposition of TWFE estimate.
Decomposes the TWFE estimate into a weighted average of all possible 2x2 DiD comparisons, revealing which comparisons drive the estimate and whether problematic “forbidden comparisons” are involved.
- Parameters:
data (pd.DataFrame) – Panel data with unit and time identifiers.
outcome (str) – Name of outcome variable column.
unit (str) – Name of unit identifier column.
time (str) – Name of time period column.
first_treat (str) – Name of column indicating when each unit was first treated. The values
0andnp.infare reserved as never-treated sentinels; a real treatment cohort withfirst_treat == 0would be folded intoUand should be re-labeled to a non-sentinel value before fitting. Units whosefirst_treatis at or before the first observable period (first_treat <= min(time), excluding the sentinels) are automatically remapped to theU(untreated) bucket per Goodman-Bacon (2021) footnote 11 with aUserWarning. SeeBaconDecomposition.fit()for the full contract andBaconDecompositionResults.n_always_treated_remappedfor the count. The user’s originalfirst_treatcolumn is preserved unchanged.weights (str, default="exact") –
Weight calculation method:
”exact” (default): Variance-based weights from Goodman-Bacon (2021) Theorem 1, Eqs. 7-9 and 10e-g. Paper-faithful and the standard methodology contract.
”approximate”: Fast simplified formula. Opt in for speed-sensitive diagnostic loops; numerical output may differ from R
bacondecomp::bacon().
- Returns:
Decomposition results showing: - TWFE estimate and its weighted-average breakdown - List of all 2x2 comparisons with estimates and weights - Total weight by comparison type (clean vs forbidden)
- Return type:
Examples
>>> twfe = TwoWayFixedEffects() >>> decomp = twfe.decompose( ... data, outcome='y', unit='id', time='t', first_treat='treat_year' ... ) >>> decomp.print_summary() >>> # Check weight on forbidden comparisons >>> if decomp.total_weight_later_vs_earlier > 0.2: ... print("Warning: significant forbidden comparison weight")
Notes
This decomposition is essential for understanding potential TWFE bias in staggered adoption designs. The three comparison types are:
Treated vs Never-treated: Clean comparisons using never-treated units as controls. These are always valid.
Earlier vs Later treated: Uses later-treated units as controls before they receive treatment. These are valid.
Later vs Earlier treated: Uses already-treated units as controls. These “forbidden comparisons” can introduce bias when treatment effects are dynamic (changing over time since treatment).
See also
bacon_decomposeStandalone decomposition function
BaconDecompositionClass-based decomposition interface
CallawaySantAnnaRobust estimator that avoids forbidden comparisons
- __init__(robust=True, cluster=None, vcov_type=None, alpha=0.05, inference='analytical', n_bootstrap=999, bootstrap_weights='rademacher', seed=None, rank_deficient_action='warn', conley_coords=None, conley_cutoff_km=None, conley_metric='haversine', conley_kernel='bartlett', conley_lag_cutoff=None)
- get_params()
Get estimator parameters (sklearn-compatible).
Returns the raw user input for
vcov_type(Nonewhen the value was alias-derived fromrobust). This preserves the backward-compat remap semantics across clones: a clone ofDifferenceInDifferences(robust=False, cluster="unit")must behave the same as the original on a clustered fit, which requires the clone’s__init__to seevcov_type=None(so it flags_vcov_type_explicit=False) rather than the alias-resolved"classical"(which would mark it explicit and skip the CR1 remap).- Returns:
Estimator parameters suitable for passing to
__init__.- Return type:
Dict[str, Any]
- predict(data)
Predict outcomes using fitted model.
- Parameters:
data (pd.DataFrame) – DataFrame with same structure as training data.
- Returns:
Predicted values.
- Return type:
np.ndarray
- print_summary()
Print summary to stdout.
- Return type:
None
- set_params(**params)
Set estimator parameters (sklearn-compatible).
After assignment, the
robust/vcov_typepair is re-validated via the samediff_diff.linalg.resolve_vcov_type()helper used by__init__. Invalid combinations (e.g.robust=Falsewithvcov_type="hc2") raiseValueErrorinstead of leaving the object in an inconsistent state.- Parameters:
**params – Estimator parameters.
- Return type:
self
- summary()
Get summary of estimation results.
- Returns:
Formatted summary.
- Return type:
SyntheticDiD (alias: SDiD)#
Synthetic control combined with DiD (Arkhangelsky et al. 2021).
- class diff_diff.SyntheticDiD[source]
Bases:
DifferenceInDifferencesSynthetic Difference-in-Differences (SDID) estimator.
Combines the strengths of Difference-in-Differences and Synthetic Control methods by re-weighting control units to better match treated units’ pre-treatment trends.
This method is particularly useful when:
You have few treated units (possibly just one)
Parallel trends assumption may be questionable
Control units are heterogeneous and need reweighting
You want robustness to pre-treatment differences
- Parameters:
zeta_omega (float, optional) – Regularization for unit weights. If None (default), auto-computed from data as
(N1 * T1)^(1/4) * noise_levelmatching R’s synthdid.zeta_lambda (float, optional) – Regularization for time weights. If None (default), auto-computed from data as
1e-6 * noise_levelmatching R’s synthdid.alpha (float, default=0.05) – Significance level for confidence intervals.
variance_method (str, default="placebo") –
Method for variance estimation:
”placebo”: Placebo-based variance matching R’s synthdid::vcov(method=”placebo”). Implements Algorithm 4 from Arkhangelsky et al. (2021). Library default (R’s default is
"bootstrap"; we default to placebo because it is unconditionally available on pweight-only survey designs and avoids the ~5–30× slowdown of the refit bootstrap). See REGISTRY.md §SyntheticDiDNote (default variance_method deviation from R)for rationale.”bootstrap”: Paper-faithful pairs bootstrap — Arkhangelsky et al. (2021) Algorithm 2 step 2, also the behavior of R’s default synthdid::vcov(method=”bootstrap”) (which rebinds
attr(estimate, "opts")withupdate.omega=TRUE, so the renormalized ω is only Frank-Wolfe initialization). Re-estimates ω̂_b and λ̂_b via two-pass sparsified Frank-Wolfe on each bootstrap draw. Survey support (PR #352): pweight-only fits use the constant per-control survey weight asrw; full-design fits (strata/PSU/FPC) use Rao-Wu rescaled weights per draw. Both compose with the weighted Frank-Wolfe kernel (min ||A·diag(rw)·ω - b||² + ζ²·Σ rw_i ω_i²); the FW returns ω on the standard simplex, thenω_eff = rw·ω/Σ(rw·ω)is composed for the SDID estimator. See REGISTRY.md §SyntheticDiDNote (survey + bootstrap composition)for the argmin-set caveat.”jackknife”: Jackknife variance matching R’s synthdid::vcov(method=”jackknife”). Implements Algorithm 3 from Arkhangelsky et al. (2021). Deterministic (N_control + N_treated iterations), uses fixed weights (no re-estimation). The
n_bootstrapparameter is ignored for this method.
n_bootstrap (int, default=200) – Number of replications for variance estimation. Used for: - Bootstrap: Number of bootstrap samples - Placebo: Number of random permutations (matches R’s replications argument) Ignored when
variance_method="jackknife".seed (int, optional) – Random seed for reproducibility. If None (default), results will vary between runs.
- results_
Estimation results after calling fit().
- Type:
- is_fitted_
Whether the model has been fitted.
- Type:
Examples
Basic usage with panel data:
>>> import pandas as pd >>> from diff_diff import SyntheticDiD >>> >>> # Panel data with units observed over multiple time periods >>> # Treatment occurs at period 5 for treated units >>> data = pd.DataFrame({ ... 'unit': [...], # Unit identifier ... 'period': [...], # Time period ... 'outcome': [...], # Outcome variable ... 'treated': [...] # 1 if unit is ever treated, 0 otherwise ... }) >>> >>> # Fit SDID model >>> sdid = SyntheticDiD() >>> results = sdid.fit( ... data, ... outcome='outcome', ... treatment='treated', ... unit='unit', ... time='period', ... post_periods=[5, 6, 7, 8] ... ) >>> >>> # View results >>> results.print_summary() >>> print(f"ATT: {results.att:.3f} (SE: {results.se:.3f})") >>> >>> # Examine unit weights >>> weights_df = results.get_unit_weights_df() >>> print(weights_df.head(10))
Notes
The SDID estimator (Arkhangelsky et al., 2021) computes:
- τ̂ = (Ȳ_treated,post - Σ_t λ_t * Y_treated,t)
Σ_j ω_j * (Ȳ_j,post - Σ_t λ_t * Y_j,t)
Where: - ω_j are unit weights (sum to 1, non-negative) - λ_t are time weights (sum to 1, non-negative)
- Unit weights ω are chosen to match pre-treatment outcomes:
min ||Σ_j ω_j * Y_j,pre - Y_treated,pre||²
This interpolates between: - Standard DiD (uniform weights): ω_j = 1/N_control - Synthetic Control (exact matching): concentrated weights
Conley spatial-HAC rejection. SyntheticDiD does not support the Conley (1999) spatial-HAC analytical sandwich. Passing
vcov_type="conley"or any non-NoneConley keyword (conley_coords,conley_cutoff_km,conley_metric,conley_kernel) to__init__orset_paramsraisesTypeError. Rationale: SyntheticDiD’s variance is derived from bootstrap / jackknife / placebo resampling (Arkhangelsky et al. 2021 Algorithms 2–4), not the sandwich identity Conley plugs into. Adding Conley support would require either an analytical SDID sandwich path or a spatial-block bootstrap (Politis-Romano 1994 territory). Tracked as a follow-up inTODO.md.References
Arkhangelsky, D., Athey, S., Hirshberg, D. A., Imbens, G. W., & Wager, S. (2021). Synthetic Difference-in-Differences. American Economic Review, 111(12), 4088-4118.
- __init__(zeta_omega=None, zeta_lambda=None, alpha=0.05, variance_method='placebo', n_bootstrap=200, seed=None, lambda_reg=None, zeta=None, vcov_type=None, conley_coords=None, conley_cutoff_km=None, conley_metric=None, conley_kernel=None, conley_lag_cutoff=None)[source]
- Parameters:
zeta_omega (float | None)
zeta_lambda (float | None)
alpha (float)
variance_method (str)
n_bootstrap (int)
seed (int | None)
lambda_reg (float | None)
zeta (float | None)
vcov_type (str | None)
conley_cutoff_km (float | None)
conley_metric (str | None)
conley_kernel (str | None)
conley_lag_cutoff (int | None)
- fit(data, outcome, treatment, unit, time, post_periods=None, covariates=None, survey_design=None)[source]
Fit the Synthetic Difference-in-Differences model.
- Parameters:
data (pd.DataFrame) – Panel data with observations for multiple units over multiple time periods.
outcome (str) – Name of the outcome variable column.
treatment (str) – Name of the treatment group indicator column (0/1). Should be 1 for all observations of treated units (both pre and post treatment).
unit (str) – Name of the unit identifier column.
time (str) – Name of the time period column.
post_periods (list, optional) – List of time period values that are post-treatment. If None, uses the last half of periods.
covariates (list, optional) – List of covariate column names. Covariates are residualized out before computing the SDID estimator.
survey_design (SurveyDesign, optional) –
Survey design specification. Only pweight weight_type is supported. Replicate-weight designs are rejected. All three variance methods support both pweight-only and full strata/PSU/FPC designs:
method pweight-only strata/PSU/FPC bootstrap ✓ weighted FW ✓ weighted FW + Rao-Wu (PR #355) placebo ✓ ✓ stratified permutation + weighted FW jackknife ✓ ✓ PSU-level LOO + stratum aggregation
Bootstrap composes Rao-Wu rescaled weights per draw with the weighted-Frank-Wolfe kernel; see REGISTRY.md §SyntheticDiD
Note (survey + bootstrap composition).Placebo under full design uses within-stratum permutation (pseudo-treated sampled from controls in each treated-containing stratum) with weighted-FW refit per draw; fit-time feasibility guards raise
ValueErrorwhen a treated stratum has fewer controls than treated units (seeNote (survey + placebo composition)).Jackknife under full design uses PSU-level LOO with stratum aggregation (Rust & Rao 1996); anti-conservative with few PSUs per stratum — prefer
bootstrapwhen tight SE calibration matters in that regime (seeNote (survey + jackknife composition)).
- Returns:
Object containing the ATT estimate, standard error, unit weights, and time weights.
- Return type:
- Raises:
ValueError – If required parameters are missing, data validation fails, or a non-pweight survey design is provided. Under survey designs, also raises when: - The total survey mass on either arm is zero (
w_control.sum() == 0orw_treated.sum() == 0). Every unit on that arm would have weight 0, encoding an unidentified target population (PR #355 R7 P1). - The composed effective-control mass(unit_weights * w_control).sum()is zero. Frank-Wolfe sparsifiesunit_weightsto exact zeros by design, so even when at least one control has positive survey weight, the FW solution may concentrate all mass on controls whose survey weights are 0. Raising up front avoids a silent0/0in theomega_effnormalization (PR #355 R12 P1). -survey_designdeclaresfpcwith no explicitpsu=. SDID Rao-Wu then treats each unit as its own PSU, sofpcmust be>=the number of units (unstratified) or>=the per-stratum unit count (stratified). Front-door checked aftercollapse_survey_to_unit_levelso the user sees a targeted error instead of a bootstrap-exhaustion failure (PR #355 R8 P1).NotImplementedError – If
survey_designcarries replicate weights (BRR/Fay/JK1/ JKn/SDR) — SyntheticDiD has no replicate-weight variance path. All three variance methods (placebo, bootstrap, jackknife) accept pweight-only and full strata/PSU/FPC analytical designs; only replicate-weight designs are rejected.
- set_params(**params)[source]
Set estimator parameters.
Applies updates transactionally: if
_validate_config()rejects the post-update state, the instance is rolled back to the pre-call values so a raisedValueErrorleaves the object consistent with its pre-call configuration.Mirrors
__init__’s defensive rejection ofvcov_type/conley_*non-None values: SyntheticDiD uses bootstrap/jackknife/ placebo variance, not the analytical sandwich, so any Conley kwarg would be silently ignored otherwise (forbidden byfeedback_no_silent_failures). Tracked in TODO.md for a follow-up that wires Conley to a non-bootstrap variance path.- Return type:
- predict(data)
Predict outcomes using fitted model.
- Parameters:
data (pd.DataFrame) – DataFrame with same structure as training data.
- Returns:
Predicted values.
- Return type:
np.ndarray
- print_summary()
Print summary to stdout.
- Return type:
None
- summary()
Get summary of estimation results.
- Returns:
Formatted summary.
- Return type: