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

FIR and IIR filters

"A moving average is just an FIR filter that never went to design school."

A Well-Tuned AI Agent

The Big Picture

Every linear, time-invariant filter you will ever deploy on a sensor stream belongs to one of two families, defined by a single structural choice: does the output depend only on past inputs, or also on past outputs? That choice splits the world into finite impulse response (FIR) and infinite impulse response (IIR) filters. It decides how much memory the filter needs, whether it can distort the timing of your signal, whether it can blow up, and how many multiplies your microcontroller burns per sample. Section 6.1 gave you two hand-tuned instances, the moving average and the exponential smoother. This section names the families they belong to and hands you the design freedom to build anything in between.

This section assumes you are comfortable with discrete-time signals, the sampling rate \(f_s\), and the notion of a frequency response from Chapter 3. We stay deliberately structural here: the specific low-pass, high-pass, band-pass, and notch shapes come in Section 6.3, and real-time latency budgets in Section 6.6.

The two difference equations

A linear time-invariant filter turns an input stream \(x[n]\) into an output \(y[n]\) with a difference equation. The general form is

$$y[n] = \sum_{k=0}^{M} b_k\, x[n-k] \;-\; \sum_{k=1}^{N} a_k\, y[n-k].$$

The two sums are the whole story. The \(b_k\) weight the current and past inputs: this is the feedforward part. The \(a_k\) weight past outputs and feed them back in: this is the feedback part.

A FIR filter sets every \(a_k = 0\). The output is a fixed weighted sum of the last \(M+1\) input samples, nothing more. Its impulse response, what comes out when you feed in a single spike, is exactly the coefficient list \(\{b_k\}\), and then it is over. Finite in, finite out: that is where "finite impulse response" comes from. A length-5 moving average is the FIR filter with \(b_k = 1/5\).

An IIR filter keeps at least one nonzero \(a_k\), so each output loops back into future outputs. A single spike therefore echoes forever, decaying but never truly reaching zero. The exponential smoother \(y[n] = \alpha x[n] + (1-\alpha) y[n-1]\) is the simplest IIR filter that exists: one feedforward tap, one feedback tap. Unroll that recursion by substituting each output back into the next one, and a single impulse at \(n=0\) produces the closed form \(h[n] = \alpha(1-\alpha)^n\): every step multiplies what is left by \((1-\alpha)\), so the response decays geometrically instead of stopping outright, which is exactly why a lone stray sample never vanishes from the output the way it does in an FIR filter, it only fades below the noise floor after roughly \(1/\alpha\) samples, the filter's effective settling time. That \(1/\alpha\) knob is the same one Section 6.1 tuned by feel; this section now explains why turning it trades responsiveness for smoothing.

Key Insight

FIR versus IIR is not "simple versus fancy." It is feedforward-only versus feedback. That one bit determines four downstream properties at once: guaranteed stability, the possibility of exact linear phase, the number of coefficients needed for a given sharpness, and how a transient (or a numerical error) propagates. Pick the family first, then design within it.

Poles, zeros, and why IIR can explode

Take the z-transform of the difference equation and the filter becomes a ratio of polynomials, the transfer function

$$H(z) = \frac{\sum_{k=0}^{M} b_k z^{-k}}{1 + \sum_{k=1}^{N} a_k z^{-k}}.$$

The roots of the numerator are zeros (frequencies the filter kills); the roots of the denominator are poles (frequencies it amplifies). A FIR filter has denominator equal to 1, so it has no poles anywhere except the origin. This is why a FIR filter is always stable: there is no denominator to divide by zero, no feedback loop to run away. An IIR filter is stable only if every pole sits strictly inside the unit circle \(|z| < 1\). Push a pole to or past the circle, through an aggressive design or through finite-precision rounding on a 16-bit device, and the output diverges or oscillates on its own.

That fragility buys enormous efficiency. Feedback lets an IIR filter place sharp resonances and deep notches with a handful of coefficients, because each pole does heavy lifting. Matching the same steepness with a FIR filter can take ten to a hundred times as many taps. That tradeoff is the recurring theme of this section, worth carrying with you past the last page: FIR spends coefficients to buy safety and phase honesty; IIR spends safety and phase honesty to buy coefficients.

Phase: the property sensor engineers forget

Magnitude response tells you which frequencies survive. Phase response tells you how much each frequency is delayed, and getting it wrong warps the shape of a waveform even when the spectrum looks fine. A symmetric FIR filter (coefficients that read the same forwards and backwards) has exactly linear phase: every frequency is delayed by the identical number of samples, \((M)/2\). The waveform comes out shifted in time but undistorted. IIR filters cannot be linear phase; their feedback smears different frequencies by different delays, so a sharp ECG R-peak or an accelerometer impact edge shifts and skews.

Common Misconception: Linear Phase Is Not the Same as No Delay

It is tempting to read "linear phase" as "phase-distortion-free, therefore delay-free." A linear-phase FIR filter still shifts every sample by the same \(M/2\)-sample lag; what it guarantees is that every frequency is shifted by that identical amount, so the waveform's shape survives intact, not that the delay is zero. Two sensor streams filtered with FIR filters of different lengths, or one filtered stream compared against a raw timestamp, will drift out of alignment by exactly the difference in their \(M/2\) delays unless you compensate explicitly. Forgetting this is a common source of silent, constant-offset timing bugs in multi-sensor pipelines.

