Part II: Classical Signal Processing and Feature Engineering
Chapter 6: Filtering and Denoising Sensor Signals

Robust filtering for outliers and spikes

"A moving average asked one liar for his opinion and averaged it in. The median asked the whole room and believed the person standing in the middle."

A Level-Headed AI Agent

The big picture

Every filter you met so far in this chapter, moving averages (Section 6.1), FIR and IIR designs (Section 6.2), and their frequency-selective cousins (Section 6.3), is linear: the output is a weighted sum of inputs. Linear filters are wonderful against broadband and band-limited noise, and useless against a single wild sample. One corrupted reading of \(10^6\) counts, dragged into a 32-tap average, poisons 32 consecutive outputs. Sensors produce exactly this kind of corruption constantly: a loose connector, a cosmic-ray bit flip in an ADC, a footstep shaking an accelerometer, a motion artifact yanking a PPG trace off-scale. This section is about filters that see the spike and refuse to be moved by it. The tool is not a better frequency response; it is a better statistic, one with a high breakdown point.

Assume you have read Chapter 4 on estimation, so terms like estimator, bias, and robustness are familiar, and Chapter 2 on how physical sensors actually fail (saturation, dropouts, impulse pickup). Those failure modes are the enemy here.

Why the mean breaks and the median does not

The core idea comes from robust statistics: the breakdown point of an estimator is the fraction of arbitrarily bad samples it can tolerate before its output can be pushed to \(\pm\infty\). The sample mean has a breakdown point of \(0\): a single infinite value makes the mean infinite. The sample median has a breakdown point of \(0.5\): you must corrupt half the window before the middle value can be forced arbitrarily far. That single fact is why a median filter shrugs off spikes that a moving average smears.

Concretely, a length-\(w\) sliding median filter replaces each sample \(x_t\) with the median of the window \(\{x_{t-k}, \dots, x_{t+k}\}\), where \(w = 2k+1\). An isolated spike is, by construction, an extreme rank in its window, so it never lands in the middle and is discarded. What survives untouched is a step edge, because on either side of the step a majority of the window agrees. This is the property linear low-pass filters cannot offer: a low-pass filter smooths everything it touches, spikes and edges alike, while a median filter smooths only what disagrees with its neighbors, so a genuine edge survives untouched. The price is nonlinearity: there is no transfer function, and you reason about medians in the sample domain, not the frequency domain of Section 6.4.

Key insight

Impulse noise is a problem of influence, not of frequency. A spike has energy at every frequency, so no band-selective filter can isolate it without also touching the signal. The fix is to change the aggregation rule from "sum" (unbounded influence per sample) to "rank" (bounded influence per sample). Robust filtering is the sample-domain answer to a problem that the frequency domain cannot solve.

Misconception: the median is not a strictly better moving average

It is tempting to treat the median filter as a free upgrade: robust and at least as smooth as a moving average. It is not: on purely Gaussian noise, the median is a less efficient estimator of the local mean than the sample mean, with asymptotic relative efficiency only \(2/\pi \approx 0.64\), so it is noisier than a matched moving average on exactly the data averaging was built for. Reach for the median or Hampel filter when spikes are present; reach for Section 6.1's averaging, or the combination filter below, when they are not.

The Hampel filter: detect, then decide

A plain median filter has one blunt setting, the window, and it rewrites every sample, so it slightly rounds clean peaks you wanted to keep. The Hampel filter is smarter: it only replaces a sample when that sample is a statistically surprising outlier, and it leaves everything else exactly as measured. It is a decision-directed filter built on two robust statistics per window: the median \(m_t\) as the location estimate, and the median absolute deviation (MAD) as the scale estimate,

$$ \text{MAD}_t = \operatorname{median}_{i \in W}\bigl(\,|x_i - m_t|\,\bigr), \qquad \hat{\sigma}_t = 1.4826 \cdot \text{MAD}_t . $$

