Foundations for Robotics, ML & Control

Multivariate Gaussians
& Covariance

A GPS fix isn't a dot on the map, it's a blob, and the blob has a shape. The multivariate Gaussian is the math that describes that shape, fuses two of them into one, and tells you exactly how many “sigmas” away a reading has to be before you call it impossible.

Prerequisites: Bayes’ Rule + Eigenvalues & Eigenvectors (2×2 case). That's it.
10
Chapters
9+
Simulations
0
Assumed Knowledge

Chapter 0: Why, The Blob, Not the Dot

Open the raw data from your phone's GPS chip and you'll find two numbers sitting next to your location: a latitude/longitude, and a field called accuracy, reported in meters. Most people read that as “you're somewhere inside a circle of this radius.” That's already useful. But it's also wrong in a way that matters, and untangling why is the entire point of this lesson.

A single accuracy number describes a circle, equal uncertainty in every direction. Real GPS error isn't circular. The receiver estimates your position by triangulating from several satellites, and the satellites overhead are never arranged symmetrically. When the satellites you can see are clustered more toward the north-south sky than east-west, your east-west position gets pinned down tightly while your north-south position stays sloppy. The error isn't a circle; it's a tilted oval, wide in one direction and narrow in another, and the tilt itself carries information, it's telling you exactly which direction the measurement doesn't trust.

Here's the phrase that will anchor this whole lesson: a measurement's uncertainty isn't a number, it's a shape. A single sigma (a standard deviation) is enough to describe uncertainty along one axis, like “my speedometer is accurate to plus or minus 2 km/h.” The moment you have two or more correlated quantities, x and y position, temperature and humidity, this stock's return and that stock's return, the uncertainty stops being a single number and becomes a matrix. That matrix is called the covariance matrix, written Σ (capital Greek sigma), and the shape it draws is an ellipse. This lesson is about learning to read that matrix the way you'd read a speedometer: at a glance, telling direction, and size, and whether two blobs actually agree with each other.

The whole idea in one sentence: A multivariate Gaussian is what a bell curve becomes once you track two or more correlated quantities at once, its uncertainty cloud is a tilted ellipse, and the covariance matrix Σ is the compact description of exactly how tilted and how stretched that ellipse is.

Why should an engineer care about the tilt and not just the size? Because the tilt is where the free information lives. Picture a delivery drone that only trusts its GPS's east-west reading and ignores the correlation with north-south. If the true error cloud is a 45° diagonal cigar, large error, but almost entirely in one specific diagonal direction, a filter that ignores the tilt will systematically either panic over safe readings or accept dangerous ones, because it's using the wrong shape to decide what's plausible. Every optimal sensor fusion algorithm you'll ever use, including the Kalman filter, is doing nothing more exotic than correctly tracking this ellipse as it changes shape over time.

This lesson builds that machinery from the ground up. We start with the familiar single-variable bell curve (Chapter 1), then add a second correlated variable and watch the bell curve become a tilted dome sitting over an ellipse (Chapter 2). From there we learn to measure “how many sigmas away” a point is once the world is tilted, the Mahalanobis distance (Chapter 3), and how learning one variable narrows your uncertainty about a correlated one, which is secretly what every Kalman update is doing (Chapter 4). We'll transform ellipses through matrices (Chapter 5), generate our own correlated random samples from scratch (Chapter 6), and combine two independent uncertain estimates into one sharper belief using nothing but algebra (Chapter 7). Chapter 8 is the payoff: a full two-sensor fusion lab you can drag around with your mouse. Chapter 9 is your map for choosing the right tool and where to go next.

Let's make the blob concrete before touching any formula. Below, imagine 200 independent GPS fixes taken at the exact same spot, the true position never moves, only the receiver's noise does. Drag the sliders for the spread in the east-west direction, the spread in the north-south direction, and the correlation between them, and watch the cloud of dots change shape. At correlation zero with equal spreads you get a fuzzy circle. Push the correlation up and the cloud leans into a diagonal cigar, even though nothing about the individual x-spread or y-spread changed.

200 GPS fixes at one true location, watch the blob change shape

The orange dot is the true position. Each blue dot is one noisy fix. Drag the sliders; click “Resample” to draw a fresh 200 points from the current shape.

Spread east-west (σx) 14 m
Spread north-south (σy) 14 m
Correlation (ρ) 0.00
Σ = [[196, 0], [0, 196]] m²

Watch what happens at the extremes. Set both spreads equal and correlation to zero: the cloud is a fuzzy circle, because error in the east-west direction tells you nothing about error in the north-south direction, they're independent. Now push correlation to +0.9 while keeping the spreads equal: the circle collapses into a tight diagonal line running from bottom-left to top-right. A large positive error in x now almost always comes bundled with a large positive error in y. Flip the correlation to −0.9 and the cigar rotates to run from top-left to bottom-right instead. The spreads never changed, only the correlation did, and yet the shape of “where the true position plausibly is” changed completely.

Put real numbers on it. Suppose your phone's error is tight in the east-west direction, σx = 5 m, but sloppy in the north-south direction, σy = 15 m, with no correlation between them. A reading that jumps 12 meters east of your last fix is 12 / 5 = 2.4σx away, a big, surprising jump on the tight axis, the kind of thing worth flagging. A reading that jumps the same 12 meters to the north is only 12 / 15 = 0.8σy away, unremarkable noise on the sloppy axis. Same 12-meter jump, wildly different level of surprise, purely because the two axes don't trust each other equally. That asymmetry is exactly what a single “accuracy: 12 m” number destroys, and exactly what the covariance matrix preserves.

The three numbers that describe any 2-D blob. A 2-D Gaussian cloud is completely pinned down by three numbers once you fix the center: the spread along x (σx2), the spread along y (σy2), and how the two move together (the covariance, σxy). Those three numbers are exactly the three independent entries of the 2×2 covariance matrix Σ. Nothing else is needed, not the shape of the histogram, not higher moments, nothing. That's the entire content of “Gaussian.”

Misconception: “accuracy in meters” is a complete description. A single accuracy radius is a circle-shaped approximation of what is usually an ellipse-shaped truth. It answers “how far, worst case, roughly?” but it cannot answer “how far in which direction?” or “if I already know the east-west error, what does that tell me about north-south?” Any system that fuses two sensors, rejects outlier readings, or asks “is this new measurement even consistent with what I already believed?” needs the full ellipse, not the single radius. We'll build exactly that machinery starting in Chapter 2.
Check: two GPS receivers report the same 15-meter accuracy radius. Receiver A's true error cloud is a circle; Receiver B's is a tilted ellipse with the same area. What does the single accuracy number fail to tell you about Receiver B?

Chapter 1: One Variable, Mean, Variance, and the 68/95 Rule

Before we can tilt an ellipse, we need to be completely fluent with the un-tilted, single-axis case: the ordinary bell curve. You've almost certainly seen it before, but this chapter nails down the exact vocabulary we'll reuse for the rest of the lesson, so don't skip it even if it looks familiar.

