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

Normalization and per-device calibration

"Two sensors reading the same world will still disagree. Normalization is how you argue them into a common language without whispering the test answers on the way."

A Diplomatic AI Agent

The Big Picture

Raw sensor numbers are almost never comparable across channels, across devices, or across days. An accelerometer axis with a small mounting offset, a gas sensor whose baseline drifts as it ages, a thermal camera that reads two degrees warm from the factory: each carries a private bias and a private scale that has nothing to do with the phenomenon you care about. Normalization rescales signals into a common range so a model can learn structure instead of memorizing units, and per-device calibration removes the sensor-specific distortion so that "the same input" produces "the same number" everywhere in your fleet. Done right, these two steps are the difference between a model that transfers to a new device and one that silently fails on it. Done wrong, they are one of the most common leakage vectors in the field, quietly inflating your test scores by letting statistics from the future leak into the past.

This section assumes you understand sensor bias, gain, and drift as physical quantities, developed in Chapter 2, and the leakage-safe split boundaries (device, user, site) from Section 5.4. Here we take windowed, split data and ask how to scale and calibrate it so that a model trained on one set of devices behaves on another. We deliberately treat model-output calibration (making predicted probabilities match observed frequencies) as a separate topic, covered in Chapter 18; the word "calibration" here means the hardware sense.

Why raw units mislead a model

Train-only fit of per-device normalization statistics to prevent leakage
Figure 5.5.1: Per-device normalization statistics (mean and standard deviation) are estimated from the training partition alone, then the frozen transform is applied to training and held-out data alike, keeping devices comparable while avoiding the leakage that global, all-data statistics would introduce.

What. Most sensor channels arrive in physical or raw-count units that mix three things: the signal, a per-device offset, and a per-device scale. We model a single reading as \(x = g\,(s + b) + \varepsilon\), where \(s\) is the true stimulus, \(b\) is a bias, \(g\) is a gain, and \(\varepsilon\) is noise. Why it matters. Gradient-based models typically converge poorly when channels span wildly different magnitudes. Feed an accelerometer in \(\pm 20\ \text{m/s}^2\) and a barometer in \(\sim 10^5\ \text{Pa}\) into the same network, and the barometer dominates every dot product until the optimizer laboriously rescales its own weights. (A barometer near \(10^5\) sitting next to an accelerometer near \(20\) is roughly a 5,000x magnitude gap, so the loss is effectively barometer-only until normalization levels the channels.) Worse, if \(b\) and \(g\) differ per device, a model that learned "class A sits near value 3.1" on the training devices will be wrong on a test device whose baseline sits at 3.4. How. Normalization fixes the magnitude problem; calibration fixes the per-device offset-and-gain problem. They are related but not the same operation, and conflating them is where subtle bugs live.

Normalization schemes and where the statistics come from

Pick the wrong scaler and a single vibration spike can quietly rescale an entire channel, teaching your model to key off a one-off artifact instead of the phenomenon; the choice below is not cosmetic, it decides what your network is even able to see. What. The workhorses are per-channel z-score (subtract mean, divide by standard deviation), min-max (rescale to \([0,1]\)), and robust scaling (subtract median, divide by interquartile range). For a channel \(c\), z-scoring is

$$ \tilde{x}_{c} = \frac{x_{c} - \mu_{c}}{\sigma_{c}}, $$

where \(\mu_c\) and \(\sigma_c\) are estimated statistics. Robust scaling swaps in order statistics for the same job, \(\tilde{x}_c = (x_c - \text{med}_c)/\text{IQR}_c\), where \(\text{med}_c\) is the channel median and \(\text{IQR}_c\) is its interquartile range, so neither term moves when a lone spike lands in the tail. When to prefer which. Use z-score as the default for roughly symmetric signals; use robust scaling when outliers and spikes are common (vibration, electromyography (EMG) bursts), because a single clipping event can wreck a standard deviation; use min-max only when a signal has hard physical bounds and you truly want to preserve them. How, and the part everyone gets wrong. The statistics \(\mu_c, \sigma_c\) are learned parameters of your pipeline, so they must be estimated on the training split alone and then applied unchanged to validation and test. Estimating them over the full dataset lets the test set's mean and variance leak backward into training, which is exactly the failure Section 5.3 warns against.

