feat(dml): Caetano, Callaway, Payne & Sant'Anna (2026) bad-control lane on DMLDiD (PR-B) - #829
Conversation
Implements the covariate-unconfoundedness doubly-robust / DML estimator of Caetano, Callaway, Payne & Sant'Anna (2026), "Difference-in-differences with 'bad controls'" (arXiv:2608.03881; Eq. 10-11, Algorithm 1), as fit-time arguments on DMLDiD: fit(..., bad_control=, bad_control_covariates=). - _dr_scores.py: ccps_panel_score / ccps_panel_score_augmented (reduce to chang_panel_score with nu = m, omega = ps/(1-ps)). - _crossfit.py: per-fold generator iter_fold_fits / FoldFit under cross_fit_predict (behavior-preserving; deep-copy warning prefix now "_crossfit: could not deep-copy ..."). - dml_did.py: _compute_ccps_gt + _ccps_nuisances (m, p, nested nu / omega; parametric in-sample plug-in vs split-half swap-and-average), omega clip with warning, complete-case policy incl. treated X_t, per-cell ATT_X(g,t) diagnostic with its own IF payload; the inference tail factored into _finish_panel_cell / _cell_se_from_payload; fail-closed gates (RCS, survey_design, anticipation, universal base); name-level validation. - dml_did_results.py: bad_control / bad_control_covariates / bad_control_diagnostics fields, bad_control_summary(), summary() header lines, att_x / se_x in to_dataframe(), conditional to_dict keys. - Reporting: target-parameter clause + reference, BusinessReport identification branch + Caetano citation, practitioner refit snippet. - Tests: score-level DR / orthogonality / reduction; numpy Eq. 11 oracle; split-half oracle learners; plug-in identity; SA DGP 1 / 4 recovery + slow MC coverage; TestBadControlAPI (reduction bit-identity, validation matrix, complete cases, column order, results surface, cluster / single PSU, bootstrap + aggregations, split-half replication, reporting); iter_fold_fits contract; variance-conventions row; guides pin; R badcontrols 1.0.0 (commit 651ccc92) black-box goldens with a tolerance-based parity test (10-seed means both sides). - Docs: REGISTRY DMLDiD "Bad-control extension" block + CS Approach-1 Note + infra section; API pages; paper-review checklist flips; guides; choosing / decision tree / index / README; survey docs; references; doc-deps; variance-conventions table; TODO / DEFERRED rows; changelog fragment.
Overall assessment:
|
…cks Condition 2 (CI review M1)
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall assessment: ✅ Looks goodExecutive summary
MethodologyNo unmitigated findings. The score and influence function match Equations 10–11 and Proposition 7 ( The prior guidance issue is corrected consistently in M1 — Documented implementation choices
Code QualityNo findings. Both ATT and PerformanceNo findings. MaintainabilityNo findings. Shared cross-fitting and cell-finalization helpers limit duplicated statistical logic. Tech DebtT1 — Deferred extensions are tracked
SecurityNo findings. The R oracle is commit-pinned, and no secrets or PII were detected. Documentation/TestsNo findings. The behavior change has Runtime tests could not be collected because |
Summary
DMLDiD:fit(..., bad_control=, bad_control_covariates=). No new estimator class;bad_control=Noneis the untouched pre-existing code path (bit-identity pinned on every payload)._dr_scores.py:ccps_panel_score/ccps_panel_score_augmented(the paper's four-term orthogonal score; reduces tochang_panel_scorewithnu = m,omega = ps/(1-ps), pinned at 1e-14)._crossfit.py: per-fold generatoriter_fold_fits/FoldFitextracted undercross_fit_predict(behavior-preserving; the deep-copy fallback warning prefix becomes"_crossfit: could not deep-copy ...").dml_did.py:_compute_ccps_gt+_ccps_nuisances(m, p, nested nu / omega: parametriclinear/logituse in-sample fold-k targets = the paper's Assumption-8 plug-in;ridge/sieve/ user learners split the training complement in half, swap and average, footnote 9), omega clipped to[0, (1-trim)/trim]with a warning, one complete-case rule incl. treatedX_t, per-cellATT_X(g,t)diagnostic (Remark 6; AIPW on levels with its own IF payload through the shared SE / df branch), the plain lane's inference tail factored into_finish_panel_cell/_cell_se_from_payload; fail-closed gates (RCS,survey_design,anticipation != 0, universal base ->NotImplementedError; barecluster=supported), name-levelValueErrors.dml_did_results.py:bad_control/bad_control_covariates/bad_control_diagnosticsfields,bad_control_summary(),summary()header lines (banner, W, ATT_X cell counts, analytical-SE note on bootstrapped fits, CS-simple weighting note),att_x/se_xjoined into_dataframe()atlevel="group_time", conditionalto_dict()keys, pre-upgrade-pickle safe reads.BusinessReportidentification branch + second citation, practitioner learner-sensitivity snippet carries the bad-control args (andcontrol_group=).benchmarks/R/generate_badcontrols_golden.R+ commit-pinned installer inbenchmarks/R/requirements.R(badcontrols1.0.0 at651ccc92,ptetools1.0.0),benchmarks/data/badcontrols_{panel.csv,golden.json},benchmarks/R/README.mdsection.DMLDiD"Bad-control extension (CCPS 2026)" block (equations as implemented + Notes: nested-stage convention, omega clip, W default, base-period semantics incl. pre-periodY_{t-1}, anticipation,pi_hat, fail-closed lanes, complete cases, time-invariant bad control, aggregation weights vs Remark 4, ATT_X inference, validation scope), CS Approach-1 Note, infra section (fourth score family,iter_fold_fitscontract);docs/api/dml_did.rstmethodology / restrictions / runnable snippet;docs/api/staggered.rst"Covariates and bad controls"; paper-review checklist flipped with library annotations; guides (llms.txtsignature + Diagnostics entry,llms-full.txt, practitioner pitfall 3 rewrite, autonomous matrix); choosing / decision tree / index / README; survey roadmap + theory carve-outs;REPORTING.mdNote; references; doc-deps; regenerated variance-conventions table; TODO / DEFERRED rows; changelog fragment.Methodology references (required if estimator / math changes)
DMLDiDCS cell architecture;ATT_X(g,t)pre-test (Remark 6); Approach 1 (Theorem 1 / Proposition 3) documented on CallawaySantAnna / DMLDiD base-period covariates.docs/methodology/papers/caetano-2026-review.md, a deliberate exception to the published-source rule). R referencehugosantanna/badcontrols(GPL-3) executed only as a black-box oracle.DMLDiDNotes - nested-stage targets (in-sample plug-in for the parametric built-ins, split-half otherwise; the paper leaves the construction to the practitioner);omega_hatclipped to[0, (1-trim)/trim](S2(iv) boundedness; the paper gives no rule);Wdefault = none (R parity; Remark 5'sY_{g-1}is the recommended explicit choice);pi_hat= the cell's treated share; varying-base pre-period cells readW = Y_{t-1}; headlineattkeeps the CS "simple" weighting rather than Remark 4's overall (TODO row); RCS / anticipation / universal base /survey_designfail closed (DEFERRED rows); ATT_X is an analytical mean-effect diagnostic, never aggregated or bootstrapped.Validation
tests/test_methodology_dr_scores.py(TestCCPSScore: oracle recovery, paired double robustness, four-direction Neyman orthogonality vs purpose-built non-orthogonal comparators, reduction to Chang, augmented identity, validation),tests/test_methodology_dml_did.py(SA DGP frame + moment pins, numpy Eq. 11 / Algorithm 1 oracle at 1e-12 for ATT / SE / ATT_X, gapped time grid, Assumption-8 plug-in identity, oracle user learners on the split-half branch, DGP 1 / 4 recovery with a discriminating naive include-X_tcontrast, slow MC coverage),tests/test_dml_did.py::TestBadControlAPI(43 tests: reduction bit-identity, validation matrix, complete cases, S/R column order, results surface, diagnostics schema, omega clip, ATT_X NaN contract, split-half hand replication + PSU-cohesive halves + degeneracy, cluster / single-PSU NaN, bootstrap + aggregations incl. thetotalguard, time-invariant bad control, reporting surfaces),tests/test_crossfit.py::TestIterFoldFits,tests/test_variance_conventions.py(dml_did_bad_controlrow),tests/test_guides.py,tests/test_dml_did_bad_controls_parity.py(new; R goldens, 10-seed means both sides,|dATT| < 0.5 SE_R+ SE ratio 0.3 on theW=[W]runs andW=[Y]post cells; characterizations at 1.0 SE otherwise).ATT = 1.00andATT_X = 0.50within 3 SE (seed-pinned) with ~95% MC coverage; Rbadcontrolssingle-seed gaps <= 0.63 SE with 10-seed means agreeing within 0.1 SE, SE ratios within 1.25. Sphinx-Wbuild passes. Tutorial section deferred to PR-C (TODO row).Security / privacy
Changelog
changelog.d/20260905-bad-controls-dml.md)