Skip to content

fix: make unpurged feature-importance CV impossible to get silently - #27

Merged
Sean-Koval merged 3 commits into
mainfrom
fix/mda-purging
Aug 31, 2026
Merged

Sean-Koval merged 3 commits into
mainfrom
fix/mda-purging

Conversation

@Sean-Koval

Copy link
Copy Markdown
Contributor

⚠️ BREAKING, and deliberately so — calls omitting event_end_indices now raise instead of
returning leaked results. Fixed on the owner's instruction.

The bug

mda_importance(..., event_end_indices=None) purged nothing.

_build_intervals(None, n) produced (i, i) for every sample, so _overlaps collapsed to i == t
and the purge loop only ever removed rows that were already in the test fold.

Measured, not theorised — n=100, 5 folds, embargo=0:

Training rows kept in fold 1
Default (event_end_indices=None) 80 of 80 — zero purged
Real label spans 60

So anyone calling it with defaults got silent leakage from the exact routine meant to prevent it,
and their results looked better than they were.

Found by an agent verifying the Python workflow by actually running it — not by reading it.

The fix

Raise unless the caller either passes spans or opts out explicitly with allow_unpurged=True.

The alternative — inventing a default horizon — was rejected on the grounds that it swaps silent
leakage for silent fabrication. Better to make the caller state their intent.

The result now reports cv["method"] (purged_kfold vs kfold_embargo_only) and cv["purged"],
so an opt-out cannot masquerade as purging.

The error message explains the mechanism and the remedy rather than just failing.

Two sibling routines had the identical bug

sfi_importance and substitution_effect_report — same shape, both fixed. The Rust PurgedKFold
and hyperparameter_tuning already require samples_info_sets and were never affected.

Verification

  • Regression test written first and proven to fail against the pre-fix code: 4 failed
    (3 × DID NOT RAISE), 5 passed after the fix
  • Full suite: 37 passed · cargo fmt --check 0 · zero Rust changed
  • Docs gates after the doc updates: astro build 0 · check-content-schema 0 · check-links 0 ·
    check:api-drift 0

No existing test broke — because nothing was asserting the leaked behaviour.

Docs updated in the same PR

Four documented call sites would have started raising. They now pass event_end_indices and explain
why it is required. workflows/python-core-workflow.md had honestly documented the bug; it now
documents the fix.

Nothing automated caught thatcheck:examples compiles Rust blocks only. A Python equivalent
is tracked separately, and this PR is the argument for it.

Sean-Koval and others added 3 commits August 30, 2026 20:50
`_build_intervals(None, n)` yields degenerate one-row intervals `(i, i)`,
which can only overlap the test rows themselves, so the purge step in
`_purged_kfold_splits` removes zero additional training rows while the
returned cv block still reports `method == "purged_kfold"`.

Fails against current code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYrS8fJ2Hi75CtuEy6AZVW
`_build_intervals(None, n)` built degenerate one-row intervals `(i, i)`.
`_overlaps` reduces to `i == t` for two such intervals, so the purge loop
only ever cleared rows that were already the test rows: the training set
was every non-test row and `mda_importance`/`sfi_importance`/
`substitution_effect_report` returned leaked scores while reporting
`cv["method"] == "purged_kfold"`.

Measured on n=100, 5 folds, pct_embargo=0.0: the default trains fold 1 on
all 80 non-test rows (zero purged); with real spans it trains on 60.

`_build_intervals` now raises unless the caller passes label spans or opts
out explicitly with `allow_unpurged=True`, and the `cv` block reports
`purged` plus `kfold_embargo_only` so the opt-out cannot masquerade as
purging. Deriving spans was rejected: an invented horizon replaces silent
leakage with silent fabrication.

BREAKING: calls that omitted `event_end_indices` now raise ValueError.
That is the point -- they were returning leaked results.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYrS8fJ2Hi75CtuEy6AZVW
The purging fix makes mda_importance/sfi_importance/substitution_effect_report
raise when event_end_indices is omitted. Four documented call sites still
called them without it, and would now raise:

- moduleDocs.ts feature-diagnostics example (and its generated page)
- workflows/python-core-workflow.md, which described the old buggy default
- docs/python_bindings.md, which advertised purged defaults

The examples now pass event_end_indices and explain why it is required, and
show that cv.purged / cv.method make an explicit opt-out visible.

Nothing automated caught this: check:examples covers Rust blocks only. A
Python equivalent is tracked separately.
@Sean-Koval
Sean-Koval merged commit d98f27a into main Aug 31, 2026
2 of 6 checks passed
@Sean-Koval
Sean-Koval deleted the fix/mda-purging branch August 31, 2026 04:00
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.

1 participant