Min-max, precisely. Min-max normalization linearly maps a channel onto a fixed interval with \(\tilde{x} = (x - x_{\min})/(x_{\max} - x_{\min})\), so the smallest observed value becomes 0 and the largest becomes 1 while every gap between them stays proportional. It matters because a bounded \([0,1]\) range keeps signals with real physical limits (a normalized pixel intensity, a percent-of-scale humidity reading) inside the span a bounded activation expects, and it does this through one assumption-free subtract-and-divide rather than any distributional estimate. Reach for it only when those limits are genuine and stable; prefer z-score or robust scaling when the extremes are noisy, because a single spike moves \(x_{\max}\) and silently rescales every other sample. In short: a normalizer is a model you fit, so let it look only where your model is allowed to look.

Key Insight

A normalizer is a model. It has parameters (\(\mu\), \(\sigma\), min, max) that you fit, and fitting on data you later evaluate on is leakage, plain and simple. The tell-tale symptom is a small but stubborn gap: your leakage-free pipeline scores a point or two lower than the "fit-on-everything" version, and the difference is precisely the free information you leaked. The discipline is one sentence: fit the scaler inside the training fold, freeze it, transform everything else. In cross-validation this means re-fitting the scaler on each fold's training portion, never once over the whole array.

Per-device calibration: making devices speak the same units

Rescaling tames magnitude, but it never touches the physical reason two devices disagree about the very same input, and that is exactly where calibration takes over. What. Calibration removes the sensor-specific transfer function (the fixed input-to-output mapping a given unit imposes on top of the true signal) so that a known input maps to a known output on every unit. The classic two-point linear calibration solves for gain and offset from two reference stimuli \(s_1, s_2\) with measured responses \(x_1, x_2\): \(g = (x_2 - x_1)/(s_2 - s_1)\) and \(b = x_1 - g\,s_1\), then recovers \(\hat{s} = (x - b)/g\). Why it is separate from normalization. Normalization uses statistics of your data; calibration uses references from the physical world (a level surface for an inertial measurement unit (IMU), ice and boiling water for a thermistor, a zero-gas purge for a chemical sensor). You can calibrate each device with no labels at all, which is powerful, because it attacks device shift at its source rather than hoping the model averages it out. When you lack references. If you cannot bench-calibrate, per-device normalization is the pragmatic stand-in (a lighter-weight cousin of the instance normalization defined later in this section): compute each device's own baseline statistics (from a resting period, or a rolling window) and normalize each device by its own numbers. This needs no true \(g, b\), only that each device becomes internally consistent.

Checkpoint

So far: normalization rescales with data statistics, while calibration undoes a device's own gain and offset using physical references, and when references are unavailable you fall back to per-device debiasing so each unit at least agrees with itself.

Common Misconception

The misconception is that normalization and per-device calibration are two names for the same step, so performing one makes the other redundant. They are not: normalization rescales using statistics of your collected data and only makes channels comparable in magnitude, whereas calibration uses physical references to undo a specific device's gain and offset, so a perfectly normalized fleet can still disagree unit-to-unit until each device is calibrated (or per-device debiased) against a known input.

Step-Through: two-point linear calibration of a thermistor

