de Chaisemartin-D’Haultfœuille (dCDH) DiD#
The only modern staggered DiD estimator in diff-diff that handles non-absorbing (reversible) treatments — treatment may switch on AND off over time.
This module implements the methodology from de Chaisemartin & D’Haultfœuille
(2020/2022). The estimator ships the contemporaneous-switch path DID_M
(= DID_1 at horizon l = 1); the full multi-horizon event study
DID_l for l = 1..L_max via the L_max parameter, with normalized
estimator DID^n_l, cost-benefit aggregate delta, dynamic placebos
DID^{pl}_l, and sup-t simultaneous confidence bands; residualization-style
covariate adjustment (controls); group-specific linear trends
(trends_linear); state-set-specific trends (trends_nonparam);
heterogeneity testing; non-binary treatment; HonestDiD sensitivity
integration on placebos; survey support via Taylor-series linearization
(pweight + strata/PSU/FPC); and per-path event-study disaggregation via
by_path=k (mirrors R did_multiplegt_dyn(..., by_path=k),
including per-path backward placebos and per-path joint sup-t
simultaneous bands when n_bootstrap > 0 — Python-only extension
beyond R, which provides no joint bands at any surface) or via
paths_of_interest=[(...), ...] for an explicit user-specified
path subset (Python-only API; mutex with by_path). by_path
supports binary or integer-coded discrete (D in Z) treatment, and
composes with survey_design for analytical Binder TSL SE and
replicate-weight bootstrap variance (multiplier bootstrap under
survey + by_path remains gated; no R parity since R
did_multiplegt_dyn does not support survey weighting). by_path
and paths_of_interest also compose with heterogeneity="<col>":
per-path heterogeneity coefficient surfaces on
results.path_heterogeneity_effects (mirrors R
did_multiplegt_dyn(..., by_path, predict_het) per-by_level). When
combined with placebo=True, heterogeneity is also computed on
backward (placebo) horizons and surfaced under negative-int keys —
both globally on results.heterogeneity_effects[-l] and per-path
on results.path_heterogeneity_effects[path][-l];
to_dataframe(level="by_path") placebo rows have populated het_*
columns. survey_design + placebo + heterogeneity emits a
UserWarning at fit-time and falls back to forward-horizon-only
heterogeneity until the pre-period cell allocator is derived; forward-
horizon predict_het + survey_design continues to work unchanged.
The estimator:
Aggregates individual-level panel data to
(group, time)cellsDrops multi-switch groups by default (matches R
DIDmultiplegtDYN)Excludes singleton-baseline groups from the variance computation only (footnote 15 of the dynamic paper)
Computes per-period joiner (
DID_{+,t}) and leaver (DID_{-,t}) contributions via Theorem 3 of the AER 2020 paperAggregates them into
DID_M, the joiners-onlyDID_+, and the leavers-onlyDID_-Computes the single-lag placebo
DID_M^plWhen
L_max >= 2: computes per-groupDID_{g,l}building blocks, multi-horizonDID_l, dynamic placebosDID^{pl}_l, normalizedDID^n_l, and cost-benefit aggregatedeltaOptionally computes the TWFE decomposition diagnostic from Theorem 1 (per-cell weights, fraction negative,
sigma_fe)Inference uses the cohort-recentered analytical plug-in variance from Web Appendix Section 3.7.3 of the dynamic paper, optionally complemented by a multiplier bootstrap clustered at the group level (with sup-t simultaneous confidence bands when
L_max >= 2)
When to use ChaisemartinDHaultfoeuille:
Treatment can switch on and off over time (e.g., marketing campaigns, seasonal promotions, on/off policy cycles)
You need separate joiners (
DID_+) and leavers (DID_-) views, plus the aggregateDID_MYou want a built-in placebo and a TWFE decomposition diagnostic computed on the data you pass in (pre-filter) for direct comparison against
DID_M. The fitted TWFE diagnostic uses the FULL pre-filter cell sample (matchingtwowayfeweights()); whenfit()drops groups via the ragged-panel ordrop_larger_lowerfilters, aUserWarningis emitted to make the divergence from the post-filterDID_Msample explicit. See REGISTRY.mdChaisemartinDHaultfoeuilleNote (TWFE diagnostic sample contract)for the rationale.You want a Python implementation that matches R
DIDmultiplegtDYNatl = 1on cell-aggregated input (see REGISTRY.md for documented deviations on individual-level inputs with uneven cell sizes)
All other staggered estimators in diff-diff (CallawaySantAnna,
SunAbraham, ImputationDiD,
TwoStageDiD, EfficientDiD,
WooldridgeDiD) assume treatment is absorbing —
once treated, stays treated. ChaisemartinDHaultfoeuille is the only
library option for non-absorbing treatments.
Panel requirements (deviation from R DIDmultiplegtDYN):
Every group must have an observation at the first global period (the panel’s earliest time value). Groups missing this baseline raise
ValueErrorwith the offending group IDs.Groups with interior period gaps (missing observations between their first and last observed period) are dropped with a
UserWarning.Terminal missingness (groups observed at the baseline but missing one or more later periods - early exit / right-censoring) is supported. The group contributes from its observed periods only, masked out of the missing transitions by the per-period
presentguard in the variance computation.This is a documented deviation from R
DIDmultiplegtDYN, which supports unbalanced panels with missing-treatment-before-first-switch handling. Workaround: pre-process your panel to back-fill the baseline (or drop late-entry groups before fitting), or use R until this restriction is lifted. See theNote (deviation from R DIDmultiplegtDYN)block indocs/methodology/REGISTRY.mdfor the rationale and the exact defensive guards that make terminal missingness safe.
References:
de Chaisemartin, C. & D’Haultfœuille, X. (2020). Two-Way Fixed Effects Estimators with Heterogeneous Treatment Effects. American Economic Review, 110(9), 2964-2996.
de Chaisemartin, C. & D’Haultfœuille, X. (2022, revised 2024). Difference-in-Differences Estimators of Intertemporal Treatment Effects. NBER Working Paper 29873.
ChaisemartinDHaultfoeuille#
Main estimator class for de Chaisemartin-D’Haultfœuille (dCDH) DiD estimation.
The alias DCDH is also available.
- class diff_diff.ChaisemartinDHaultfoeuille[source]
Bases:
ChaisemartinDHaultfoeuilleBootstrapMixinde Chaisemartin-D’Haultfoeuille (dCDH) estimator.
The only modern DiD estimator in the library that handles reversible (non-absorbing) treatments - treatment may switch on AND off over time. Computes the contemporaneous-switch DiD
DID_Mfrom the AER 2020 paper (equivalentlyDID_1at horizonl = 1of the dynamic companion paper, NBER WP 29873) plus the full multi-horizon event studyDID_lforl = 1..L_maxvia theL_maxparameter onfit().Supported:
Headline
DID_Mplus multi-horizonDID_levent studyJoiners-only
DID_+and leavers-onlyDID_-decompositionsSingle-lag placebo
DID_M^pland dynamic placebosDID^{pl}_l(computed automatically by default; gate viaplacebo=False)Analytical SE via the cohort-recentered plug-in formula from Web Appendix Section 3.7.3; multiplier bootstrap clustered at the group level by default via
n_bootstrap; undersurvey_designwith strictly-coarser PSUs the bootstrap automatically upgrades to PSU-level Hall-Mammen wild clustering (see REGISTRY.mdChaisemartinDHaultfoeuilleNote on survey + bootstrap)Normalized estimator
DID^n_l, cost-benefit aggregatedelta, and sup-t simultaneous confidence bandsResidualization-style covariate adjustment (
DID^X) viacontrols=, group-specific linear trends (DID^{fd}) viatrends_linear=True, state-set-specific trends viatrends_nonparam=, heterogeneity testing, non-binary treatment, HonestDiD sensitivity integration on placebos viahonest_did=TruePer-path event-study disaggregation via
by_path=k(top-k most common observed treatment paths within the window[F_g-1, F_g-1+L_max]; requiresdrop_larger_lower=False; supports binary or integer-coded discrete treatment) or viapaths_of_interest=[(...), ...]for an explicit user-specified path subset (Python-only API; mutex withby_path=k)Survey support via
survey_design=: pweight with strata/PSU/FPC via Taylor Series Linearization (analytical) or replicate-weight variance (BRR/Fay/JK1/JKn/SDR)TWFE decomposition diagnostic from Theorem 1 of AER 2020
Only
aggregateonfit()still raisesNotImplementedError.- Parameters:
alpha (float, default=0.05) – Significance level for confidence intervals.
cluster (str, optional, default=None) –
Must be
None(the default). User-specified clustering via this kwarg is not supported — passing any non-Nonevalue raisesNotImplementedErrorat construction time (and the same gate fires fromset_params). The effective clustering depends on how you callfit():Default (no survey_design): clustered at the group level via the cohort-recentered influence-function plug-in (analytical SEs) and the multiplier bootstrap.
Under ``survey_design`` with auto-inject or explicit ``psu=group``: PSU coincides with the group and the group-level and PSU-level paths are bit-identical.
Under ``survey_design`` with strictly-coarser PSUs: the multiplier bootstrap automatically upgrades to PSU-level Hall-Mammen wild clustering.
So dCDH does NOT always cluster at the group level — see REGISTRY.md
ChaisemartinDHaultfoeuilleNotes on cluster contract and survey + bootstrap for the full matrix. Custom user-specified clustering at a coarser or finer level than the group is a planned extension.n_bootstrap (int, default=0) – Number of multiplier-bootstrap iterations.
0(default) uses only the analytical SE. Set to999or higher for stable bootstrap inference.bootstrap_weights (str, default="rademacher") – Type of multiplier-bootstrap weights:
"rademacher","mammen", or"webb". Ignored unlessn_bootstrap > 0.seed (int, optional) – Random seed for the multiplier bootstrap.
placebo (bool, default=True) – If
True(default), automatically compute the single-lag placeboDID_M^pl(AER 2020 placebo specification) on the same data. Set toFalseto skip the placebo computation for speed; the results object will still exposeplacebo_*fields, but with NaN values andplacebo_available=False.twfe_diagnostic (bool, default=True) – If
True(default), compute the TWFE decomposition diagnostic from Theorem 1 of AER 2020: per-(g, t)weights, fraction of treated cells with negative weights, andsigma_fe(the smallest cell-effect standard deviation that could flip the sign of the plain TWFE coefficient). The diagnostic answers “what would the plain TWFE estimator say on the data you passed in?”, so it runs on the FULL pre-filter cell sample (the same input as the standalonetwowayfeweights()function), NOT on the post-filter estimation sample used byDID_M. When the ragged-panel filter ordrop_larger_lowerdrops groups, the fittedresults.twfe_*values describe a LARGER sample (pre-filter) thanresults.overall_attand aUserWarningis emitted to make the divergence explicit. See REGISTRY.mdChaisemartinDHaultfoeuilleNote (TWFE diagnostic sample contract)for the full rationale.drop_larger_lower (bool, default=True) – If
True(default, matches RDIDmultiplegtDYN), drops groups whose treatment switches more than once (multi-switch groups) before estimation. This is required for the analytical variance formula to be consistent with the AER 2020 Theorem 3 point estimate — both formulas operate on the same post-drop dataset. Setting toFalseis supported for diagnostic comparison but produces an inconsistent estimator-variance pairing for multi-switch groups; a warning is emitted.by_path (int, optional, default=None) –
If set to a positive integer
k, disaggregate the per-horizon event study by the observed treatment trajectory in the window[F_g - 1, F_g, ..., F_g - 1 + L_max], reporting ATT + SE + inference for thekmost common observed paths (ties broken lexicographically on the path tuple). Ifkexceeds the number of observed paths, all paths are returned and aUserWarningis emitted.None(the default) disables the disaggregation.Requires
drop_larger_lower=False(multi-switch groups are the object of interest) andL_max >= 1(the path window depends onL_max). Compatible with non-binary integer-coded treatment (D in Z); path tuples become integer-state tuples like(0, 2, 2, 2). D values must be integer-valued (D == round(D)); aValueErroris raised at fit-time on continuous D. Compatible withsurvey_designfor analytical Binder TSL SE and replicate-weight bootstrap; per-path SE routes through the cell-period allocator, with non-path switcher-side contributions skipped (control contributions remain unchanged, matching the joiners/leavers IF convention).n_bootstrap > 0(multiplier bootstrap) undersurvey_designis not yet supported and raisesNotImplementedError. Top-k path ranking undersurvey_designremains group-cardinality-based (unweighted), not population-weight-based — survey weights do not affect which paths are selected as “top-k”.Compatible with
heterogeneity="<col>"— per-path heterogeneity coefficient is computed by re-running the Lemma 7 regression on each path-restricted switcher subsample. Cohort dummies absorb baseline (no R-divergence warning needed). Surfaces onresults.path_heterogeneity_effectskeyed{path: {l: {beta, se, t_stat, p_value, conf_int, n_obs}}}and onto_dataframe(level="by_path")viahet_*columns. Mirrors Rdid_multiplegt_dyn(..., by_path, predict_het)per-by_level. Composes withsurvey_design(analytical Binder TSL + replicate-weight) via the existing cell-period IF allocator path. Incompatible withdesign2andhonest_did(each combination raisesNotImplementedErrorin the current release).Mutually exclusive with
paths_of_interest— useby_path=kfor top-k automatic ranking by frequency, orpaths_of_interest=[(...), ...]for an explicit user- specified path list. Setting both raisesValueError.Compatible with
controls(DID^X residualization) – the per-baseline OLS residualization runs once on first-differencedYBEFORE path enumeration, so per-path point estimates, bootstrap SE, per-path placebos, and per-path sup-t bands all consume the residualizedY_matautomatically (Frisch- Waugh-Lovell). Per-period effects remain unadjusted, consistent with the existingcontrols+ per-period DID contract.Deviation from R on multi-baseline switcher panels: R
did_multiplegt_dyn(..., by_path, controls)re-runs the per-baseline residualization on each path’s restricted subsample (path’s switchers + same-baseline not-yet-treated controls), so its residualization coefficients vary per path when switchers have different baseline values. Our global- residualization architecture coincides with R on single- baseline panels (every switcher shares the sameD_{g,1}) and per-path point estimates match exactly on the one- observation-per-(g, t)regime; on multi-observation-per- cell panels the existing DID^X cell-weighting deviation from R applies (seedocs/methodology/REGISTRY.md“Note (Phase 3 DID^X covariate adjustment)”; independent of the by_path lift). On multi-baseline switcher panels, point estimates can diverge — aUserWarningis emitted at fit-time when this configuration is detected. SE inherits the cross-path cohort- sharing deviation from R documented forpath_effects.Compatible with
trends_linear(DID^{fd} group-specific linear trends) – first-differencing replacesYwithZ = Y_t - Y_{t-1}once globally before path enumeration, so per-path raw second-differences DID^{fd}_{path, l} surface onpath_effects[path]["horizons"][l]automatically. Per-path cumulated level effectsdelta_{path, l} = sum_{l'=1..l} DID^{fd}_{path, l'}are surfaced on the newresults.path_cumulated_event_study[path][l]field (mirroring the globallinear_trends_effectscumulation; inner dict keyed by horizon directly, no"horizons"wrapper). SE on the cumulated layer is the conservative upper bound (sum of per-horizon component SEs, NaN-consistent), matching the globallinear_trends_effectsSE convention. Path enumeration runs on the post-first-differencedN_mat_fd: switchers withF_g==2fail the window-eligibility check and are dropped from path enumeration entirely, so a path whose switchers all haveF_g < 3is silently absent frompath_effects(the existing globalF_g < 3warning still fires). Per-path R parity matches Rdid_multiplegt_dyn(..., by_path, trends_lin)on per-path cumulated point estimates under single-baseline panels with sufficient pre-window depth (F_g >= 4for every selected- path switcher). R re-runs the per-path full pipeline on each path’s restricted subsample; same multi-baseline divergence pattern ascontrols(aUserWarningfires when switcher baselines take multiple values). F_g=3 boundary-case divergence: F_g=3 switchers have only 1 valid pre-window Z value after first-differencing and thetime==1filter, which causes Python’s global-then-disaggregate architecture to diverge from R’s per-path full-pipeline call (30%+ on point estimates observed empirically). A separateUserWarningfires at fit-time when the panel includes any F_g=3 switchers and by_path + trends_linear is set, so practitioners hitting this boundary regime see the divergence flag explicitly. Placebo under trends_linear returns RAW per-horizon values, not cumulated – there is no per-path placebo cumulation surface (verified empirically against R via the existingjoiners_only_trends_linparity scenario).Compatible with
trends_nonparam(state-set trends) – the set membership column is validated and stored once globally (time-invariance, NaN rejection, partition coarseness checks unchanged); per-path analytical SE, bootstrap SE, per-path placebos, and per-path sup-t bands all inherit the set-restricted control pool automatically through theset_idsparameter threaded through the per-path IF helpers. Per-path R parity matches Rdid_multiplegt_dyn(..., by_path, trends_nonparam)on per-path point estimates under single-baseline panels.Compatible with
n_bootstrap > 0– the top-k paths are enumerated once on the observed data (paths held fixed across bootstrap draws, matching Rdid_multiplegt_dyn(..., by_path, bootstrap=B)) and bootstrap SE / percentile CI / percentile p-value are written topath_effects[path]["horizons"][l]in place of the analytical fields. See REGISTRY.md for the full bootstrap contract.Compatible with
placebo=True– when both are active, per-path backward-horizon placebosDID^{pl}_{path, l}forl = 1..L_maxare surfaced onresults.path_placebo_event_study[path][-l](negative-int keys mirroringplacebo_event_study). The same per-path SE convention is applied backward (joiners/leavers IF precedent; cohort-recentered plug-in with path-specific divisor); the cross-path cohort-sharing deviation from R is inherited from the analytical event-study path.With
n_bootstrap > 0, per-path joint sup-t simultaneous confidence bands are also computed across horizons1..L_maxwithin each path. A path-specific critical valuec_p(constructed from a fresh shared-weights multiplier- bootstrap draw per path) is surfaced at top level asresults.path_sup_t_bands[path] = {"crit_value", "alpha", "n_bootstrap", "method", "n_valid_horizons"}, applied per-horizon ascband_conf_intonpath_effects[path]["horizons"][l], and rendered ascband_lower/cband_uppercolumns onresults.to_dataframe(level="by_path")(mirroring the OVERALLlevel="event_study"schema). Bands cover joint inference WITHIN a single path across horizons; they do NOT provide simultaneous coverage across paths. Python-only library extension; Rdid_multiplegt_dynprovides no joint bands at any surface. See REGISTRY.mdNote (Phase 3 by_path per-path joint sup-t bands).SE convention: per-path IF parallels the joiners / leavers construction — the switcher-side contribution is zeroed for groups not in the selected path, and the cohort structure and control pool are unchanged. Plug-in SE uses the path-specific divisor
N_l_path(count of path switchers eligible at horizonl), matching howjoiners_se/leavers_seuse their respective counts as divisors. See REGISTRY.mdChaisemartinDHaultfoeuilleNoteonby_pathfor the full contract.Results are exposed on
results.path_effectsas a dict keyed by the path tuple, with nested"horizons"dicts per horizonl. Also available viaresults.to_dataframe(level="by_path").paths_of_interest (list of tuple of int, optional, default=None) –
Explicit user-specified treatment paths to disaggregate by, as an alternative to
by_path=k’s top-k automatic ranking. Each path tuple must have lengthL_max + 1and represents the treatment trajectory in the window[F_g - 1, F_g, ..., F_g - 1 + L_max], e.g.[(0, 1, 1, 1), (0, 1, 0, 0)]for two paths underL_max=3. Mutually exclusive withby_path; setting both raisesValueError.Validation:
Each path element must be an
int(boolandnp.bool_rejected;np.integeraccepted and canonicalized to Pythonint).All paths must have the same length (uniformity validated at
__init__; length match againstL_max + 1validated at fit-time).Empty list raises
ValueError.Duplicate paths are deduplicated with a
UserWarning.A path with zero observed groups in the panel emits a
UserWarningand is omitted frompath_effects.
Compatible with non-binary integer treatment (paths can contain integer states like
(0, 2, 2)).Compatible with all downstream surfaces inherited by
by_path: bootstrap, per-path placebos, per-path joint sup-t bands,controls,trends_linear,trends_nonparam,survey_design(analytical Binder TSL + replicate-weight; multiplier bootstrap under survey remains gated, same asby_path=k), andheterogeneity(per-path heterogeneity coefficient surfaces onresults.path_heterogeneity_effects). Mechanical extension to path enumeration; no methodology change.Order semantics: paths appear in
results.path_effectsin the user-specified order, modulo deduplication and unobserved-path filtering.Python-only API extension; no R equivalent. R’s
did_multiplegt_dyn(..., by_path=k)only accepts a positive int (top-k) or-1(all paths); there is no list-based path selection in R.Results expose the same surfaces as
by_path:results.path_effects(dict keyed by path tuple),results.path_placebo_event_study,results.path_sup_t_bands,results.path_cumulated_event_study(undertrends_linear), and thelevel="by_path"DataFrame.rank_deficient_action (str, default="warn") – Action when the TWFE decomposition diagnostic OLS encounters a rank-deficient design matrix:
"warn","error", or"silent". Only used whentwfe_diagnostic=True.
- results_
Estimation results after calling
fit().
- is_fitted_
Whether the model has been fitted.
- Type:
Notes
The analytical CI is conservative under Assumption 8 (independent groups) of the dynamic companion paper, and exact only under iid sampling. This is documented as a deliberate deviation from “default nominal coverage” in
REGISTRY.md.Examples
Basic single-switch panel:
>>> from diff_diff import ChaisemartinDHaultfoeuille >>> from diff_diff.prep_dgp import generate_reversible_did_data >>> data = generate_reversible_did_data(n_groups=80, n_periods=6, seed=42) >>> est = ChaisemartinDHaultfoeuille() >>> results = est.fit( ... data, outcome="outcome", group="group", ... time="period", treatment="treatment", ... ) >>> abs(results.overall_att - 2.0) < 1.0 # close to the true effect True
Methods
fit(data, outcome, group, time, treatment[, ...])Fit the dCDH estimator on individual-level panel data.
get_params()Return all
__init__parameters as a dictionary.set_params(**params)Set estimator parameters (sklearn-compatible).
- __init__(alpha=0.05, cluster=None, n_bootstrap=0, bootstrap_weights='rademacher', seed=None, placebo=True, twfe_diagnostic=True, drop_larger_lower=True, by_path=None, paths_of_interest=None, rank_deficient_action='warn')[source]
- alpha: float
- n_bootstrap: int
- bootstrap_weights: str
- results_: ChaisemartinDHaultfoeuilleResults | None
- set_params(**params)[source]
Set estimator parameters (sklearn-compatible).
Transactional: validation runs after the candidate mutations, and if any rule fails the estimator state is rolled back to its pre-call values before the exception is re-raised. Callers can therefore retry with corrected params on the same instance without repairing inconsistent intermediate state.
- Parameters:
params (Any)
- Return type:
- fit(data, outcome, group, time, treatment, aggregate=None, L_max=None, controls=None, trends_linear=None, trends_nonparam=None, honest_did=False, heterogeneity=None, design2=False, survey_design=None)[source]
Fit the dCDH estimator on individual-level panel data.
- Parameters:
data (pd.DataFrame) – Individual-level panel. Must contain columns for
outcome,group,time, andtreatment. The estimator internally aggregates to(group, time)cells.outcome (str) – Outcome variable column name.
group (str) – Group identifier column name. Treatment must be constant within each
(group, time)cell after aggregation;ValueErroris raised if any cell has fractional treatment after grouping (within-cell-varying treatment indicates a fuzzy design not supported in Phase 1).time (str) – Time period column name. Must be sortable.
treatment (str) – Per-observation treatment column. Must be numeric and constant within each
(group, time)cell. Both binary{0, 1}and non-binary (ordinal or continuous) treatment are supported. Non-binary treatment requiresL_max >= 1.aggregate (str, optional) – Reserved for Phase 3. Must be
None; any other value raisesNotImplementedError.L_max (int, optional) – Maximum event-study horizon. When set, computes
DID_lforl = 1, ..., L_maxusing the per-group building block from Equation 3 of the dynamic companion paper. WhenNone(default), only thel = 1contemporaneous- switch estimatorDID_Mis computed (Phase 1 behavior). Must be a positive integer not exceeding the number of post-baseline periods in the panel.controls (list of str, optional) – Column names for covariate adjustment via residualization-style
DID^X(Web Appendix Section 1.2). RequiresL_max >= 1. Onetheta_hatper baseline treatment value, estimated by OLS on not-yet-treated observations. NOT doubly-robust.trends_linear (bool, optional) – If
True, estimate group-specific linear trends viaDID^{fd}(Web Appendix Section 1.3, Lemma 6). RequiresL_max >= 1and at least 3 time periods.trends_nonparam (str, optional) – Column name for state-set membership. Restricts the control pool to groups in the same set (Web Appendix Section 1.4). Requires
L_max >= 1and time-invariant values per group.honest_did (bool, default=False) – Run HonestDiD sensitivity analysis (Rambachan & Roth 2023) on the placebo + event study surface. Requires
L_max >= 1. Default: relative magnitudes (DeltaRM, Mbar=1.0), targeting the equal-weight average over all post-treatment horizons (l_vec=None). Results stored onresults.honest_did_results;Nonewith a warning if the solver fails. For custom parameters (e.g., targeting the on-impact effect only vial_vec), callcompute_honest_did(results, ...)post-hoc instead.heterogeneity (str, optional) – Column name for a time-invariant covariate to test for heterogeneous effects (Web Appendix Section 1.5, Lemma 7). Per-horizon OLS regressions are computed for forward horizons (1..L_max), and ALSO for backward (placebo) horizons (-1..-L_max) when
placebo=Trueis set (post-2026-05-15: per-path placebo predict_het R-parity againstdid_multiplegt_dyn(by_path, predict_het, placebo)). Joint Wald F-test across rows is NOT computed (per-horizon inference only). Cannot be combined withcontrols,trends_linear, ortrends_nonparam. RequiresL_max >= 1. Underby_path/paths_of_interest, per-path heterogeneity coefficients also surface onresults.path_heterogeneity_effectsand onto_dataframe(level="by_path")viahet_*columns (positive AND negative-horizon rows populated whenplacebo=True). Undersurvey_design, backward- horizon (placebo) heterogeneity is NOT computed (the pre- period Binder TSL cell allocator is deferred to a follow- up methodology PR); aUserWarningfires at fit-time and forward-horizon heterogeneity continues to compute normally.design2 (bool, default=False) – If
True, identify and report switch-in/switch-out (Design-2) groups. Convenience wrapper (descriptive summary, not full paper re-estimation). Requiresdrop_larger_lower=Falseto retain 2-switch groups.survey_design (SurveyDesign, optional) – Survey design specification for design-based inference. Supports
weight_type='pweight'with two variance paths: (1) Taylor Series Linearization using strata / PSU / FPC (analytical) via the cell-period IF allocator that attributes per-(g, t)-cell mass and aggregates through Binder (1983), and (2) replicate-weight variance using BRR / Fay / JK1 / JKn / SDR methods (analytical, closed- form). Survey weights produce weighted cell means for the point estimate. Under a survey design without an explicitpsu,fit()auto-injectspsu=<group_col>as a safe default (the group is the effective sampling unit). Strata and PSU may vary across cells of a group but must be constant within each(g, t)cell (trivially true in one-obs-per-cell panels; enforced otherwise withValueError). Three supported combinations under the auto-injectedpsu=<group_col>: (1) strata constant within group (anynestflag works); (2) strata vary within group andnest=True— the resolver re-labels the synthesizedpsuuniquely within strata; (3) strata vary within group andnest=False— rejected up front with a targetedValueError; passSurveyDesign(..., nest=True)or an explicitpsu=<col>with globally-unique labels instead. Whenn_bootstrap > 0and a survey design is supplied, the multiplier bootstrap operates at the PSU level (Hall-Mammen wild PSU bootstrap) — under the default auto-inject this collapses to a group-level clustered bootstrap. Under within-group-varying PSU the bootstrap uses a cell-level wild PSU allocator — a group contributing cells to multiple PSUs receives independent multiplier draws per PSU (see the Survey + bootstrap contract Note in REGISTRY.md). Scope note (terminal missingness under any cell-period-allocator path): on panels where a terminally-missing group is in a cohort whose other groups still contribute at the missing period, every survey variance path that uses the cell- period allocator raises a targetedValueError: Binder TSL with within-group-varying PSU, Rao-Wu replicate-weight ATT (which always uses the cell allocator), and the cell-level wild PSU bootstrap. Cohort-recentering leaks centered IF mass onto cells with no positive-weight obs, which the cell-period allocator cannot allocate to any observation or PSU. Pre-process the panel (drop late-exit groups or trim to a balanced sub-panel), or — for Binder TSL only — use an explicitpsu=<group_col>so the analytical path routes through the legacy group-level allocator. Replicate ATT and within-group-varying-PSU bootstrap have no such allocator fallback. Replicate weights with ``n_bootstrap > 0`` raises ``NotImplementedError`` (replicate variance is closed-form; bootstrap would double-count variance). See REGISTRY.mdChaisemartinDHaultfoeuilleNotes for the full contract.
- Return type:
- Raises:
ValueError – If required columns are missing, treatment is not binary, or the panel has too few groups / periods.
NotImplementedError – If any forward-compat parameter is set to a non-default value, with a clear pointer to the relevant ROADMAP phase.
ChaisemartinDHaultfoeuilleResults#
Results container for dCDH estimation.
- class diff_diff.ChaisemartinDHaultfoeuilleResults[source]
Bases:
objectResults from de Chaisemartin-D’Haultfoeuille (dCDH) Phase 1 estimation.
Phase 1 ships the contemporaneous-switch estimator
DID_M(=DID_1at horizonl = 1of the dynamic companion paper) plus the joiners- only / leavers-only views, the single-lag placeboDID_M^pl, and optionally the TWFE decomposition diagnostic (per-cell weights, fraction negative,sigma_fe).Notes
The analytical confidence interval is conservative under Assumption 8 (independent groups) of the dynamic companion paper, and exact only under iid sampling. This is documented as a deliberate deviation from “default nominal coverage” in the methodology registry.
For binary treatment in Phase 1, multi-switch groups (i.e., groups that switch treatment more than once) are dropped before estimation when
drop_larger_lower=True(the default), matching the RDIDmultiplegtDYNreference. The number of dropped groups is exposed vian_groups_dropped_crossers.Inference-method switch when bootstrap is enabled. The
overall_p_value/overall_conf_int(and joiners/leavers analogues) fields are populated by normal-theory inference from the cohort-recentered analytical SE whenn_bootstrap=0(the default). Whenn_bootstrap > 0, the same fields are populated by percentile-based bootstrap inference from the multiplier bootstrap distribution computed by_compute_dcdh_bootstrap(). The t-stat (overall_t_stat, etc.) is computed from the SE in both cases, since percentile bootstrap does not define an alternative t-stat semantic.event_study_effects[1],summary(),to_dataframe(),is_significant, andsignificance_starsall read from these top-level fields and therefore reflect the bootstrap inference automatically. The single-period placebo (L_max=None) still has NaN bootstrap fields; multi-horizon placebos (L_max >= 1) have valid bootstrap SE/CI/p viaplacebo_horizon_ses/cis/p_values. See the methodology registryNote (bootstrap inference surface)for the full contract and library precedent.- overall_att
DID_M = DID_1: the contemporaneous-switch dCDH point estimate.- Type:
- overall_se
Standard error of
DID_M.- Type:
- overall_t_stat
- Type:
- overall_p_value
- Type:
- joiners_att
DID_+: the joiners-only contribution.NaNwhenjoiners_availableis False.- Type:
- joiners_se
- Type:
- joiners_t_stat
- Type:
- joiners_p_value
- Type:
- n_joiner_cells
Total number of joiner switching
(g, t)cells across all periods. Each cell counted once. Equalssum_t (#{g : D_{g,t-1}=0, D_{g,t}=1}).- Type:
- n_joiner_obs
Total raw observation count across joiner cells, summing
n_gtover the same set of cells. For balanced one-observation-per-cell panels this equalsn_joiner_cells; for individual-level inputs with multiple observations per(g, t)it can be larger.- Type:
- joiners_available
Trueif at least one joiner switching cell exists.- Type:
- leavers_att
DID_-: the leavers-only contribution.NaNwhenleavers_availableis False.- Type:
- leavers_se
- Type:
- leavers_t_stat
- Type:
- leavers_p_value
- Type:
- n_leaver_cells
Total number of leaver switching
(g, t)cells (mirror ofn_joiner_cells).- Type:
- n_leaver_obs
Total raw observation count across leaver cells (mirror of
n_joiner_obs).- Type:
- leavers_available
- Type:
- placebo_effect
DID_M^pl: the single-lag placebo.NaNwhenplacebo_availableis False.- Type:
- placebo_se
- Type:
- placebo_t_stat
- Type:
- placebo_p_value
- Type:
- placebo_available
TruewhenT >= 3and at least one qualifying placebo cell exists.- Type:
- per_period_effects
Per-period decomposition. Keys are period values; each value is a dict with the following keys:
"did_plus_t"(float): joiner effect at this period (0.0if no joiners or A11 violation)"did_minus_t"(float): leaver effect at this period"n_10_t"(int): joiner cell count"n_01_t"(int): leaver cell count"n_00_t"(int): stable-untreated cell count"n_11_t"(int): stable-treated cell count"did_plus_t_a11_zeroed"(bool): True when joiners exist but no stable-untreated controls (Assumption 11 violation, period contributes 0 to numerator with non-zero weight in denominator)"did_minus_t_a11_zeroed"(bool): mirror for leavers
- Type:
- twfe_weights
Per-cell TWFE decomposition weights from Theorem 1 of de Chaisemartin & D’Haultfoeuille (2020). Columns:
group,time,weight. Computed on the FULL pre-filter cell sample passed by the user (the same input the standalonetwowayfeweights()function uses) — NOT the post-filter estimation sample described byoverall_attandgroups. Whenfit()drops groups via the ragged-panel ordrop_larger_lowerfilters,results.twfe_*andresults.overall_attdescribe different samples and aUserWarningis emitted; see REGISTRY.mdChaisemartinDHaultfoeuilleNote (TWFE diagnostic sample contract)for the rationale. Only populated whentwfe_diagnostic=True.- Type:
pd.DataFrame, optional
- twfe_fraction_negative
Fraction of treated-cell weights that are negative.
> 0is the diagnostic for the heterogeneous-treatment-effect bias of the plain TWFE estimator on the FULL pre-filter cell sample (NOT the post-filter estimation sample). See thetwfe_weightsdocstring above for the sample contract.- Type:
float, optional
- twfe_sigma_fe
Smallest standard deviation of per-cell treatment effects that could flip the sign of the plain TWFE estimator (Corollary 1 of the AER 2020 paper). Computed on the FULL pre-filter cell sample.
- Type:
float, optional
- twfe_beta_fe
The plain TWFE coefficient computed on the FULL pre-filter cell sample, for comparison with
overall_att. Note that the two are computed on different samples whenfit()filters drop groups — see thetwfe_weightsdocstring above for the sample contract.- Type:
float, optional
- groups
Group identifiers in the post-filter sample.
- Type:
- time_periods
Time periods in the panel.
- Type:
- n_obs
Total observations after filtering.
- Type:
- n_treated_obs
Treated observations in the post-filter sample.
- Type:
- n_switcher_cells
When
L_max=None: number of switching(g, t)cells (N_S = sum_t (n_10_t + n_01_t)). WhenL_max >= 1: number of eligible switcher groups at horizon 1 (N_1). Previously this field always held the cell count; forL_max >= 1it was repurposed to hold the per-group count that matches theDID_1estimand. Originally equals once regardless of how many original observations fed into it. This is theN_Sdenominator ofDID_Mper AER 2020 Theorem 3 — cell counts, not within-cell observation counts.- Type:
- n_cohorts
Distinct cohorts
(D_{g,1}, F_g, S_g)after filtering.- Type:
- n_groups_dropped_crossers
Number of groups dropped because they were multi-switch (matches R’s
drop_larger_lower=TRUEbehavior).0whendrop_larger_lower=Falseor no crossers exist.- Type:
- n_groups_dropped_singleton_baseline
Number of groups whose baseline
D_{g,1}is unique in the post-drop panel (footnote 15 of the dynamic paper). They are excluded from the cohort-recentered VARIANCE computation only — they remain in the point-estimate sample as period-based stable controls (see REGISTRY.mdChaisemartinDHaultfoeuillefor the period-vs-cohort deviation that makes this distinction matter).- Type:
- n_groups_dropped_never_switching
Number of groups with
S_g = 0(never switched). Reported for backwards compatibility only. Per the Round 2 full influence-function fix, never-switching groups are NOT excluded from the variance: they contribute via their stable-control roles in the per-period IF formula. The field name retains “dropped” for API stability but no actual exclusion happens.- Type:
- alpha
Significance level used for confidence intervals.
- Type:
- event_study_effects
Populated with horizon
1whenL_max=None, or horizons1..L_maxwhenL_max >= 1. WhenL_max >= 1, uses the per-groupDID_{g,l}path; whenL_max=None, uses the per-periodDID_Mpath.- Type:
dict, optional
- normalized_effects
Normalized estimator
DID^n_l. Populated whenL_max >= 1.- Type:
dict, optional
- cost_benefit_delta
Cost-benefit aggregate
delta. Populated whenL_max >= 2.- Type:
dict, optional
- sup_t_bands
Sup-t simultaneous confidence-band metadata for the OVERALL event-study surface. Holds
{"crit_value": float, "alpha": float, "n_bootstrap": int, "method": str}. Populated whenn_bootstrap > 0AND there are at least 2 valid horizons with finite bootstrap SE > 0 AND a strict majority (more than 50%) of sup-t draws are finite. The band itself is written per-horizon ascband_conf_intonevent_study_effects[l].Noneotherwise. Python-only library extension; Rdid_multiplegt_dynprovides no joint / sup-t bands.- Type:
dict, optional
- covariate_residuals
DID^Xfirst-stage diagnostics: per-baselinetheta_hat,n_obs, andr_squared. Populated whencontrolsis set.- Type:
pd.DataFrame, optional
- linear_trends_effects
Cumulated
DID^{fd}level effectsdelta^{fd}_l. Keyed by horizon. Populated whentrends_linear=True.- Type:
dict, optional
- heterogeneity_effects
Per-horizon heterogeneity test results
beta^{het}_l. Populated whenheterogeneityis set.- Type:
dict, optional
- design2_effects
Design-2 switch-in/switch-out descriptive summary. Populated when
design2=True.- Type:
dict, optional
- path_effects
Per-path event-study effects keyed by observed treatment trajectory (tuple of int). Populated when
by_pathis a positive int ORpaths_of_interestis a list of int tuples at estimator construction. Each entry holds{"n_groups": int, "frequency_rank": int, "horizons": {l: {"effect", "se", "t_stat", "p_value", "conf_int", "n_obs"}}}forl = 1..L_max. Underpaths_of_interest, dict-insertion order matches the user- specified path order;frequency_rankis the within- selected-paths rank by descending observed-group count (decoupled from iteration order).- Type:
dict, optional
- path_placebo_event_study
Per-path backward-horizon placebos
DID^{pl}_{path, l}forl = 1..L_max, keyed by observed treatment trajectory (tuple of int). Inner dict keys are negative ints (-lfor lagl) to mirror theplacebo_event_studyconvention so a unified{**path_effects[p]["horizons"], **path_placebo_event_study[p]}view is well-formed across forward and backward horizons. Each inner entry holds{"effect", "se", "t_stat", "p_value", "conf_int", "n_obs"}. Populated when (by_pathis a positive int ORpaths_of_interestis set) ANDplacebo=TrueANDL_max >= 1. Empty-state contract mirrorspath_effects:Nonewhenby_path / paths_of_interest + placebowas not requested;{}when requested but no observed path has a complete window[F_g-1, F_g-1+L_max]within the panel (the same regime wherepath_effectsreturns{}, with the sameUserWarningat fit-time). Downstream callers should distinguish the two states. Inherits the cross-path cohort-sharing SE deviation from R documented forpath_effects. See REGISTRY.mdNote (Phase 3 by_path ...)→ “Per-path placebos”.- Type:
dict, optional
- path_heterogeneity_effects
Per-path heterogeneity test results (Web Appendix Section 1.5, Lemma 7) when
heterogeneityis set AND (by_path=korpaths_of_interest=[(...), ...]) is set. Inner dict keyed by horizon directly (no"horizons"wrapper); each entry holds{"beta", "se", "t_stat", "p_value", "conf_int", "n_obs"}, wherebetais the heterogeneity coefficient on the path- restricted switcher subsample - plain OLS on the non-survey path, WLS-on-pweights undersurvey_design. Cohort dummies in the design matrix absorb baseline by construction. Empty-state contract mirrorspath_effects:Nonewhen not requested;{}when requested but no path has eligible switchers. Mirrors Rdid_multiplegt_dyn(..., by_path, predict_het)per-by_level dispatch. See REGISTRY.mdNote (Phase 3 by_path ...)→ “Per-path heterogeneity testing”.- Type:
dict, optional
- path_cumulated_event_study
Per-path cumulated level effects
delta_{path, l} = sum_{l'=1..l} DID^{fd}_{path, l'}forl = 1..L_max, keyed by observed treatment trajectory (tuple of int). Inner dict is keyed by horizon directly (no"horizons"wrapper); each entry holds{"effect", "se", "t_stat", "p_value", "conf_int", "n_obs"}. Populated when (by_pathis a positive int ORpaths_of_interestis set) ANDtrends_linear=TrueANDL_max >= 1;Noneotherwise. Mirrors the globallinear_trends_effectscumulation: SE on the cumulated layer is the conservative upper bound (sum of per-horizon component SEs frompath_effects[path]["horizons"][l]["se"], NaN-consistent). Built AFTER bootstrap propagation so the cumulated SE / t / p / CI are derived from the FINAL post-bootstrap per-horizon SEs whenn_bootstrap > 0. Surfaced ascumulated_effect/cumulated_secolumns onto_dataframe(level="by_path")(always-present, NaN-when- None) and as a per-path “Cumulated Level Effects” sub-section insummary(). See REGISTRY.mdNote (Phase 3 by_path ...)→ “Per-path linear-trends DID^{fd}”.- Type:
dict, optional
- path_sup_t_bands
Per-path joint sup-t simultaneous-band metadata, keyed by observed treatment trajectory (tuple of int). Each entry holds
{"crit_value": float, "alpha": float, "n_bootstrap": int, "method": str, "n_valid_horizons": int}. Populated when (by_pathis a positive int ORpaths_of_interestis set) ANDn_bootstrap > 0. The band itself is applied per-horizon ascband_conf_intonpath_effects[path]["horizons"][l]and rendered ascband_lower/cband_uppercolumns onto_dataframe(level="by_path"). Empty-state contract:Nonewhen not requested (no bootstrap, or bothby_pathandpaths_of_interestareNone);{}when requested but no path passed both gates (>=2valid horizons with finite bootstrap SE> 0AND a strict majority — more than 50% — of finite sup-t draws). Bands cover joint inference WITHIN a single path across horizons; they do NOT provide simultaneous coverage across paths. Inherits the cross-path cohort-sharing SE deviation from R documented forpath_effects(the bootstrap SE used as the t-stat denominator carries the same deviation). Python-only library extension; Rdid_multiplegt_dynprovides no joint / sup-t bands at any surface. See REGISTRY.mdNote (Phase 3 by_path per-path joint sup-t bands).- Type:
dict, optional
- honest_did_results
HonestDiD sensitivity analysis bounds (Rambachan & Roth 2023). Populated when
honest_did=Trueinfit()or by callingcompute_honest_did(results)post-hoc. Contains identified set bounds, robust confidence intervals, and breakdown analysis.- Type:
HonestDiDResults, optional
- survey_metadata
Populated when
fit(..., survey_design=sd)is called;Noneotherwise. Carries the resolved survey design summary (weight_type, strata/PSU counts,df_survey, weight range, and replicate-method info when applicable).df_surveyis threaded into survey-aware inference (t-distribution at all analytical surfaces) and consumed bycompute_honest_did()to produce survey-aware critical values.- Type:
Any, optional
- bootstrap_results
Bootstrap inference results when
n_bootstrap > 0.- Type:
DCDHBootstrapResults, optional
Methods
summary([alpha])Generate a formatted summary of dCDH estimation results.
print_summary([alpha])Print the formatted summary to stdout.
to_dataframe([level])Convert results to a DataFrame at the requested level of aggregation.
- overall_att: float
- overall_se: float
- overall_t_stat: float
- overall_p_value: float
- joiners_att: float
- joiners_se: float
- joiners_t_stat: float
- joiners_p_value: float
- n_joiner_cells: int
- n_joiner_obs: int
- joiners_available: bool
- leavers_att: float
- leavers_se: float
- leavers_t_stat: float
- leavers_p_value: float
- n_leaver_cells: int
- n_leaver_obs: int
- leavers_available: bool
- placebo_effect: float
- placebo_se: float
- placebo_t_stat: float
- placebo_p_value: float
- placebo_available: bool
- per_period_effects: Dict[Any, Dict[str, Any]]
- groups: List[Any]
- time_periods: List[Any]
- n_obs: int
- n_treated_obs: int
- n_switcher_cells: int
- n_cohorts: int
- n_groups_dropped_crossers: int
- n_groups_dropped_singleton_baseline: int
- n_groups_dropped_never_switching: int
- twfe_weights: pd.DataFrame | None = None
- alpha: float = 0.05
- covariate_residuals: pd.DataFrame | None = None
- honest_did_results: 'HonestDiDResults' | None = None
- survey_metadata: Any | None = None
- bootstrap_results: DCDHBootstrapResults | None = None
- property att: float
- property se: float
- property p_value: float
- property t_stat: float
- property coef_var: float
SE / abs(DID_M); NaN when DID_M is 0 or SE non-finite.
- property is_significant: bool
True iff overall
DID_Mp-value is belowalpha.
- property significance_stars: str
Significance stars for the overall
DID_M.
- summary(alpha=None)[source]
Generate a formatted summary of dCDH estimation results.
- Parameters:
alpha (float, optional) – Significance level for the confidence interval header. Defaults to
self.alpha.- Returns:
Formatted multi-block summary including overall
DID_M, joiners-only / leavers-only views, the placebo, the TWFE decomposition diagnostic, and a footer of significance codes.- Return type:
- print_summary(alpha=None)[source]
Print the formatted summary to stdout.
- Parameters:
alpha (float | None)
- Return type:
None
- to_dataframe(level='overall')[source]
Convert results to a DataFrame at the requested level of aggregation.
- Parameters:
level (str, default="overall") –
One of:
"overall": single-row table with the overall estimand (DID_MwhenL_max=None,DID_1whenL_max=1,deltawhenL_max >= 2)."joiners_leavers": up to three rows for the overall,DID_+, andDID_-(binary panels only)."per_period": one row per time period withdid_plus_t,did_minus_t, switching cell counts, and the A11-zeroed flags."event_study": one row per horizon (positive and negative/placebo), including a reference period at horizon 0. Available whenL_max >= 1."normalized": one row per horizon for the normalized effectsDID^n_l. Available whenL_max >= 1."twfe_weights": per-(group, time) TWFE decomposition weights table. Only available whentwfe_diagnostic=Truewas passed tofit()."heterogeneity": one row per horizon for the heterogeneity testbeta^{het}_l. Available whenheterogeneityis passed tofit()."linear_trends": one row per horizon for the cumulated trend-adjusted level effectsdelta^{fd}_l. Available whentrends_linear=True."design2": Design-2 switch-in/switch-out descriptive summary. Available whendesign2=True."by_path": one row per (path, horizon) when eitherby_path=korpaths_of_interest=[(...), ...]was passed to the estimator. Columns:path,frequency_rank,n_groups,horizon,effect,se,t_stat,p_value,conf_int_lower,conf_int_upper,n_obs,cband_lower,cband_upper,cumulated_effect,cumulated_se,het_beta,het_se,het_t_stat,het_p_value,het_conf_int_lower,het_conf_int_upper. Thehorizoncolumn takes negative ints for placebo rows whenplacebo=True. Thecband_*columns mirror the OVERALLlevel="event_study"schema (joint sup-t simultaneous bands); they are populated for positive-horizon rows of paths with a finite per-path sup-t crit (n_bootstrap > 0) and NaN otherwise (placebo rows, unbanded paths, or the requested-but-empty fallback DataFrame). Thecumulated_*columns mirror the globallinear_trends_effectscumulation; populated for positive-horizon rows whentrends_linear=Trueis also set, NaN for placebo rows or non-trends_linear fits (always-present, NaN-when-None — same convention ascband_*). Thehet_*columns surface the per-path heterogeneity coefficient (Web Appendix Section 1.5, Lemma 7) whenheterogeneity="<col>"is also set. Populated for positive-horizon (forward) rows whenever heterogeneity is requested, AND for negative-horizon (placebo) rows whenplacebo=Trueis also set (post-2026-05-15: per-path placebo predict_het R-parity againstdid_multiplegt_dyn(by_path, predict_het, placebo)). NaN for non-heterogeneity fits / the requested-but-empty fallback DataFrame, AND for placebo rows undersurvey_design(forward-only fallback — backward-horizon survey predict_het is deferred until the pre-period cell allocator is derived; aUserWarningfires at fit-time whensurvey_design + placebo + heterogeneityare co-set). Always-present, NaN-when-None — same convention ascband_*andcumulated_*.
- Return type:
pd.DataFrame
- __init__(overall_att, overall_se, overall_t_stat, overall_p_value, overall_conf_int, joiners_att, joiners_se, joiners_t_stat, joiners_p_value, joiners_conf_int, n_joiner_cells, n_joiner_obs, joiners_available, leavers_att, leavers_se, leavers_t_stat, leavers_p_value, leavers_conf_int, n_leaver_cells, n_leaver_obs, leavers_available, placebo_effect, placebo_se, placebo_t_stat, placebo_p_value, placebo_conf_int, placebo_available, per_period_effects, groups, time_periods, n_obs, n_treated_obs, n_switcher_cells, n_cohorts, n_groups_dropped_crossers, n_groups_dropped_singleton_baseline, n_groups_dropped_never_switching, event_study_effects=None, L_max=None, placebo_event_study=None, twfe_weights=None, twfe_fraction_negative=None, twfe_sigma_fe=None, twfe_beta_fe=None, alpha=0.05, normalized_effects=None, cost_benefit_delta=None, sup_t_bands=None, covariate_residuals=None, linear_trends_effects=None, trends_linear=None, heterogeneity_effects=None, design2_effects=None, path_effects=None, path_placebo_event_study=None, path_heterogeneity_effects=None, path_cumulated_event_study=None, path_sup_t_bands=None, honest_did_results=None, survey_metadata=None, bootstrap_results=None, _estimator_ref=None)
- Parameters:
overall_att (float)
overall_se (float)
overall_t_stat (float)
overall_p_value (float)
joiners_att (float)
joiners_se (float)
joiners_t_stat (float)
joiners_p_value (float)
n_joiner_cells (int)
n_joiner_obs (int)
joiners_available (bool)
leavers_att (float)
leavers_se (float)
leavers_t_stat (float)
leavers_p_value (float)
n_leaver_cells (int)
n_leaver_obs (int)
leavers_available (bool)
placebo_effect (float)
placebo_se (float)
placebo_t_stat (float)
placebo_p_value (float)
placebo_available (bool)
per_period_effects (Dict[Any, Dict[str, Any]])
groups (List[Any])
time_periods (List[Any])
n_obs (int)
n_treated_obs (int)
n_switcher_cells (int)
n_cohorts (int)
n_groups_dropped_crossers (int)
n_groups_dropped_singleton_baseline (int)
n_groups_dropped_never_switching (int)
L_max (Optional[int])
twfe_weights (Optional[pd.DataFrame])
twfe_fraction_negative (Optional[float])
twfe_sigma_fe (Optional[float])
twfe_beta_fe (Optional[float])
alpha (float)
cost_benefit_delta (Optional[Dict[str, Any]])
sup_t_bands (Optional[Dict[str, Any]])
covariate_residuals (Optional[pd.DataFrame])
trends_linear (Optional[bool])
design2_effects (Optional[Dict[str, Any]])
path_effects (Optional[Dict[Tuple[int, ...], Dict[str, Any]]])
path_placebo_event_study (Optional[Dict[Tuple[int, ...], Dict[int, Dict[str, Any]]]])
path_heterogeneity_effects (Optional[Dict[Tuple[int, ...], Dict[int, Dict[str, Any]]]])
path_cumulated_event_study (Optional[Dict[Tuple[int, ...], Dict[int, Dict[str, Any]]]])
path_sup_t_bands (Optional[Dict[Tuple[int, ...], Dict[str, Any]]])
honest_did_results (Optional['HonestDiDResults'])
survey_metadata (Optional[Any])
bootstrap_results (Optional[DCDHBootstrapResults])
_estimator_ref (Optional[Any])
- Return type:
None
DCDHBootstrapResults#
Multiplier-bootstrap inference results, populated when n_bootstrap > 0.
- class diff_diff.DCDHBootstrapResults[source]
Bases:
objectResults from ChaisemartinDHaultfoeuille (dCDH) multiplier bootstrap inference.
The bootstrap is a library extension beyond the dCDH papers, which propose only the analytical cohort-recentered plug-in variance from Web Appendix Section 3.7.3 of the dynamic companion paper. Provided for consistency with CallawaySantAnna / ImputationDiD / TwoStageDiD.
Per-target SE / CI / p-value are populated for the three scalar dCDH estimands implemented in Phase 1: overall (
DID_M), joiners (DID_+), and leavers (DID_-). When a target is not available in the underlying data (e.g., no leavers), the matching fields areNone.Phase 1 per-period placebo (L_max=None) bootstrap is NOT computed. The dynamic companion paper Section 3.7.3 derives the cohort-recentered analytical variance for
DID_lonly, not for the per-periodDID_M^pl. Theplacebo_se/placebo_ci/placebo_p_valuefields below remainNonefor Phase 1. Multi-horizon placebos (L_max >= 1) have valid SE viaplacebo_horizon_ses- this is a library extension applying the same IF/variance structure to the placebo estimand (see REGISTRY.md dynamic placebo SE Note).- n_bootstrap
Number of bootstrap iterations.
- Type:
- weight_type
Type of bootstrap weights:
"rademacher","mammen", or"webb".- Type:
- alpha
Significance level used for confidence intervals.
- Type:
- overall_se
Bootstrap standard error for
DID_M.- Type:
- overall_p_value
Bootstrap p-value for
DID_M.- Type:
- joiners_se
Bootstrap SE for joiners-only
DID_+(Noneif no joiners).- Type:
float, optional
- joiners_p_value
Bootstrap p-value for joiners-only
DID_+.- Type:
float, optional
- leavers_se
Bootstrap SE for leavers-only
DID_-(Noneif no leavers).- Type:
float, optional
- leavers_p_value
Bootstrap p-value for leavers-only
DID_-.- Type:
float, optional
- placebo_se
Nonefor the Phase 1 single-period placebo (L_max=None). Multi-horizon placebo bootstrap SE is onplacebo_horizon_ses.- Type:
float, optional
- placebo_p_value
Nonefor single-period placebo. Seeplacebo_horizon_p_values.- Type:
float, optional
- bootstrap_distribution
Full bootstrap distribution of the overall
DID_Mestimator (shape:(n_bootstrap,)). Stored for advanced diagnostics; suppressed from__repr__.- Type:
np.ndarray, optional
- n_bootstrap: int
- weight_type: str
- alpha: float
- overall_se: float
- overall_p_value: float
- __init__(n_bootstrap, weight_type, alpha, overall_se, overall_ci, overall_p_value, joiners_se=None, joiners_ci=None, joiners_p_value=None, leavers_se=None, leavers_ci=None, leavers_p_value=None, placebo_se=None, placebo_ci=None, placebo_p_value=None, bootstrap_distribution=None, event_study_ses=None, event_study_cis=None, event_study_p_values=None, placebo_horizon_ses=None, placebo_horizon_cis=None, placebo_horizon_p_values=None, cband_crit_value=None, path_ses=None, path_cis=None, path_p_values=None, path_placebo_ses=None, path_placebo_cis=None, path_placebo_p_values=None, path_cband_crit_values=None, path_cband_n_valid_horizons=None)
- Parameters:
n_bootstrap (int)
weight_type (str)
alpha (float)
overall_se (float)
overall_p_value (float)
joiners_se (float | None)
joiners_p_value (float | None)
leavers_se (float | None)
leavers_p_value (float | None)
placebo_se (float | None)
placebo_p_value (float | None)
bootstrap_distribution (ndarray | None)
cband_crit_value (float | None)
path_cis (Dict[Tuple[int, ...], Dict[int, Tuple[float, float]]] | None)
path_p_values (Dict[Tuple[int, ...], Dict[int, float]] | None)
path_placebo_ses (Dict[Tuple[int, ...], Dict[int, float]] | None)
path_placebo_cis (Dict[Tuple[int, ...], Dict[int, Tuple[float, float]]] | None)
path_placebo_p_values (Dict[Tuple[int, ...], Dict[int, float]] | None)
path_cband_crit_values (Dict[Tuple[int, ...], float] | None)
path_cband_n_valid_horizons (Dict[Tuple[int, ...], int] | None)
- Return type:
None
Convenience Function#
- diff_diff.chaisemartin_dhaultfoeuille(data, outcome, group, time, treatment, **kwargs)[source]#
One-shot convenience wrapper around
ChaisemartinDHaultfoeuille.Equivalent to:
ChaisemartinDHaultfoeuille(**init_kwargs).fit( data, outcome=..., group=..., time=..., treatment=..., **fit_kwargs, )
All keyword arguments are split between
__init__andfitbased on which signature accepts them. Useful for one-line use in scripts.- Parameters:
- Return type:
Standalone TWFE Decomposition Diagnostic#
The TWFE decomposition diagnostic from Theorem 1 of de Chaisemartin &
D’Haultfœuille (2020) is also available as a standalone function for
users who want the diagnostic without fitting the full estimator. It
returns per-cell weights, the fraction of treated cells with negative
weights, and sigma_fe — the smallest standard deviation of per-cell
treatment effects that could flip the sign of the plain TWFE coefficient.
- diff_diff.twowayfeweights(data, outcome, group, time, treatment, rank_deficient_action='warn', survey_design=None)[source]#
Standalone TWFE decomposition diagnostic.
Computes the per-cell weights, fraction negative, and
sigma_fefrom Theorem 1 of de Chaisemartin & D’Haultfoeuille (2020), without fitting the full dCDH estimator. Mirrors the standalone Statatwowayfeweightspackage.- Parameters:
data (pd.DataFrame) – Individual-level panel.
outcome (str)
group (str)
time (str)
treatment (str)
rank_deficient_action (str, default="warn") – Action when the FE design matrix is rank-deficient.
survey_design (SurveyDesign, optional) – If provided, cell aggregation uses survey-weighted cell means (matching
fit(..., survey_design=sd).twfe_*). Required to preserve fit-vs-helper parity under survey-backed inputs. Onlyweight_type='pweight'is supported; other types raise ValueError. Replicate-weight designs (BRR/Fay/JK1/JKn/SDR) are accepted — the TWFE diagnostic has no SE field onTWFEWeightsResult, so replicate weights only affect the cell aggregation path (aggregated numbers are identical tofit(..., survey_design=sd).twfe_*under the same input).
- Returns:
Object with attributes
weights(DataFrame),fraction_negative(float),sigma_fe(float), andbeta_fe(float).- Return type:
- class diff_diff.TWFEWeightsResult[source]
Bases:
objectLightweight container for the standalone
twowayfeweightshelper.Returned by
twowayfeweights(). Mirrors the per-cell decomposition information that the dCDH estimator stores on its results object whentwfe_diagnostic=True, but available as a standalone function for users who only want the diagnostic without fitting the full estimator.- __init__(weights, fraction_negative, sigma_fe, beta_fe)[source]
- weights
- fraction_negative
- sigma_fe
- beta_fe
Example Usage#
Basic usage with reversible treatment:
from diff_diff import ChaisemartinDHaultfoeuille
from diff_diff.prep import generate_reversible_did_data
data = generate_reversible_did_data(
n_groups=80, n_periods=6, pattern="single_switch", seed=42,
)
est = ChaisemartinDHaultfoeuille()
results = est.fit(
data,
outcome="outcome",
group="group",
time="period",
treatment="treatment",
)
results.print_summary()
Joiners and leavers views:
print(f"DID_M (overall): {results.overall_att:.3f}")
print(f"DID_+ (joiners): {results.joiners_att:.3f}")
print(f"DID_- (leavers): {results.leavers_att:.3f}")
print(f"Placebo (DID^pl): {results.placebo_effect:.3f}")
Per-period decomposition:
for t, cell in results.per_period_effects.items():
print(
f"t={t}: DID+={cell['did_plus_t']:.3f} "
f"({cell['n_10_t']} joiners, {cell['n_00_t']} stable_0 controls)"
)
Multiplier bootstrap inference:
est = ChaisemartinDHaultfoeuille(
n_bootstrap=999, bootstrap_weights="rademacher", seed=42,
)
results = est.fit(
data, outcome="outcome", group="group",
time="period", treatment="treatment",
)
# When n_bootstrap > 0, the top-level overall_*/joiners_*/leavers_*
# p-value and conf_int fields hold percentile-based bootstrap
# inference (not normal-theory recomputations from the bootstrap SE).
# The t-stat is computed from the SE in both cases. See REGISTRY.md
# `Note (bootstrap inference surface)` for the full contract.
print(f"Top-level p-value (bootstrap): {results.overall_p_value:.4f}")
print(f"Top-level CI (bootstrap): {results.overall_conf_int}")
print(f"bootstrap_results.overall_se: {results.bootstrap_results.overall_se:.3f}")
print(f"bootstrap_results.overall_ci: {results.bootstrap_results.overall_ci}")
Standalone TWFE diagnostic (without fitting the full estimator):
from diff_diff import twowayfeweights
diagnostic = twowayfeweights(
data, outcome="outcome", group="group", time="period", treatment="treatment",
)
print(f"Plain TWFE coefficient: {diagnostic.beta_fe:.3f}")
print(f"Fraction of negative weights: {diagnostic.fraction_negative:.3f}")
print(f"sigma_fe (sign-flipping threshold): {diagnostic.sigma_fe:.3f}")
The DCDH alias:
from diff_diff import DCDH
est = DCDH() # equivalent to ChaisemartinDHaultfoeuille()