The constant \(1.4826\) rescales the MAD so that \(\hat{\sigma}_t\) estimates the standard deviation of clean Gaussian data (the MAD of a standard normal is \(1/1.4826 \approx 0.6745\)). A sample is flagged and replaced by the median when it lies more than \(n_\sigma\) robust standard deviations from the local median:

$$ |x_t - m_t| > n_\sigma \, \hat{\sigma}_t \;\Rightarrow\; x_t \leftarrow m_t . $$

Both statistics have breakdown point \(0.5\), so the detector itself cannot be fooled by the very spikes it hunts. A single outlier barely perturbs \(m_t\) or \(\text{MAD}_t\), so it stands out cleanly and gets clipped, while the rest of the trace passes through verbatim. The threshold \(n_\sigma\) (commonly \(3\)) tunes aggressiveness, and the window \(w\) tunes how local the notion of "normal" is. This detect-then-replace logic is the small-window sibling of the change and anomaly detection you will study in Chapter 12; the difference is that here the goal is to repair the stream in place, not to raise an alarm.

import numpy as np

def hampel(x, window=7, n_sigma=3.0):
    """Robust spike removal. Replaces flagged outliers with the local median."""
    x = np.asarray(x, dtype=float)
    k = window // 2
    y = x.copy()
    n_replaced = 0
    for t in range(k, len(x) - k):
        seg = x[t - k : t + k + 1]
        m = np.median(seg)
        mad = np.median(np.abs(seg - m))
        sigma = 1.4826 * mad
        if sigma > 0 and abs(x[t] - m) > n_sigma * sigma:
            y[t] = m
            n_replaced += 1
    return y, n_replaced

rng = np.random.default_rng(0)
clean = np.sin(np.linspace(0, 8 * np.pi, 400))
noisy = clean + 0.05 * rng.standard_normal(400)
noisy[[40, 41, 190, 300]] += [6.0, -5.0, 7.0, -8.0]   # inject spikes
repaired, hits = hampel(noisy, window=7, n_sigma=3.0)
print(f"replaced {hits} samples; "
      f"max error clean vs repaired = {np.abs(clean - repaired).max():.3f}")
A from-scratch Hampel identifier. The four injected spikes are flagged and clipped to the local median; the sine and its mild Gaussian noise pass through, so the maximum residual against the clean signal stays small. The MAD-based scale is what lets the detector ignore the spikes while measuring them.

The code above shows the whole mechanism in about fifteen lines, which is worth doing once to understand it. In production you will not hand-roll the sliding median.

Library shortcut