A single measurement, a speedometer reading, a resistor's resistance, a delivery time, that's corrupted by lots of small, independent sources of noise tends to land close to some central value most of the time, and far from it rarely, in a very specific bell-shaped pattern. That pattern is the Gaussian distribution (also called the normal distribution), and it's completely described by just two numbers: the mean μ (mu), which is where the peak sits, and the variance σ2 (sigma-squared), which controls how wide the bell is. The square root of the variance, σ (sigma), is the standard deviation, it's in the same units as the original measurement, which is why people quote σ (“accurate to ±2 km/h”) far more often than σ2.

f(x) = 1σ√(2π) · exp( −(x−μ)2 ⁄ 2σ2 )

Read the formula in two pieces. The exponent, −(x−μ)2/2σ2, is the important part: it measures how far x is from the mean, in units of σ, then squares that distance and flips its sign. Far from the mean, the squared distance is large and positive, so the exponent is a large negative number, and exp() of a large negative number is close to zero, the curve is low out there. Near the mean, the exponent is close to zero, and exp(0) = 1, the curve peaks there. The front piece, 1/(σ√(2π)), is just a constant that scales the whole curve so the total area underneath sums to exactly 1, because f(x) is a probability density, not a probability by itself.

A common trip-up: density is not probability. f(x) can be larger than 1, that's fine, because f(x) is a density, not a probability. You only get an actual probability by integrating f(x) over a range, like the chance x falls between 58 and 62. Asking “what's the probability x equals exactly 60.000000...” is a trick question for a continuous variable, that probability is zero, because there are infinitely many exact values to land on. Always ask about a range.

The single most useful fact about the Gaussian, the one you should be able to recite instantly, is the 68/95/99.7 rule: about 68% of the area under the curve lies within one σ of the mean, about 95% lies within two σ, and about 99.7% lies within three σ. This isn't a coincidence you need to derive from scratch every time, it's a fixed geometric property of this exact bell shape, and it's worth memorizing because it turns “how many sigma away” into an instant gut-check on how surprising something is.

Hand-check: the speedometer example

Say a car's cruise control holds speed with mean μ = 60 km/h and standard deviation σ = 2 km/h. First question: what fraction of the time is the actual speed between 58 and 62 km/h? Convert the boundaries into σ units first, this conversion is called a z-score: z = (x − μ) / σ. For x = 58: z = (58 − 60) / 2 = −1. For x = 62: z = (62 − 60) / 2 = +1. So “between 58 and 62” is exactly “within one σ of the mean,” and the 68/95/99.7 rule says that's about 68% of the time.

Second question: how surprising is a reading of 65 km/h? z = (65 − 60) / 2 = 2.5. That's two-and-a-half standard deviations out, beyond the 95% band (which stops at z = 2) but not yet at the 99.7% band (z = 3). A quick lookup (or a calculator's normal CDF) says only about 0.6% of readings should land this far out or farther. A single 65 km/h reading isn't proof the cruise control is broken, but if you see several readings out here, something changed.

Drag μ and σ, then click anywhere to read a z-score

The shaded bands mark 1σ and 2σ from the mean. Click on the axis to drop a marker and see its z-score and which band it falls in.

Mean (μ) 60
Std dev (σ) 2.0
Click the curve to test a value.

Notice what happens as you drag σ up: the peak drops and the curve flattens, because the total area underneath must always stay exactly 1, spreading the same unit of “probability mass” over a wider range necessarily lowers the peak. This trade-off, height for width, is the entire geometry of a 1-D Gaussian, and it's about to become far richer once we let a second variable in.

python
import math

def gaussian_pdf(x, mu, sigma):
    # the two-piece formula, written out explicitly
    coeff = 1.0 / (sigma * math.sqrt(2 * math.pi))
    expo  = -((x - mu) ** 2) / (2 * sigma ** 2)
    return coeff * math.exp(expo)

def z_score(x, mu, sigma):
    return (x - mu) / sigma

mu, sigma = 60.0, 2.0
print(z_score(65, mu, sigma))   # 2.5  -- two-and-a-half sigma out
print(gaussian_pdf(60, mu, sigma))  # peak density, about 0.1995

# the library equivalent -- same numbers, one line each
from scipy.stats import norm
print(norm.pdf(60, loc=mu, scale=sigma))          # matches gaussian_pdf
print((65 - mu) / sigma)                        # matches z_score
When to reach for it: a factory tolerance check. A bolt-cutting machine is specified to cut bolts to 50.00 mm with σ = 0.08 mm. Engineering tolerance allows 49.85, 50.15 mm. That window is ±0.15/0.08 ≈ ±1.9σ, just inside the 95% band, so roughly 1 in 20 bolts is expected to fail tolerance by chance alone, before anything is actually broken. Why reach for a single-variable Gaussian instead of tracking every bolt's raw millimeter deviation by eye? Because σ converts “how far off” into “how often,” which is the number a quality engineer actually needs to decide whether 5% scrap is acceptable or the machine needs recalibrating.
Check: a sensor has μ = 100 and σ = 4. Roughly what fraction of readings land between 92 and 108?

Chapter 2: Two Variables, Covariance, Correlation, and the Ellipse

Now let a second variable in. Instead of tracking only your east-west GPS error, track east-west and north-south together. Each still has its own mean and variance, but there's a new question we couldn't even ask with one variable: when x drifts high, does y tend to drift high too, drift low, or not care at all? The number that answers this is the covariance.

Cov(X, Y) = E[ (X − μx) · (Y − μy) ]

Read it as: for every sample, multiply how far x is above its mean by how far y is above its mean, then average that product over all samples. If x and y tend to be above their means together (or below together), the products are mostly positive and the average, the covariance, comes out positive. If one tends to be above its mean exactly when the other is below, the products are mostly negative and the covariance is negative. If the two have no relationship, positive and negative products cancel out and the covariance lands near zero.

Covariance has one annoying property: its size depends on the units of x and y, so “a covariance of 8” means nothing on its own until you know whether x and y are in millimeters or kilometers. The fix is to normalize by each variable's own spread, giving the correlation coefficient:

ρ = Cov(X, Y) ⁄ (σx · σy)

ρ (rho) is always between −1 and +1 no matter what units you started in. ρ = 0 means no linear relationship; ρ = +1 means x and y move in perfect lockstep; ρ = −1 means they move in perfect opposition. This is the same ρ you dragged in Chapter 0's slider, now you know exactly what number it is.

Stack the variance of x, the variance of y, and their covariance into a single 2×2 table and you get the covariance matrix Σ, the object this entire lesson is about:

Σ = [ [ σx2, σxy ], [ σxy, σy2 ] ]

