Skip to content

docs(dml): tutorial 32 - double ML DiD (numbers-locked) - #800

Merged
igerber merged 2 commits into
mainfrom
dml-tutorial
Aug 29, 2026
Merged

docs(dml): tutorial 32 - double ML DiD (numbers-locked)#800
igerber merged 2 commits into
mainfrom
dml-tutorial

Conversation

@igerber

@igerber igerber commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • New tutorial docs/tutorials/32_dml_did.ipynb (numbers-locked; committed outputs): the DMLDiD walkthrough required by the new-estimator checklist, now that feat(dml): DMLDiD survey-design and cluster support on both lanes (DML survey PR) #796 completed the survey/cluster surface.
  • The centerpiece is a bias demo on a staggered DGP with nonlinear confounding in BOTH nuisances: the linear outcome learner misses the DGP-implied truth (2.2388) by ~5 SE (2.5909 ± 0.0743) while the sieve (2.2804) and a hand-rolled duck-typed PolynomialRidge learner (2.2818) recover it via double robustness — robustness verified across 30 seeds in prototyping (linear mean bias +0.284, t = 16.9; sieve/poly t = 0.3).
  • Also covers: cross-fit diagnostics with the rate-condition caveat, event-study/group aggregation, HonestDiD (universal-base fit; relative-magnitude restriction stated in consecutive-first-difference units; robust CI qualified as an API illustration since the DGP deliberately violates the joint nuisance-rate condition), bootstrap sup-t bands on the default varying base (universal-base reference cells have structurally NaN bootstrap SEs), the panel=False RCS lane with its Assumption 2.3 warning as a teaching beat, a brief survey_design= example (PSU-cohesive folds, df = n_PSU − n_strata = 16), and seed semantics via two explicit seeds.
  • Registration: index.rst Advanced Methods card + toctree (meta count corrected to the post-add directory total, 33), tutorials README entries for 31 (previously missing) and 32, doc-deps tutorial entries on dml_did.py / dml_did_results.py / _learners.py, CHANGELOG, TODO row DML PR-B1 removed, and a dev-status kernelspec-note correction.
  • Companion drift test tests/test_t32_dml_did_drift.py (20 tests, ~0.3s): rendered-surface quote pins (including semantic pins for the Chang-vs-Sant'Anna-Zhao score-family distinction and the coverage qualifications), full DGP + fit re-derivation for sections 3/4/6/7 (survey lane included), ALL_CODE_CELL_HASHES source pins with a mutation negative control, source-fragment sync pins for the duplicated DGP/learner, an RCS warning gate requiring exactly one Assumption 2.3 UserWarning and failing on any other warning, and kernelspec/no-error hygiene. The sup-t cband golden forces the NumPy multiplier-weight backend (Rust draws a different, equally valid matrix from the same seed per the REGISTRY weight-backend identity Note); the suite passes under both backends.

Methodology references (required if estimator / math changes)

  • Method name(s): N/A — no estimator/math changes (documentation + tests only). The tutorial narrative follows the existing REGISTRY DMLDiD section (Chang 2020) and HonestDiD section (Rambachan & Roth 2023).
  • Paper / source link(s): Chang (2020), The Econometrics Journal 23(2), https://doi.org/10.1093/ectj/utaa001; Rambachan & Roth (2023), Review of Economic Studies 90(5).
  • Any intentional deviations from the source (and why): None — the notebook explicitly flags where its demo departs from theory (deliberately misspecified propensity ⇒ downstream SEs/CIs labeled illustrative, per the REGISTRY Theorem-1 rate-condition note).

Validation

  • Tests added/updated: tests/test_t32_dml_did_drift.py (new, 20 tests; green under both the Rust and pure-Python backends). tests/test_docs_ia.py + tests/test_doc_deps_integrity.py green after registration.
  • Backtest / simulation / notebook evidence (if applicable): notebook executed once via nbconvert on DIFF_DIFF_BACKEND=python and verified with a one-notebook nbmake run; every quoted number validated in prototype scripts before locking (30-seed robustness sweep for the bias demo); Sphinx -W docs build green.

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Fulfills the CONTRIBUTING new-estimator-checklist tutorial obligation for
DMLDiD (TODO row DML PR-B1, removed here) now that survey/cluster support
(#796) completed the estimator surface.

docs/tutorials/32_dml_did.ipynb (16 md + 15 code cells, committed outputs;
executed once via nbconvert on the pure-Python backend):

- A 600-unit staggered panel DGP with nonlinear confounding in BOTH
  nuisances (expit-of-quadratic assignment; nonlinear covariate-dependent
  trend), engineered so the bias demo is robust: across 30 seeds the linear
  outcome learner's mean bias is +0.284 (t = 16.9) while sieve and a
  hand-rolled duck-typed PolynomialRidge learner are unbiased (t = 0.3).
  At the locked display seed: linear 2.5909 vs truth 2.2388 (4.7 SE off),
  sieve 2.2804 / PolynomialRidge 2.2818 (within ~1 SE).
- The comparison estimand is overall_att with the DGP-implied truth
  computed from the fit's own per-cell n_treated weights (the panel lane's
  _aggregate_simple recipe). The narrative presents Chang's moment as a
  DISTINCT score family from the Sant'Anna-Zhao DR score (REGISTRY DR-score
  families note; conditional g-hat(X) vs unconditional treated share p-hat)
  and carries the Theorem-1 caveat: recovery works via double robustness
  with the propensity deliberately misspecified in every arm, so the
  table's SEs/CIs are illustrative, not theory-backed.
