"You can know exactly where the signal was, or roughly where it is. Ask a filter for both and it will quietly charge you in milliseconds."
A Patient AI Agent
The big picture
Every smoothing filter in this chapter buys quieter output with the same coin: delay. Averaging suppresses noise precisely because it mixes the present sample with past ones, and mixing in the past means the output lags the truth. This is not a flaw in any particular design; it is a conservation law. A causal filter that reduces broadband noise by a factor \(R\) must, on average, delay the signal by an amount that grows with \(R\). This section makes that law quantitative, shows you how to read the exchange rate for the moving average, exponential smoother, and FIR filters you already built, and teaches you to pick an operating point on the tradeoff curve deliberately rather than discovering it in the field. It also shows the two legitimate ways to cheat: process data offline with zero-phase filtering, or replace blind averaging with a model of the signal, which is the doorway to the state estimators of Part III.
This section assumes the filters of Sections 6.1 through 6.6 and the idea of group delay and phase from Chapter 3. It also leans on the estimator language of Chapter 4: reducing noise is variance reduction, and delay is a form of bias in time. The tension between them is the bias-variance tradeoff wearing a clock.
Two currencies: variance down, delay up
Fix a concrete measure for each side. For noise reduction, feed the filter zero-mean white noise of variance \(\sigma^2\) and read the output variance. For a filter with taps \(h[k]\) summing to one (unity DC gain, so it passes a steady signal untouched), the output noise variance is \(\sigma^2 \sum_k h[k]^2\). The noise gain \(G = \sum_k h[k]^2\) tells you the multiplier; the reduction factor is \(1/G\). For delay, use the group delay at low frequency, the center of mass of the impulse response, \(\bar{\tau} = \sum_k k\, h[k]\) samples. These two numbers, \(G\) and \(\bar{\tau}\), are the price tags.
Work them out for the length-\(N\) moving average, where every tap equals \(1/N\). The noise gain is \(G = N \cdot (1/N)^2 = 1/N\), so noise variance drops by \(N\) and noise standard deviation drops by \(\sqrt{N}\). The delay is \(\bar{\tau} = (N-1)/2\) samples. Eliminate \(N\) and the exchange rate is stark: the output noise variance is \(\sigma^2 / (2\bar{\tau}+1)\). To cut noise variance in half you accept one extra half-sample of delay; to cut it tenfold you accept about 4.5 samples; each additional factor of noise reduction costs linearly more lag. There is no window length that escapes this, because a longer window is both the source of the averaging and the source of the delay.
Key insight
Noise reduction and delay are not two independent dials you can set separately; for any linear smoother they are two readings off the same impulse response. Widen the response and both move together: quieter and later. The engineering question is never "how do I get both?" but "given the cost of being late in this system, how much noise reduction can I afford?" The answer depends entirely on what consumes the output: a display tolerates lag a stabilization loop cannot.
Reading the exchange rate for the exponential smoother
The exponential moving average (EMA) of Section 6.1, \(y_t = \alpha x_t + (1-\alpha) y_{t-1}\), makes the same tradeoff with a single knob \(\alpha \in (0,1]\). Its steady-state noise gain is \(G = \alpha / (2-\alpha)\), and its low-frequency group delay is \(\bar{\tau} = (1-\alpha)/\alpha\) samples. Small \(\alpha\) means heavy smoothing: \(G \to 0\) (strong noise reduction) but \(\bar{\tau} \to \infty\) (heavy lag). The two formulas let you convert a requirement into a setting. Suppose a wearable needs its heart-rate estimate to lag by no more than 2 samples: solve \((1-\alpha)/\alpha \le 2\) to get \(\alpha \ge 1/3\), which fixes the best noise reduction available under that latency budget at \(G = (1/3)/(5/3) = 1/5\). The delay constraint, not your taste, sets the smoothing.
import numpy as np
def tradeoff_curve(sigma=1.0):
rng = np.random.default_rng(0)
noise = sigma * rng.standard_normal(200_000)
print(f"{'filter':>16} {'delay[smp]':>10} {'noise_gain':>11} {'std_out':>8}")
# moving average of length N
for N in (2, 4, 8, 16, 32):
h = np.ones(N) / N
y = np.convolve(noise, h, mode="valid")
print(f"{'MA N=%d' % N:>16} {(N-1)/2:>10.1f} {(h @ h):>11.4f} {y.std():>8.4f}")
# EMA with smoothing factor alpha
for a in (0.5, 0.25, 0.1):
y = np.empty_like(noise); y[0] = noise[0]
for t in range(1, len(noise)):
y[t] = a * noise[t] + (1 - a) * y[t - 1]
print(f"{'EMA a=%.2f' % a:>16} {(1-a)/a:>10.1f} {a/(2-a):>11.4f} {y.std():>8.4f}")
tradeoff_curve()
noise_gain matches the closed forms \(1/N\) and \(\alpha/(2-\alpha)\), and the measured output standard deviation on white noise tracks \(\sigma\sqrt{G}\). Reading down the table, every gain in noise reduction is paid for in the delay column: there is no row that is both quiet and prompt.The table the code prints is the Pareto frontier of first-order smoothing: each row is one achievable (delay, noise) pair, and no linear causal smoother of that family beats it on both axes at once. Plot delay on one axis and residual noise on the other and you get a curve bending toward the origin that you can approach but never cross.
Practical example: a quadrotor's altitude loop
A drone team fused a barometer and an accelerometer for altitude hold. The raw barometric height was noisy at roughly 30 cm RMS, so an engineer dropped in a length-32 moving average and the trace looked beautifully smooth on the bench. In flight the aircraft developed a slow vertical oscillation and occasionally porpoised. The cause was the filter's delay: \((32-1)/2 = 15.5\) samples at 50 Hz is about 310 ms of lag in the feedback path. The controller was reacting to where the drone had been a third of a second ago, which ate the loop's phase margin and drove it unstable. Shortening to a length-6 average (2.5 samples, 50 ms) restored stability at the cost of a noisier height, and the real fix was to stop smoothing blindly and fuse the fast accelerometer with the slow barometer in a complementary or Kalman filter (Chapter 9), which delivers low noise and low delay by using a motion model instead of a longer window.
Two legitimate ways to cheat the curve
The tradeoff is a law only for causal, blind linear filters. Relax either adjective and you gain room. First, if you are processing a recording rather than a live stream, run the filter forward and then backward over the data. The two passes cancel each other's phase, giving zero-phase output: the noise reduction of two filter passes with exactly zero net delay. This is what scipy.signal.filtfilt does, and it is the right tool for offline analysis, labeling, and building leakage-safe features (Chapter 5). The catch is that it uses future samples, so it is forbidden in any real-time loop and, if used carelessly to make features for a forecasting model, leaks the future into the past.
Second, stop averaging blindly and start predicting. A moving average is late because it assumes nothing about the signal and can only report a delayed mean. If you instead fit a local model, a line, a physical motion equation, a constant-velocity assumption, you can extrapolate it to the present instant and cancel most of the delay while keeping the noise averaging. This is the entire premise of the Kalman family in Chapter 9 and of the model-based smoothers that follow. The tradeoff does not vanish, but a good model shifts the whole Pareto curve inward, so that at any delay you buy more noise reduction than blind averaging allows.
Library shortcut
You do not have to derive group delay by hand to read a filter's price tag. scipy.signal.group_delay((b, a)) returns the delay in samples across frequency for any FIR or IIR design in one line, replacing a dozen lines of finite-difference phase unwrapping, and scipy.signal.filtfilt(b, a, x) gives zero-phase offline filtering in a single call rather than a hand-managed forward-backward pass with edge padding. Use them to plot the (delay, noise) operating point of a candidate filter before you commit it to a firmware build (Chapter 60).
Choosing an operating point by consequence
The right point on the curve is set by what being late costs in your system, so classify the consumer of the output. In a closed control loop (the drone above, a robot joint, an engine), delay is the enemy: it directly erodes phase margin and can destabilize the loop, so you spend your latency budget frugally and prefer model-based estimation over long windows. For a human-facing display (a fitness watch showing pace, a thermostat readout), a few hundred milliseconds of lag is invisible, so you can smooth hard and prize a clean number. For event detection and alarms (fall detection, arrhythmia flags, machine-fault triggers), delay becomes detection latency with real consequences, and you must report it as part of the system spec, not hide it inside a filter. Across all three, make the delay explicit and budgeted: measure it with group_delay, write it in the design document, and account for it when you time-align this stream against others in fusion (Chapter 48). An unbudgeted 300 ms hiding in a smoother is the kind of bug that only shows up in the field.
Exercise
Using the tradeoff_curve code, (a) plot residual noise standard deviation against group delay for moving averages of length 2 to 64 and overlay the EMA points for \(\alpha \in \{0.5, 0.25, 0.1, 0.05\}\); confirm the EMA sits on essentially the same frontier as the moving average. (b) Add a step input to the noise and measure the actual time each filter takes to reach 90% of the step; compare it to the predicted group delay. (c) Run one moving average with scipy.signal.filtfilt and show that the step is now centered (zero delay) but that the filter has peeked at future samples, which you must forbid in a causal deployment.
Self-check
- For a length-\(N\) moving average, write the noise gain and the group delay, and state the exchange rate between output noise variance and delay.
- Why can zero-phase filtering with
filtfiltachieve strong noise reduction at zero net delay, and why is it nonetheless illegal in a real-time control loop? - A Kalman filter can give both lower noise and lower delay than a moving average tuned to the same noise level. What extra ingredient does it use that blind averaging lacks?
Lab 6
design filters for accelerometer and vibration signals; quantify latency vs noise reduction.
Bibliography
Foundational signal processing
Oppenheim, A. V., and Schafer, R. W. (2010). Discrete-Time Signal Processing (3rd ed.). Pearson.
The standard reference for group delay, phase response, and the causality constraints that make the delay-versus-noise tradeoff a hard limit rather than a tuning choice.
A freely available, intuition-first treatment of moving averages, their step response and delay, and why longer windows trade responsiveness for smoothness.
Estimation and the model-based escape
The classic forward-backward smoother; the offline analogue of zero-phase filtering, showing how a model plus future data removes delay entirely for recorded signals.
Introduces the recursive estimator that beats blind averaging by predicting from a motion model, shifting the delay-versus-noise frontier inward. The formal starting point for Chapter 9.
Tools and smoothing practice
Documents group_delay and filtfilt, the two functions this section uses to read a filter's delay price tag and to run zero-phase offline smoothing.
The local-polynomial smoother that reduces noise while preserving peak shape, an early example of using a model of the signal to buy noise reduction at lower effective delay than a plain average.
What's Next
In Chapter 7, we stop asking how to smooth a signal in time and start asking what is in it. Spectral and time-frequency analysis reveals the frequency content that the delay-versus-noise tradeoff has been quietly reshaping all along, and gives you the tools to decide which parts of a signal are noise to be removed and which are the structure you came to measure.