Part III: State Estimation and Classical Inference
Chapter 9: Bayesian Filtering and the Kalman Family

Smoothing vs filtering

"I gave the analyst the same track the autopilot flew, and she found the wobble I never felt. She had one advantage I did not: she already knew how the flight ended."

A Hindsight-Rich AI Agent

Prerequisites

This section builds directly on the Kalman recursion of Section 9.3 and the recursive-Bayesian view of Section 9.2, where the filtered estimate was defined as the posterior over the hidden state given every measurement up to now. It reuses the linear-Gaussian state-space model \(x_t = F x_{t-1} + w_t,\ y_t = H x_t + v_t\) with process covariance \(Q\) and measurement covariance \(R\) from Section 9.1, and the conditional-probability and Gaussian-marginalization facts from Chapter 4. The latency-versus-accuracy intuition first appeared for simple averages in Chapter 6.

The same state, estimated with two different information sets

Everything in this chapter so far has been a filter: at time \(t\) it reports the best guess of the state using only measurements \(y_1,\dots,y_t\), because in a live system that is all you have. But sensor data is often analyzed after the fact, from a log, a flight recorder, or an overnight batch. When you already hold the whole record \(y_1,\dots,y_T\), estimating an earlier state \(x_t\) while ignoring everything that happened after it is leaving information on the table. A smoother uses the future to sharpen the past. This section defines the three smoothing problems, derives the workhorse backward pass that turns any Kalman filter into a smoother almost for free, and, most important for a deployed system, tells you when the future is worth waiting for and when it is not.

Three questions, three information sets

What separates filtering from smoothing is purely which measurements the estimate is allowed to see. Filtering computes the posterior \(p(x_t \mid y_{1:t})\), conditioned on the past and present. Smoothing computes \(p(x_t \mid y_{1:T})\) for some \(T > t\), conditioned on future measurements as well. There is also a third relative, prediction, \(p(x_{t+k}\mid y_{1:t})\), which conditions on strictly less than filtering and is what a filter does internally on every time-update step.

Why the extra conditioning helps is that a future measurement is an indirect observation of a past state. If the tracker sees an object at position \(y_{t+3}\), that reading constrains where \(x_t\) could plausibly have been three steps earlier, because the dynamics couple them. Bayes' rule lets that later evidence flow backward. The result is never worse and usually strictly better: the smoothed covariance satisfies \(P_{t\mid T} \preceq P_{t\mid t}\) in the positive-semidefinite ordering, so every state's uncertainty shrinks or stays equal once the future is folded in.

How smoothing is deployed depends on how much future you are willing to wait for, which gives three named variants:

Smoothing spends latency to buy accuracy; that is the only trade

The filter is the smoother with zero lag. As you allow the estimate to see further into the future, uncertainty falls monotonically toward the fixed-interval limit, but so does your timeliness. A control loop that must act now cannot use future it does not have, so it filters. An analyst reconstructing what happened has all the future there is, so she smooths. Fixed-lag sits between them: pick the smallest \(L\) whose accuracy gain justifies the delay, because gains saturate quickly once \(L\) exceeds the system's correlation time. This is the same latency-versus-smoothness dial from Chapter 6, now with a Bayesian meaning attached to the delay.

The RTS smoother: one backward pass over the filter

The elegant fact that makes smoothing practical is that fixed-interval smoothing needs no new heavy machinery. You run the ordinary Kalman filter forward once, storing the filtered means \(\hat x_{t\mid t}\), the predicted means \(\hat x_{t+1\mid t}\), and both covariances at every step. Then a single backward sweep, the Rauch-Tung-Striebel (RTS) recursion, corrects each state using the smoothed estimate of the one after it. Starting from \(\hat x_{T\mid T}\) (the filter's last output already equals the smoothed value there) and running \(t = T-1\) down to \(1\):

$$C_t = P_{t\mid t}\,F^\top\,P_{t+1\mid t}^{-1},$$ $$\hat x_{t\mid T} = \hat x_{t\mid t} + C_t\left(\hat x_{t+1\mid T} - \hat x_{t+1\mid t}\right),$$ $$P_{t\mid T} = P_{t\mid t} + C_t\left(P_{t+1\mid T} - P_{t+1\mid t}\right)C_t^\top.$$

The smoother gain \(C_t\) weighs how much the future correction, the bracketed difference between the smoothed and predicted next state, should be pulled back onto \(x_t\). Where the dynamics are confident (small predicted covariance), little is changed; where the filter was uncertain, the future reshapes the estimate substantially. The whole backward pass is linear in \(T\) and touches only quantities the forward filter already produced, so smoothing a batch costs about twice a single filtering run.

import numpy as np

def rts_smoother(xf, Pf, xp, Pp, F):
    """Rauch-Tung-Striebel fixed-interval smoother.
    xf, Pf: filtered means/covariances x_{t|t}, P_{t|t}   (length T)
    xp, Pp: predicted means/covariances x_{t+1|t}, P_{t+1|t} (length T)
    Returns smoothed means xs and covariances Ps."""
    T = len(xf)
    xs, Ps = xf.copy(), Pf.copy()
    for t in range(T - 2, -1, -1):
        C = Pf[t] @ F.T @ np.linalg.inv(Pp[t])     # smoother gain
        xs[t] = xf[t] + C @ (xs[t + 1] - xp[t])     # correct with the future
        Ps[t] = Pf[t] + C @ (Ps[t + 1] - Pp[t]) @ C.T
    return xs, Ps
A complete fixed-interval RTS smoother in a dozen lines: it consumes the arrays a forward Kalman filter already stored and walks them backward, so the only new cost is one matrix inverse per step. This is the backward pass invoked in the wearable example below.

Reconstructing a stride from a wrist, after the run

A running-watch team estimates foot-strike timing and stride length from a wrist inertial sensor. In real time the watch filters: it must buzz cadence feedback within a step, so it reports \(\hat x_{t\mid t}\) and accepts the jitter that comes with zero lag. But the nightly training summary is built after the run from the logged signal, and there the app smooths. Running the RTS pass over the recorded session pulls each stride estimate toward what the following strides revealed, cutting the stride-length variance by roughly a third and removing the phantom double-steps that the live filter emitted whenever the arm swing briefly desynchronized. Same sensor, same model, same \(Q\) and \(R\); the only difference is that the summary is allowed to know how the run continued. The activity-recognition stack that consumes these strides is developed in Chapter 26.

The backward pass is one method call

The hand-rolled loop above is worth writing once for understanding, but in production you should not maintain the bookkeeping of four stored arrays and a matrix inverse. In filterpy, a fitted KalmanFilter exposes rts_smoother(means, covariances), which runs the entire fixed-interval recursion from the filter's own batch_filter output. That collapses roughly 15 lines of index-careful backward looping (plus the storage plumbing) into a single call, and it handles time-varying \(F_t\) and \(Q_t\) that the snippet above assumes constant. For nonlinear models the same idea appears as the extended and unscented RTS smoothers you will meet in Chapter 10.

Choosing filtering, fixed-lag, or full smoothing

When to smooth is a systems decision, not a statistical one, because the accuracy gain is always real but is only usable if you can afford the latency. Filter when an actuator, alarm, or control law consumes the estimate immediately: an anti-lock braking controller or a balancing robot cannot wait for the future, so the filter is the only honest choice. Use fixed-lag smoothing when the consumer tolerates a bounded delay, such as a gesture recognizer that can hold its decision for 100 milliseconds, or a lidar tracker feeding a planner that already looks a few frames ahead; pick \(L\) around the state's autocorrelation time and stop, because further lag buys almost nothing. Use full fixed-interval smoothing for any offline product: flight-data forensics, sleep-stage scoring from an overnight recording, sensor-fusion ground truth, or building the clean labels that a downstream learner will train on. That last use is why smoothing recurs throughout this book: the smoothed trajectory is frequently the best available reference signal, and the batch-smoothing view generalizes into the factor-graph formulation of Chapter 11, where all timestamps are optimized jointly rather than in a forward-then-backward sweep.

One caution carries over from Section 9.5: smoothing sharpens estimates only when the model is right. If \(Q\) and \(R\) are mistuned or the dynamics are wrong, the backward pass will confidently propagate that error across the whole batch, producing a trajectory that looks beautifully consistent and is uniformly biased. Smoothing narrows the reported uncertainty, which makes a miscalibrated model more dangerous, not less. Validate the filter first; only a trustworthy forward pass earns a backward one.

Exercise: measure what the future is worth

Simulate a constant-velocity 1-D tracker (position and velocity state) with process noise \(Q\) and measurement noise \(R\), and run the Kalman filter to obtain \(P_{t\mid t}\). Then run the RTS smoother and record \(P_{t\mid T}\). (1) Plot the position-variance ratio \(P_{t\mid T}/P_{t\mid t}\) across \(t\); explain why the two curves meet at \(t=T\) and diverge most in the middle of the batch. (2) Implement fixed-lag smoothing by re-running the smoother over sliding windows of length \(L\), and plot final position error against \(L\) for \(L = 0,1,2,5,10,25\). At roughly what \(L\) does the curve flatten, and how does that knee relate to the ratio \(Q/R\)?

Self-check

  1. State the exact difference between the information sets behind \(p(x_t\mid y_{1:t})\) and \(p(x_t\mid y_{1:T})\), and why the second can never have larger covariance.
  2. Which smoothing variant (fixed-interval, fixed-lag, fixed-point) fits each case: reconstructing a whole logged flight; estimating the instant of a bat-ball impact; a live tracker allowed a 50 ms delay?
  3. The RTS backward pass needs no new measurements. What four quantities from the forward filter must you store to run it, and why does a mistuned \(R\) make smoothing riskier than filtering?

What's Next

In Section 9.7, we close the chapter by turning the estimator on itself: the failure modes that a smoothed, confident-looking trajectory can hide, and the online diagnostics (innovation whiteness, normalized estimation-error squared, covariance-consistency tests) that tell you whether the filter you just built deserves to be trusted before you ever run a backward pass over it.