Trace the two-point recipe with real numbers. Use two references: an ice bath, so \(s_1 = 0\ ^\circ\text{C}\), and boiling water, so \(s_2 = 100\ ^\circ\text{C}\). This particular thermistor reads \(x_1 = 0.4\) in the ice bath and \(x_2 = 98.9\) in the boiling water (raw units, already offset and slightly under-scaled). Compute the gain: \(g = (x_2 - x_1)/(s_2 - s_1) = (98.9 - 0.4)/(100 - 0) = 0.985\). Compute the offset: \(b = x_1 - g\,s_1 = 0.4 - 0.985 \times 0 = 0.4\). Now a fresh reading arrives, \(x = 50.7\). Recover the true stimulus: \(\hat{s} = (x - b)/g = (50.7 - 0.4)/0.985 = 51.07\ ^\circ\text{C}\). A second unit that read \(0.4\) at ice and \(101.2\) at boiling would get \(g = (101.2 - 0.4)/100 = 1.008\), a different divisor, and the same raw \(50.7\) would decode to \((50.7 - 0.4)/1.008 = 49.90\ ^\circ\text{C}\) on it. Two devices, one raw number, two corrected answers: that gap is precisely what calibration exists to close.

Practical Example: an air-quality fleet that aged out of its own model

A city deployed a few hundred low-cost electrochemical NO\(_2\) sensors on lampposts. The first model, trained on three months of data referenced against a regulatory-grade station, performed beautifully in validation. Six months later, accuracy had collapsed on half the fleet. The cause was baseline drift: each electrochemical cell's zero-offset creeps as the electrolyte ages, at a rate unique to each unit. The single global normalizer, fit once at deployment, had frozen a baseline that every sensor had since wandered away from. The fix was a per-device rolling baseline: each unit subtracts its own recent low-percentile reading (its estimated zero) before normalization, so the model sees drift-corrected concentrations regardless of a cell's age. Accuracy recovered without retraining. The lesson: when the offset is per-device and time-varying, the correction must be per-device and time-varying too, and this is squarely a distribution-shift problem of the kind Chapter 66 treats in depth.

Real-World Application: consumer wearables

Apple's Core Motion framework does not hand apps raw accelerometer and gyroscope counts; it applies per-device factory calibration plus a continuous runtime bias estimate, so the "user acceleration" and attitude an app receives are already offset-and-gain corrected across hundreds of millions of physically different units. That factory-plus-runtime split is exactly this section's distinction: the factory table is hardware calibration against a known reference, while the runtime bias tracker is per-device debiasing against the device's own recent statistics, the pragmatic stand-in when no fresh reference is available.

Instance normalization and the shift it defends against

That runtime bias tracker, which leans on a device's own recent statistics, points to a lighter-weight move you can make even when no resting baseline is available. What. You can normalize each window by its own statistics at inference time: subtract the window mean, divide by its per-channel standard deviation. This is instance normalization; its reversible form (normalize the input, then add the statistics back to the output) is the forecasting literature's reversible instance normalization. Why. It removes the slow per-window offset and scale shifts a global scaler cannot track, the very drift that breaks cross-device and cross-day generalization. When. Prefer it when shape carries the class and absolute level does not (activity recognition, many fault-detection tasks); avoid it when the level is the signal (a barometer for altitude, a thermometer for fever), where you would normalize away what you want to predict. Chapter 13 revisits this design space for deep models.

Mental Model

Instance normalization is the auto-level button in a photo app. Point a camera at the same scene at noon and at dusk and the two shots differ mostly in overall brightness and contrast, not in what is pictured; auto-level rescales each photo by its own histogram so the darkest pixel becomes black and the brightest becomes white, and the two suddenly look alike. Normalizing each window by its own mean and standard deviation does the identical thing to a signal: it strips away the per-window brightness (offset) and contrast (scale) so the model compares the shape of the event rather than the lighting it happened under. And just as auto-level ruins a photo whose subject literally is the sky's brightness, instance normalization erases the answer when the absolute level is the label.

Research Frontier

Fixed instance normalization removes only a single mean and scale per window, yet real sensor drift changes both within a window and across it, making the signal non-stationary (its statistics keep shifting over time rather than holding steady). SAN (Slice Adaptive Normalization), introduced by Liu and colleagues in "Adaptive Normalization for Non-stationary Time Series Forecasting: A Temporal Slice Perspective" (NeurIPS 2023), splits each series into slices, learns to predict each slice's evolving statistics, and normalizes then denormalizes per slice. On non-stationary forecasting benchmarks it improves on the earlier reversible instance normalization (RevIN, 2022) precisely because it tracks statistics that move inside the window instead of freezing one per-window estimate, a direction that speaks directly to the cross-device, time-varying drift this section handles by hand.