Σ is always symmetric (the top-right entry equals the bottom-left entry, because Cov(X,Y) = Cov(Y,X)) and its diagonal entries are always non-negative (a variance can't be negative). The joint distribution over x and y is now a surface sitting above the plane, a dome, taller where probability is denser, and if you slice that dome horizontally at any height, the slice you get is an ellipse. Σ is exactly what tells you the size, shape, and tilt of every one of those ellipses.

Hand-check: from Σ to the ellipse's axes

Let's use a concrete covariance matrix and find its ellipse by hand, the same way Chapter 2 of the Eigenvalues lesson found stretch directions for a general matrix, because that's exactly what we're doing here.

Σ = [ [4, 2], [2, 4] ]

Read off the plain-English numbers first: σx2 = 4 so σx = 2; σy2 = 4 so σy = 2; the covariance is 2, so ρ = 2 / (2·2) = 0.5. Two equally-spread variables with moderate positive correlation. If Σ were diagonal (covariance zero), the ellipse would be a circle aligned with the axes. The off-diagonal 2 is exactly what tilts it.

To find the tilt precisely, we need Σ's eigenvalues and eigenvectors, the special directions Σ only stretches, never rotates (exactly the definition from the Eigenvalues lesson). The characteristic equation is det(Σ − λI) = 0. For a 2×2 matrix, the two shortcuts are: eigenvalues sum to the trace (4 + 4 = 8) and multiply to the determinant (4·4 − 2·2 = 16 − 4 = 12). So we need two numbers that sum to 8 and multiply to 12: solving λ2 − 8λ + 12 = 0 with the quadratic formula gives λ = (8 ± √(64−48)) / 2 = (8 ± √16) / 2 = (8 ± 4) / 2, so λ1 = 6 and λ2 = 2. Check: 6 + 2 = 8 ✓ and 6 × 2 = 12 ✓.

Now the directions. For λ1 = 6, solve (Σ − 6I)v = 0: subtracting 6 from the diagonal gives [[−2, 2], [2, −2]], and this matrix sends v = (1, 1) to (−2+2, 2−2) = (0, 0), exactly what “equals zero” means, so v1 = (1, 1), the 45° diagonal. For λ2 = 2, solve (Σ − 2I)v = 0: this gives [[2, 2], [2, 2]], which sends v = (1, −1) to (2−2, 2−2) = (0, 0), so v2 = (1, −1), the 135° diagonal. The two eigenvectors are perpendicular, as they always are for a symmetric matrix like Σ.

Translate the eigen-decomposition into the picture: the ellipse's long axis points along (1, 1), the 45° line, with a semi-axis length proportional to √λ1 = √6 ≈ 2.449. Its short axis points along (1, −1), the 135° line, with semi-axis length proportional to √λ2 = √2 ≈ 1.414. Notice the semi-axis uses √λ, not λ itself, because λ is a variance along that direction, and a length needs a standard deviation, which is variance's square root. This is the single most important geometric fact in the whole lesson: the eigenvectors of Σ are the ellipse's axes, and the square roots of the eigenvalues are how long those axes are.

The covariance ellipse and its eigenvector axes

Drag the sliders. The orange and teal lines are the eigenvector axes, scaled by √λ. Watch them stay perpendicular no matter how you tilt the ellipse.

σx 2.0
σy 2.0
ρ 0.50
Σ = [[4.0, 2.0], [2.0, 4.0]]  |  λ1=6.00, λ2=2.00
python
import numpy as np

Sigma = np.array([[4, 2], [2, 4]])
eigvals, eigvecs = np.linalg.eigh(Sigma)   # eigh: Sigma is symmetric
print(eigvals)   # [2. 6.]  -- ascending order
print(eigvecs)   # columns are the unit eigenvectors, (1,-1)/sqrt2 then (1,1)/sqrt2
axis_lengths = np.sqrt(eigvals)            # [1.414, 2.449] -- semi-axis lengths
When to reach for it: a two-asset investment portfolio. Stock A returns have variance 4 (in percentage-points squared), stock B the same, and their covariance is 2, the exact numbers above. The portfolio's total risk isn't just “A's risk plus B's risk”; the eigen-decomposition reveals a genuine risk direction (the 45° combination, both stocks up or down together, variance 6) and a genuine hedge direction (the 135° combination, one up while the other is down, variance only 2). Why look at the covariance matrix instead of each stock's variance separately? Because a portfolio split 50/50 along the hedge direction (long A, short B) carries less risk than either stock alone, information invisible if you only ever look at the diagonal.
Misconception: the diagonal tells the whole story. It's tempting to think “σx = 2 and σy = 2, so the blob is just a circle of radius 2.” It isn't, unless ρ = 0. The off-diagonal covariance is what tilts a circle into an ellipse and can make one direction far riskier than either individual variance suggests, the 45° direction above has variance 6, fifty percent more than either stock's own variance of 4. Reading only the diagonal of Σ is like reading only two entries of a matrix and assuming the rest are zero.
Check: for Σ = [[4, 2], [2, 4]], the eigenvector (1, 1) points along the ellipse's long axis. What does that direction correspond to in a two-stock portfolio with this covariance?

Chapter 3: Mahalanobis Distance, How Many Sigmas, in a Tilted World

Chapter 1 gave us a clean way to ask “how surprising is this value” along a single axis: compute the z-score, (x−μ)/σ. Chapter 2 built a tilted ellipse out of two correlated variables. Now we need the two-variable version of “how many sigma away,” and the honest answer is that ordinary straight-line distance gets this badly wrong once the world is tilted.

Here's the trap. Take the ellipse from Chapter 2, Σ = [[4, 2], [2, 4]], centered at the origin. Consider two points, both exactly √10 ≈ 3.16 units from the center in ordinary Euclidean distance: P = (3, 1) and Q = (3, −1). Same straight-line distance from the mean. But P sits almost exactly along the ellipse's long axis (the 45° direction, where the data is spread wide and such a jump is unremarkable), while Q sits closer to the short axis (the 135° direction, where the data is tight and the same jump is a much bigger deal). Treating P and Q as equally surprising, just because a ruler says they're equidistant, throws away everything Chapter 2 taught us about the shape.

Misconception: distance is distance. Ordinary (Euclidean) distance measures space as if it were an untilted, equally-scaled grid, a plain city map. A correlated Gaussian doesn't live on a plain grid; some directions are “cheap” (high variance, a big move there is common) and some are “expensive” (low variance, a big move there is rare). A distance metric that doesn't know the difference will systematically misjudge which points are actually outliers.

The fix is the Mahalanobis distance, named after statistician P. C. Mahalanobis, and it's a beautifully small edit to the ordinary distance formula:

D2 = (x − μ)T · Σ−1 · (x − μ)

Compare this to ordinary squared distance, which is (x−μ)T(x−μ), the same expression but with an identity matrix silently standing in for Σ−1. Inserting Σ−1 (sigma-inverse) does the job of “dividing out” the scale and correlation before measuring distance: it stretches the cheap (high-variance) directions back down and shrinks the expensive (low-variance) directions back up, until the ellipse becomes a perfect circle again. Once the space is un-tilted like that, plain distance on the corrected coordinates is exactly the right notion of “how many sigma away,” which is precisely what Mahalanobis distance measures.

Hand-check: same Euclidean distance, very different Mahalanobis distance

First we need Σ−1 for Σ = [[4, 2], [2, 4]]. For a 2×2 matrix [[a,b],[c,d]], the inverse is (1/det)·[[d,−b],[−c,a]], where det = ad−bc. Here det = 4·4 − 2·2 = 16−4 = 12 (the same determinant from Chapter 2, no surprise, since det(Σ) is the product of the eigenvalues, 6×2 = 12). So:

Σ−1 = 112 · [ [4, −2], [−2, 4] ] = [ [13, −16], [−16, 13] ]

Now point P = (3, 1). First multiply Σ−1 by P: top entry = ⅓×3 + (−⅝)×1 = 1 − ⅝ = ⅝×5 = 56; bottom entry = (−⅝)×3 + ⅓×1 = −½ + ⅓ = −16. So Σ−1P = (56, −16). Then D2 = P · (Σ−1P) = 3×56 + 1×(−16) = 15616 = 146 = 73 ≈ 2.333. So DP = √2.333 ≈ 1.53σ.

Now point Q = (3, −1). Top entry of Σ−1Q: ⅓×3 + (−⅝)×(−1) = 1 + ⅝ = 76. Bottom entry: (−⅝)×3 + ⅓×(−1) = −½ − ⅓ = −56. So Σ−1Q = (76, −56). Then D2 = 3×76 + (−1)×(−56) = 216 + 56 = 266 = 133 ≈ 4.333. So DQ = √4.333 ≈ 2.08σ.

There it is, in real numbers: P and Q are the exact same Euclidean distance (√10 ≈ 3.16) from the mean, but P is only 1.53 sigma away while Q is 2.08 sigma away, a 36% larger surprise, purely because Q's direction fights the correlation instead of riding along with it. This is the number every anomaly detector, every outlier gate, and every consistency check in this lesson is built on.

How far is “too far”? For an n-dimensional Gaussian, D2 follows a chi-squared distribution with n degrees of freedom. In 2-D (n = 2), the value that 95% of genuine samples fall under is χ20.95,2 ≈ 5.991. Both P (2.33) and Q (4.33) are comfortably under that gate, both are plausible draws from this Gaussian. But take a third point, R = (4, −2): repeating the same recipe gives Σ−1R = (53, −43), so D2 = 4×53 + (−2)×(−43) = 203 + 83 = 283 ≈ 9.33, which clears the 5.991 gate, R is a 95%-confidence outlier, even though its Euclidean distance from the origin (√20 ≈ 4.47) isn't dramatically larger than P and Q's.

Drag the point, watch Euclidean distance disagree with Mahalanobis distance

The dashed rings are Mahalanobis-distance contours for Σ = [[4,2],[2,4]] (1σ, 2σ, and the 95% chi-squared gate). Drag the dot; the readout shows both distances live.

Drag the point to test it.

The same computation, three ways

python, plain Python, no libraries
def mahalanobis_2d(x, y, Sigma):
    a, b = Sigma[0][0], Sigma[0][1]
    c, d = Sigma[1][0], Sigma[1][1]
    det = a * d - b * c
    if abs(det) < 1e-12:
        raise ValueError("Sigma is singular")
    # Sigma^-1 = (1/det) * [[d,-b],[-c,a]], applied directly to (x,y)
    inv_x = (d * x - b * y) / det
    inv_y = (-c * x + a * y) / det
    return x * inv_x + y * inv_y   # this is D^2

Sigma = [[4, 2], [2, 4]]
print(mahalanobis_2d(3, 1, Sigma))    # 2.333...
print(mahalanobis_2d(3, -1, Sigma))   # 4.333...
numpy
import numpy as np

def mahalanobis(x, mu, Sigma):
    diff = x - mu
    return float(diff @ np.linalg.inv(Sigma) @ diff)

Sigma = np.array([[4., 2.], [2., 4.]])
mu = np.zeros(2)
print(mahalanobis(np.array([3., 1.]), mu, Sigma))    # 2.333...
print(mahalanobis(np.array([3., -1.]), mu, Sigma))   # 4.333...
pytorch
import torch

def mahalanobis_t(x, mu, Sigma):
    diff = (x - mu).unsqueeze(-1)                    # column vector, shape (2,1)
    Sigma_inv = torch.linalg.inv(Sigma)
    return (diff.T @ Sigma_inv @ diff).squeeze()

Sigma = torch.tensor([[4., 2.], [2., 4.]])
mu = torch.zeros(2)
print(mahalanobis_t(torch.tensor([3., 1.]), mu, Sigma))    # tensor(2.333...)
print(mahalanobis_t(torch.tensor([3., -1.]), mu, Sigma))   # tensor(4.333...) -- autograd-ready
When to reach for it: anomaly detection across a sensor farm. A wind turbine logs vibration amplitude and rotor RPM every second; under healthy operation the two are correlated (faster spin, proportionally more vibration). A new reading with unusually high vibration for its RPM has a small Euclidean shift from “normal” but a large Mahalanobis distance, because it moves against the learned correlation instead of along it. Why Mahalanobis instead of a simple two-threshold check (“alert if vibration > X or RPM > Y”)? Independent thresholds can't see combinations, a turbine spinning at completely normal RPM with abnormally high vibration for that speed sails right past two per-variable thresholds, and only a distance that accounts for the correlation catches it.
Check: two points are the same Euclidean distance from the mean of a correlated Gaussian. Point A lies along the long (high-variance) axis; point B lies along the short (low-variance) axis. Which has the larger Mahalanobis distance?

Chapter 4: Conditioning, Slicing the Blob

Suppose you have the correlated x-y blob from Chapter 2 and you get to observe x exactly, a wheel encoder tells you the robot's forward distance precisely, and you want to know what that implies about a correlated quantity y, say lateral drift, that you haven't measured. This chapter is about the two different ways of asking “what happens to y,” and why they give very different answers.

The first way is the marginal distribution of y: ignore x entirely and ask what y looks like on its own, averaged over every possible value of x. Geometrically, this is squashing the 2-D dome flat onto the y-axis, like shining a flashlight through the blob from the side and looking at the shadow it casts. The marginal of y is just a plain 1-D Gaussian with mean μy and variance σy2, read straight off the diagonal of Σ. Correlation plays no role here at all, the shadow is the same shape no matter how tilted the blob is, because squashing along one axis erases the tilt.

The second way is the conditional distribution of y given a specific, known value of x: not the whole shadow, but a single slice through the dome at exactly x = c. Picture cutting a loaf of bread at one point and looking at the cross-section, rather than flattening the whole loaf. That cross-section is also a Gaussian, a wonderful, non-obvious fact about the multivariate Gaussian family, but it has a different mean and, crucially, a smaller variance than the marginal, because knowing x removes exactly the part of y's uncertainty that the correlation could explain.

Slicing is not squashing. It's easy to conflate “the distribution of y” with “the distribution of y given x,” but they answer different questions and, when ρ ≠ 0, they are never the same Gaussian. The marginal answers “what do I know about y with no other information?” The conditional answers “what do I know about y now that I've pinned down x?” Confusing the two is exactly the mistake of ignoring a sensor you already have.

The formulas for the conditional mean and variance follow directly from Σ, and they are worth deriving in words before symbols. If ρ is positive and you observe x above its mean, that pulls the conditional mean of y upward too, by an amount that scales with how strongly correlated they are (ρ) and how the two variables' natural scales compare (σyx). And however you observe x, the conditional variance shrinks by a factor of (1 − ρ2), regardless of which specific value x took:

μy|x = μy + ρ · (σy ⁄ σx) · (x − μx)     σ2y|x = σ2y · (1 − ρ2)

Notice the variance formula doesn't depend on which value of x you observed at all, only on how correlated x and y are. That's a genuinely surprising property of the Gaussian family: how much you learn about y depends only on ρ, while what you learn (the shifted mean) depends on the actual observed value of x.

Hand-check: how much does observing x actually buy you?

Use the familiar Σ = [[4, 2], [2, 4]] with μ = (0, 0), so σx = σy = 2 and ρ = 0.5. Suppose we observe x = 3. Plugging in: μy|x=3 = 0 + 0.5 × (2/2) × (3 − 0) = 0.5 × 1 × 3 = 1.5. So learning x = 3 shifts our best guess for y from 0 (the unconditional mean) up to 1.5, the correlation drags y along partway, but not all the way to 3, because ρ = 0.5 is a partial relationship, not a perfect one.

Now the variance: σ2y|x = 4 × (1 − 0.52) = 4 × (1 − 0.25) = 4 × 0.75 = 3, so the conditional standard deviation is √3 ≈ 1.73, down from the marginal σy = 2. Observing x shrank our uncertainty about y from 2 to 1.73, a real but modest 13.5% reduction, exactly what you'd expect from a moderate correlation of 0.5. Crank ρ up toward 1 and the shrink factor (1−ρ2) races to zero, a near-perfect correlation means knowing x tells you almost everything about y. At ρ = 0, the factor is exactly 1: an uncorrelated x tells you nothing, and the conditional equals the marginal, which is exactly the geometric intuition from Chapter 2's circle case.

Drag the vertical line to condition on x, compare the slice to the shadow

Top: the 2-D blob with a draggable conditioning line at x = c. Bottom: the marginal (shadow, unconditioned) distribution of y versus the conditional (slice) distribution of y given x = c.

Condition on x = c 3.0
ρ 0.50
μy|x = 1.50, σy|x = 1.73 (marginal σy = 2.00)
When to reach for it: the measurement update inside every Kalman filter. A Kalman filter's “update step” is nothing but this conditioning formula applied over and over: the filter has a prior joint belief over state and a would-be measurement, then it conditions on the measurement it actually received, and out pops a new mean (the state estimate moves toward the measurement, scaled by correlation) and a shrunk covariance. The famous Kalman gain is precisely the ρ·(σyx) term above, generalized to matrices. Why phrase it as conditioning rather than a bespoke “filter update” rule? Because conditioning is provably the optimal way to combine correlated Gaussian information, the Kalman filter isn't a clever heuristic, it's this exact formula wearing a different name. See the Kalman Filter lesson for the full derivation.
Check: if ρ = 0 between x and y, what happens when you condition on x?

Chapter 5: Linear Transforms, Where Ellipses Come From

Every ellipse we've drawn so far has been sitting still. Real systems move: a robot predicts where it'll be one time-step from now given where it is now and how fast it's going; a camera projects a 3-D point into 2-D pixels; a unit conversion turns meters into feet. Each of these is a linear transform, multiply by a matrix, maybe add a constant, and this chapter answers the question every one of those systems needs answered: if I know the uncertainty before the transform, what's the uncertainty after it?

Say x is Gaussian with mean μ and covariance Σ, and you compute y = Ax + b for some matrix A and constant vector b. The mean transforms exactly as you'd hope, because expectation is linear: E[Ax+b] = A·E[x] + b = Aμ + b. The covariance is less obvious, but derivable from the definition of covariance and the algebra of expectations, and the result is a clean sandwich:

y = Ax + b   ⇒   μy = Aμ + b,    Σy = A · Σ · AT

Why a matrix on both sides rather than, say, just AΣ or an elementwise A·A applied to each variance? Because covariance measures how pairs of components move together, and a linear transform mixes every input component into every output component. The A on the left carries the transform's effect on the “first copy” of the deviation, and the AT on the right carries its effect on the “second copy”, remember covariance is built from a product of two deviations, (x−μ) times itself, so both factors need to pass through the same transform.

Misconception: just scale the diagonal. It's tempting to transform Σ by applying A only to its diagonal entries, treating x and y's variances independently. That's only correct if A has no off-diagonal terms mixing the two components. The moment A blends inputs together, even a simple shear, or “new position = old position + old velocity”, the sandwich AΣAT is unavoidable, because it's the only formula that correctly carries covariance terms into the new coordinates too.

Hand-check: the Kalman prediction step, by hand

Take a state x = (position, velocity) with Σ = [[4, 2], [2, 4]], position variance 4, velocity variance 4, and a covariance of 2 between them (perhaps because both were estimated from the same noisy trajectory fit). A constant-velocity motion model over one time unit says: new position = old position + old velocity, new velocity = old velocity, unchanged. In matrix form that's y = Ax with A = [[1, 1], [0, 1]].

Compute AΣ first, one entry at a time. Row 1 of A is [1, 1]; dotted with column 1 of Σ, (4, 2), gives 1×4 + 1×2 = 6; dotted with column 2, (2, 4), gives 1×2 + 1×4 = 6. Row 2 of A is [0, 1]; dotted with column 1 gives 0×4 + 1×2 = 2; dotted with column 2 gives 0×2 + 1×4 = 4. So AΣ = [[6, 6], [2, 4]].

Now finish with (AΣ)AT, where AT = [[1, 0], [1, 1]]. Row 1 of AΣ is [6, 6]; dotted with column 1 of AT, (1, 1), gives 6+6 = 12; dotted with column 2, (0, 1), gives 0+6 = 6. Row 2 of AΣ is [2, 4]; dotted with column 1 gives 2+4 = 6; dotted with column 2 gives 0+4 = 4. The result:

Σnew = AΣAT = [ [12, 6], [6, 4] ]

Read the story in these numbers. Position's variance tripled, from 4 to 12, because the new position inherited both its own old uncertainty and velocity's uncertainty and the cross-talk between them (that's exactly what “new position = old position + old velocity” means for uncertainty: three sources stack up). Velocity's variance stayed exactly 4, because the model says velocity doesn't change, nothing new was added or removed from what we already believed about it. This is not a toy exercise: it is, entry for entry, the predict step of a Kalman filter with no process noise. Compare this to Chapter 4's conditioning formula, which was the update step, together, predict-then-update is the entire Kalman filter, and both halves are this lesson's algebra wearing a different hat.

One more useful fact, needed later: if x1 and x2 are independent Gaussians, their sum x1 + x2 is Gaussian too, with mean μ12 and covariance Σ12, covariances simply add when independent sources of randomness combine. This is exactly how a Kalman filter injects process noise Q into a prediction: it adds Q's covariance to the already-transformed AΣAT, because the true motion model has its own independent randomness layered on top of the deterministic A.

Drag a matrix onto an ellipse

The blue ellipse is the input Σ = [[4,2],[2,4]]. Adjust A's four entries; the orange ellipse shows AΣAT live.

A11 1.0
A12 1.0
A21 0.0
A22 1.0
Σnew = [[12.0, 6.0], [6.0, 4.0]]
numpy
import numpy as np

Sigma = np.array([[4., 2.], [2., 4.]])
A     = np.array([[1., 1.], [0., 1.]])   # constant-velocity model, dt=1

Sigma_new = A @ Sigma @ A.T
print(Sigma_new)   # [[12. 6.] [6. 4.]] -- matches the hand calc

# independent sums add covariance directly
Sigma_1 = np.array([[1., 0.], [0., 1.]])
Sigma_2 = np.array([[0.5, 0.], [0., 0.5]])
print(Sigma_1 + Sigma_2)   # [[1.5 0.] [0. 1.5]] -- process noise added to a prediction
When to reach for it: the predict step of a Kalman filter. Every Kalman filter alternates two moves: predict (this chapter, propagate Σ through the motion model A) and update (Chapter 4's conditioning). A drone's altitude-and-climb-rate estimate grows less certain every second it isn't measured, precisely because AΣAT spreads the ellipse out along the direction the motion model injects uncertainty. Why not just widen a single “altitude error” number by hand each step? Because the growth is directional and correlated with climb-rate uncertainty, ignoring the off-diagonal terms under-estimates how fast a *drifting* altitude estimate actually degrades. See the Kalman Filter lesson for the full predict-update loop.
Check: state x = (position, velocity) has Σ = [[4,2],[2,4]]. After applying A = [[1,1],[0,1]] (constant-velocity prediction), why does the new position variance (12) exceed the old position variance (4) by more than just adding the old velocity variance (4)?

Chapter 6: Sampling, Building the Blob From Scratch

Every chapter so far has worked with the analytic ellipse. Sometimes you need the opposite: actual random samples with a given covariance, for example to feed synthetic sensor noise into a filter you're testing before it ever sees a real robot, or to run a Monte Carlo check that your formulas actually behave the way the math says they should. This chapter builds correlated samples from nothing but a source of plain, independent, standard-normal randomness.

“Standard normal” means mean 0, variance 1, no correlation with anything, the easiest Gaussian to generate (the Box-Muller transform, used elsewhere on this site, turns two uniform random numbers into one standard normal draw). Call a pair of independent standard normal numbers z = (z1, z2). The question is: what linear transform turns z into a sample x with our target mean μ and covariance Σ?

Chapter 5 already answered exactly this question in reverse: if z has covariance I (the identity, no correlation, unit variance in every direction) and we compute x = μ + Lz for some matrix L, then x's covariance is L·I·LT = LLT. So we need a matrix L with LLT = Σ. That's called a Cholesky decomposition, and for a symmetric positive-definite matrix (which every valid covariance matrix is) it always exists and is unique if we insist L is lower-triangular, zero above the diagonal.

Σ = L · LT,    L = [ [l11, 0], [l21, l22] ]   ⇒   x = μ + Lz

Hand-check: solving for L, then drawing two samples

Solve for L's three entries against Σ = [[4, 2], [2, 4]] by multiplying L by LT symbolically and matching entries. The (1,1) entry of LLT is l112, which must equal 4, so l11 = 2. The (2,1) entry is l21·l11, which must equal 2, so l21 = 2/2 = 1. The (2,2) entry is l212 + l222, which must equal 4, so 1 + l222 = 4, l222 = 3, l22 = √3 ≈ 1.732. So L = [[2, 0], [1, 1.732]]. Quick sanity check on the (1,2) entry (should be 0 since L is lower-triangular and LT is upper-triangular): l11·0 + 0·l22 = 0. ✓

Now draw two samples. For z = (1, 0): x = μ + Lz = (0,0) + (2×1 + 0×0, 1×1 + 1.732×0) = (2, 1). For z = (0, 1): x = (0,0) + (2×0 + 0×1, 1×0 + 1.732×1) = (0, 1.732). Notice the second sample got a nonzero y even though z's y-component (1) never touched a nonzero entry of the first column of L, the l21 entry is exactly what leaks a bit of z1's influence into y when z1 is nonzero, and it's precisely this leak that creates correlation between the two output components. Two independent inputs; one correlated pair out. That's what the Cholesky factor buys you.

Misconception: just scale each axis independently. A tempting shortcut is x = (μx + σxz1, μy + σyz2), scale each standard normal by its own target standard deviation and call it done. This gets the variances exactly right but the correlation is always exactly zero, no matter what Σ's off-diagonal says, because z1 never touches y or vice versa. Only the lower-triangular l21 term, mixing z1 into the y output, can inject correlation. Skipping it silently throws away the tilt.
Draw samples and watch the empirical covariance converge to Σ

Each click draws 20 fresh Cholesky-transformed samples. The dashed ellipse is the true Σ = [[4,2],[2,4]]; the readout compares it to the empirical covariance measured from every sample drawn so far.

N = 0  |  empirical Σ not yet estimated
numpy
import numpy as np

def cholesky_2x2(Sigma):
    a, b, d = Sigma[0,0], Sigma[1,0], Sigma[1,1]
    l11 = np.sqrt(a)
    l21 = b / l11
    l22 = np.sqrt(d - l21**2)
    return np.array([[l11, 0.], [l21, l22]])

Sigma = np.array([[4., 2.], [2., 4.]])
L = cholesky_2x2(Sigma)             # [[2. 0.] [1. 1.732]] -- matches the hand calc
print(np.allclose(L, np.linalg.cholesky(Sigma)))  # True -- NumPy agrees

rng = np.random.default_rng(0)
z = rng.standard_normal((10000, 2))     # 10,000 independent standard normals
mu = np.array([0., 0.])
x = mu + z @ L.T                          # correlated samples, vectorized
print(np.cov(x.T))                       # converges to [[4,2],[2,4]] as N grows
When to reach for it: stress-testing a filter before it flies. Before a Kalman filter ever touches a real drone, engineers replay it against synthetic sensor noise with a known, deliberately-tilted covariance, verifying the filter's own uncertainty estimate keeps pace with reality across a hundred thousand simulated flights. Why generate correlated noise via Cholesky instead of just recording real flight noise and replaying it? Because Cholesky sampling lets you dial in specific, controlled covariance structures, including rare, nasty ones a real test flight might never happen to produce, and check the filter's gate (Chapter 3's Mahalanobis test) doesn't fire on healthy data or miss genuinely bad data.
Check: why does scaling two independent standard normals by σx and σy separately fail to produce a correlated sample, even when the target Σ has a nonzero off-diagonal?

Chapter 7: Products of Gaussians, Fusion in Closed Form

Here's the question every sensor-fusion system has to answer: you have two independent, uncertain estimates of the same quantity, a GPS fix and an IMU-predicted position, say, each shaped like its own Gaussian blob. How do you combine them into a single, better estimate, and can you prove it's actually better rather than just a reasonable-sounding average?

The answer comes from a genuinely beautiful fact: if you multiply two Gaussian probability densities together as functions of x (not caring yet that the product isn't itself a normalized probability), the result is proportional to another Gaussian. Multiplying two bell-shaped curves gives you back a bell-shaped curve, a closure property most probability distributions don't have. This is precisely what Bayes' Rule is doing when the prior and the likelihood are both Gaussian: posterior ∝ prior × likelihood, and Gaussian × Gaussian ∝ Gaussian, so the whole update stays inside the Gaussian family forever. No numerical integration required, ever.

The cleanest way to see how the two combine is to stop working with covariance and switch to its reciprocal, the precision matrix, Λ = Σ−1 (this is exactly the matrix Chapter 3 needed for Mahalanobis distance). Precision measures confidence directly: a small covariance (tight, confident belief) means a large precision, and vice versa. When you multiply two independent Gaussians together, their precisions simply add, and their precision-weighted means add too:

Λpost = Λ1 + Λ2     μpost = Σpost · ( Λ1μ1 + Λ2μ2 ),    where Σpost = Λpost−1

Read the precision-addition rule as: confidence is additive. Two independent sources of information are always at least as informative together as either one alone, you can never become less certain by adding an independent measurement, no matter how noisy that measurement is. This single fact is the mathematical reason sensor fusion works at all.

Hand-check: fusing a vague prior with a sharp measurement

Take a prior belief about position from dead-reckoning, vague and isotropic: μ1 = (0, 0), Σ1 = [[4, 0], [0, 4]] (standard deviation 2 in both directions, no correlation). Now a GPS fix comes in, more confident: μ2 = (3, 1), Σ2 = [[1, 0], [0, 1]] (standard deviation 1, four times more precise). Because both are diagonal, inverting is just reciprocating each diagonal entry: Λ1 = [[0.25, 0], [0, 0.25]] and Λ2 = [[1, 0], [0, 1]].

Add the precisions: Λpost = [[0.25+1, 0], [0, 0.25+1]] = [[1.25, 0], [0, 1.25]]. Invert back to covariance (reciprocate again, since it's still diagonal): Σpost = [[1/1.25, 0], [0, 1/1.25]] = [[0.8, 0], [0, 0.8]]. Check the headline claim: 0.8 is smaller than both inputs' variances (4 and 1), fusing two independent estimates always leaves you strictly more confident than your best individual estimate, not merely as confident as the better one.

For the mean: Λ1μ1 = [[0.25,0],[0,0.25]]·(0,0) = (0, 0). Λ2μ2 = [[1,0],[0,1]]·(3,1) = (3, 1). Sum: (0,0) + (3,1) = (3, 1). Finally μpost = Σpost·(3,1) = (0.8×3, 0.8×1) = (2.4, 0.8). Sanity check the weighting: the posterior mean sits exactly 80% of the way from the prior's mean (0,0) toward the measurement's mean (3,1), and 80% is exactly the fraction of total precision that the four-times-sharper measurement contributes (1 / 1.25 = 0.8). The vague prior only gets a 20% vote, precisely because it earned only 20% of the combined confidence.

The same computation, three ways

python, plain Python, no libraries (diagonal case)
def fuse_diag(mu1, var1, mu2, var2):
    # works per-axis since both covariances are diagonal here
    prec1, prec2 = 1.0 / var1, 1.0 / var2
    prec_post = prec1 + prec2
    var_post  = 1.0 / prec_post
    mu_post   = var_post * (prec1 * mu1 + prec2 * mu2)
    return mu_post, var_post

for axis in [(0.0, 4.0, 3.0, 1.0), (0.0, 4.0, 1.0, 1.0)]:
    mu1, var1, mu2, var2 = axis
    print(fuse_diag(mu1, var1, mu2, var2))
# (2.4, 0.8) then (0.8, 0.8) -- matches the hand calc, axis by axis
numpy (general, non-diagonal case)
import numpy as np

def fuse_gaussians(mu1, Sigma1, mu2, Sigma2):
    Lam1, Lam2 = np.linalg.inv(Sigma1), np.linalg.inv(Sigma2)
    Sigma_post = np.linalg.inv(Lam1 + Lam2)
    mu_post    = Sigma_post @ (Lam1 @ mu1 + Lam2 @ mu2)
    return mu_post, Sigma_post

mu1, Sigma1 = np.array([0.,0.]), np.eye(2) * 4.
mu2, Sigma2 = np.array([3.,1.]), np.eye(2) * 1.
mu_post, Sigma_post = fuse_gaussians(mu1, Sigma1, mu2, Sigma2)
print(mu_post)      # [2.4 0.8]
print(Sigma_post)   # [[0.8 0.] [0. 0.8]]
pytorch
import torch

def fuse_gaussians_t(mu1, Sigma1, mu2, Sigma2):
    Lam1, Lam2 = torch.linalg.inv(Sigma1), torch.linalg.inv(Sigma2)
    Sigma_post = torch.linalg.inv(Lam1 + Lam2)
    mu_post    = Sigma_post @ (Lam1 @ mu1 + Lam2 @ mu2)
    return mu_post, Sigma_post

mu1, Sigma1 = torch.zeros(2), torch.eye(2) * 4.
mu2, Sigma2 = torch.tensor([3., 1.]), torch.eye(2)
mu_post, Sigma_post = fuse_gaussians_t(mu1, Sigma1, mu2, Sigma2)
print(mu_post, Sigma_post)   # tensor([2.4, 0.8]) -- gradients flow through if mu1/Sigma1 require_grad
When to reach for it: fusing GPS with an IMU-based prediction. A drone's IMU-integrated dead-reckoning gives a vague position guess right after a GPS dropout; a fresh GPS fix arrives, sharper but not perfect. Precision-weighted fusion is exactly the numbers above: the sharper GPS pulls the estimate most of the way toward itself, but the dead-reckoning estimate still contributes, and the fused result is more confident than either alone. Why not just trust whichever sensor is usually more accurate and ignore the other? Because “usually more accurate” still carries real information the rest of the time, discarding the vaguer source throws away free precision, and near a GPS dropout it may be all you have.
Misconception: just average the two means. A plain average, (μ12)/2, is only correct when both estimates are equally confident. Here it would give (1.5, 0.5), noticeably off from the correct (2.4, 0.8), because it ignores that the measurement is four times more precise than the prior. Worse, a plain average of the covariances, (Σ12)/2 = [[2.5,0],[0,2.5]], is not just wrong, it's backwards, it says fusing is riskier than trusting the sharp sensor alone (variance 1), when fusing two independent sources should only ever make you more confident, never less.
Drag the prior and the measurement, watch the posterior form

The blue ellipse is the prior, the orange ellipse is the measurement, and the green ellipse is the precision-weighted posterior.

Prior σ (both axes) 2.0
Measurement σ (both axes) 1.0
μpost=(2.40, 0.80), σpost=0.89 (sharper than both inputs)
Check: you fuse a prior with variance 9 and a measurement with variance 1 (independent, same mean-quantity). Which is closest to the posterior variance?

Chapter 8: Showcase, The 2-D Fusion Lab

Every idea in this lesson comes together here. Below are two independent sensor estimates of the same 2-D quantity, call them Sensor A and Sensor B, each a full ellipse with its own center, spread, and tilt, exactly the objects Chapter 2 taught you to read. Drag either ellipse's center to move it. Use the sliders to stretch, squash, and rotate each one. Watch three things happen automatically, live, every time you touch a control.

First, the lab computes the Mahalanobis distance between the two sensors' means, not a point and a mean this time, but two whole estimates asking each other “are we even talking about the same place?” It does this exactly the way Chapter 3 taught, except the covariance used for the gate is ΣA + ΣB, the combined spread of “how far apart these two readings could plausibly be by chance alone” (this follows from Chapter 5's rule that independent covariances add). Second, it checks that distance against the 95% chi-squared gate from Chapter 3 and shows a clear accept/reject verdict. Third, whenever the gate passes, it computes the precision-weighted posterior from Chapter 7, the fused estimate, and draws it as a third ellipse, always smaller than either input.

Drag the ellipse centers. Rotate and resize with the sliders. Watch the gate and the fusion.

Blue = Sensor A. Orange = Sensor B. Green = fused posterior (only drawn when the gate passes, unless you force it).

,
Sensor A
σmajor2.0
σminor1.2
Rotation30°
Sensor B
σmajor1.3
σminor0.8
Rotation120°

Play the “break it” scenario. Click Reset to a consistent pair first: Sensor A sits near (0,0) with ΣA = [[4,0],[0,4]] (in its own un-rotated form), Sensor B sits nearby with a smaller, rotated ellipse. Compute the gate by hand: with A's mean near the origin and B's mean only a couple of units away, the Mahalanobis distance between the means, measured against ΣAB, lands comfortably under 5.991, the gate passes, the green posterior ellipse appears, and it's visibly smaller than both inputs, exactly as Chapter 7 predicts.

Now click Break it. Sensor B's center jumps to somewhere the two ellipses barely overlap. Redo the same hand-calc shape: say A stays at μA = (0,0) with ΣA = [[4,0],[0,4]], and B jumps to μB = (6,6) with ΣB = [[1,0],[0,1]]. The difference of means is (6,6); the combined covariance is ΣAB = [[5,0],[0,5]], whose inverse is [[0.2,0],[0,0.2]]. The Mahalanobis distance squared is 6×(6×0.2) + 6×(6×0.2) = 7.2 + 7.2 = 14.4, more than double the 5.991 gate. The lab flags this pair as inconsistent and refuses to fuse by default.

Why the gate matters, check the “Force-fuse anyway” box. Force the fusion through on the broken pair and watch the green ellipse land confidently between the two disagreeing sensors, looking exactly as authoritative as it did in the consistent case, small, tidy, certain. But it's wrong: the math assumed both sensors were telling the truth about the same quantity, and here they clearly aren't (one is likely malfunctioning, stale, or looking at a different target). A fused estimate is only as trustworthy as its gate. This is precisely why real Kalman filters run an innovation gate before every update, a small Mahalanobis distance says “these numbers agree, blend them”; a large one says “something is wrong, and blending anyway will manufacture false confidence.”

One more thing worth dragging around: rotate Sensor A and Sensor B to very different angles, so their long axes point in different directions, and watch the fused posterior's shape. It's neither input's shape, it's typically rounder and smaller than both, because each sensor is confident along a different direction, and fusing them fills in each other's blind spot. This is the deepest reason multi-sensor systems out-perform any single sensor: two ellipses tilted differently cover more of the plane with high confidence than either one alone, and the precision-addition math from Chapter 7 captures that automatically, you never have to reason about it by hand.

Check: you drag Sensor B far from Sensor A and the gate rejects the pair (Mahalanobis distance well above the chi-squared threshold). You force-fuse anyway. What best describes the result?

Chapter 9: Choose, Map and Connections

Ten chapters, one covariance matrix Σ running through every single one of them. This closing chapter is a map: which tool answers which question, and where each of them shows up again once you leave this lesson.

ToolCore question it answersCares about correlation?Reach for it when…
Single Gaussian (1-D)How surprising is one number?No, only one axis existsYou have one noisy scalar and need a z-score or a confidence interval.
Covariance Σ + ellipseWhat shape is my uncertainty?Yes, ρ is the tiltTwo or more correlated quantities need one confidence region, not separate error bars.
Mahalanobis distanceHow many sigmas away, given the tilt?YesDeciding whether a point is a plausible member of a cluster or an outlier.
MarginalWhat do I know about y alone?Ignores it by constructionYou only ever observe y and never get to see the correlated x.
ConditionalWhat does knowing x tell me about y?Yes, this is exactly what shrinks σYou just observed one correlated variable and need to update belief in another.
Linear transform (AΣAT)How does uncertainty move through a model?Yes, preserves and reshapes itPropagating uncertainty through a motion model, unit conversion, or projection.
Cholesky samplingHow do I generate realistic correlated noise?Yes, injected via the off-diagonal of LStress-testing a filter or simulator against a specific synthetic covariance.
Product of Gaussians (fusion)How do I combine two independent estimates?Yes, both inputs' tilts carry throughTwo independent sensors (or a prior and a measurement) estimate the same quantity.

Every entry in that table has a name you've now derived by hand, not just memorized: 6 and 2 as the eigenvalues of [[4,2],[2,4]]; 1.53σ and 2.08σ as two points at the identical Euclidean distance; 1.5 and 3 as a conditional mean and variance; 12 and 4 as a covariance transformed through a motion model; 2 and 1.732 as a Cholesky factor's entries; 2.4, 0.8, and 0.8 as a fused mean and variance. None of it was magic, every number came from the same handful of 2×2 operations: determinant, inverse, eigen-decomposition, matrix multiply.

The one-paragraph summary. A multivariate Gaussian is fully described by a mean vector and a covariance matrix Σ. Σ's eigenvectors are the axes of its uncertainty ellipse and its eigenvalues (square-rooted) are the axis lengths. Mahalanobis distance uses Σ−1 to measure “how many sigma away” correctly, accounting for the tilt. Conditioning on a correlated variable always shrinks uncertainty, never grows it. Linear transforms move Σ via the sandwich AΣAT. Cholesky decomposition builds correlated samples from independent ones. And multiplying two independent Gaussians (fusion) simply adds their precisions, confidence is additive.

This lesson was deliberately built as the missing middle chapter between two things you may already know. Bayes' Rule tells you, in general, how to update a belief with evidence: posterior ∝ prior × likelihood. This lesson showed you exactly what happens to that rule when everything involved is Gaussian, the update collapses into closed-form algebra on means and covariances, no integrals required, which is Chapter 7's entire content. The Kalman Filter, meanwhile, is nothing but this lesson's predict step (Chapter 5's AΣAT) alternating forever with this lesson's update step (Chapter 4's conditioning, equivalently Chapter 7's fusion), if the Kalman Filter ever felt like a black box of Greek letters, it's because you hadn't yet built the covariance-ellipse intuition this lesson exists to give you.

Bayes' Rule
general update: posterior ∝ prior × likelihood
↓ specialize to Gaussians
This lesson
Σ, ellipses, Mahalanobis, conditioning, fusion, all closed-form
↓ run predict/update forever
Kalman Filter
linear motion + linear measurement, exactly this algebra on a loop
↓ linearize a nonlinear model first
Extended Kalman Filter (EKF)
same update, with A and H replaced by local Jacobians each step

Two lessons worth visiting next, in order. First The Kalman Filter, you'll recognize the predict step as Chapter 5 and the update step as Chapter 4/7 immediately, and the “Kalman gain” will read as exactly the ρ·(σyx) term from Chapter 4, generalized to matrices. Then The Extended Kalman Filter, for when the motion or measurement model isn't linear, it linearizes around the current estimate first (a local A or H matrix, called a Jacobian) and then runs the exact same Gaussian algebra this lesson built. If Bayes' Rule itself still feels shaky, Bayes' Rule is the more general foundation this whole lesson specialized.

What this lesson deliberately left out. Real covariance matrices are often bigger than 2×2, a self-driving car might track a dozen correlated state variables at once. Every formula here generalizes directly (eigen-decomposition, Mahalanobis distance, conditioning, and fusion all work in n dimensions exactly as written, just with bigger matrices), but hand arithmetic stops being practical past 2×2 or 3×3, which is exactly why NumPy's np.linalg.eigh, np.linalg.inv, and np.linalg.cholesky exist, they're doing the same three operations this lesson did by hand, just at a scale a pencil can't reach.
Check: which single fact ties together Chapter 4 (conditioning) and Chapter 7 (products of Gaussians) as “the same idea wearing two names”?