- Cross-fit diagnostics (p_hat / n_clipped / fold losses + the
  rate-condition caveat; n_clipped=0 framed as no-trimming-required, not
  established overlap), event-study/group aggregation, HonestDiD
  (universal-base main fit; relative-magnitude restriction stated in
  consecutive-first-difference units; robust CI [2.1393, 2.5677] at M=1.0,
  qualified as an API illustration), and a varying-base bootstrap fit for
  sup-t bands (universal-base reference cells have structurally NaN
  bootstrap SEs, so the sup-t demo keeps the default base; point estimates
  bit-identical; nominal-coverage caveat carried).
- panel=False RCS lane (A2.3 warning as the teaching beat) + a brief
  survey_design= mention (PSU-cohesive folds, df = n_PSU - n_strata = 16),
  and seed semantics via two explicit seeds (no committed seed=None cell).

Registration: index.rst Advanced Methods card + toctree (meta count set to
the post-add directory total 33, correcting a pre-existing off-by-one),
tutorials README catalog entries for 31 (previously missing) and 32,
doc-deps tutorial entries on dml_did.py / dml_did_results.py /
_learners.py, CHANGELOG, and a dev-status kernelspec-note correction (10
of 33 committed notebooks carry the plain python3 kernelspec the t31/t32
hygiene tests pin).

tests/test_t32_dml_did_drift.py (20 tests, 0.3s): rendered-surface quote
pins (including semantic pins for the score-family distinction, RM units,
and coverage qualifications), full DGP + fit re-derivation (sections
3/4/6/7 including the survey lane; the sup-t cband golden forces the NumPy
multiplier-weight backend, since Rust draws a different equally-valid
matrix from the same seed per the REGISTRY weight-backend identity Note —
verified under both backends), ALL_CODE_CELL_HASHES source pins with a
mutation negative control, source-fragment sync pins for the duplicated
DGP/learner, an RCS warning gate that requires exactly one Assumption 2.3
UserWarning and fails on any other warning, and the kernelspec/no-error
hygiene test.

Verified: nbmake run green on DIFF_DIFF_BACKEND=python; drift suite green
under both the Rust and pure-Python backends; docs-IA +
doc-deps-integrity green; Sphinx -W build green.
@github-actions

Copy link
Copy Markdown

Overall assessment

Looks good — no unmitigated P0/P1 findings.

Executive summary

  • No estimator, weighting, variance, identification, or default behavior changed.
  • Prior methodology concerns are resolved: score-family distinction, nuisance-rate caveat, HonestDiD units, and coverage qualifications are explicit.
  • Two P2 documentation inaccuracies remain.
  • Notebook JSON and test syntax validate. Full pytest execution was unavailable because pytest is not installed.

