diff_diff.WooldridgeDiD#

class diff_diff.WooldridgeDiD[source]#

Bases: BaseEstimator

Extended Two-Way Fixed Effects (ETWFE) DiD estimator.

Implements the Wooldridge (2025) saturated cohort×time regression (Empirical Economics 69(5), 2545-2587; DOI 10.1007/s00181-025-02807-z) and Wooldridge (2023) nonlinear extensions (logit, Poisson). Produces all four jwdid_estat aggregation types: simple, group, calendar, event. Opt-in surfaces include paper W2025 Section 7 cohort-share aggregation (aggregate(weights="cohort_share"), Eqs. 7.4 + 7.6) and paper W2025 Section 8 heterogeneous cohort-specific linear trends (cohort_trends=True, Eq. 8.1; OLS path only).

Parameters:
  • method ({"ols", "logit", "poisson"}) – Estimation method. "ols" is the linear baseline — valid for any response (Wooldridge 2023) and the usual choice for continuous outcomes; "logit" for binary or fractional outcomes; "poisson" for count data. When method="ols" is used on a binary ({0, 1}) or non-negative integer-count outcome, a UserWarning notes that a matching nonlinear model (logit / Poisson) is often the more appropriate specification — it imposes parallel trends on the link scale rather than in levels, and Wooldridge’s (2023) simulations show the linear model both biased and less precise for such outcomes when the nonlinear mean holds. It rests on a different identifying assumption than linear OLS, so it is a recommended comparison, not an automatic switch; suppress via warnings.filterwarnings.

  • control_group ({"not_yet_treated", "never_treated"}) –

    Which units serve as the comparison group. “not_yet_treated” (jwdid default) uses all untreated observations at each time period.

    ”never_treated” restricts the comparison pool to never-treated units ON THE OLS PATH ONLY, where every (g, t) cell except each cohort’s reference is emitted, so treated units’ pre-treatment rows sit in their own indicators rather than the baseline. On the nonlinear paths (method="logit" / "poisson") only post-treatment cells are emitted – including all cells would make each cohort dummy collinear with the sum of its own indicators – so treated units’ pre-treatment rows ARE part of the identifying comparison there, exactly as under "not_yet_treated". This asymmetry is pre-existing and structural; see the REGISTRY note. Note that n_control_units counts never-treated UNITS on this setting regardless of method, so on the nonlinear paths it under-reports the rows actually doing the comparison (tracked in TODO.md).

  • anticipation (int) – Number of periods before treatment onset to include as treatment cells (anticipation effects). 0 means no anticipation. Must be a non-negative integer; bool is rejected.

  • demean_covariates (bool) – If True (jwdid default), xtvar covariates are demeaned within each cohort×period cell before entering the regression. Set to False to replicate jwdid’s xasis option.

  • alpha (float) – Significance level for confidence intervals.

  • cluster (str or None) – Column name to use for cluster-robust SEs. Defaults to the unit identifier passed to fit().

  • n_bootstrap (int) – Number of bootstrap replications. 0 disables bootstrap.

  • bootstrap_weights ({"rademacher", "webb", "mammen"}) – Bootstrap weight distribution.

  • seed (int or None) – Random seed for reproducibility.

  • rank_deficient_action ({"warn", "error", "silent"}) – How to handle rank-deficient design matrices.

  • vcov_type ({"classical", "hc1", "hc2", "hc2_bm", "conley"}, default "hc1") –

    Variance-covariance family for the analytical sandwich, OLS path only. hc1 (default) preserves the prior bit-equal CR1 Liang-Zeger cluster-robust behavior via the within-transform path. hc2_bm auto-routes to a full-dummy saturated design (intercept + treatment cells + unit dummies + time dummies) — FWL preserves cohort coefficients but NOT the hat matrix, so HC2 leverage and Bell-McCaffrey Satterthwaite DOF must be computed on the full FE projection (matches clubSandwich::vcovCR(lm(...), type="CR2") + coef_test()$df_Satt). classical / hc2 are supported via the same full-dummy route AND an auto-drop of the unit auto-cluster (one-way families don’t compose with cluster_ids per the linalg validator). Explicit cluster="X" + one-way vcov_type raises at the validator. "conley" (Conley 1999 spatial-HAC) threads the conley_* params through solve_ols on the within-transform design (conley_lag_cutoff=0 = within-period spatial only; >0 adds within-unit Bartlett serial — the panel-aware path, not pooled cross-sectional, since conley_time / conley_unit are always supplied); the unit auto-cluster is dropped (an explicit cluster= enables the spatial+cluster product kernel) and survey_design= / weights / n_bootstrap>0 are rejected. Conley is OLS-path-only; it routes through the full-dummy design when cohort_trends=True (same as the other full-dummy families), and its vcov flows through aggregate("group"|"calendar"|"event").

    method in {"logit","poisson"} + vcov_type != "hc1" is REJECTED at __init__: the GLM QMLE sandwich path uses pseudo- residuals, and CR2-BM composition with QMLE on canonical-link pseudo- residuals needs derivation + R parity (tracked in DEFERRED.md). Survey designs combined with vcov_type != "hc1" raise NotImplementedError at fit() because the survey TSL / replicate- refit variance overrides the analytical sandwich.

  • cohort_trends (bool, default False) – When True, adds linear dg_i · t cohort-specific trend interactions to the design matrix per paper W2025 Section 8 / Eq. 8.1. Under a heterogeneous-trends DGP this recovers τ even when parallel trends fails (paper Section 8.3). OLS-path only: cohort_trends=True + method {"logit","poisson"} raises NotImplementedError at __init__. Auto-routes to the full-dummy design regardless of vcov_type (matching the absorb→fixed_effects auto-route). Each cohort that RECEIVES a trend column must have ≥ 2 observed pre-periods in the final analysis sample for dg_i · t to be separately identified from cohort + time FE; fit() raises ValueError otherwise. The check runs after comparison-support filtering and unidentified-cohort exclusion, and skips the last cohort on all-eventually-treated panels because that cohort gets no trend column. On such panels the last cohort’s trend column is dropped per paper Section 5.4, matching the cell-level normalization applied to the design, so cohort_trend_coefs carries G-1 entries. cohort_trends=True + survey_design raises NotImplementedError at fit() (deferred follow-up). cohort_trends=True + control_group="never_treated" also raises NotImplementedError at fit(). The OLS + never_treated branch emits the (g, t) placebo cell dummies (paper Section 4.4 placebo coverage) minus each cohort’s reference cell, and the appended dg_i · t trend columns are still spanned — jointly by those cells and the unit fixed effects, which absorb 1{cohort=g} and so recover the omitted reference. The Section 8 trend specification is therefore unidentified on this branch. Use control_group="not_yet_treated" (the default) for the cohort_trends surface.

  • df_convention ({"residual", "cluster", "normal"}, default "residual") – Degrees-of-freedom convention for the OLS analytical t/p/CI (per-cell and aggregated). "residual" (default) uses the fitted residual df — the 3.9 fix: the default hc1 arms previously used silent normal-theory z; classical/hc2 keep their historical n rank(X) values bit-for-bit; "cluster" uses the Stata/fixest cluster df G 1 on hc1-clustered fits (inert on one-way and conley families); "normal" deliberately uses normal-theory z at the fallback level. Survey design df and hc2_bm Bell-McCaffrey DOF always take precedence; the logit/poisson arms are knob-independent (survey df or normal theory — an explicitly non-default value warns at fit time). The default flips to "cluster" at v4.

  • unsupported_period_action ({"drop", "error"}, default "drop") – How to handle periods lacking the required comparison support. "drop" removes those periods before estimation and warns; "error" raises ValueError before removing them. Support requires a positive-weight never-treated observation on OLS with control_group="never_treated"; other paths also admit observations before g - anticipation. This policy is independent of rank_deficient_action and does not control unidentified-cohort exclusion. With survey_design, "drop" still raises NotImplementedError if periods would be removed, because survey domain estimation is not supported; "error" raises ValueError after the existing pre-filter configuration, cohort, and survey-design checks. Later validation (including covariate columns, nonlinear outcomes, and some explicit cluster columns) is not preflighted: an unsupported-period refusal can precede those input errors.