The leakage-safe fit and transform boundary diagrammed in Figure 5.5.1 makes the rule visual: statistics flow out of the training devices only, and no arrow ever runs backward from the held-out devices into the fitted scaler. The code below shows the same pattern end to end: fit a per-channel scaler on the training devices only, then apply a per-device baseline correction that each device computes from its own resting statistics. Note that nothing about the validation devices touches the fitted scaler's parameters.

Leakage-safe fit and transform boundary FIT (train devices only) TRANSFORM (every device) Train devices windowed, split data Fit scaler estimate mu, sigma Frozen mu, sigma Val / test devices each supplies its own baseline Transform (x - baseline - mu) / sigma Normalized, debiased signal frozen params no back-flow
Figure 5.5.1: The leakage-safe fit and transform boundary. The scaler is fit on the training devices only, producing frozen statistics mu and sigma. Those frozen parameters cross into the transform step, where every device (including validation and test) is normalized and debiased with its own baseline. The red crossed arrow marks the forbidden path: no statistic from a held-out device ever flows back into the fit.
import numpy as np

class LeakageSafeScaler:
    """Per-channel z-score fit on TRAIN groups only, plus per-device debias."""
    def fit(self, X_train):                    # X_train: (N, L, C) train windows
        flat = X_train.reshape(-1, X_train.shape[-1])
        self.mu = flat.mean(0)                  # (C,) learned on train ONLY
        self.sigma = flat.std(0) + 1e-8
        return self

    def transform(self, X, device_baseline=None):
        Xc = X.astype(np.float64)
        if device_baseline is not None:         # per-device offset removal
            Xc = Xc - device_baseline           # (C,) this device's own zero
        return (Xc - self.mu) / self.sigma

# Fit on training devices, freeze, then transform each device by ITS baseline.
scaler = LeakageSafeScaler().fit(X_train)       # never sees val/test devices
base_val = X_val_rest.mean((0, 1))              # this device's resting mean (C,)
X_val_norm = scaler.transform(X_val, device_baseline=base_val)
print(X_train.shape[-1], "channels; scaler frozen on train")
The LeakageSafeScaler.fit and transform methods in action: \(\mu, \sigma\) are estimated on training devices only and frozen, then each held-out device supplies its own device_baseline from a resting period, so per-device offset is removed without leaking any test-set statistics into the fitted parameters.

Right Tool: let a pipeline object enforce the fit/transform boundary

Hand-rolling fold-safe scaling, remembering to fit only on training rows, and threading frozen statistics through cross-validation is roughly 30 to 50 lines of error-prone bookkeeping, and the errors are invisible (they only show up as inflated scores). sklearn's StandardScaler or RobustScaler wrapped in a Pipeline and driven by cross_val_score with a grouped splitter collapses this to about 3 lines and makes leakage structurally hard: the pipeline re-fits the scaler inside each fold automatically. You still decide the scheme and the group key; the library guarantees the scaler never sees the fold it is scoring.

Exercise

Take a multi-device IMU dataset with device-disjoint train/test splits from Section 5.4. (a) Train a small classifier twice: once with a scaler fit on the full dataset, once with a scaler fit on the training devices only. Report the test-accuracy gap and explain which number is honest. (b) Add a synthetic per-device offset of \(0.3\ \text{m/s}^2\) to one held-out device and measure the accuracy drop. (c) Apply per-device baseline debiasing and show it recovers most of the loss. Relate your findings to the bias term \(b\) in the measurement model from Chapter 2.

