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 - TwoWayFixedEffects estimator

  • diff_diff.synthetic_did - SyntheticDiD estimator

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

Difference-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=True maps to vcov_type="hc1"; robust=False maps to vcov_type="classical". Explicit vcov_type overrides robust unless 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 robust alias.

    • "classical": non-robust OLS SEs, sigma_hat^2 * (X'X)^{-1}.

    • "hc1": heteroskedasticity-robust HC1 with n/(n-k) adjustment (library default). With cluster=, uses CR1 (Liang-Zeger).

    • "hc2": leverage-corrected meat (one-way only). Errors with cluster=; use "hc2_bm" for clustered Bell-McCaffrey.

    • "hc2_bm": one-way HC2 + Imbens-Kolesar (2016) Satterthwaite DOF; with cluster=, 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_dof in linalg.py and the REGISTRY.md note). Weighted CR2-BM (survey_design= paths) is a separate gate.

    • "conley": Conley 1999 spatial-HAC sandwich. Pass conley_coords=(lat_col, lon_col), conley_cutoff_km=<float>, and conley_lag_cutoff=<int> on the constructor; pass unit=<col> as a fit-time kwarg to fit() (NOT on __init__; unused unless Conley is set; not part of get_params() / set_params()). The block-decomposed panel sandwich (matches R conleyreg with lag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded). Explicit cluster=<col> enables the combined spatial + cluster product kernel; survey_design= and inference='wild_bootstrap' both raise NotImplementedError.

  • 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>, and conley_lag_cutoff=<int> on the constructor; the unit identifier is passed as a fit-time arg to fit(...) (NOT on __init__) — it is unused unless vcov_type="conley" and is therefore not part of get_params() / set_params() (which return constructor-arg dicts). The block-decomposed panel sandwich (matching R conleyreg with lag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicit cluster=<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 — absent cluster=, pure Conley spatial HAC applies. survey_design= + Conley and inference='wild_bootstrap' + Conley both raise NotImplementedError.

  • conley_cutoff_km – Conley (1999) spatial-HAC variance configuration. Pass conley_coords=(lat_col, lon_col), conley_cutoff_km=<float>, and conley_lag_cutoff=<int> on the constructor; the unit identifier is passed as a fit-time arg to fit(...) (NOT on __init__) — it is unused unless vcov_type="conley" and is therefore not part of get_params() / set_params() (which return constructor-arg dicts). The block-decomposed panel sandwich (matching R conleyreg with lag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicit cluster=<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 — absent cluster=, pure Conley spatial HAC applies. survey_design= + Conley and inference='wild_bootstrap' + Conley both raise NotImplementedError.

  • conley_metric – Conley (1999) spatial-HAC variance configuration. Pass conley_coords=(lat_col, lon_col), conley_cutoff_km=<float>, and conley_lag_cutoff=<int> on the constructor; the unit identifier is passed as a fit-time arg to fit(...) (NOT on __init__) — it is unused unless vcov_type="conley" and is therefore not part of get_params() / set_params() (which return constructor-arg dicts). The block-decomposed panel sandwich (matching R conleyreg with lag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicit cluster=<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 — absent cluster=, pure Conley spatial HAC applies. survey_design= + Conley and inference='wild_bootstrap' + Conley both raise NotImplementedError.

  • conley_kernel – Conley (1999) spatial-HAC variance configuration. Pass conley_coords=(lat_col, lon_col), conley_cutoff_km=<float>, and conley_lag_cutoff=<int> on the constructor; the unit identifier is passed as a fit-time arg to fit(...) (NOT on __init__) — it is unused unless vcov_type="conley" and is therefore not part of get_params() / set_params() (which return constructor-arg dicts). The block-decomposed panel sandwich (matching R conleyreg with lag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicit cluster=<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 — absent cluster=, pure Conley spatial HAC applies. survey_design= + Conley and inference='wild_bootstrap' + Conley both raise NotImplementedError.

  • conley_lag_cutoff – Conley (1999) spatial-HAC variance configuration. Pass conley_coords=(lat_col, lon_col), conley_cutoff_km=<float>, and conley_lag_cutoff=<int> on the constructor; the unit identifier is passed as a fit-time arg to fit(...) (NOT on __init__) — it is unused unless vcov_type="conley" and is therefore not part of get_params() / set_params() (which return constructor-arg dicts). The block-decomposed panel sandwich (matching R conleyreg with lag_cutoff > 0) sums within-period spatial pairs plus within-unit Bartlett serial pairs (lag=0 excluded to avoid double-counting). Explicit cluster=<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 — absent cluster=, pure Conley spatial HAC applies. survey_design= + Conley and inference='wild_bootstrap' + Conley both raise NotImplementedError.

results_

Estimation results after calling fit().

Type:

DiDResults

is_fitted_

Whether the model has been fitted.

Type:

bool

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]
Parameters:
  • robust (bool)

  • cluster (str | None)

  • vcov_type (str | None)

  • alpha (float)

  • inference (str)

  • n_bootstrap (int)

  • bootstrap_weights (str)

  • seed (int | None)

  • rank_deficient_action (str)

  • conley_coords (Tuple[str, str] | None)

  • conley_cutoff_km (float | None)

  • conley_metric (str)

  • conley_kernel (str)

  • conley_lag_cutoff (int | None)

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 R conleyreg with lag_cutoff > 0) needs the unit identifier to compute the per-unit serial sum. Mirrors MultiPeriodDiD.fit(unit=...)() and TwoWayFixedEffects.fit(unit=...)(). Fit-time only — NOT a constructor kwarg, so it is not part of get_params() / set_params() (which return constructor-arg dicts). Ignored when vcov_type is not "conley".

Returns:

Object containing estimation results.

Return type:

DiDResults

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 (None when the value was alias-derived from robust). This preserves the backward-compat remap semantics across clones: a clone of DifferenceInDifferences(robust=False, cluster="unit") must behave the same as the original on a clustered fit, which requires the clone’s __init__ to see vcov_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_type pair is re-validated via the same diff_diff.linalg.resolve_vcov_type() helper used by __init__. Invalid combinations (e.g. robust=False with vcov_type="hc2") raise ValueError instead of leaving the object in an inconsistent state.

Parameters:

**params – Estimator parameters.

Return type:

self

summary()[source]

Get summary of estimation results.

Returns:

Formatted summary.

Return type:

str

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

Multi-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=True maps to vcov_type="hc1"; robust=False maps to vcov_type="classical". Explicit vcov_type overrides robust unless 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). With vcov_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_dof helper in linalg.py; matches clubSandwich’s Wald_test(test="HTZ")$df_denom at 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 robust alias.

    • "classical": non-robust OLS SEs, sigma_hat^2 * (X'X)^{-1}.

    • "hc1": heteroskedasticity-robust HC1 with n/(n-k) adjustment (library default). With cluster=, uses CR1 (Liang-Zeger).

    • "hc2": leverage-corrected meat (one-way only). Errors with cluster=; 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. With cluster=, dispatches to Pustejovsky-Tipton (2018) CR2 cluster-robust with a Bell-McCaffrey Satterthwaite contrast DOF on the post-period average (see cluster above for parity details). Weighted CR2-BM (survey_design=) is still gated.

    • "conley": Conley 1999 spatial-HAC sandwich via the panel block-decomposed form (matches R conleyreg with lag_cutoff > 0). Pass conley_coords=(lat_col, lon_col), conley_cutoff_km=<float>, and conley_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_time is auto-derived from the time column at fit-time and normalized to dense panel-period codes 0..T-1 so conley_lag_cutoff always counts panel periods (works for int / datetime64 / pd.Period / string encodings). Explicit cluster=<col> enables the combined spatial + cluster product kernel (Wave A #119; cluster must be constant within each unit across periods). Restrictions: survey_design= and inference="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_coords is a (lat_col, lon_col) tuple of column names on data. conley_lag_cutoff is 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_coords is a (lat_col, lon_col) tuple of column names on data. conley_lag_cutoff is 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_coords is a (lat_col, lon_col) tuple of column names on data. conley_lag_cutoff is 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_coords is a (lat_col, lon_col) tuple of column names on data. conley_lag_cutoff is 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_coords is a (lat_col, lon_col) tuple of column names on data. conley_lag_cutoff is the within-unit Bartlett lag (non-negative int; 0 means within-period spatial only, no serial component).

results_

Estimation results after calling fit().

Type:

MultiPeriodDiDResults

is_fitted_

Whether the model has been fitted.

Type:

bool

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 other vcov_type values, use the cluster parameter 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:

MultiPeriodDiDResults

Raises:

ValueError – If required parameters are missing or data validation fails.

summary()[source]

Get summary of estimation results.

Returns:

Formatted summary.

Return type:

str

__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)
Parameters:
  • robust (bool)

  • cluster (str | None)

  • vcov_type (str | None)

  • alpha (float)

  • inference (str)

  • n_bootstrap (int)

  • bootstrap_weights (str)

  • seed (int | None)

  • rank_deficient_action (str)

  • conley_coords (Tuple[str, str] | None)

  • conley_cutoff_km (float | None)

  • conley_metric (str)

  • conley_kernel (str)

  • conley_lag_cutoff (int | None)

get_params()

Get estimator parameters (sklearn-compatible).

Returns the raw user input for vcov_type (None when the value was alias-derived from robust). This preserves the backward-compat remap semantics across clones: a clone of DifferenceInDifferences(robust=False, cluster="unit") must behave the same as the original on a clustered fit, which requires the clone’s __init__ to see vcov_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_type pair is re-validated via the same diff_diff.linalg.resolve_vcov_type() helper used by __init__. Invalid combinations (e.g. robust=False with vcov_type="hc2") raise ValueError instead 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: DifferenceInDifferences

Two-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" and inference="analytical", the unit auto-cluster is dropped because the classical family is by construction one-way only and the validator rejects cluster_ids + classical. The user’s explicit choice of the classical family wins over the TWFE default in that narrow analytical-inference case. Under inference="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 raises NotImplementedError with workarounds: use vcov_type="hc1" (HC1/ CR1 survive FWL), or switch to DifferenceInDifferences(fixed_effects= [...]) where the dummies appear in the full design. Tracked in TODO.md under Methodology/Correctness; also documented in docs/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>, and conley_lag_cutoff=<int> on the constructor; the time / unit arrays are auto-derived from the estimator’s time and unit column-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 scores S = X_demeaned * residuals_demeaned form the same meat as the full-dummy-expansion design. The temporal kernel is hardcoded Bartlett regardless of conley_kernel (matches conleyreg::time_dist). Explicit cluster=<col> + Conley enables the combined spatial + cluster product kernel K_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). When cluster= 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:

DiDResults

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 0 and np.inf are reserved as never-treated sentinels; a real treatment cohort with first_treat == 0 would be folded into U and should be re-labeled to a non-sentinel value before fitting. Units whose first_treat is at or before the first observable period (first_treat <= min(time), excluding the sentinels) are automatically remapped to the U (untreated) bucket per Goodman-Bacon (2021) footnote 11 with a UserWarning. See BaconDecomposition.fit() for the full contract and BaconDecompositionResults.n_always_treated_remapped for the count. The user’s original first_treat column 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:

BaconDecompositionResults

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:

  1. Treated vs Never-treated: Clean comparisons using never-treated units as controls. These are always valid.

  2. Earlier vs Later treated: Uses later-treated units as controls before they receive treatment. These are valid.

  3. 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_decompose

Standalone decomposition function

BaconDecomposition

Class-based decomposition interface

CallawaySantAnna

Robust 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)
Parameters:
  • robust (bool)

  • cluster (str | None)

  • vcov_type (str | None)

  • alpha (float)

  • inference (str)

  • n_bootstrap (int)

  • bootstrap_weights (str)

  • seed (int | None)

  • rank_deficient_action (str)

  • conley_coords (Tuple[str, str] | None)

  • conley_cutoff_km (float | None)

  • conley_metric (str)

  • conley_kernel (str)

  • conley_lag_cutoff (int | None)

