Part VII: Health, Biosignals, and Wearable AI
Chapter 28: Biosignal Foundations

Patient-level splits and leakage

"I scored 0.98 on the held-out set. It turns out I had simply learned to recognize Mr. Patel's heartbeat, and Mr. Patel was in both piles."

An Overfitted AI Agent

Prerequisites

This section builds directly on the general leakage-safe dataset discipline introduced in Chapter 5 and specializes it to physiology. It assumes you have met the biosignals and their labels earlier in this chapter (Sections 28.1 and 28.5) and are comfortable with basic cross-validation. No new mathematics is required beyond counting groups.

The Big Picture

A biosignal carries two things at once: the clinical event you want to detect, and an indelible signature of the person it was recorded from. A resting ECG identifies an individual almost as reliably as a fingerprint. If even one heartbeat from a patient lands in your training set and another lands in your test set, your model can score brilliantly by memorizing the person rather than learning the pathology. Patient-level splitting is the cheapest, highest-leverage decision in biosignal machine learning: skip it, and a paper's 0.97 AUC quietly collapses to 0.71 the moment it meets a patient the model has never seen.

Why a random split lies to you

The default in most tutorials is a random split: shuffle every window, deal 80 percent to train and 20 percent to test. For independent samples this is fine. Biosignal windows are not independent. Consecutive heartbeats from one Holter recording share the same lead placement, the same electrode-skin impedance, the same QRS morphology, the same baseline wander, and the same idiosyncratic physiology. Two windows one second apart are near-duplicates. A random split scatters these near-duplicates across train and test, so the test set is answered largely by lookup rather than by generalization.

The consequence is a systematic optimistic bias. Formally, honest generalization asks how a model trained on patients \(P_{\text{train}}\) performs on a new patient \(p^\ast \notin P_{\text{train}}\). A random split instead estimates performance conditioned on having already seen \(p^\ast\), a quantity no deployment will ever enjoy. The gap between the two can be enormous. On arrhythmia and seizure benchmarks, moving from record-mixed to subject-disjoint evaluation routinely erases ten to thirty points of reported accuracy. The model was never that good; the protocol was that generous.

Key Insight

The correct unit of splitting is not the window, the beat, or the recording. It is the person. Every sample tied to one individual must live entirely on one side of the train/validation/test partition. Anything finer than the person leaks identity, and identity in physiology is a shortcut so predictive that gradient descent will always take it if you leave the door open.

Misconception: A Fixed Random Seed Is Not the Same as Patient-Disjoint

Many practitioners assume that setting random_state=42, or otherwise fixing a seed, makes their split trustworthy because it is reproducible. Reproducibility and independence are different properties: a fixed seed guarantees you get the same split every time, not a leak-free one. If that split happens to scatter one patient's windows across train and test, every rerun reproduces the identical leak, so the inflated score looks stable across reruns. That stability is exactly what lets the mistake survive review: reviewers check that a result reproduces, not that the split respects group structure.

The taxonomy of biosignal leakage

Patient overlap is the headline offender, but several quieter leaks share the same signature: a validation score that will not survive contact with a new hospital. Naming them makes them checkable.

Identity leakage is the same person in two splits, the case above. Recording leakage is subtler: even with disjoint patients, overlapping sliding windows from a single recording can straddle a fold boundary, so a window centered at \(t\) and another at \(t+1\) share most of their samples and are effectively the same example counted twice. The fix is a guard gap: after assigning windows to folds, drop any window whose center falls within one window length of the boundary, so no training sample shares a time step with a test sample. Size the gap to the longest dependency the model can exploit, usually the window length itself and wider under overlapping augmentation; apply it whenever the stride is shorter than the window length, the default for sliding-window pipelines. Device and site leakage is the trap where a rare class was collected on one machine or at one clinic: the model learns the device's transfer function, not the disease, a failure mode we return to under distribution shift in Chapter 66. Preprocessing leakage happens when you fit a normalizer, a filter's statistics, or a PCA basis on the full dataset before splitting, letting test-set statistics bleed into training; fit every transform on the training fold only. Label leakage occurs when the reference standard (Section 28.5) encodes a shortcut, for example a lead that was only connected when the annotating cardiologist already suspected the finding.

A wearable that aced the lab and failed the wrist

A team building an atrial-fibrillation detector for a consumer smartwatch trained on a PPG corpus of 400 volunteers. They used a random 80/20 split of ten-second windows and reported 0.96 AUC. The regulatory reviewer asked a single question: were any volunteers in both sets? They were. Roughly 30 windows per person, scattered randomly, meant almost every test window had a sibling in training. Re-run with a patient-disjoint split, the AUC fell to 0.79, and the false-positive rate on healthy wrists tripled. The model had partly learned each volunteer's pulse-wave shape, which is stable within a person and useless across people. The fix cost three lines of code; discovering it after launch would have cost a recall. This is the same discipline that Chapter 30 treats as non-negotiable for any PPG cardiovascular claim.

Grouped cross-validation, and its stratified cousin