Self-Check

  1. Why is fitting a StandardScaler on your entire dataset before splitting a form of leakage, and roughly what does it do to your reported test score?
  2. Give one sensing task where instance (per-window) normalization would destroy the label, and explain why.
  3. Your fleet's sensors drift at different rates over months. Why can a single global normalizer fit at deployment not fix this, and what per-device correction can?

Try It: watch leakage inflate your score

You can reproduce the leakage gap in well under an hour with a laptop and scikit-learn.

  1. Load the UCI Human Activity Recognition dataset (or any multi-subject IMU set) with sklearn.datasets or a CSV read, keeping the per-subject IDs and treating each subject as a "device".
  2. Build a subject-disjoint split with GroupShuffleSplit(n_splits=1, test_size=0.3) using the subject IDs as groups, so no subject appears in both train and test.
  3. Fit a StandardScaler on the ENTIRE feature matrix, transform train and test with it, train a LogisticRegression, and record test accuracy. This is the leaky baseline.
  4. Now put StandardScaler and LogisticRegression in a Pipeline, fit it on the training subjects only, and record test accuracy again; the point-or-two drop is the leakage you just removed.
  5. Add a constant \(0.3\) offset to one test subject's channels, watch accuracy fall, then subtract that subject's own per-channel mean before scoring and confirm the honest pipeline recovers most of the loss.

The gap between steps 3 and 4 is the free information leakage buys you, and step 5 shows per-device debiasing paying it back in full.

Normalization and calibration are the last preprocessing step before modeling and the one most likely to quietly corrupt an otherwise careful pipeline. Treat the scaler as a fitted model with a strict train-only fitting rule, keep hardware calibration distinct from data-statistics normalization, and push per-device corrections down to the device when its bias is private and time-varying. These habits are what let a model cross the gap from the devices it trained on to the ones it will actually run on.

The 61-cent bug that flew to Mars

In 1999 NASA's Mars Climate Orbiter fired its engines to slip into orbit and was never heard from again. The cause was a calibration mismatch of the purest kind: the ground software from Lockheed Martin reported thruster impulse in pound-force-seconds while NASA's navigation software expected newton-seconds, a fixed gain factor of about 4.45 that nobody applied. The two systems were each internally consistent and each individually correct, exactly like two uncalibrated sensors that agree with themselves and disagree with each other. A 125-million-dollar spacecraft was lost because one constant scale factor, the sort a single calibration line removes, silently multiplied every maneuver by 4.45. Normalization would not have saved it; only reconciling the two units, that is, calibration, would.

Lab: measure the leakage tax on the UCI HAR dataset

Goal. Quantify, in accuracy points, how much a global scaler inflates test scores versus a fold-safe one, and then show per-device debiasing recovering a synthetic offset. Budget 20 to 30 minutes.

Tools. Python with scikit-learn and numpy; the UCI Human Activity Recognition Using Smartphones dataset (30 subjects, treat each subject as a "device"), loadable from the UCI repository or the OpenML mirror.

Steps and what to vary. (1) Build a subject-disjoint split with GroupShuffleSplit keyed on subject ID. (2) Train a LogisticRegression twice: once with a StandardScaler fit on the entire feature matrix (leaky), once with the scaler inside a Pipeline fit on training subjects only (honest). (3) Vary the test fraction (0.2, 0.3, 0.4) and the random seed across five runs. (4) Add a constant offset (try 0.3, 1.0, and 3.0 in standardized units) to one held-out subject's channels, then subtract that subject's own per-channel mean before scoring.

What to observe. The leaky-minus-honest accuracy gap should be small but consistently positive, and its sign should never flip; that persistent gap is the leaked information. The injected offset should drop accuracy sharply, and per-device debiasing should recover most of it without any retraining. Note how the recovered fraction shrinks as the offset grows past the range the classifier ever saw in training.

What's Next

In Section 5.6, we turn from the signal side to the label side: annotation quality, weak and noisy labels, and the label delay that shifts your ground truth in time. A perfectly normalized dataset with sloppy labels still trains a sloppy model, and label noise is best treated as its own source of error.