SciPy ships the pieces: a pure median filter is one call, scipy.signal.medfilt(x, kernel_size=7) or the faster scipy.ndimage.median_filter. For the full Hampel identifier, the hampel PyPI package (or sktime's HampelFilter transformer) replaces roughly 15 lines of windowing, MAD scaling, and threshold logic with one constructor call, vectorized so a million-sample trace runs in milliseconds instead of a Python loop. Keep the hand-written version only for teaching and for microcontroller ports where you cannot carry SciPy (see Chapter 61).

A family of rank and morphological filters

The median is one point on a spectrum of rank-order filters that sort the window and pick an order statistic; the weighted median, for instance, repeats trusted samples before sorting to bias toward the current sample for lower delay. Min and max filters (the two extreme ranks) are the atoms of morphological filtering: an opening (min then max) erases positive spikes, a closing (max then min) erases negative dropouts, and the two compose to strip both polarities while preserving broad structure. The mechanism is a size argument, not a statistical one: the min pass (erosion) suppresses any feature narrower than the structuring window, spike or genuine peak alike, and the max pass (dilation) restores the width of whatever survived. Size the window narrower than your shortest genuine feature and wider than your longest expected spike, and the opening keeps the feature while deleting the spike; get the sizing wrong and, unlike the Hampel filter, morphological filtering has no statistical safety net. It is the standard tool for baseline-wander and spike removal in ECG and other biosignals (Chapter 29), respecting sharp QRS peaks a linear filter would blunt. When you need both robustness and smoothness, chain a median stage (kill the spikes) into a short linear smoother from Section 6.1 (attenuate the residual broadband noise); this combination filter is the workhorse of practical robust preprocessing.

Practical example: a triaxial accelerometer on a press brake

An industrial monitoring team streams 3.2 kHz vibration from a metal-stamping press into a bearing-health model. Each stamping impact and passing forklift injects a full-scale \(\pm 16\,g\) clip lasting one or two samples. Their first pipeline used a 64-tap FIR low-pass; the clipped samples rang through it, producing 64-sample smears the anomaly model kept reporting as incipient bearing faults, a stream of false alarms. Swapping the front end for a length-7 Hampel filter at \(n_\sigma = 3\) fixed it: spikes were clipped to the local median before any spectral feature was computed, the smears vanished, and the false-alarm rate dropped by an order of magnitude, while genuine bearing signatures (broadband, sustained, not isolated) passed straight through untouched.

Research frontier: making the median differentiable

Sorting has near-zero gradient almost everywhere, so a median filter cannot simply be dropped into a network and trained end-to-end. Differentiable relaxations of sorting, built on optimal transport or temperature-softened comparisons, let a model learn where and how aggressively to apply rank-based robustness during training rather than as fixed preprocessing. Whether such a learned front-end beats a classical Hampel filter ahead of the sequence models in Chapter 13 enough to justify the added training cost remains open.

Tuning, latency, and what to watch for

Two knobs dominate. The window \(w\) sets robustness and delay: a wider window tolerates longer bursts (up to \(k\) consecutive bad samples for a length-\(2k+1\) median), but a causal implementation costs \(k\) samples of latency, since the current output needs \(k\) future samples. The threshold \(n_\sigma\) sets the false-positive rate: too small and you clip real transients, too large and you miss real spikes. Three failure modes deserve caution. First, if genuine outliers exceed half the window, even the median breaks, so size \(w\) to the worst expected burst. Second, a degenerate window of identical values gives \(\text{MAD}=0\); guard the division (the code above tests sigma > 0). Third, and this is a leakage trap the book returns to often: fit any spike detector on training data only, since choosing \(n_\sigma\) by peeking at the test set, or letting a whole-dataset MAD leak future statistics into past decisions, inflates reported denoising quality; use the leakage-safe splits from Chapter 5. Finally, these filters discard information: a "spike" might be the event you care about (an impact, a seizure spike, a gunshot), so when outliers are signal rather than corruption, detect and route them instead of deleting them.

Exercise

Take the hampel function above and a clean 1 Hz sine sampled at 200 Hz. (a) Corrupt 2% of samples with random \(\pm 10\) spikes and sweep \(n_\sigma \in \{1, 2, 3, 4, 5\}\); plot the count of true spikes removed versus the count of clean samples wrongly clipped, and identify the knee. (b) Replace the isolated spikes with bursts of length 3, 5, and 9 at a fixed \(w = 7\), and confirm empirically where the median's breakdown point defeats it. (c) Add a genuine sharp triangular transient to the signal and show that a length-64 FIR low-pass blunts it while the Hampel filter leaves it intact.

Self-check

  1. Why does a single \(+10^6\) sample corrupt many outputs of a moving average but only its own output (at most) under a median filter? Frame your answer in terms of breakdown point.
  2. What does the factor \(1.4826\) accomplish in the Hampel filter, and what would break if you dropped it?
  3. You have negative dropouts (occasional samples pinned to zero) but must preserve sharp positive peaks. Which morphological operation do you reach for, and why not a symmetric median?

What's Next

In Section 6.6, we turn the robustness we just gained into a real-time budget. A median or Hampel filter that needs \(k\) future samples buys its outlier rejection with latency, so the next section is about designing filters that run causally, sample-by-sample, inside a low-latency streaming loop, and about the delay that any such choice imposes before Section 6.7 makes the delay-versus-noise tradeoff explicit.