DMLDiD — Double/Debiased Machine Learning DiD#
Chang (2020)’s double/debiased machine learning (DML) estimator for
Difference-in-Differences with covariates, extended to staggered adoption:
each Callaway-Sant’Anna style group-time cell \((g, t)\) is estimated
as a 2-period Chang problem with cross-fitted machine-learning nuisance
functions and a Neyman-orthogonal score, so the ATT estimate is
first-order insensitive to the regularization bias of the nuisance
learners. panel=True (the default) runs Case 1 (repeated outcomes) on
panel data; panel=False runs Case 2 (repeated cross sections) on
declared cross-sectional data — one observation per row. The classic
2-period design is the degenerate single-cell case of either lane.
Identification follows Abadie (2005): CONDITIONAL parallel trends — the untreated potential-outcome trend is parallel across treated and control units only after conditioning on covariates \(X\). Chang’s contribution is valid \(\sqrt{n}\) inference when the two nuisance functions — the propensity score \(g_0(X) = P(D=1|X)\) plus, on panel fits, the control outcome-change regression \(\ell_0(X) = E[\Delta Y | X, D=0]\) (Case 1) or, on RCS fits, the control level regression \(\ell_{20}(X) = E[(T - \lambda) Y | X, D=0]\) (Case 2) — are estimated by machine learning under DML2 cross-fitting — PROVIDED the learners satisfy Chang’s rate conditions (Assumption 3.1(f) for Case 1 / 3.2(h) for Case 2: each nuisance at \(o_p(n^{-1/4})\) in the \(L_2\) norm, plus a product remainder bound). Cross-fitting removes overfitting bias but cannot substitute for the rate conditions, and the reported fold losses do not verify them; slow learners silently invalidate normal inference (see the REGISTRY DMLDiD “First-stage rate condition” assumption bullet).
Covariates are required: Chang’s estimator exists for the
conditional-on-covariates setting. For an unconditional staggered DiD use
CallawaySantAnna (with or without covariates).
Methodology#
Per-cell orthogonal score (Chang 2020, Equation 3.1). For cell
\((g, t)\) with base period \(b\) selected positionally (the CS /
R did rule), let \(\Delta Y_i = Y_{it} - Y_{ib}\), treated
indicator \(D_i = 1\{\text{cohort}_i = g\}\), and cell treated share
\(\hat p\). The uncentered score summand is:
and \(\widehat{ATT}(g,t)\) is its sample mean over the cell. Both nuisances are cross-fitted OUT-OF-FOLD (DML2, K folds, per-cell D-stratified assignment): the propensity by the classifier learner, the outcome-change regression by the regressor learner trained on the cell’s control units only (Chang’s \(I_{kz}^c\)).
Variance (Chang 2020, Theorem 2). The augmented score
\(\bar\psi_i = \text{summand}_i - D_i \hat\theta / \hat p\) folds in
the finite-dimensional treated-share correction; the per-cell standard
error is the plug-in \(\sqrt{\overline{\bar\psi^2} / n}\). This exact
object was matched to DoubleML at machine precision in the committed
parity spikes (benchmarks/doubleml/).
Case 2 — repeated cross sections (Chang 2020, Equation 3.2;
panel=False). The cell pools the two periods’ rows (post indicator
\(T_i = 1\{\text{time}_i = t\}\)) and scores LEVEL outcomes:
with \(\hat\lambda = \text{mean}(T)\) the post-period sampling share and
\(\hat\ell_2\) the SINGLE cross-fitted control-only regression of
\((T - \hat\lambda)Y\) on \(X\) (Chang’s \(I_{kz}^c\)) — one
regression, deliberately different from the Sant’Anna-Zhao/DoubleML
four-regression RCS score. The Theorem 2 variance carries BOTH
finite-dimensional corrections: the treated-share fold-in plus an explicit
\(\hat G_{2\lambda}(T_i - \hat\lambda)\) term (the λ-correction the
paper’s proof structure warns is easy to omit), with
\(\hat G_{2\lambda}\) the sample mean of the closed-form
\(\partial_\lambda \psi_2\). Folds are stratified on the four
\(D \times T\) classes so every training fold carries control rows in
both periods. RCS aggregation weights are FIXED cohort row masses (the
CS-RCS convention, keeping the variance the influence function of the
reported aggregate); aggregate('total') is unavailable on RCS fits.
Bad controls (Caetano, Callaway, Payne & Sant’Anna 2026;
fit(..., bad_control=, bad_control_covariates=)). A “bad control”
is a time-varying covariate \(X_t\) that treatment can affect.
Conditioning on it at \(t\) biases DiD; conditioning only on its
pre-treatment value (pass it in covariates - the panel lane reads
covariates at the cell’s base period) is the paper’s Approach 1. This
lane implements the paper’s covariate-unconfoundedness approach: parallel
trends conditional on the bad control’s UNTREATED path, with the bad
control’s untreated evolution identified given its base-period value, the
optional bad_control_covariates \(W\) (the outcome name means the
base-period outcome, the paper’s Remark 5 recommendation), and the
covariates \(Z\). Per cell, with \(R = [X_t, X_b, Z]\) and
\(S = [X_b, W, Z]\), the Neyman-orthogonal doubly-robust score
(paper Equation 10) is
with four cross-fitted nuisances: the control outcome-change regression
\(\hat m\) on \(R\), the propensity \(\hat p\) on \(S\), and
two NESTED second-stage regressions - \(\hat\nu\) (the first-stage
predictions regressed on \(S\) among training controls) and
\(\hat\omega\) (the propensity odds regressed on \(R\), clipped to
\([0, (1-\text{trim})/\text{trim}]\) with a warning). With the parametric
built-in learners the nested targets are the paper’s own plug-in; with
ridge / sieve / user learners the training complement is split in
half (footnote 9). The variance is the same augmented-score plug-in as the
plain lane. Each cell also reports \(\widehat{ATT}_X(g,t)\), the
paper’s Remark 6 pre-test (the effect of treatment on the bad control
itself, an AIPW mean-effect diagnostic with its own analytical SE), via
results.bad_control_summary(): pre-period rows (\(t < g\)) assess
the identifying assumptions MP-5 / MP-8 and should be zero (a nonzero
value flags a possible violation), post-period rows (\(t \ge g\)) are
the Condition-2 check (a nonzero value is evidence treatment affects the
covariate; a zero value does not establish the converse). Restrictions: panel lane only, bare
cluster= only (survey_design= raises), anticipation=0 and
base_period='varying' only. The headline att keeps the CS
“simple” weighting, not the paper’s Remark 4 overall. Validated against
the authors’ R package badcontrols as a tolerance-based black box
(tests/test_dml_did_bad_controls_parity.py) plus the paper’s
Supplementary DGPs 1 and 4; see docs/methodology/REGISTRY.md “DMLDiD”
> “Bad-control extension (CCPS 2026)” for every convention.
Aggregation. DMLDiD writes the CallawaySantAnna per-cell
influence-function payload and inherits the CS aggregation and
multiplier-bootstrap machinery: event-study / group / simple aggregations
(plus total on panel fits — RCS fits fail total closed) are produced
post-fit via results.aggregate(...),
with sup-t uniform bands and bootstrap replay on bootstrapped fits.
See docs/methodology/REGISTRY.md “DMLDiD” for the full equations,
implementation Notes (global \(\hat p\) convention, D-stratified
folds, pooled fold weighting, trimming policy, skip-reason vocabulary)
and the DoubleML parity anchors.
Basic usage#
import diff_diff as dd
est = dd.DMLDiD(
propensity_learner="logit",
outcome_learner="sieve", # adaptive polynomial sieve (IC-selected)
n_folds=5,
seed=42, # reproducible fold draws
)
results = est.fit(
df,
outcome="y",
unit="unit",
time="time",
first_treat="first_treat",
covariates=["x1", "x2"], # REQUIRED
)
print(results.summary())
# Post-fit aggregation (event study with sup-t bands on bootstrapped fits)
es = results.aggregate("event_study")
print(es.to_dataframe())
Bad control (Caetano et al. 2026): a covariate treatment can affect, such
as an occupation or earnings measure, is passed as bad_control rather
than in covariates; bad_control_covariates=[outcome] conditions the
bad control’s evolution on the base-period outcome (Remark 5), and the
explicit control_group="not_yet_treated" is the paper’s Proposition 2
comparison group:
from diff_diff import DMLDiD
data = data.assign(
occupation=lambda d: 0.7 * d["x1"] + 0.3 * d["x2"] + 0.5 * d["treated"] * d["post"]
)
bc = DMLDiD(control_group="not_yet_treated", seed=0).fit(
data,
outcome="y",
unit="unit",
time="time",
first_treat="first_treat",
covariates=["x3"],
bad_control="occupation",
bad_control_covariates=["y"],
)
print(bc.summary())
print(bc.bad_control_summary()) # per-cell ATT_X(g, t): pre-period rows should be ~0 (MP-5 / MP-8);
# nonzero post-period rows = treatment moves the covariate
Learner configuration#
propensity_learner and outcome_learner accept a string name
(library defaults: "logit"; "linear", "ridge", "sieve") or
ANY object satisfying the duck-typed learner protocol —
fit(X, y) plus predict (regressor) or predict_proba
(classifier). On no-design and bare-cluster= fits the cross-fitting
is unweighted, so a sample_weight parameter on fit is not
required there; a DECLARED survey_design= fit passes
sample_weight into both nuisances, and a user learner whose fit
cannot take sample_weight by keyword is rejected up front with a
TypeError. A user-constructed or scikit-learn
estimator object plugs in directly; string names select library defaults
only. SieveLearner is the exported configurable
learner (DMLDiD(outcome_learner=SieveLearner(k_max=3))).
With seed set, fits are reproducible with the library’s deterministic
built-in learners; a user-supplied STOCHASTIC learner must additionally be
seeded by the user (e.g. sklearn random_state).
API Reference#
- class diff_diff.DMLDiD[source]
Bases:
CallawaySantAnnaBootstrapMixin,CallawaySantAnnaAggregationMixin,BaseEstimatorChang (2020) DML DiD: staggered ATT(g,t) with cross-fitted ML nuisances.
panel=True(default) estimates Case 1 on panel data;panel=Falseestimates Case 2 on declared repeated cross sections (level outcomes, λ-corrected variance).Per (g, t) cell: K-fold cross-fitting of the propensity (
propensity_learner, out-of-foldpredict_proba) and the control-only outcome regression (outcome_learner, trained on the cell’s controls, Chang’sI_kz^c), then the pooled orthogonal score mean and the augmented-score plug-in SE. On panel fits the folds are D-stratified and the outcome nuisance is the outcome-change regressionE[dY | X, D=0]; on RCS fits the folds are D x T stratified and the nuisance is the level regressionE[(T - lam)Y | X, D=0]with the lambda-corrected variance. (Under a survey/cluster design whose PSU is strictly coarser than the sampling unit, folds are PSU-COHESIVE instead of stratified, and the per-cell SE is design-based rather than the plug-insqrt(mean(psi_bar**2)/n); declared designs also weight the moments — REGISTRY DMLDiD survey Notes.) Aggregation (event study, group, simple; plus total on panel non-survey fits — RCS and declared-survey fits failtotalclosed) is POST-FIT viaresults.aggregate().- Parameters:
propensity_learner (str or object, default "logit") –
"logit", or ANY object withfit/predict_proba(the classifier protocol) — e.g. a user-constructed or sklearn estimator. String names select library defaults only.outcome_learner (str or object, default "linear") –
"linear","ridge","sieve", or ANY object withfit/predict(the regressor protocol).n_folds (int, default 5) – Cross-fitting folds K (DML2; per-cell assignment).
control_group (str, default "never_treated") –
"never_treated"or"not_yet_treated"(CS semantics).anticipation (int, default 0) – Anticipation periods (shifts the base period and the not-yet-treated threshold; CS semantics).
alpha (float, default 0.05) – Significance level.
n_bootstrap (int, default 0) – Multiplier-bootstrap iterations (0 = analytical inference).
bootstrap_weights (str, optional) –
"rademacher"(default),"mammen", or"webb".seed (int, optional) – Root seed for the per-cell fold draws and the bootstrap. With
seed=None, POINT ESTIMATES vary across fits — cross-fitting draws random folds; setseedfor reproducible results with the library’s deterministic built-in learners. The same seed yields DIFFERENT folds with vs without a coarser-than-unit PSU design (PSU-cohesive folds consume the RNG differently than stratified folds — a config change, not a reproducibility break). A user-supplied STOCHASTIC learner object must additionally be seeded by the user (e.g. sklearnrandom_state) —cross_fit_predictdeep-copies the learner template where copyable but never seeds its internal RNG.base_period (str, default "varying") –
"varying"or"universal"(CS semantics; universal materializes per-cohort zero reference cells).cband (bool, default True) – Uniform (sup-t) bands on the post-fit event-study aggregation’s bootstrap replay.
pscore_trim (float, default 0.01) – Propensity clip bound: fitted propensities are clipped to
[pscore_trim, 1 - pscore_trim]after the extremeness warning (clip, never drop; Chang’s paper gives no trimming rule). Must be in(0, 0.5)and large enough that1 - pscore_trim < 1in float64 (a sub-ulp trim would disable the upper clip).panel (bool, default True) –
Trueestimates Chang’s Case 1 (repeated outcomes) on panel data (one row per unit-period, cell score on outcome CHANGES).Falseestimates Case 2 (repeated cross sections) on DECLARED cross-sectional data — one observation per row with a row-uniqueunitID, cell score on outcome LEVELS with the post-period sampling share λ̂ and the λ-corrected Theorem 2 variance. Case 2 additionally assumes stationary cross-sectional sampling (Chang Assumption 2.3: each wave samples the SAME target population — the composition of(D, X)is stable across waves, while outcomes are the period-specific potential outcomes, so trends and treatment effects are expected, not violations), which is not data-checkable —fit()warns.aggregate('total')is unavailable on RCS fits (fails closed, the library-wide RC convention).cluster (str, optional) – Column name for COARSER-than-unit clustering. The column is synthesized into a
SurveyDesign(psu=cluster)(or injected as the PSU of a declared PSU-less design): it drives PSU-cohesive cross-fitting folds (when strictly coarser than the sampling unit), the per-cell cluster-robust SE, the bootstrap draw structure, anddf_inference— while the moment kernels stay UNWEIGHTED and nosample_weightreaches the learners. Whensurvey_design=also supplies a PSU, the design’s PSU wins (warning on differing partitions).
- __init__(propensity_learner='logit', outcome_learner='linear', n_folds=5, control_group='never_treated', anticipation=0, alpha=0.05, n_bootstrap=0, bootstrap_weights=None, seed=None, base_period='varying', cband=True, pscore_trim=0.01, panel=True, cluster=None)[source]
- Parameters:
- Return type:
None
- results_: DMLDiDResults | None
- fit(data, outcome, unit, time, first_treat, covariates=None, survey_design=None, bad_control=None, bad_control_covariates=None)[source]
Estimate staggered ATT(g,t) via per-cell cross-fitted Chang scores.
- Parameters:
survey_design (SurveyDesign, optional) – Complex survey design (pweight-only). Declared designs weight the moment kernels (Hajek p-hat/lambda-hat/theta) and pass
sample_weightinto the nuisance learners (user learner objects must acceptsample_weightby keyword — a learner without it is rejected up front). Two variance lanes: full-design TSL (weights/strata/PSU/FPC) switches cross-fitting to PSU-cohesive folds when the PSU is strictly coarser than the sampling unit and routes the per-cell and aggregate variances through the design-based kernels withdf = n_PSU - n_stratat-inference; replicate-weight designs (BRR / Fay / JK1 / JKn / SDR) compute per-cell AND aggregate variances by IF-reweighting the cross-fitted scores withdf = rank(replicate matrix) - 1t-inference (nuisances are not re-estimated per replicate; REGISTRY DMLDiD Note). Replicate designs rejectcluster=andn_bootstrap > 0combinations. Survey support is a documented library extension of Chang (2020), which assumes i.i.d. sampling — Theorem 2’s coverage claim does not carry over (REGISTRY DMLDiD Notes).bad_control (str, optional) – Column name of a “bad control” (Caetano, Callaway, Payne & Sant’Anna 2026): a time-varying covariate that treatment can affect. When set, each panel cell runs the paper’s Neyman-orthogonal doubly-robust score (Equation 10 / Algorithm 1) instead of the Chang score - parallel trends conditional on the bad control’s UNTREATED path, with its untreated evolution identified by covariate unconfoundedness given its base-period value,
bad_control_covariatesandcovariates- and stores the per-cellATT_X(g, t)pre-test (Remark 6) onresults.bad_control_diagnostics/results.bad_control_summary(). The column is read at the cell’s base period AND att(both must be finite for a unit to join the cell) and must NOT also appear incovariates(the “include the bad control” bias; to condition only on its pre-treatment value - the paper’s Approach 1 - pass it incovariatesalone instead). Requirespanel=True,anticipation=0,base_period="varying"and nosurvey_design(each raisesNotImplementedError; barecluster=is supported).covariatesstays required. DefaultNone= the plain Chang lane (unchanged).bad_control_covariates (iterable of str, optional) – Extra pre-treatment covariates
Wfor the bad control’s unconfoundedness model (the propensity design is[X_base, W, Z]; the outcome regressions use[X_t, X_base, Z]). Every entry is read at the cell’s base period; theoutcomename is allowed and means the base-period outcome (Y_{g-1}, the paper’s Remark 5 recommendation:bad_control_covariates=[outcome]). Names may not overlapcovariatesorbad_control. DefaultNone= noW(matches the Rbadcontrolsdefault); results store(). Requiresbad_control.data (DataFrame)
outcome (str)
unit (str)
time (str)
first_treat (str)
- Return type:
- class diff_diff.dml_did_results.DMLDiDResults[source]
Bases:
CallawaySantAnnaResultsResults from DMLDiD (Chang 2020 DML DiD, staggered ATT(g,t)).
Inherits the full Callaway-Sant’Anna results surface —
att/sealiases,to_dataframe, post-fitaggregate()(simple / event_study / group, plus total on panel non-survey fits; repeated-cross-section AND declared-survey fits failtotalclosed) with bootstrap replay — and adds the DML provenance fields below.cluster_name/n_clusters/df_inference/survey_metadataare populated on survey/cluster=fits (CS conventions:survey_metadatamarks a DECLARED design;df_inferencecarries the bare-cluster df). Every inherited CS-only field that DMLDiD never populates (epv_*,pscore_fallback,allow_unbalanced_panel,used_rc_on_unbalanced_panel,influence_functions,event_study_effects/event_study_vcov/event_study_vcov_index/event_study_df) stays at its inherited default and is inert.group_effectsstaysNonepermanently —aggregate("group")returns a separate container and never mutates this object.panelis NOT inert: it carries the fit’s DECLARED design —Truefor the Case 1 panel lane,Falsefor the Case 2 repeated-cross-section lane (DMLDiD(panel=False)) — and report rendering plus the aggregationn_kindread it for “units” vs “observations” semantics.- propensity_learner, outcome_learner
The learner specs as configured — the string name verbatim; a passed LIBRARY-native learner object keeps its configuration-aware
repr(); a FOREIGN object is recorded as its qualified class name only (its__repr__could embed sensitive content). Never the object itself: result pickles must not retain arbitrary user objects.- Type:
- n_folds
Cross-fitting fold count K as REQUESTED (the configuration value).
- Type:
- effective_n_folds
The REALIZED fold count when it differs from
n_folds— set only when a coarse survey/cluster PSU design reduced K to the global PSU count to preserve cluster cohesion (warned at fit);Noneotherwise.- Type:
int, optional
- cross_fit_diagnostics
Per-
(g, t)cross-fit diagnostics: per-stage fold losses and fit counts,p_hat, propensity clip count, and the fold-seed derivation. Cells that failed before fold assignment carry no entry.- Type:
dict, optional
- seed
Root seed for the per-cell fold draws (and the bootstrap when enabled). With
seed=Nonethe recorded per-fit entropy is drawn from OS randomness.- Type:
int, optional
- n_bootstrap, bootstrap_weights, cband
Multiplier-bootstrap configuration used at fit.
- bad_control
The bad-control column of a Caetano, Callaway, Payne & Sant’Anna (2026) fit (
fit(bad_control=...));Noneon plain Chang fits.- Type:
str, optional
- bad_control_covariates
The
Wcolumns (user order; the outcome name means the outcome at the cell’s base period);()when a bad control was declared withoutW;Noneon plain fits.
- bad_control_diagnostics
Per-
(g, t)ATT_X(g, t)pre-test (Remark 6): the AIPW mean-effect of treatment on the bad control att(pre-period cells assess MP-5 / MP-8 and should be zero; post-period cells check that treatment affects the covariate) with keyseffect/se/t_stat/p_value/conf_int/n_treated/n_control. Analytical SE only (never bootstrapped or aggregated). Retained cells only;Noneon plain fits. Read throughbad_control_summary().- Type:
dict, optional
- propensity_learner: Any = 'logit'
- outcome_learner: Any = 'linear'
- n_folds: int = 5
- n_bootstrap: int = 0
- cband: bool = True
- summary(alpha=None)[source]
Formatted summary with the DML banner and cross-fit header block.
Post-processes the parent summary: replaces the hardcoded Callaway-Sant’Anna banner line, relabels the normal-theory statistic columns (z, not t), and prepends the DML provenance header (never reimplements the parent body).
- Parameters:
alpha (float, optional) – Accepted for signature uniformity. The stored intervals were computed at fit time; a value different from the stored
alpharaises rather than silently relabeling fit-time intervals (percentile bootstrap intervals in particular cannot be reconstructed from the reported SE).- Return type:
- bad_control_summary()[source]
Per-cell
ATT_X(g, t)pre-test table (Caetano et al. 2026, Remark 6).One row per RETAINED cell of a bad-control fit: the AIPW mean effect of treatment on the bad control at
t(att_x), its analytical SE and inference, andpost(t >= g - anticipation). Pre-period rows assess covariate unconfoundedness (MP-5); post-period rows are a mean-effect check of Condition 2 (a nonzero value is evidence the treatment moves the bad control; a zero value does not establish the converse). Typed-empty on plain fits.- Return type:
- to_dataframe(level='group_time')[source]
Parent DataFrame; on bad-control fits the
group_timelevel gainsatt_x/se_x(analytical, unlike a bootstrappedse).
- to_dict()[source]
Headline dict extended with DML provenance (JSON-serializable).
- __init__(group_time_effects, overall_att, overall_se, overall_t_stat, overall_p_value, overall_conf_int, groups, time_periods, n_obs, n_treated_units, n_control_units, alpha=0.05, control_group='never_treated', base_period='varying', anticipation=0, panel=True, allow_unbalanced_panel=False, used_rc_on_unbalanced_panel=False, event_study_effects=None, group_effects=None, influence_functions=None, event_study_vcov=None, event_study_vcov_index=None, event_study_df=None, bootstrap_results=None, cband_crit_value=None, pscore_trim=0.01, survey_metadata=None, epv_diagnostics=None, epv_threshold=10, pscore_fallback='error', vcov_type='hc1', cluster_name=None, n_clusters=None, df_inference=None, reference_event_times=None, _aggregation_kit=None, propensity_learner='logit', outcome_learner='linear', n_folds=5, effective_n_folds=None, cross_fit_diagnostics=None, seed=None, n_bootstrap=0, bootstrap_weights=None, cband=True, bad_control=None, bad_control_covariates=None, bad_control_diagnostics=None)
- Parameters:
overall_att (float)
overall_se (float)
overall_t_stat (float)
overall_p_value (float)
n_obs (int)
n_treated_units (int)
n_control_units (int)
alpha (float)
control_group (str)
base_period (str)
anticipation (int)
panel (bool)
allow_unbalanced_panel (bool)
used_rc_on_unbalanced_panel (bool)
influence_functions (np.ndarray | None)
event_study_vcov (np.ndarray | None)
event_study_vcov_index (list | None)
event_study_df (float | None)
bootstrap_results (CSBootstrapResults | None)
cband_crit_value (float | None)
pscore_trim (float)
survey_metadata (Any | None)
epv_diagnostics (Dict[Tuple[Any, Any], Dict[str, Any]] | None)
epv_threshold (float)
pscore_fallback (str)
vcov_type (str)
cluster_name (str | None)
n_clusters (int | None)
df_inference (int | None)
_aggregation_kit (Any | None)
propensity_learner (Any)
outcome_learner (Any)
n_folds (int)
effective_n_folds (int | None)
seed (int | None)
n_bootstrap (int)
bootstrap_weights (str | None)
cband (bool)
bad_control (str | None)
- Return type:
None
- class diff_diff.SieveLearner[source]
Bases:
objectPolynomial-sieve regressor with information-criterion degree selection.
For each candidate total degree
K = 1..k_max(autok_max = floor(n_pos**0.2), floored at 1, wheren_posis the positive-weight support count; degrees whose basis dimension reachesn_posare inadmissible), builds the monomial basis via the EfficientDiD sieve helper, fits by (W)LS, and scoresIC(K) = n_pos * log(RSS_w / n_pos) + c_n * p_Kwithc_n = 2(AIC) orlog(n_pos)(BIC). Degrees with an ill-conditioned (weighted) design Gram are skipped (one consolidatedUserWarning). If EVERY degree is inadmissible on a non-empty support, falls back to an intercept-only (weighted-mean) fit with aUserWarning; genuinely degenerate inputs (empty data, fewer than 2 positive-weight rows) raise a targetedValueErrorinstead (the fail-closed sibling contract).The standardization statistics
center_/scale_are UNWEIGHTED even undersample_weight— the helper standardizes for numerical conditioning only, and fitted values are standardization-invariant. Fitted state is plain floats/ndarrays (deep-copyable, picklable);fitfully re-initializes it (sklearn fit-reset semantics).- __init__(k_max=None, criterion='bic')[source]
- fit(X, y, sample_weight=None)[source]
- Parameters:
- Return type:
Restrictions#
Warning
The following restrictions apply to the current implementation:
Covariates required —
fit(covariates=None)or an empty list raises, directing toCallawaySantAnna.Declared designs only —
panel=Trueneeds one row per (unit, period);panel=Falseneeds ROW-UNIQUE unit IDs (one observation per row) and additionally assumes STATIONARY cross-sectional sampling (Chang Assumption 2.3: each wave samples the same target population — the composition of \((D, X)\) is stable across waves while outcomes are the period-specific potential outcomes; warned at fit, not data-checkable).aggregate('total')is unavailable on RCS fits (fails closed). Repeated-cross-section data is typically SURVEY data (BRFSS / ACS / CPS) — pass a pweightSurveyDesignviasurvey_design=for weighted RCS.Survey/cluster support (both lanes) —
survey_design=(pweight-only) weights the moment kernels and passessample_weightinto the nuisance learners; two variance lanes. Full-design TSL (weights/strata/PSU/FPC) switches to PSU-cohesive cross-fitting folds when the PSU is strictly coarser than the sampling unit and uses design-based variance withdf = n_PSU - n_stratat-inference: PSU designs get the cluster-robust survey kernel per cell, strata/FPC-only designs use the weighted influence-function per-cell SE with the full design entering the AGGREGATE variances (the CallawaySantAnna convention). Replicate-weight designs (BRR / Fay / JK1 / JKn / SDR) compute per-cell AND aggregate variances by IF-reweighting the cross-fitted scores withdf = rank(replicate matrix) - 1t-inference (nuisances are not re-estimated per replicate); replicate +cluster=and replicate +n_bootstrap > 0are rejected. This is a documented LIBRARY EXTENSION of Chang (2020), which assumes i.i.d. sampling — on the weighted-λ RCS lane Theorem 2’s coverage claim does not carry over (REGISTRY DMLDiD Notes). Barecluster=(constructor) keeps the kernels unweighted and affects folds, variance and df only.aggregate('total')fails closed on declared-survey fits.Propensity clipping, never dropping — fitted propensities are clipped to
[pscore_trim, 1 - pscore_trim]after an extremeness warning (the paper gives no trimming rule).Per-cell complete cases — on panel fits, a unit with a missing/non-finite outcome or a non-finite base-period covariate at a cell is excluded from that cell only; on RCS fits the exclusion is row-level (a non-finite outcome or covariate on the row — there is no base-period covariate). One consolidated
UserWarningreports the drops.Degenerate cells skip loudly — a cell that cannot be cross-fitted (fewer members than folds, a singleton treated/control stratum, a fail-closed learner error) is recorded as a NaN cell with a machine-readable
skip_reasonand reported in a consolidated warning; surviving cells still aggregate.Event-study surface is post-fit only — fit-time
event_study_effectsis never populated; callresults.aggregate('event_study').Bad-control lane —
fit(bad_control=...)requirespanel=True,anticipation=0andbase_period='varying', and nosurvey_design(each raisesNotImplementedError; barecluster=is supported). The bad control may not appear incovariates(the “include the bad control” bias);covariatesstays required. Units with a non-finite bad control at the base period or att, or a non-finite bad-control covariate at the base period, leave that cell (complete cases).ATT_Xis analytical only: never bootstrapped or aggregated.
See also
CallawaySantAnnaThe unconditional / low-dimensional-covariates staggered estimator whose (g, t) architecture, aggregation, and bootstrap DMLDiD reuses.
EfficientDiDChen, Sant’Anna & Xie (2025) efficient staggered estimation with sieve nuisances (fixed-form, not cross-fitted).
LWDiDLee & Wooldridge rolling-transformation DiD (cross-sectional TE estimation after a unit-specific transform).