Methods

__init__([method, control_group, ...])

fit(data, outcome, unit, time[, ...])

Fit the ETWFE model.

get_params([deep])

Get estimator parameters (sklearn-compatible).

set_params(**params)

Set estimator parameters (sklearn-compatible, transactional).

Attributes

results_

__init__(method='ols', control_group='not_yet_treated', anticipation=0, demean_covariates=True, alpha=0.05, cluster=None, n_bootstrap=0, bootstrap_weights='rademacher', seed=None, rank_deficient_action='warn', vcov_type='hc1', cohort_trends=False, conley_coords=None, conley_cutoff_km=None, conley_metric='haversine', conley_kernel='bartlett', conley_lag_cutoff=None, df_convention='residual', unsupported_period_action='drop')[source]#
Parameters:
  • method (str)

  • control_group (str)

  • anticipation (int)

  • demean_covariates (bool)

  • alpha (float)

  • cluster (str | None)

  • n_bootstrap (int)

  • bootstrap_weights (str)

  • seed (int | None)

  • rank_deficient_action (str)

  • vcov_type (str)

  • cohort_trends (bool)

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

  • conley_cutoff_km (float | None)

  • conley_metric (str)

  • conley_kernel (str)

  • conley_lag_cutoff (int | None)

  • df_convention (str)

  • unsupported_period_action (str)

Return type:

None

classmethod __new__(*args, **kwargs)#