Methodology

  • P3 — Documented extensions/deviations.
    Impact: The staggered-cell construction, survey extension, multiplier bootstrap, and HonestDiD relative-magnitude FLCI extend or deviate from the cited papers. All are explicitly documented in REGISTRY.md, including the conservative naive FLCI and lack of survey coverage theory (docs/methodology/REGISTRY.md:L2822-L2830, L2930-L2936, L3028-L3048, L5672).
    Concrete fix: None required. Optionally label these as library extensions directly in the tutorial.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings. The comprehensive cell hashes and focused numerical re-derivations appropriately support the numbers-locked workflow.

Tech Debt

No findings. Removing the tutorial TODO is supported by the delivered notebook, registrations, and drift suite.

Security

No findings. No secrets or sensitive data were identified.

Documentation/Tests

  • P2 — Bootstrap point-estimate claim ignores the base-period change.
    Impact: The text says point estimates are bit-identical, but the analytical fit uses base_period="universal" while the bootstrap fit uses the default "varying". Only post-treatment cells and overall_att coincide; the displayed pre-treatment estimates differ (docs/tutorials/32_dml_did.ipynb:L1007-L1015, tests/test_t32_dml_did_drift.py:L353-L368).
    Concrete fix: State that the overall and post-treatment estimates are identical here, while pre-treatment estimates change with the base-period regime.

  • P2 — Custom-learner deepcopy contract is overstated.
    Impact: The tutorial says learners must survive copy.deepcopy, but the implementation warns and reuses non-copyable learners under a fit-reset assumption. Stateful learners can then leak across folds (docs/tutorials/32_dml_did.ipynb:L494-L497; diff_diff/_crossfit.py:L61-L92).
    Concrete fix: Document the warning-and-reuse fallback and advise that non-copyable learners must fully reset on every fit().

…ment the non-copyable-learner fallback

PR #800 review P2s: (1) the bootstrap fit keeps the default varying base
while the analytical fit is universal-base, so only the overall ATT and
post-treatment cells are bit-identical - the prose now says so and flags
the differing pre-treatment rows as a base-regime change, not a bootstrap
effect; (2) a learner that cannot be deep-copied is not rejected - it is
reused across folds with a loud UserWarning relying on fit-reset behavior,
so the prose now documents the fallback and the leak risk for stateful
non-copyable learners instead of claiming deepcopy is mandatory
(_crossfit._fresh_learner contract). Markdown-only; code cells, outputs,
and hashes untouched.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 545c31fcfbaf26f51be9c499488bf701b849c513


Overall assessment

Looks good — no unmitigated P0/P1 findings.

Executive summary

  • Documentation and tests only; estimator math and defaults are unchanged.
  • Both prior P2 findings are resolved.
  • DMLDiD, bootstrap, survey, and HonestDiD deviations are Registry-documented.
  • Static notebook, hash, syntax, and registration checks passed. Runtime tests were unavailable because pytest/numpy are not installed.

Methodology

  • P3 — Documented methodology extensions.
    Impact: Staggered DML cells, multiplier bootstrap, survey inference, and HonestDiD’s naive ΔRM FLCI extend or deviate from the papers, but are explicitly registered (docs/methodology/REGISTRY.md:L2930, L3028, L5672). The tutorial appropriately qualifies coverage (docs/tutorials/32_dml_did.ipynb:L490-L493, L1004-L1021).
    Concrete fix: None required.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

  • P3 — Paper-authored RCS replication remains tracked.
    Impact: The drift suite uses a library-authored RCS DGP rather than Chang’s §4 simulations; this is tracked in TODO.md:L85.
    Concrete fix: None required for this PR.

Security

No findings.

Documentation/Tests

  • P3 — Prior findings resolved.
    Impact: Bootstrap identity is now correctly limited to overall/post-treatment estimates, with pre-period differences explained (docs/tutorials/32_dml_did.ipynb:L1011-L1021). The non-copyable learner fallback is accurately documented (L494-L501) and matches implementation.
    Concrete fix: None required.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 29, 2026
@igerber
igerber merged commit f3f189b into main Aug 29, 2026
39 of 40 checks passed
@igerber
igerber deleted the dml-tutorial branch August 29, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant