A practical intro to Kalman filters
This is pretty close to what I do every day.
How do you estimate something you canβt directly observe β when your data is noisy?
numpy makes the matrix maths readable β almost looks like the equationsmatplotlib gives instant visual feedback while youβre building intuitionfilterpy, pykalman, scipy are one pip install awayπ Python lets you focus on the idea, not the language
In our lab: we measure NO2 across a city with low-cost sensors.
Theyβre cheap enough to deploy everywhere β but noisy.
We have two sources of information:
π The key idea: combine them based on uncertainty
Predict β Measure β Correct
\[x_k = A \, x_{k-1} + w_k\]
We predict the next state with some uncertainty.
\[z_k = H \, x_k + v_k\]
We observe a noisy version of the state.
\[x_k = x_k^- + K_k \underbrace{(z_k - H x_k^-)}_{\text{innovation}}\]
Prediction + correction
π Correction depends on how much we trust the measurement
We couldβ¦
But we needed:
π So we built a small version ourselves
Equations
\[x_k^- = A \, x_{k-1}\] \[P_k^- = A P_{k-1} A + Q\]
\[K_k = \frac{P_k^-}{P_k^- + R}\]
\[x_k = x_k^- + K_k(z_k - x_k^-)\] \[P_k = (1 - K_k) P_k^-\]
π One equation, one line β thatβs it
class KalmanFilter1D:
def __init__(self, x0, P0, A, H, Q, R):
self.x, self.P = x0, P0
self.A, self.H, self.Q, self.R = A, H, Q, R
def predict(self):
self._xp = self.A * self.x
self._Pp = self.A * self.P * self.A + self.Q
def update(self, z, R=None):
R = R if R is not None else self.R
K = self._Pp / (self._Pp + self.H * self.H * R)
self.x = self._xp + K * (z - self.H * self._xp)
self.P = (1 - K * self.H) * self._Pp
def step(self, measurements):
self.predict()
for z, R in (measurements if hasattr(measurements, '__iter__')
else [(measurements, self.R)]):
self.update(z, R)
def filter(self, measurements):
for z in measurements:
self.step(z)
return np.array(self._xs)
# + smooth() for RTS β coming upkf = KalmanFilter1D(
x0=40.0, P0=10.0,
A=1, H=1,
Q=0.3, R=25.0
)
estimates = kf.filter(meas)
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(t, meas, '.', color="tomato", label="Sensor", alpha=0.6)
ax.plot(t, true, '-', color="white", label="True NO2", linewidth=2)
ax.plot(t, estimates, '-', color="limegreen", label="Filtered", linewidth=2.5)
ax.set_ylabel("NO2 (ΞΌg/mΒ³)")
ax.legend()
plt.tight_layout()
plt.show()
Kalman filtering only uses:
π past + current data
Offline setting β sensor logs processed overnight:
π Yes β smoothing
π Use future information to improve past estimates
\[x_k^s = x_k + C_k \left( x_{k+1}^s - x_{k+1}^- \right)\]
π We adjust estimates using future information
def smooth(self):
xs, Ps = np.array(self._xs), np.array(self._Ps)
xpreds, Ppreds = np.array(self._xpreds), np.array(self._Ppreds)
smoothed = xs.copy()
for k in range(len(xs) - 2, -1, -1):
C = Ps[k] * self.A / Ppreds[k+1] # smoother gain
smoothed[k] = xs[k] + C * (smoothed[k+1] - xpreds[k+1])
return smoothedkf2 = KalmanFilter1D(
x0=40.0, P0=10.0,
A=1, H=1, Q=0.3, R=25.0
)
kf2.filter(meas)
smoothed = kf2.smooth()
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(t, meas, '.', color="tomato", label="Sensor", alpha=0.5)
ax.plot(t, true, '-', color="white", label="True NO2", linewidth=2)
ax.plot(t, kf2.estimates, '-', color="limegreen", label="Filtered", linewidth=2, alpha=0.7)
ax.plot(t, smoothed, '-', color="gold", label="Smoothed", linewidth=2.5)
ax.set_ylabel("NO2 (ΞΌg/mΒ³)")
ax.legend()
plt.tight_layout()
plt.show()
| Filter | RTS Smoother | |
|---|---|---|
| Data used | past + present | full series |
| Use case | real-time | offline reanalysis |
| Accuracy | good | better |
π Same model β just more information
The Kalman filter is a state space model.
A state space model has two parts:
\[x_k = A \, x_{k-1} + w_k \quad \text{(transition β how state evolves)}\] \[z_k = H \, x_k + v_k \quad \text{(observation β what sensors see)}\]
statsmodels does this internallyπ State space is the framework β Kalman is the efficient algorithm to solve it
| ARIMA / Prophet | Kalman Filter | |
|---|---|---|
| Primary goal | Forecasting | State estimation |
| Multiple sensors | No | Yes β built in |
| Uncertainty | Confidence intervals | Propagated at every step |
| Real-time updates | Refit needed | Natural β just run the loop |
| Physics/domain model | No | Yes β you define A, Q |
π ARIMA asks βwhat comes next?β
π Kalman asks βwhat is true right now, given noisy observations?β
pykalmanfilterpystatsmodelsπ These implement filtering and smoothing
But: building it once makes it much clearer
The Apollo Guidance Computer used a Kalman filter.
π You can read the original code on GitHub.
If it worked there, itβll work for our NO2 sensors.
So far β simulated signals.
UCI Air Quality dataset β collected in an Italian city
Two things to do before fusing:
-200 sentinel in this dataset)We run the same approach with our own sensors:
The filter fuses them: cheap sensors benefit from reference accuracy without being overwritten.
Build it once from scratch β youβll trust it much more
Questions?
Code from this talk: kalman_talk.qmd