The mechanism that prevents identity leakage is grouped cross-validation: attach a group label (the patient ID) to every sample and require that no group crosses a fold boundary. In scikit-learn this is GroupKFold. The complication is that clinical datasets are also severely imbalanced: a positive class may live in a handful of patients, and a naive grouped split can hand an entire fold zero positives, making its metric meaningless. StratifiedGroupKFold solves both at once, keeping groups intact and balancing class proportions across folds as far as the group structure allows.

import numpy as np
from sklearn.model_selection import StratifiedKFold, StratifiedGroupKFold
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score

rng = np.random.default_rng(0)
n_patients, beats = 60, 40
# Each patient has an identity signature; label depends only on a weak clinical feature.
pid   = np.repeat(np.arange(n_patients), beats)
ident = rng.normal(size=n_patients)[pid]                 # strong per-person shortcut
y     = np.repeat(rng.integers(0, 2, n_patients), beats) # label is a patient property
clin  = 0.4 * y + rng.normal(scale=1.0, size=len(y))     # weak honest signal
X     = np.column_stack([clin, ident + rng.normal(scale=0.05, size=len(y))])

def cv_auc(splitter, groups=None):
    scores = []
    for tr, te in splitter.split(X, y, groups):
        m = LogisticRegression().fit(X[tr], y[tr])
        scores.append(roc_auc_score(y[te], m.predict_proba(X[te])[:, 1]))
    return np.mean(scores)

naive   = cv_auc(StratifiedKFold(5, shuffle=True, random_state=0))
grouped = cv_auc(StratifiedGroupKFold(5), groups=pid)
print(f"random (leaky) AUC : {naive:.3f}")
print(f"patient-disjoint AUC: {grouped:.3f}")
The random split lets the model exploit the per-person ident feature and reports an inflated AUC; the patient-disjoint split forces it to rely on the weak honest clin signal and reveals the true, far lower score. The only change is passing groups=pid to a grouped splitter.

Run this and the two numbers disagree sharply, and the disagreement is the entire lesson: the gap between them measures exactly how much your reported score depends on having seen the patient before. Report both during development and treat the gap as a leakage alarm, not a nuisance.

Right Tool: StratifiedGroupKFold

Writing a correct stratified-and-grouped partitioner by hand (greedy group assignment that balances both class counts and fold sizes, with reproducible ordering) is roughly 60 to 80 lines and easy to get subtly wrong. Scikit-learn's StratifiedGroupKFold does it in one line and guarantees no group crosses a fold. Pair it with sklearn.model_selection.GroupShuffleSplit for a single held-out patient set. You supply the patient IDs; the library handles the combinatorics.

Nested groups, and when the person is not enough

Sometimes the person is the wrong grouping key because a coarser correlation dominates. If each hospital used a different ECG cart, or each study site enrolled a different demographic, then even patient-disjoint folds can leak site identity. The principle generalizes: split on the coarsest unit that shares a nuisance signature. For a multi-site trial that often means leave-one-site-out evaluation nested inside patient-disjoint folds, which simultaneously stress-tests generalization to new hardware and new populations. This hierarchical view (person within device within site) connects to the broader benchmarking protocol developed in Chapter 65.

A related caution belongs to privacy, not just accuracy. Because a physiological signal re-identifies its owner, a model that has memorized patients can leak who they are, an attack surface that Chapter 34 examines under biometric re-identification. Patient-disjoint splitting is thus doing double duty: it makes your metrics honest and it discourages the model from encoding identity in the first place.

Research Frontier: Cross-Dataset Patient Leakage in Foundation Models

Patient-disjoint splitting inside one dataset is not the end of the story. Large sensor and biosignal foundation models (Chapter 20) pretrain on recordings pooled from many public sources that are not always disjoint from one another: archives get re-released under new names, and a patient recorded for one arrhythmia study may resurface in an unrelated sleep-staging release. If a downstream benchmark's test patients were already present, even indirectly, in the pretraining corpus, the model can encode their identity before fine-tuning ever begins. Auditing pretraining corpora for cross-dataset subject overlap, typically by fingerprinting the raw waveform rather than trusting stated patient IDs, is an active, largely unsolved problem as these models scale.

Exercise

Take any public arrhythmia dataset with a patient ID column (MIT-BIH is a good choice). Train the same classifier twice: once with StratifiedKFold on shuffled beats, once with StratifiedGroupKFold grouped by patient. (1) Report both mean AUCs and the gap. (2) Add overlapping sliding windows with a 50 percent stride and confirm the gap widens. (3) Introduce a guard gap that drops windows within one window length of a fold boundary and show the guard gap shrinks the residual leak in the grouped split. Write one sentence stating which number you would put in a paper and why.

Self-Check

  1. Why does splitting on individual windows, rather than on patients, produce an optimistically biased estimate of clinical performance?
  2. You have a positive class present in only 6 of 200 patients. Which splitter do you reach for, and what failure does plain GroupKFold risk here?
  3. Name two leaks that survive a correct patient-disjoint split, and give the one-line fix for each.

What's Next

In Section 28.7, we widen the lens from evaluation hygiene to the regulatory and safety context that surrounds any biosignal model destined for a real body, previewing the standards, risk classes, and clinical-validation expectations that later chapters in this part make concrete.