Skip to content

fix(dml): fail closed when cross-fit learners cannot be isolated (mirror of #816) - #817

Closed
igerber wants to merge 6 commits into
mainfrom
mirror/816-dml-crossfit-isolation
Closed

igerber wants to merge 6 commits into
mainfrom
mirror/816-dml-crossfit-isolation

Conversation

@igerber

@igerber igerber commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Mirror of third-party PR #816 (fixes #814) so the label-gated CI and the CI AI reviewer can run on the contributor's change. The contributor's two commits are carried verbatim; one maintainer commit on top narrows a documentation claim. Do not merge this PR on its own - findings feed back to #816.

Summary

  • Reject custom DML learner templates whose deepcopy fails or returns the original object at configuration time, before any group-time cell is fitted (contributor commit 9cd5649).
  • Keep lazy per-fold cloning as a fail-closed backstop for direct cross_fit_predict callers; a per-fold clone failure propagates as a sanitized TypeError, never a NaN-cell skip; a failed re-fit clears prior fitted state; fold clones are re-validated for the learner protocol and, on declared survey fits, sample_weight support (contributor commit bb187a3).
  • Narrow the "before any cell" wording on the changelog fragment, API guide, REGISTRY, and tutorial 32 so only the one-copy preflight carries that guarantee (maintainer commit 50109fe).

Methodology references (required if estimator / math changes)

  • Method name(s): DMLDiD cross-fitting learner isolation (_crossfit.py, _learners.py, dml_did.py)
  • Paper / source link(s): Chang (2020), Double/debiased machine learning for difference-in-differences models - the out-of-fold nuisance contract this hardens
  • Any intentional deviations from the source (and why): None. No score, weighting, aggregation, or variance change; built-in learner outputs are unchanged.

Validation

  • Tests added/updated: tests/test_crossfit.py, tests/test_dml_did.py (copy failure, self-returning __deepcopy__, clone protocol loss, weighted clone loss, panel/RCS hard-error propagation, re-fit state clearing, survey preflight, sanitized traceback, sklearn preflight via importorskip)
  • Backtest / simulation / notebook evidence (if applicable): tutorial 32 markdown-only wording update, no re-execution needed

Security / privacy

  • Confirm no secrets/PII in this PR: Yes (the token=SECRET-... strings in the new tests are fake sentinels asserting that foreign exception text is withheld)

Changelog

  • changelog.d/ fragment added (or N/A - no user-visible change): Yes (changelog.d/20260903-dml-crossfit-learner-isolation.md)

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Overall assessment

✅ Looks good — no unmitigated P0 or P1 findings.

Executive summary

  • Affects DMLDiD learner isolation only; scores, weighting, aggregation, and variance remain unchanged.
  • Hard failure preserves Chang’s requirement that nuisance learners train only on fold complements.
  • Per-fold protocol and survey-weight checks correctly propagate TypeError.
  • Failed refits now consistently clear prior fitted state.
  • One minor documentation ambiguity remains.

Methodology

  • Severity: P3 — informational, mitigated
    • Impact: Only top-level clone identity is verified; a custom __deepcopy__ may still share nested mutable state. This is explicitly documented in docs/methodology/REGISTRY.md:L838-L846, so it is not a defect under the review policy.
    • Concrete fix: None required.
    • Chang Algorithm 1’s auxiliary-sample contract remains intact; no score or SE formulas changed (docs/methodology/papers/chang-2020-review.md:L171-L181, diff_diff/_crossfit.py:L579-L607).

Code Quality

No findings. Clone, protocol, and sample-weight validation are centralized and errors remain targeted.

Performance

No findings. The additional configuration-time copies are bounded and appropriate for validating learner isolation.

Maintainability

No findings. The same helpers cover DMLDiD and direct cross_fit_predict callers.

Tech Debt

No new untracked correctness debt identified.

Security

No findings. Foreign copy-exception messages and traceback context are suppressed at diff_diff/_crossfit.py:L70-L86; tests cover secret-bearing sentinel messages.

Documentation/Tests

  • Severity: P3
    • Impact: The fallback-era phrase “deep-copies the template where copyable” remains in diff_diff/dml_did.py:L294-L298 and docs/methodology/REGISTRY.md:L2930-L2937. It is ambiguous now that uncopyable templates always raise.
    • Concrete fix: Replace it with “requires a distinct deep copy and raises TypeError otherwise”; explicitly say callers must supply an initially unfitted template.

Test coverage is strong across panel/RCS, survey weighting, protocol loss, sanitized errors, and failed-refit cleanup. Runtime tests could not be executed because pytest/NumPy are unavailable; changed Python files passed AST parsing, the notebook passed JSON parsing, and the changelog-fragment checker passed.

shawcharles and others added 5 commits September 26, 2026 21:16
Context:
- Cross-fit reuse after a failed or identity-returning deepcopy can carry fitted state and prior-fold training data into a later fold.
- DMLDiD must reject an invalid learner template before any group-time cell is fitted.

Changes:
- Require a distinct top-level deepcopy for cross-fit learner templates and raise a sanitised TypeError otherwise.
- Probe custom DML learner specifications during configuration validation while retaining the direct cross_fit_predict backstop.
- Document the top-level clone boundary and cover copy failures, identity copies, sklearn templates, exact OOF isolation, and pre-cell error propagation.

Verification:
- pytest -q tests/test_crossfit.py tests/test_dml_did.py tests/test_changelog_fragments.py tests/test_docs_ia.py tests/test_doc_deps_integrity.py
- Python 3.9 targeted regression tests, including sklearn preflight
- ruff, black, scoped mypy, changelog compiler, and native cross-fit output comparison against upstream/main
- Not run: full default suite was stopped after an interrupted long-running attempt.
Context:
- Cross-fit learner templates must fail closed when cloning cannot provide an independent, usable learner, without exposing stale DML results after a failed re-fit.

Changes:
- Validate clone protocol and weighted-fit capability during DML preflight and for each direct cross-fit fold; delayed errors name the public nuisance parameter.
- Clear DMLDiD fitted state before every fit attempt, and cover panel, repeated-cross-section, survey, and direct helper failure paths.
- Correct the DML tutorial and documentation registry, and record the warning-to-TypeError behavioural change.

Verification:
- /tmp/diff-diff-814-python39/bin/python -m pytest -q tests/test_crossfit.py tests/test_dml_did.py tests/test_survey_dml.py tests/test_changelog_fragments.py tests/test_docs_ia.py tests/test_doc_deps_integrity.py (665 passed, 6 skipped)
- black --check, ruff check, and mypy --follow-imports=skip on touched source/test modules
- python -m json.tool docs/tutorials/32_dml_did.ipynb and python .claude/scripts/changelog_compile.py check
- Not run: full repository test suite.
Context:
- PR #816 review identified overbroad pre-cell failure claims and optional sklearn tests that CI never exercised.

Changes:
- Adapt the documentation corrections from mirror commit 50109fe to distinguish preflight failures from later hard TypeErrors.
- Require unfitted templates and distinct copies in both seed descriptions.
- Add unpinned scikit-learn to the dev extra and both Rust workflow dependency lists; retain importorskip guards.
- Leave estimator execution and the separate StackedDiD sentinel contract unchanged.

Verification:
- Python 3.12 focused cross-fit, DML, changelog and documentation suites: 582 passed.
- Both sklearn integration tests passed without skips on Python 3.9.
- Black, Ruff, changelog compiler, TOML/YAML and notebook JSON validation passed.
- Known failure: test_sklearn_clone_round_trip_if_available[StackedDiD] fails on sentinel parameter equality with sklearn installed; reproduced on Python 3.9 in both this branch and unchanged base e982340.
- Not run: full repository suite, Sphinx build or remote CI matrix.
Context:
- Adding scikit-learn to CI enables the StackedDiD clone round-trip test, which fails because copied omitted-parameter sentinels compare unequal.

Changes:
- Return self from the stateless _NotSupplied marker's shallow and deep copy methods.
- Cover sentinel identity, isolation of neighbouring mutable values, and canonical and deprecated StackedDiD control parameters during sklearn cloning.
- Preserve alias resolution, deprecation warnings and existing pickle behaviour; add a changelog fragment and correct the clone-test dependency description.

Verification:
- Reproduced six copy/clone failures before the source fix.
- Python 3.13 focused estimator, clone, changelog and documentation checks: 1010 passed, 13 skipped.
- Python 3.9 estimator, clone, survey DML, methodology and bad-control parity checks: 790 passed, 13 skipped, 5 deselected.
- Ruff and Black across diff_diff and tests, whole-package mypy, changelog compiler, notebook JSON and git diff --check passed.
- Not run: Rust backend tests, notebook execution and remote CI matrix. The broader pure-Python suite is still running.
Context:
- The rebase retained two descriptions of the removed warning-and-reuse path, contradicting the fail-closed learner contract.

Changes:
- Align the methodology registry and documentation dependency note with lazy per-fold cloning and sanitised TypeError propagation.
- State that failures never reuse the template or become DegenerateFoldError skips; leave runtime code and unrelated baseline failures unchanged.

Verification:
- Documentation-dependency, documentation-IA, tutorial-drift and changelog tests: 367 passed.
- Changelog compiler, YAML parsing, stale-fallback text scan and git diff --check passed.
- Not run for this documentation-only correction: full suite, Rust matrix, Sphinx build or notebook execution; Sphinx and nbmake are absent from the verification environment.
@igerber
igerber force-pushed the mirror/816-dml-crossfit-isolation branch from 50109fe to ac1ef63 Compare September 27, 2026 12:09
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: ac1ef63c3f022980a54636bdef46bf168e3f78b6


Overall assessment

✅ Looks good — no unmitigated P0 or P1 findings.

Executive summary

  • DMLDiD learner isolation now fails closed without changing scores, weighting, aggregation, or variance.
  • This preserves Chang’s auxiliary-sample cross-fitting contract.
  • The prior ambiguous “where copyable” wording is resolved.
  • Clone failures are sanitized, propagate as hard errors, and clear stale fitted state.
  • One stale internal dependency decision remains.

Methodology

  • Severity: P3 — informational, mitigated
    • Impact: Top-level identity cannot prove isolation of nested mutable state in custom __deepcopy__ implementations.
    • Concrete fix: None required; this limitation is explicitly documented at docs/methodology/REGISTRY.md:L864-L872.
    • Chang Algorithm 1’s fold-complement training remains intact (docs/methodology/papers/chang-2020-review.md:L171-L181). No estimator or SE formulas changed.

Code Quality

No findings. Clone, protocol, and weighted-fit validation are centralized in diff_diff/_crossfit.py:L64-L115.

Performance

No findings. The configuration-time copies add bounded overhead.

Maintainability

  • Severity: P2
    • Impact: DEFERRED.md:L152 says scikit-learn deliberately remains outside dev dependencies and clone tests are opportunistic, while pyproject.toml:L58-L64 now makes it a dev dependency. The decision record is misleading.
    • Concrete fix: Remove or rewrite that DEFERRED row to record the new policy: scikit-learn is required for development/CI but remains optional at runtime.

Tech Debt

No new untracked correctness debt.

Security

No findings. Copy-exception text and traceback context are suppressed, while only exception and learner class names are exposed.

Documentation/Tests

No findings. Both behavioral changes have changelog fragments, and the prior documentation ambiguity is fixed.

Static parsing and the changelog-fragment check passed. Runtime tests could not run because the environment lacks pytest and the scientific Python dependencies.

…optional at runtime

Supersedes the 2026-08-01 decision row that kept scikit-learn out of the dev
extra; #816 adds it (unpinned) to the dev extra and both rust-test.yml
install steps so the sklearn clone round-trip and DML cross-fit learner
tests always run in CI. Runtime posture unchanged; importorskip guards stay.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 37b93fc0618115eb45838fd95a90dc81ea498205


Overall assessment

✅ Looks good — no unmitigated P0 or P1 findings.

Executive summary

  • DMLDiD cross-fitting learner isolation is the affected method; estimator scores, weighting, aggregation, and variance are unchanged.
  • Fail-closed cloning enforces Chang’s fold-complement nuisance-training contract.
  • Clone failures propagate as sanitized hard errors and clear stale fitted state.
  • The documented nested-state limitation is P3-informational.
  • The prior scikit-learn dependency-record inconsistency is resolved.

Methodology

  • Severity: P3 — informational, mitigated
    • Impact: Top-level identity cannot prove isolation of nested mutable state in custom __deepcopy__ implementations.
    • Concrete fix: None required; explicitly documented in docs/methodology/REGISTRY.md:L864-L872.
    • Chang Algorithm 1’s auxiliary-sample contract remains intact (docs/methodology/papers/chang-2020-review.md:L171-L181), with clone failures correctly escaping cell-skip handling (diff_diff/_crossfit.py:L585-L607).

Code Quality

No findings. Clone, protocol, and weighted-fit validation are centralized in diff_diff/_crossfit.py:L64-L115.

Performance

No findings. Preflight copies add bounded configuration-time overhead.

Maintainability

No findings. The prior P2 is resolved: DEFERRED.md:L152 now agrees with pyproject.toml:L58-L64 and the CI installation steps.

Tech Debt

No new untracked correctness debt.

Security

No findings. Copy-exception messages and chaining are suppressed while retaining actionable class names.

Documentation/Tests

No findings. Both user-visible changes have valid changelog fragments, and tests cover panel/RCS propagation, bad-control stages, survey weighting, stale-state clearing, sanitization, and sentinel cloning.

In-memory compilation and the changelog-fragment check passed. Runtime tests could not run because this environment lacks pytest and NumPy.

@igerber

igerber commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Mirror served its purpose: the CI review is clean at 37b93fc, which is also the head of the contributor's PR #816 - the one to merge.

@igerber igerber closed this Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DML cross-fitting can reuse stateful custom learners across folds

3 participants