get_params()

Get estimator parameters (sklearn-compatible).

Returns the raw user input for vcov_type (None when the value was alias-derived from robust). This preserves the backward-compat remap semantics across clones: a clone of DifferenceInDifferences(robust=False, cluster="unit") must behave the same as the original on a clustered fit, which requires the clone’s __init__ to see vcov_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_type pair is re-validated via the same diff_diff.linalg.resolve_vcov_type() helper used by __init__. Invalid combinations (e.g. robust=False with vcov_type="hc2") raise ValueError instead 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:

str

SyntheticDiD (alias: SDiD)#

Synthetic control combined with DiD (Arkhangelsky et al. 2021).

class diff_diff.SyntheticDiD[source]

Bases: DifferenceInDifferences

Synthetic 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_level matching R’s synthdid.

  • zeta_lambda (float, optional) – Regularization for time weights. If None (default), auto-computed from data as 1e-6 * noise_level matching 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 §SyntheticDiD Note (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") with update.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 as rw; 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 §SyntheticDiD Note (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_bootstrap parameter 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:

SyntheticDiDResults

is_fitted_

Whether the model has been fitted.

Type:

bool

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-None Conley keyword (conley_coords, conley_cutoff_km, conley_metric, conley_kernel) to __init__ or set_params raises TypeError. 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 in TODO.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_coords (Tuple[str, 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 ValueError when a treated stratum has fewer controls than treated units (see Note (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 bootstrap when tight SE calibration matters in that regime (see Note (survey + jackknife composition)).

Returns:

Object containing the ATT estimate, standard error, unit weights, and time weights.

Return type:

SyntheticDiDResults

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() == 0 or w_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 sparsifies unit_weights to 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 silent 0/0 in the omega_eff normalization (PR #355 R12 P1). - survey_design declares fpc with no explicit psu=. SDID Rao-Wu then treats each unit as its own PSU, so fpc must be >= the number of units (unstratified) or >= the per-stratum unit count (stratified). Front-door checked after collapse_survey_to_unit_level so the user sees a targeted error instead of a bootstrap-exhaustion failure (PR #355 R8 P1).

  • NotImplementedError – If survey_design carries 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.

get_params()[source]

Get estimator parameters.

Return type:

Dict[str, Any]

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 raised ValueError leaves the object consistent with its pre-call configuration.

Mirrors __init__’s defensive rejection of vcov_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 by feedback_no_silent_failures). Tracked in TODO.md for a follow-up that wires Conley to a non-bootstrap variance path.

Return type:

SyntheticDiD

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:

str