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:

\[\text{summand}_i = \frac{D_i - \hat g(X_i)\,(1 - D_i)/(1 - \hat g(X_i))}{\hat p}\,\bigl(\Delta Y_i - \hat\ell(X_i)\bigr)\]

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:

\[\text{summand}_i = \frac{D_i - \hat g(X_i)}{\hat p\,\hat\lambda(1-\hat\lambda)\,(1 - \hat g(X_i))}\,\bigl((T_i - \hat\lambda)Y_i - \hat\ell_2(X_i)\bigr)\]

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

\[\text{summand}_i = \frac{D_i}{\hat\pi}\bigl(\Delta Y_i - \hat\nu(S_i)\bigr) - \frac{1 - D_i}{\hat\pi}\Bigl[\bigl(\hat m(R_i) - \hat\nu(S_i)\bigr)\frac{\hat p(S_i)}{1 - \hat p(S_i)} + \bigl(\Delta Y_i - \hat m(R_i)\bigr)\hat\omega(R_i)\Bigr]\]

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, BaseEstimator

Chang (2020) DML DiD: staggered ATT(g,t) with cross-fitted ML nuisances.

panel=True (default) estimates Case 1 on panel data; panel=False estimates 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-fold predict_proba) and the control-only outcome regression (outcome_learner, trained on the cell’s controls, Chang’s I_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 regression E[dY | X, D=0]; on RCS fits the folds are D x T stratified and the nuisance is the level regression E[(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-in sqrt(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 fail total closed) is POST-FIT via results.aggregate().

Parameters:
  • propensity_learner (str or object, default "logit") – "logit", or ANY object with fit/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 with fit/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; set seed for 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. sklearn random_state) — cross_fit_predict deep-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 that 1 - pscore_trim < 1 in float64 (a sub-ulp trim would disable the upper clip).

  • panel (bool, default True) – True estimates Chang’s Case 1 (repeated outcomes) on panel data (one row per unit-period, cell score on outcome CHANGES). False estimates Case 2 (repeated cross sections) on DECLARED cross-sectional data — one observation per row with a row-unique unit ID, 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, and df_inference — while the moment kernels stay UNWEIGHTED and no sample_weight reaches the learners. When survey_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_weight into the nuisance learners (user learner objects must accept sample_weight by 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 with df = n_PSU - n_strata t-inference; replicate-weight designs (BRR / Fay / JK1 / JKn / SDR) compute per-cell AND aggregate variances by IF-reweighting the cross-fitted scores with df = rank(replicate matrix) - 1 t-inference (nuisances are not re-estimated per replicate; REGISTRY DMLDiD Note). Replicate designs reject cluster= and n_bootstrap > 0 combinations. 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_covariates and covariates - and stores the per-cell ATT_X(g, t) pre-test (Remark 6) on results.bad_control_diagnostics / results.bad_control_summary(). The column is read at the cell’s base period AND at t (both must be finite for a unit to join the cell) and must NOT also appear in covariates (the “include the bad control” bias; to condition only on its pre-treatment value - the paper’s Approach 1 - pass it in covariates alone instead). Requires panel=True, anticipation=0, base_period="varying" and no survey_design (each raises NotImplementedError; bare cluster= is supported). covariates stays required. Default None = the plain Chang lane (unchanged).

  • bad_control_covariates (iterable of str, optional) – Extra pre-treatment covariates W for 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; the outcome name is allowed and means the base-period outcome (Y_{g-1}, the paper’s Remark 5 recommendation: bad_control_covariates=[outcome]). Names may not overlap covariates or bad_control. Default None = no W (matches the R badcontrols default); results store (). Requires bad_control.

  • data (DataFrame)

  • outcome (str)

  • unit (str)

  • time (str)

  • first_treat (str)

  • covariates (Iterable[str] | None)

Return type:

DMLDiDResults

class diff_diff.dml_did_results.DMLDiDResults[source]

Bases: CallawaySantAnnaResults

Results from DMLDiD (Chang 2020 DML DiD, staggered ATT(g,t)).

Inherits the full Callaway-Sant’Anna results surface — att/se aliases, to_dataframe, post-fit aggregate() (simple / event_study / group, plus total on panel non-survey fits; repeated-cross-section AND declared-survey fits fail total closed) with bootstrap replay — and adds the DML provenance fields below. cluster_name/ n_clusters/df_inference/survey_metadata are populated on survey/cluster= fits (CS conventions: survey_metadata marks a DECLARED design; df_inference carries 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_effects stays None permanently — aggregate("group") returns a separate container and never mutates this object. panel is NOT inert: it carries the fit’s DECLARED design — True for the Case 1 panel lane, False for the Case 2 repeated-cross-section lane (DMLDiD(panel=False)) — and report rendering plus the aggregation n_kind read 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:

str

n_folds

Cross-fitting fold count K as REQUESTED (the configuration value).

Type:

int

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); None otherwise.

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=None the 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=...)); None on plain Chang fits.

Type:

str, optional

bad_control_covariates

The W columns (user order; the outcome name means the outcome at the cell’s base period); () when a bad control was declared without W; None on plain fits.

Type:

tuple of str, optional

bad_control_diagnostics

Per-(g, t) ATT_X(g, t) pre-test (Remark 6): the AIPW mean-effect of treatment on the bad control at t (pre-period cells assess MP-5 / MP-8 and should be zero; post-period cells check that treatment affects the covariate) with keys effect/se/t_stat/p_value/conf_int/n_treated/ n_control. Analytical SE only (never bootstrapped or aggregated). Retained cells only; None on plain fits. Read through bad_control_summary().

Type:

dict, optional

propensity_learner: Any = 'logit'
outcome_learner: Any = 'linear'
n_folds: int = 5
effective_n_folds: int | None = None
cross_fit_diagnostics: Dict[Any, Dict[str, Any]] | None = None
seed: int | None = None
n_bootstrap: int = 0
bootstrap_weights: str | None = None
cband: bool = True
bad_control: str | None = None
bad_control_covariates: Tuple[str, ...] | None = None
bad_control_diagnostics: Dict[Any, Dict[str, Any]] | None = None
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 alpha raises rather than silently relabeling fit-time intervals (percentile bootstrap intervals in particular cannot be reconstructed from the reported SE).

Return type:

str

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, and post (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:

DataFrame

to_dataframe(level='group_time')[source]

Parent DataFrame; on bad-control fits the group_time level gains att_x / se_x (analytical, unlike a bootstrapped se).

Parameters:

level (str)

Return type:

DataFrame

to_dict()[source]

Headline dict extended with DML provenance (JSON-serializable).

Return type:

Dict[str, Any]

__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:
Return type:

None

class diff_diff.SieveLearner[source]

Bases: object

Polynomial-sieve regressor with information-criterion degree selection.

For each candidate total degree K = 1..k_max (auto k_max = floor(n_pos**0.2), floored at 1, where n_pos is the positive-weight support count; degrees whose basis dimension reaches n_pos are inadmissible), builds the monomial basis via the EfficientDiD sieve helper, fits by (W)LS, and scores IC(K) = n_pos * log(RSS_w / n_pos) + c_n * p_K with c_n = 2 (AIC) or log(n_pos) (BIC). Degrees with an ill-conditioned (weighted) design Gram are skipped (one consolidated UserWarning). If EVERY degree is inadmissible on a non-empty support, falls back to an intercept-only (weighted-mean) fit with a UserWarning; genuinely degenerate inputs (empty data, fewer than 2 positive-weight rows) raise a targeted ValueError instead (the fail-closed sibling contract).

The standardization statistics center_/scale_ are UNWEIGHTED even under sample_weight — the helper standardizes for numerical conditioning only, and fitted values are standardization-invariant. Fitted state is plain floats/ndarrays (deep-copyable, picklable); fit fully re-initializes it (sklearn fit-reset semantics).

__init__(k_max=None, criterion='bic')[source]
Parameters:
  • k_max (int | None)

  • criterion (str)

Return type:

None

fit(X, y, sample_weight=None)[source]
Parameters:
Return type:

SieveLearner

predict(X)[source]
Parameters:

X (ndarray)

Return type:

ndarray

Restrictions#

Warning

The following restrictions apply to the current implementation:

  • Covariates required — fit(covariates=None) or an empty list raises, directing to CallawaySantAnna.

  • Declared designs only — panel=True needs one row per (unit, period); panel=False needs 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 pweight SurveyDesign via survey_design= for weighted RCS.

  • Survey/cluster support (both lanes) — survey_design= (pweight-only) weights the moment kernels and passes sample_weight into 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 with df = n_PSU - n_strata t-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 with df = rank(replicate matrix) - 1 t-inference (nuisances are not re-estimated per replicate); replicate + cluster= and replicate + n_bootstrap > 0 are 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). Bare cluster= (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 UserWarning reports 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_reason and reported in a consolidated warning; surviving cells still aggregate.

  • Event-study surface is post-fit only — fit-time event_study_effects is never populated; call results.aggregate('event_study').

  • Bad-control lane — fit(bad_control=...) requires panel=True, anticipation=0 and base_period='varying', and no survey_design (each raises NotImplementedError; bare cluster= is supported). The bad control may not appear in covariates (the “include the bad control” bias); covariates stays required. Units with a non-finite bad control at the base period or at t, or a non-finite bad-control covariate at the base period, leave that cell (complete cases). ATT_X is analytical only: never bootstrapped or aggregated.

See also

CallawaySantAnna

The unconditional / low-dimensional-covariates staggered estimator whose (g, t) architecture, aggregation, and bootstrap DMLDiD reuses.

EfficientDiD

Chen, Sant’Anna & Xie (2025) efficient staggered estimation with sieve nuisances (fixed-form, not cross-fitted).

LWDiD

Lee & Wooldridge rolling-transformation DiD (cross-sectional TE estimation after a unit-specific transform).