Repository navigation
Prevent overlapping ERP epoch windows in chronological within-session CV - #1235
lindicaphxag-tech wants to merge 56 commits into
Conversation
|
I am not super supportive of this design, like, I feel that we can have the same thing just using the native split or implement some time split CV with stratification. The PR is also touch many extra sensitive areas of moabb. |
|
Thanks for the design feedback. I checked the native alternatives against issue #280: I agree the cross-cutting integration needs a clear scope boundary. Would you prefer this issue to cover only run-held-out evaluation (which leaves single-run recordings unresolved), or is the opt-in single-run time/purge protocol in scope? I am keeping the PR as draft until that design question is resolved. I also fixed the two failures from the first full CI run: MNE offsets event samples when concatenating runs, so the regression now compares timing within each run; and the cross-run purge test now uses valid non-overlapping epochs with an explicit cross-run overlap candidate. I moved metadata assertions back into their original test. On the new head |
|
Update on the new head: I merged current |
|
CI update for head |
|
I followed up on the stratified-time-split suggestion in commit |
|
CI has advanced on current head |
|
I’ve made the draft ready for review. The stratified-time suggestion is implemented with dynamic-programming boundary selection while keeping each test fold contiguous and purging exact sample overlap. The complete upstream matrix is green now: Ubuntu Python 3.11–3.14, macOS/Windows Python 3.12, docs, and the braindecode-dev job; pre-commit, link, changelog, and Codecov checks also pass. Event timing is opt-in, so the default paradigm metadata schema is unchanged. Please review the updated scope and let me know if you want a narrower integration. |
|
Can you do some visual, ala scikit learn, to represent exactly what this split is? I have the feeling that we are making a mistake here, but I may be wrong. |
|
A concrete narrower implementation is available for your scope review: fork-only Draft PR #6 (not a competing upstream submission). Since the earlier version, I corrected a scientifically meaningful recording-identity issue: identical |
…r overflowing intervals in upstream review PR
…R, including translation and overflow
… two Python versions
…ding bot-head drift
… epoch overlap purging
…andas dtype widening
…ter pandas object conversion
…32 precision-bound source SHA
…venance before object conversion
… precision-provenance HEAD
bruAristimunha
left a comment
There was a problem hiding this comment.
Thanks @lindicaphxag-tech for the work, the figure and the honest scope note on filtering. One review round on the current head (the branch also conflicts with develop now):
The problem in #280 is real, but this version is too large for what it guarantees: about 350 lines in splitters.py (a dynamic-programming boundary search and extensive float16/float32 coordinate handling), a new include_epoch_timing option in BaseParadigm.get_data, and evaluation changes to inject the groups. And, as the docstring says, it still does not isolate train and test after the continuous-recording filter, which is the main leakage path for overlapping ERP epochs in MOABB.
The splitter-only shape of your fork draft (lindicaphxag-tech#6, no change to BaseParadigm.get_data or the evaluations) is the right direction: a standalone splitter that takes the event timing as groups, used through cv_class / cv_kwargs. Trimmed further:
- keep only the contiguous within-run blocks and the half-open overlap purge;
- drop the DP class balancing and the extra CI workflow (fail with a clear message when a fold lacks a class, as you already do) and the float16/float32 special cases (accept integer sample columns, reject the rest);
- document the filtering limitation in the class docstring, as now.
If you open that as a new PR, I'll review it quickly. I'll leave this one open until then so the discussion stays linked.
# Conflicts: # docs/source/whats_new.rst # moabb/tests/test_evaluations.py # moabb/tests/test_splits.py
|
Following the requested scope reduction, I opened #1329 as a new standalone upstream PR. It is built from current The independent Link Check currently fails only on two existing |
Summary
Addresses the direct shared-raw-sample failure described in #280: an opt-in chronological within-session CV splitter that removes overlapping epoch extraction intervals from train/test folds. It does not guarantee post-filter signal independence.
Problem
Random trial-wise CV can put epochs that share samples from the same continuous recording into train and test. With a 500-sample epoch and 100-sample SOA, trial-disjoint folds are not signal-disjoint.
Approach
PurgedEpochKFold, a sklearn-compatible group-aware splitter;(subject, session, run)identity so reused run names cannot cause false cross-session/subject purges;event_sampleandepoch_n_samplesonly on demand, preserving the default paradigm metadata schema;WithinSessionEvaluation(..., cv_class=PurgedEpochKFold)is selected;n_purged_overlapandpurge_fractionas per-fold protocol diagnostics.Exact large-integer interval correctness (current head)
The original sample parser passed integer event/sample-window coordinates through
float64; above2**53that can collapse distinct integer sample indices and invalidate exact half-open overlap decisions.fb067c4preserves trueint64sample identity, refuses ambiguous large floating inputs, and prevents overflow when constructing interval stops.d0100fcadds exact translation-invariance, boundary and fail-closed regressions; pre-commit formatting45a35dcwas an earlier review head. The latest source has additional floating-dtype provenance validation.Independent exact-source native CI: Python 3.11/3.13 focused PurgedEpochKFold tests both SUCCESS. That workflow checks out the immutable
45a35dcSHA and runs the actualmoabb/tests/test_splits.py -k purged_epochtests, rather than duplicating the parser in a toy implementation. The official full test matrix on the current head runs separately; focused green tests do not replace full upstream acceptance.Floating-point precision provenance (latest bot-formatted Head
fc75f39)Even valid finite integer-looking timing values can already have lost one sample of precision in their original input type.
float32(2**24+1)equalsfloat32(2**24);float16(2**11+1)similarly aliasesfloat16(2**11). A further trap:pandas.DataFrame.to_numpy(dtype=object)can widen NumPy and nullableFloat32columns into Python floats, erasing their ORIGINAL precision and allowing a too-late float64-only check to miss the collision.The new source changes
1940231and nullable-float extensionc0d742efail closed before dtype widening and also guard direct object-array NumPy float scalar inputs. Five original tests and two nullable-pandas tests cover lost low-precision integer identity, missing samples, safe small floats, direct bypass attempts and interval widths. Bot commitfc75f39only reformatted two test statements, with no behavior change.First exact-source run
27dd803passed BOTH Python 3.11 and 3.13. Independent validation of latest bot-formattedfc75f39is running separately from the latest full MOABB upstream matrix. All regression evidence is on actual splitter source, not a fabricated reproduction. The existing preprocessing/IIR scope caveat remains unchanged.Reused run-label correctness
Run labels like
"0"are routinely reused across subjects or sessions. The original splitter grouped solely byrun, which could place independent recordings on the same sample axis and incorrectly remove training epochs that merely have overlapping numeric sample indices from different raw files. Source fixdfa052aencodes the available(subject, session, run)tuple without changing the three-column groups shape or default metadata. Regressioncfca859verifies independent sessions, subjects and refusal of missing recording identity. The cross-recording fix first reached bot-formatted Head3c76a2c. Independent immutable-SHA validation passed both Python 3.11 and 3.13 on the realmoabb/tests/test_splits.py -k purged_epochregression subset. The upstream full cross-platform matrix and maintainer Review are separate gates.Minimal usage
No manual chronology labels are required; the evaluation requests event timing from the processed epoch stream.
Regression design
The synthetic witness uses 100-sample SOA and 500-sample epochs:
StratifiedKFoldhas train/test interval overlap;PurgedEpochKFoldhas zero same-run overlap;BaseParadigm.get_data()keeps its existingsubject/session/runmetadata by default and only adds timing when requested.Scope / limitation
Important preprocessing caveat discovered in review:
BaseParadigm.make_process_pipelines()applies the standard continuous-Raw IIR band-pass before cutting epochs. A zero-phase IIR filter has nonlocal temporal response, so disjoint extraction intervals do not prove that the resulting filtered signals contain no cross-boundary contribution. This PR prevents direct reuse of overlapping raw sample-index windows; it does not claim strict signal independence or eliminate all possible temporal leakage. A stricter protocol requires fold-aware preprocessing or independently justified transformation-specific support. A new synthetic regression (moabb/tests/test_purged_preprocessing_boundary.py) demonstrates this counterexample explicitly and preserves the narrower contract.The v1 contract assumes a fixed event-locked epoch window within an evaluated partition. The interval is represented on the processed epoch sample grid. It intentionally does not infer chronology from DataFrame row order and does not claim to model arbitrary time-warping post-processing. If a chronological partition cannot provide class support in every fold, the splitter fails closed rather than interleaving time blocks.
The review suggestion to use time-split CV with stratification is addressed by selecting near-equal contiguous boundaries with dynamic programming; temporal order remains intact and overlap is purged exactly. Earlier heads completed the full upstream matrix. The exact
fc75f39precision-provenance head passed the official full test matrix on Ubuntu Python 3.11–3.14, macOS Python 3.12, and Windows Python 3.12, plus Docs, link/news checks and Braindecode integration. Maintainer approval of the API/scope is still required. The four runtime files carry timing metadata only for this opt-in evaluation path; default metadata remains unchanged. This PR is ready for review.Exact-head validation update (2026-10-13): The current head
fc75f39b498b44db5b8663ddaa05d30cb0a51a32has completed the official Test matrix successfully on Ubuntu Python 3.11/3.12/3.13/3.14, Windows Python 3.12, and macOS Python 3.12. Docs, news check, link check, and braindecode-dev integration are also successful. No fresh CI/merge-conflict blocker is known; the outstanding gate is maintainer approval of the API/scope. The guarantee is exclusively direct raw-epoch interval nonoverlap, not post-filtered signal independence (whole-recording IIR preprocessing remains an explicit limitation). The narrow alternative exists only as a fork Draft, not as a competing upstream PR.