Practical Example: the ECG that lied about its timing

A clinical team built a wearable ECG patch and cleaned the 0.5 to 40 Hz band with a compact 4th-order Butterworth IIR filter, chosen because it ran cheaply on the patch's tiny core. The heart-rate estimate was perfect. But the derived feature the cardiologists actually wanted, the QT interval (the time from the Q wave to the end of the T wave), came out systematically biased, because the IIR filter delayed the low-frequency T wave more than the sharp QRS complex. Switching the offline analysis to a linear-phase FIR filter, and reserving the cheap IIR only for the on-device live display, removed the bias. The lesson: when a downstream measurement is a timing between events, phase linearity is not a luxury. This same care carries into Chapter 29.

Zero-phase filtering: cheating with hindsight

When you process a recording after the fact rather than live, you can sidestep the phase problem entirely. Run any filter forward, then run it backward over its own output. The forward pass delays each frequency by some amount; the backward pass applies the identical delay in the opposite direction, and the two cancel exactly. The result has zero phase distortion and double the magnitude attenuation, from even a cheap IIR filter. This is filtfilt. It is offline only, because the backward pass needs future samples you do not have in a live stream; that constraint is exactly what Section 6.6 wrestles with.

import numpy as np
from scipy.signal import firwin, butter, lfilter, filtfilt

fs = 100.0          # accelerometer sample rate, Hz
cutoff = 5.0        # keep motion below 5 Hz, drop sensor hiss above

# FIR: 51 symmetric taps -> exact linear phase, always stable
fir_b = firwin(numtaps=51, cutoff=cutoff, fs=fs)

# IIR: 4th-order Butterworth -> same job, 5 coeffs, but nonlinear phase
iir_b, iir_a = butter(N=4, Wn=cutoff, btype="low", fs=fs)

x = np.load("wrist_accel.npy")               # a noisy 1-D signal

y_fir     = lfilter(fir_b, 1.0, x)           # causal, one-directional
y_iir     = lfilter(iir_b, iir_a, x)         # causal, cheap, phase-warped
y_iir_zp  = filtfilt(iir_b, iir_a, x)        # offline, zero-phase

print(len(fir_b), len(iir_b))                # 51 vs 5 coefficients
The same 5 Hz low-pass built two ways on a 100 Hz wrist-accelerometer stream. The FIR needs 51 taps for linear phase; the 4th-order Butterworth IIR needs 5 coefficients but warps timing unless you wrap it in filtfilt for offline zero-phase output. The printed 51 vs 5 is the efficiency gap in one line.

Right Tool: let SciPy do the design math

Deriving Butterworth pole locations or a windowed-sinc FIR by hand runs to 30-plus lines of transcendental algebra, and it is easy to get subtly wrong. scipy.signal.firwin and scipy.signal.butter collapse each design to a single call and return coefficients ready for lfilter: a roughly 30-to-1 reduction, and the library, not you, carries the numerical precision of the pole placement. The forward-backward zero-phase trick, another 15 or so lines of edge-padding and filter-state bookkeeping if done correctly by hand, is one call to filtfilt. You supply the cutoff and the order; the library supplies the numerics.

Choosing a family

Reach for FIR when phase linearity matters (timing features, waveform morphology, multi-channel alignment), when you need guaranteed stability on fixed-point hardware, or when the filter must be adapted or retrained online. Reach for IIR when compute and memory are scarce and a few coefficients must do sharp work, as on the microcontrollers of Chapter 61, and when a modest, predictable phase shift is acceptable. Note the family relationship to state estimation: the recursive, feedback structure of an IIR filter is the same idea that, generalized to a probabilistic state model, becomes the Kalman filter of Chapter 9.

Research Frontier: Filters That Learn Their Own Coefficients

Classical IIR design fixes \(a_k\) and \(b_k\) once, computed from a target magnitude response and a chosen order. An active line of deep learning research, structured state-space sequence models such as S4 and Mamba, instead treats those feedback coefficients as learnable parameters trained end to end on data, using specialized parameterizations (diagonal or low-rank state matrices) that keep every pole provably inside the unit circle throughout training. The result behaves like a single IIR filter with hundreds of poles, stable by construction, tuned not to a hand-specified cutoff but to whatever spectral structure minimizes the training loss. Chapter 16 picks up this thread in full.

Exercise

Take the 100 Hz accelerometer signal from the code above. (1) Design a 5 Hz low-pass as both a 51-tap FIR and a 4th-order IIR. (2) Feed each a single unit impulse and plot the impulse responses; confirm the FIR ends after 51 samples while the IIR rings on. (3) Overlay lfilter (causal IIR) against filtfilt (zero-phase) on a sharp step edge and measure, in samples, how far the causal version shifts the edge. Report the shift and relate it to the filter order.

Self-Check

  1. Which coefficients, \(a_k\) or \(b_k\), make a filter recursive, and what property of the impulse response follows from having any nonzero \(a_k\)?
  2. Why is a FIR filter unconditionally stable while an IIR filter is not?
  3. You need to measure the interval between two peaks in a vibration signal from a recorded dataset. Which filtering approach preserves the timing, and does it work in a live stream?

What's Next

In Section 6.3, we put these two families to work shaping frequency: designing low-pass, high-pass, band-pass, and notch responses, reading their magnitude plots, and matching each shape to a concrete sensor-cleaning job.