Wooldridge Extended Two-Way Fixed Effects (ETWFE)#
Extended Two-Way Fixed Effects estimator from Wooldridge (2025, 2023),
based on the Stata jwdid package specification (Friosavila 2021),
with documented SE/aggregation deviations noted in the Methodology Registry.
This module implements ETWFE via a single saturated regression that:
Estimates ATT(g,t) for each cohort×time treatment cell simultaneously
Supports linear (OLS), Poisson QMLE, and logit link functions
Uses ASF-based ATT for nonlinear models: E[f(η₁)] − E[f(η₀)]
Computes delta-method SEs for all aggregations (event_study, group, calendar, simple)
Supports paper W2025 cohort-share aggregation via
aggregate(weights="cohort_share")(Eqs. 7.4 + 7.6; default is cell-count matching Statajwdid_estat)Supports paper W2025 Section 8 heterogeneous cohort trends via
cohort_trends=True(OLS path only; auto-routes to full-dummy mode; requirescontrol_group="not_yet_treated"— the default — andsurvey_design=None; thenever_treatedand survey paths are fail-closed withNotImplementedErrorbecause the placebo-cell basis remaining collinear with the trend columns through the unit fixed effects / unvalidated survey-TSL composition would make the trend specification unidentified or unverified — see Methodology Registry for the full contract)Follows the Stata jwdid specification for OLS defaults and nonlinear paths (see Methodology Registry for documented SE/aggregation deviations)
When to use WooldridgeDiD:
Staggered adoption design with heterogeneous treatment timing
Nonlinear outcomes (binary, count, non-negative continuous)
You want a single-regression approach matching Stata’s
jwdidYou need event-study, group, calendar, or simple ATT aggregations
You need paper W2025 cohort-share aggregation weights as an alternative to the default cell-count weighting
You need heterogeneous cohort-specific linear trends when parallel trends is violated (paper W2025 Section 8)
References:
Wooldridge, J. M. (2025). Two-way fixed effects, the two-way Mundlak regression, and difference-in-differences estimators. Empirical Economics, 69(5), 2545-2587. DOI 10.1007/s00181-025-02807-z.
Wooldridge, J. M. (2023). Simple approaches to nonlinear difference-in-differences with panel data. The Econometrics Journal, 26(3), C31-C66.
Friosavila, F. (2021).
jwdid: Stata module for ETWFE. SSC s459114.
WooldridgeDiD#
Main estimator class for Wooldridge ETWFE.
unsupported_period_action="drop" (default) removes periods lacking the
required comparison support and warns. Use
WooldridgeDiD(unsupported_period_action="error") to raise ValueError
before those periods are removed; the error names the periods and affected
observation count. This applies to all three methods, independently of
rank_deficient_action. It does not control unidentified-cohort exclusion
or relax other identification checks.
Support requires a positive-weight never-treated observation for OLS with
control_group="never_treated". All other paths also admit observations
before their cohort’s g - anticipation threshold. Unsupported periods
can therefore occur even when never-treated units exist elsewhere in the panel.
Existing pre-filter configuration, cohort, and survey-design checks retain
precedence. Later checks, including covariate columns, nonlinear outcomes, and
non-Conley explicit cluster columns, are not preflighted: an unsupported-period
refusal can precede those input errors. With survey_design, "error"
raises ValueError for unsupported periods, while "drop" retains the
existing NotImplementedError because survey domain estimation is unsupported.
- class diff_diff.WooldridgeDiD[source]
Bases:
BaseEstimatorExtended 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_estataggregation 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. Whenmethod="ols"is used on a binary ({0, 1}) or non-negative integer-count outcome, aUserWarningnotes 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 viawarnings.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 thatn_control_unitscounts never-treated UNITS on this setting regardless of method, so on the nonlinear paths it under-reports the rows actually doing the comparison (tracked inTODO.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;
boolis rejected.demean_covariates (bool) – If True (jwdid default),
xtvarcovariates are demeaned within each cohort×period cell before entering the regression. Set to False to replicate jwdid’sxasisoption.alpha (float) – Significance level for confidence intervals.
cluster (str or None) – Column name to use for cluster-robust SEs. Defaults to the
unitidentifier passed tofit().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_bmauto-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 (matchesclubSandwich::vcovCR(lm(...), type="CR2") + coef_test()$df_Satt).classical/hc2are 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). Explicitcluster="X"+ one-wayvcov_typeraises at the validator."conley"(Conley 1999 spatial-HAC) threads theconley_*params throughsolve_olson the within-transform design (conley_lag_cutoff=0= within-period spatial only;>0adds within-unit Bartlett serial — the panel-aware path, not pooled cross-sectional, sinceconley_time/conley_unitare always supplied); the unit auto-cluster is dropped (an explicitcluster=enables the spatial+cluster product kernel) andsurvey_design=/weights/n_bootstrap>0are rejected. Conley is OLS-path-only; it routes through the full-dummy design whencohort_trends=True(same as the other full-dummy families), and its vcov flows throughaggregate("group"|"calendar"|"event").methodin{"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 withvcov_type != "hc1"raiseNotImplementedErroratfit()because the survey TSL / replicate- refit variance overrides the analytical sandwich.cohort_trends (bool, default False) – When True, adds linear
dg_i · tcohort-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"}raisesNotImplementedErrorat__init__. Auto-routes to the full-dummy design regardless ofvcov_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 fordg_i · tto be separately identified from cohort + time FE;fit()raisesValueErrorotherwise. 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, socohort_trend_coefscarriesG-1entries.cohort_trends=True+survey_designraisesNotImplementedErroratfit()(deferred follow-up).cohort_trends=True+control_group="never_treated"also raisesNotImplementedErroratfit(). 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 appendeddg_i · ttrend columns are still spanned — jointly by those cells and the unit fixed effects, which absorb1{cohort=g}and so recover the omitted reference. The Section 8 trend specification is therefore unidentified on this branch. Usecontrol_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 historicaln − rank(X)values bit-for-bit;"cluster"uses the Stata/fixest cluster dfG − 1on 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"raisesValueErrorbefore removing them. Support requires a positive-weight never-treated observation on OLS withcontrol_group="never_treated"; other paths also admit observations beforeg - anticipation. This policy is independent ofrank_deficient_actionand does not control unidentified-cohort exclusion. Withsurvey_design,"drop"still raisesNotImplementedErrorif periods would be removed, because survey domain estimation is not supported;"error"raisesValueErrorafter 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
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).
- __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_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
- property results_: WooldridgeDiDResults
- fit(data, outcome, unit, time, first_treat=<not supplied>, exovar=None, xtvar=None, xgvar=None, survey_design=None, cohort=<not supplied>)[source]
Fit the ETWFE model. See class docstring for parameter details.
- Parameters:
data (DataFrame with panel data (long format))
outcome (outcome column name)
unit (unit identifier column)
time (time period column)
first_treat (first treatment period column (0 or NaN = never treated))
exovar (time-invariant covariates added without interaction/demeaning)
xtvar (time-varying covariates (demeaned within cohort×period cells) – when
demean_covariates=True)xgvar (covariates interacted with each cohort indicator)
survey_design (SurveyDesign, optional) – Survey design specification for complex survey data. Supports stratified, clustered, and weighted designs via Taylor Series Linearization (TSL). Replicate-weight designs raise
NotImplementedError.cohort (str, optional) – Deprecated alias for
first_treat(row M-032); warns withFutureWarningand will be removed in 4.0.
- Return type:
- get_params(deep=True)
Get estimator parameters (sklearn-compatible).
- Parameters:
deep (bool, default True) – Accepted for sklearn compatibility (
sklearn.base.clonecallsget_params(deep=False)) and ignored: no diff-diff estimator nests another estimator.- Returns:
Estimator parameters suitable for passing to
__init__. Keys follow the__init__signature order. Where a class stores a parameter’s raw value under a different attribute (_PARAM_ATTR_ALIASES), the raw value is returned so thattype(est)(**est.get_params())reconstructs the same configuration.- Return type:
Dict[str, Any]
- set_params(**params)
Set estimator parameters (sklearn-compatible, transactional).
Validates the merged configuration by constructing a throwaway instance of
type(self)- so a rejected call raises the same error__init__would and leaves this estimator unchanged - then adopts the probe’s parameter attributes and the class’s declared derived-config attributes. Fitted state is never touched.- Parameters:
**params (Any) – Estimator parameters. Unknown names raise
ValueErrorbefore any validation or mutation.self (TSelf)
**params
- Return type:
self
WooldridgeDiDResults#
Results container returned by WooldridgeDiD.fit().
unsupported_period_action records the fit-time policy and is included in
to_dict() and summary(). It remains unchanged by post-fit aggregation
or subsequent estimator reconfiguration.
cohort_trend_coefs (populated under cohort_trends=True, OLS path
only): Dict[g → δ_g] keyed by treated cohort. The reported slopes
are relative to the baseline trend absorbed by the design — the
never-treated cohort’s trend (when a never-treated cohort exists) OR
the last cohort’s trend (when no never-treated cohort exists, per
paper W2025 Section 5.4’s all-eventually-treated drop rule). On
all-treated panels the last cohort is intentionally absent from the
dict; its slope is the baseline (zero in deviation form).
Note
With the default unsupported_period_action="drop", all-eventually-treated
panels estimate. The paper’s Section 5.4
rule is applied to the cohort × time cells as well as the trend
columns: periods where no unit is untreated carry no identified
ATT(g, t), so they are removed from the estimation sample before the
solve and the last cohort becomes the reference. The reduction is
always reported — the number of observations and periods dropped, the
reason, and any cohort left without cells. Stata’s jwdid performs
the same reduction silently, reporting only a smaller N.
See docs/methodology/REGISTRY.md → ## WooldridgeDiD (ETWFE) →
“Heterogeneous cohort trends” for the full normalization contract.
- class diff_diff.wooldridge_results.WooldridgeDiDResults[source]
Bases:
BaseResultsResults from WooldridgeDiD.fit().
Core output is
group_time_effects: a dict keyed by (cohort_g, time_t) with per-cell ATT estimates and inference. Call.aggregate(type, weights=...)to compute any of the fourjwdid_estataggregation types under either the default cell-count weighting (weights="cell", matches Statajwdid_estat) or the paper W2025 opt-in cohort-share weighting (weights="cohort_share", Eqs. 7.4 / 7.6; restricted totype ∈ {"simple", "event"}).cohort_trend_coefscarries Section 8 / Eq. 8.1 estimatedδ_gslopes when the fit was produced underWooldridgeDiD(cohort_trends=True).aggregation_weightsis keyed by aggregation type and records the active weighting scheme that wrote to each cached surface (surfaced insummary()/to_dataframe()/__repr__).unsupported_period_actionrecords the fit’s policy for periods lacking comparison support ("drop"or"error").Methods
aggregate(type[, weights])Compute and store one of the four jwdid_estat aggregation types.
summary([aggregation, alpha])Print formatted summary table.
- group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]]
key=(g,t), value={att, se, t_stat, p_value, conf_int}
- overall_att: float
- overall_se: float
- overall_t_stat: float
- overall_p_value: float
- method: str = 'ols'
- control_group: str = 'not_yet_treated'
- n_obs: int = 0
- n_treated_units: int = 0
- n_control_units: int = 0
- alpha: float = 0.05
- anticipation: int = 0
- vcov_type: str = 'hc1'
- cohort_trends: bool = False
- df_convention: str | None = None
The estimator’s
df_conventionconfiguration echoed onto the results (“residual” | “cluster” | “normal”; added 3.9). Governs the OLS analytical arms only; GLM inference is knob-independent.
- unsupported_period_action: str = 'drop'
Fit-time comparison-support period policy (
"drop"or"error"). Appended last to preserve generated constructor positional indexes."error"fits only when no period needs comparison-support filtering; other identification guards, including cohort exclusion, still apply.
- __setstate__(state)[source]
Restore pickled state, migrating renamed/added stored fields.
Unpickling bypasses
__init__/__post_init__(theBaseResultspickle-migration contract; SyntheticDiDResults precedent). A pickle written before 3.9 stores_df_one_way(classical/hc2-only residual df) instead of_df_analytic_fallback— carry the old value over so post-fitaggregate()on a legacy result reproduces its fit-time classical/hc2 inference (legacy hc1 pickles carried None there, which restores the pre-3.9 normal-theory aggregation for them).df_convention(added 3.9) defaults to"residual". Missingunsupported_period_actiondefaults to"drop".
- __init__(group_time_effects, overall_att, overall_se, overall_t_stat, overall_p_value, overall_conf_int, group_effects=None, calendar_effects=None, event_study_effects=None, method='ols', control_group='not_yet_treated', groups=<factory>, time_periods=<factory>, n_obs=0, n_treated_units=0, n_control_units=0, alpha=0.05, anticipation=0, survey_metadata=None, vcov_type='hc1', cluster_name=None, n_clusters=None, conley_lag_cutoff=None, cohort_trend_coefs=<factory>, _bootstrap_used=False, cohort_trends=False, aggregation_weights=<factory>, _gt_weights=<factory>, _n_g_per_cohort=<factory>, _cohort_units_dropped=<factory>, _gt_vcov=None, _gt_keys=<factory>, _df_survey=None, _bm_per_cell_dof=<factory>, _bm_artifacts=None, _df_analytic_fallback=None, df_convention=None, unsupported_period_action='drop')
- Parameters:
overall_att (float)
overall_se (float)
overall_t_stat (float)
overall_p_value (float)
method (str)
control_group (str)
n_obs (int)
n_treated_units (int)
n_control_units (int)
alpha (float)
anticipation (int)
survey_metadata (Any | None)
vcov_type (str)
cluster_name (str | None)
n_clusters (int | None)
conley_lag_cutoff (int | None)
_bootstrap_used (bool)
cohort_trends (bool)
_gt_vcov (ndarray | None)
_df_survey (int | None)
_bm_artifacts (Tuple[ndarray, ndarray, ndarray, Dict[Tuple[Any, Any], int]] | None)
_df_analytic_fallback (float | None)
df_convention (str | None)
unsupported_period_action (str)
- Return type:
None
- aggregate(type, weights='cell')[source]
Compute and store one of the four jwdid_estat aggregation types.
- Parameters:
type ("simple" | "group" | "calendar" | "event_study") – (“event” is a deprecated spelling of “event_study”; warns with
FutureWarningand is rejected in 4.0 - row M-086.)weights ("cell" | "cohort_share", default "cell") – Aggregation weighting scheme.
"cell"(default) uses cell- countn_{g,t}observation counts and matches Statajwdid_estat."cohort_share"uses paper W2025 Eq. 7.4ω̂_g = N_g / Σ_{g'} N_{g'} M_{g'}fortype="simple"and Eq. 7.6ω̂_{ge} = N_g / Σ_{g': g'+e ≤ T} N_{g'}fortype="event_study". Both formulas reduce toN_g-proportional per-cell weights with the appropriate normalization. The two schemes coincide on balanced panels with uniform within-cohort cell counts (paper Section 7.5). The cohort-share scheme is supported only fortype="simple"andtype="event_study"; the paper provides no explicit cohort-share formula for"group"or"calendar"aggregations and the library raisesValueErrorto preserve a fail-closed contract.chaining. (Returns self for)
- Return type:
Notes
When
vcov_type == "hc2_bm", aggregated inference (t_stat / p_value / conf_int) uses Bell-McCaffrey Satterthwaite contrast-specific DOFs rather than the survey/None default. The BM DOFs are computed lazily from_bm_artifactsvia_compute_cr2_bm_contrast_dofand fail-closed (NaN inference) when the helper raises or returns NaN — perfeedback_bm_contrast_dof_fail_closed. The contrast column is rebuilt under the activeweightsscheme so the BM DOF reflects the actual weighting used by ATT + SE.
- summary(aggregation=<not supplied>, *, alpha=None)[source]
Print formatted summary table.
- Parameters:
aggregation (str, optional) – DEPRECATED (row M-087; retired in 4.0): which aggregation to display (“simple”, “group”, “calendar”, “event_study”). From 4.0
summary()takes the library-uniformalpha-only signature and renders the headline simple row; select other aggregations viaaggregate(...)/to_dataframe(level=).alpha (float, keyword-only, optional) – Accepted for signature uniformity (spec section 5). The stored confidence columns were computed at fit time; passing a value different from the stored
alpharaises rather than silently recomputing or mislabeling - refit at the desired level instead. Keyword-only during the 3.9 shim window becauseaggregationstill holds position 1; it becomes the sole positional parameter in 4.0, matching every other results class.
- Return type:
- to_dict()[source]
Convert headline results to a dictionary.
- Returns:
Canonical inference row plus scalar metadata. Detailed group-time / aggregated tables are available via
to_dataframe(level=...).- Return type:
Dict[str, Any]
- to_dataframe(level=<not supplied>, aggregation=<not supplied>)[source]
Export aggregated effects to a DataFrame.
- Parameters:
level ("simple" | "group" | "calendar" | "event_study" | "gt") – Use “gt” to export raw group-time effects. (“event” is a deprecated spelling of “event_study”; warns and is rejected in 4.0 - row M-086.)
aggregation (str, optional) – Deprecated alias for
level(row M-044); warns withFutureWarningand will be removed in 4.0.
- Return type:
- plot_event_study(weights='cell', **kwargs)[source]
Event study plot. Always calls
aggregate('event_study', weights=weights).- Parameters:
weights ("cell" | "cohort_share", default "cell") – Aggregation weighting scheme threaded into the underlying
aggregate("event_study", ...)call."cohort_share"produces paper W2025 Eq. 7.6 cohort-share-by-exposure weights (post-treatmentk >= 0only); inference fields are fail-closed to NaN per the Section 7.5 conditional-on-shares contract documented in REGISTRY, and the plot suppresses error bars / CI bands to honor the fail-closed contract (the conditional-on-shares SE would build a misleading normal-theory CI in the plotter).**kwargs – Forwarded to
diff_diff.visualization.plot_event_study.
- Return type:
None
Notes
The wrapper unconditionally re-aggregates the event study under the requested
weightsscheme. This avoids the stale-cache hazard where a priorplot_event_study(weights="cohort_share")call would leave the cachedevent_study_effectsrestricted tok >= 0(per the Eq. 7.6 scope), and a subsequentplot_event_study()(defaultweights="cell") call would silently reuse the cohort-share-keyed cache instead of restoring the full event range including pre-period placebo leads.
- property att: float
- property se: float
- property p_value: float
- property t_stat: float
Example Usage#
Basic OLS (follows Stata jwdid y, ivar(unit) tvar(time) gvar(cohort)):
import pandas as pd
from diff_diff import WooldridgeDiD
df = pd.read_stata("mpdta.dta")
df['first_treat'] = df['first_treat'].astype(int)
m = WooldridgeDiD()
r = m.fit(df, outcome='lemp', unit='countyreal', time='year', first_treat='first_treat')
r.aggregate('event_study').aggregate('group').aggregate('simple')
print(r.to_dataframe(level='event_study'))
print(r.to_dataframe(level='group'))
print(r.summary())
Note
When method="ols" is applied to a binary ({0, 1}) or non-negative
integer-count outcome, fit() emits a UserWarning noting that a
matching nonlinear model (method="logit" / method="poisson") is often
the more appropriate specification for such outcomes — it imposes parallel
trends on the link/index scale rather than in levels (Wooldridge 2023 notes
level-PT is only valid for continuous/unbounded outcomes), and in that
paper’s simulations the linear model is both biased and less precise where
the nonlinear mean holds. It rests on a different identifying assumption
than linear OLS, so treat it as a recommended comparison, not an automatic
switch. OLS remains a valid QMLE for any response (Wooldridge 2023);
suppress the hint via warnings.filterwarnings. The check is heuristic:
bounded discrete (binomial-style) outcomes with a known upper bound are not
separately detected from unbounded counts.
View cohort×time cell estimates (post-treatment):
for (g, t), v in sorted(r.group_time_effects.items()):
if t >= g:
print(f"g={g} t={t} ATT={v['att']:.4f} SE={v['se']:.4f}")
Poisson QMLE for non-negative outcomes
(follows Stata jwdid emp, method(poisson)):
import numpy as np
df['emp'] = np.exp(df['lemp'])
m_pois = WooldridgeDiD(method='poisson')
r_pois = m_pois.fit(df, outcome='emp', unit='countyreal',
time='year', first_treat='first_treat')
r_pois.aggregate('event_study').aggregate('group').aggregate('simple')
print(r_pois.summary())
Logit for binary outcomes
(follows Stata jwdid y, method(logit)):
m_logit = WooldridgeDiD(method='logit')
r_logit = m_logit.fit(df, outcome='hi_emp', unit='countyreal',
time='year', first_treat='first_treat')
r_logit.aggregate('group').aggregate('simple')
print(r_logit.to_dataframe(level='group'))
Aggregation Methods#
Call .aggregate(type, weights=...), then export with
.to_dataframe(level=...) (summary() prints the headline simple
aggregation):
Type |
Description |
Stata equivalent |
|---|---|---|
|
ATT by relative time k = t − g |
|
|
ATT averaged across post-treatment periods per cohort |
|
|
ATT averaged across cohorts per calendar period |
|
|
Overall weighted average ATT |
|
Weighting schemes (weights="cell" default, weights="cohort_share"
opt-in):
weights="cell"(default) — cell-countn_{g,t}weighting; matches Statajwdid_estat. Supported for all four aggregation types.weights="cohort_share"— paper W2025 Eq. 7.4 (simple) and Eq. 7.6 (event, restricted tok >= 0) cohort-share weighting. Supported only fortype="simple"andtype="event_study"; raises ontype ∈ {"group","calendar"}(no paper closed-form). Inference fields (t-stat / p-value / conf-int) are fail-closed toNaNwith aUserWarningdocumenting the conditional-on-shares limitation (paper W2025 Section 7.5). Raises onsurvey_design is not None(design-consistent cohort totals pending follow-up).
Comparison with Other Staggered Estimators#
Feature |
WooldridgeDiD (ETWFE) |
CallawaySantAnna |
ImputationDiD |
|---|---|---|---|
Approach |
Single saturated regression |
Separate 2×2 DiD per cell |
Impute Y(0) via FE model |
Nonlinear outcomes |
Yes (Poisson, Logit) |
No |
No |
Covariates |
Via regression (linear index) |
OR, IPW, DR |
Supported |
SE for aggregations |
Delta method |
Multiplier bootstrap |
Multiplier bootstrap |
Stata equivalent |
|
|
|