Part I: Foundations of Sensory AI
Chapter 5: Sensor Data Engineering and Leakage-Safe Datasets

Device, user, site, session, and machine splits

"The test set exists to answer one question: what happens when something new walks in the door. If the door already knows the visitor, you asked a different question and reported the wrong answer."

A Disciplined AI Agent

The Big Picture

The previous section named the disease: leakage lets test information contaminate training and inflates your numbers. This section prescribes the cure that fixes most sensor leakage in one move: group-aware splitting. Instead of shuffling windows at random, you decide which real-world entity your model must generalize across, hold all of that entity's data on one side of the split, and never let it straddle. The entity might be a device, a wearer, a factory site, a recording session, or a physical machine. Choosing it correctly is the single highest-leverage decision in your evaluation protocol, because it defines what "new" means for your deployed system. Choose it wrong and a model that has secretly memorized individuals will post a benchmark it can never reproduce in the field.

This section assumes you have already cut streams into windows (Section 5.2) and understand the leakage mechanisms catalogued in Section 5.3. Here we turn the diagnosis into a construction procedure. We take a windowed, labeled dataset carrying entity identifiers and produce train, validation, and test partitions whose accuracy estimate matches the deployment question you actually care about.

The grouping unit is a modeling decision, not a default

What. A grouping unit is the entity across whose boundary your model must generalize at deployment. Every window belongs to exactly one value of that unit (this wearer, that machine, this recording session), and a group-disjoint split guarantees that all windows sharing a group value land entirely in train or entirely in test, never split between them. Why. Sensor data is riddled with nuisance signals that are constant within an entity and irrelevant to the label: a specific accelerometer's bias, one person's gait idiosyncrasy, a particular lathe's bearing hum, a room's acoustic reverberation. A random split scatters an entity's windows across train and test, so the model can lower its loss by recognizing the entity rather than the phenomenon. That shortcut evaporates the instant a genuinely new entity appears.

How. Pick the unit by asking a single question: at deployment, what will be new? The answer names your holdout axis. The common units, roughly ordered from finest to coarsest, are:

Key Insight

The grouping unit and the deployment claim are the same object viewed twice. "Our fall detector works for new users" is only supported by a subject-disjoint test. "Our bearing model works on unseen pumps" requires a machine-disjoint test. "Our air-quality model transfers to new cities" demands a site-disjoint test. If the sentence in your paper or product spec says "new X", then X is your grouping unit, and any split that lets an X appear on both sides is measuring a weaker claim than the one you are making. Match the split to the sentence.

Nested entities and the strictest-boundary rule

Real datasets carry several nested identifiers at once. A wearable study has subjectdevice (a loaner watch) ⊃ session (each day's wear) ⊃ window. These nest: every session belongs to one device, every device to one subject. The rule: split at the coarsest boundary your deployment claim requires, and the finer boundaries take care of themselves automatically, because holding out a subject also holds out all of that subject's devices and sessions.

The danger is a partial identifier that silently couples the two sides. If one subject wore two devices and you split by device, that subject's physiology now appears in both partitions even though no device does; you have a subject leak hiding inside a clean-looking device split. Formally, a split into train set \(\mathcal{T}\) and evaluation set \(\mathcal{E}\) is group-safe for unit \(g\) when their group sets are disjoint:

$$ \{\, g(w) : w \in \mathcal{T} \,\} \;\cap\; \{\, g(w) : w \in \mathcal{E} \,\} \;=\; \varnothing. $$

When several units matter at once (say subject and site), the safe construction holds out on the composite key, or splits on the coarsest unit that dominates the others. Verify the intersection is empty for every unit you claim to generalize across, not just the one you sorted on.

Practical Example: the seizure detector that only knew four patients

A clinical team trained an EEG seizure detector on continuous recordings from a small epilepsy-monitoring unit and reported 97% window-level accuracy from a random 80/20 split. Deployed on a new admission, it collapsed to little better than chance. The autopsy was textbook nested leakage. With only a handful of patients but days of recording each, a random window split placed thousands of windows from every patient into both train and test. The model had learned each patient's individual EEG signature, electrode montage, and baseline artifacts, then recognized the patient and recalled their seizure pattern. Re-run as a patient-disjoint split (leave-one-patient-out), accuracy fell to a sober 78%, which was the real, deployable number. The 19-point gap was pure identity memorization. Nothing about the model changed; only the split told the truth. EEG and clinical validation return in Chapter 31.

Constructing grouped splits in practice

The mechanics are short once the group vector exists. You never shuffle windows; you shuffle groups, then gather every window of the chosen groups. For cross-validation you want each fold's test groups disjoint from its training groups, which is exactly what grouped K-fold provides. The snippet below shows a manual subject-disjoint split and the grouped-CV equivalent, and prints the group overlap so you can prove disjointness rather than assume it.

import numpy as np
from sklearn.model_selection import GroupKFold, GroupShuffleSplit

# X: (N, L, C) windows; y: (N,) labels; groups: (N,) subject id per window
rng = np.random.default_rng(0)
N = 2000
groups = rng.integers(0, 20, size=N)      # 20 subjects
y = rng.integers(0, 3, size=N)

# One held-out split: whole subjects go to test, none shared
gss = GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=0)
train_idx, test_idx = next(gss.split(np.zeros(N), y, groups))
overlap = set(groups[train_idx]) & set(groups[test_idx])
print("shared subjects:", len(overlap))    # must be 0

# K-fold where every fold is subject-disjoint
for k, (tr, te) in enumerate(GroupKFold(n_splits=5).split(np.zeros(N), y, groups)):
    assert not (set(groups[tr]) & set(groups[te]))
    print(f"fold {k}: {len(set(groups[te]))} test subjects, {len(te)} windows")
Subject-disjoint splitting with scikit-learn. GroupShuffleSplit makes one held-out partition; GroupKFold rotates disjoint subject sets through the test fold. The explicit set-intersection check and the assertion are the point: they turn "I think this is clean" into a proof that no group crosses the boundary.

Two refinements matter for honest numbers. First, groups are rarely balanced (one subject may contribute ten times the windows of another), so a group-disjoint split can accidentally load all of one class onto one side; use stratified-grouped variants or report per-group performance to catch it. Second, with few groups the estimate is high-variance: leaving out one of five machines makes the score swing wildly, so prefer leave-one-group-out cross-validation and report the spread, not a single fold. This ties directly to the small-sample uncertainty tools from Chapter 4.

Right Tool: grouped splitters replace a folder of bookkeeping

Rolling your own group-disjoint, class-stratified, K-fold rotation with balance checks and leakage assertions is roughly 60 to 90 lines of index juggling that is easy to get subtly wrong (the off-by-one that lets one group slip across is exactly the bug that inflated the seizure detector). Scikit-learn's GroupKFold, StratifiedGroupKFold, GroupShuffleSplit, and LeaveOneGroupOut reduce it to one constructor plus a groups= argument, about 2 lines, and they guarantee the disjointness invariant for you. Your only remaining job is to build the correct group vector, which is where the real thinking lives.

When a pure entity split is not enough

When to go further. Group-disjoint splitting removes identity leakage, but two related traps survive it. If your data has a strong temporal structure (a machine degrades over months), you may also need the test period to come after the training period so you do not train on the future, combining a machine split with a temporal cutoff. And if the label is correlated with the group (all positive cases came from one site), even a clean split can hand the model a spurious site-equals-label shortcut. The defense is to check that each class is represented across multiple groups on both sides, and to prefer coarser or composite grouping when a nuisance variable and the label are confounded. These distribution-shift concerns are the running theme of Chapter 66, and the leakage-safe benchmarking discipline is formalized in Chapter 65.

Exercise

Take a human-activity dataset with columns for subject, device, session, and label. (a) Report accuracy under a random window split, a session-disjoint split, and a subject-disjoint split, and rank the three numbers before you run them. (b) One subject in your data wore two devices; construct a device-disjoint split and prove, with a set intersection, whether any subject leaks across it. (c) In two sentences, state which of the three splits you would put in a product datasheet claiming "works for new users", and why the other two would overstate the claim.

Self-Check

  1. Your deployment claim is "detects faults on pumps we have never serviced". Name the grouping unit, and explain why a session-disjoint split would not support that claim.
  2. You split cleanly by device, but every device belongs to one of three subjects and two subjects appear on both sides. What kind of leakage remains, and how do you remove it?
  3. Why does leave-one-group-out cross-validation give a more trustworthy estimate than a single held-out group when you only have five machines, and what should you report alongside the mean?

Group-aware splitting is deceptively simple to implement and decisive in effect: the same model and the same data can post a triumphant or a sober number depending only on where you draw the boundary. Draw it at the entity your deployment must generalize across, prove the groups are disjoint, and your accuracy finally means what it says.

What's Next

In Section 5.5, we tackle the nuisance that made device and subject splits necessary in the first place: per-unit variation. We will see how normalization and per-device calibration remove sensor-specific bias and scale so the model learns the signal rather than the serial number, and, crucially, how to fit those transforms inside the training fold only, so the cure for one leak does not quietly open another.