Chapter 6 Exercises and Solutions
Thirteen exercises, worked in full. Every distribution, posterior and covariance below is checked numerically — against a grid, a simulation, or both — and the gaps are reported.
Four of them (6.6, 6.7, 6.11, 6.13) are proofs rather than computations. Each one is short, each is used earlier in the chapter, and each is worth doing rather than skipping.
How to use this page
Section titled “How to use this page”6.1 and 6.2 are drills on §6.2 and §6.4 — do them by hand. 6.3 and 6.4 are the two flavours of Bayes: conjugate updating and a discrete two-hypothesis problem. 6.5 and 6.12 are the payoff: between them they derive the Kalman filter and the Gaussian-linear-model posterior using nothing but §6.5’s closure rules, and they are the reason the chapter spent so long on marginals and conditionals. 6.6 to 6.11 and 6.13 fill in the results the chapter used without proof.
6.1 — Marginals and conditionals of a joint table
Section titled “6.1 — Marginals and conditionals of a joint table”Given over with five states and with three:
a. The marginals, by the sum rule (Equation 6.20) — add along the axis you are removing:
Both sum to , and the whole table sums to — so it is a valid joint.
b. The conditionals, by Equations 6.13 and 6.14 — divide by the relevant total.
Conditioning on fixes a row and divides by that row’s total, :
Conditioning on fixes a column and divides by :
Each sums to — the check that catches a transposed conditional.
Note how different the two are in character: given , the mass is concentrated on and ( each); given , is nearly uniform. Conditioning on one variable tells you a lot here; conditioning on the other tells you little.
6.2 — The bimodal mixture
Section titled “6.2 — The bimodal mixture”a. The marginals. Two facts compose. A Gaussian’s marginal is Gaussian with the corresponding block (Equation 6.68), and marginalising is linear, so it passes through the mixture weights. So each marginal is a one-dimensional mixture of the marginals of the components:
Note the off-diagonal disappears from both — marginalising discards exactly the cross-covariance information.
b. Mean, mode and median of each marginal. The means are exact by linearity (Equation 6.34); the modes and medians come from the density on a fine grid.
| mean (exact) | mean (measured) | mode | median | modes found | |
|---|---|---|---|---|---|
| 2 | |||||
| 1 |
Three different answers per marginal, and the two marginals do not even agree on how many humps there are — is bimodal, is unimodal. That is the book’s point in Figure 6.4, and it is why the mode of , , is at neither component mean ( and ): the two humps have merged into one whose peak sits between them.
c. The two-dimensional mean and mode.
exactly. The mode, by grid search, is at — essentially the first component’s centre, , despite that component carrying only of the weight. Measured densities: at against at . The first component wins because it is far tighter (covariance against a much broader block), so its mass is concentrated into a taller peak.
And the mean is not near either mode. sits in the low-density gap between the two clusters — a point the distribution almost never produces.
6.3 — A conjugate prior for the compiler
Section titled “6.3 — A conjugate prior for the compiler”A program compiles or does not, modelled as .
Choose the prior. Table 6.2: the conjugate prior for a Bernoulli likelihood is the Beta. Take .
Compute the posterior. For independent runs with successes, the likelihood is , so
The successes go into and the failures into — pseudo-counts again.
Checked: with and a simulated successes in runs, the posterior is with mean . Against a -point grid computation: worst density gap .
Note the whole run history collapses to two numbers. You never need to keep the individual .
6.4 — Which bag did the mango come from?
Section titled “6.4 — Which bag did the mango come from?”Bag 1: four mangos, two apples. Bag 2: four mangos, four apples. The coin shows heads with probability ; heads means bag 1.
Set it up as Bayes. The prior is the coin: , . The likelihoods are the bag compositions:
The evidence (Equation 6.27):
The posterior:
and . Simulated over four million trials: .
Read the direction of the update. The prior on bag 2 was ; the posterior is . Seeing a mango made bag 2 less likely, because bag 1 is the mango-richer bag ( against ). Both the prior and the likelihood point the same way here, so they reinforce.
6.5 — The Kalman filter, from §6.5 alone
Section titled “6.5 — The Kalman filter, from §6.5 alone”with .
a. What is the form of ?
Gaussian. The justification is two closure properties and nothing else. is an affine map of a Gaussian, so Gaussian (Equation 6.88). Adding independent Gaussian noise keeps it Gaussian (Equation 6.78). So each is Gaussian given , and the whole trajectory is jointly Gaussian — it is built by repeatedly applying operations that never leave the family.
b. Assume .
1. The prediction. Push the current belief through the dynamics:
Equation 6.88 for the map, Equation 6.78 for the noise. Note the covariance grows — prediction adds uncertainty.
2. The joint with the next observation. is another affine map, so stack them:
writing . The cross-covariance is by Equation 6.52.
3. Observe and condition. Equations 6.66 and 6.67, verbatim:
That is the Kalman filter. No new mathematics appeared: predict with 6.78 and 6.88, update with 6.66 and 6.67.
Worked with , and an observation :
| value | |
|---|---|
| predicted mean | |
| predicted trace | |
| Kalman gain | |
| posterior mean | |
| posterior trace | |
| eigenvalues of |
The trace more than halved, and the difference is positive semidefinite as Equation 6.67 requires. The zero eigenvalue is meaningful: with a scalar observation, only one direction in the two-dimensional state can be informed. The other learned nothing.
6.6 — Prove Equation 6.44
Section titled “6.6 — Prove Equation 6.44”Expand the definition and use linearity:
Now , so the last two terms are :
The only step is linearity of expectation (Equation 6.34), and the only subtlety is remembering that is a constant so it comes out of the expectation. Measured on eight numbers: both ways, gap .
6.7 — Prove Equation 6.45
Section titled “6.7 — Prove Equation 6.45”Expand the square inside the double sum:
Take the three pieces separately. (the inner sum over contributes copies), and likewise . The cross term factorises: . So
which is twice the raw-score variance of Equation 6.44. Measured: against , gap — and it took terms to compute what terms give.
The geometric reading the book offers: the pairwise distances between points and the distances from their centre carry the same information.
6.8 — The Bernoulli in natural parameter form
Section titled “6.8 — The Bernoulli in natural parameter form”Take logs and regroup:
Matching against :
Verified at and : worst gap .
6.9 — Binomial, Beta, and their product
Section titled “6.9 — Binomial, Beta, and their product”Binomial, at fixed :
so , , , and — the Bernoulli’s, scaled by . Verified across all outcomes at , : worst gap .
Beta. Here the random variable is :
so , , , and . Verified over a grid at , : worst gap .
Their product is exponential-family too. Both are exponentials of linear functions of , so multiplying adds the exponents — which means adding the natural parameters:
still of the same form. That is conjugacy, restated. The posterior is because natural parameters add — the same fact Example 6.11 derived by staring at the exponents.
6.10 — The product of two Gaussians, two ways
Section titled “6.10 — The product of two Gaussians, two ways”a. By completing the square. The product’s exponent is . Collect powers of : the quadratic term is , so the new precision is and hence
The linear term is , and matching it against gives
Everything left over is free of , and collecting it gives Equation 6.76’s constant.
b. In exponential-family form. From Example 6.13, a Gaussian’s natural parameters are . Multiplying two densities adds natural parameters (as in 6.9), so immediately
which is the same answer with no algebra at all. That is the point of asking for both routes.
Verified at , : the log scaling constant is from Equation 6.76, and integrating the product over a grid gives against the predicted — a relative gap of .
6.11 — Iterated expectations
Section titled “6.11 — Iterated expectations”Show .
The product rule (Equation 6.22) turns into :
using the sum rule to collapse the inner sum. Two rules, three lines.
Verified on a joint: both ways, gap . The conditional means were — very different from each other and from . The identity is about their weighted average, not about any one of them.
6.12 — Manipulation of Gaussian random variables
Section titled “6.12 — Manipulation of Gaussian random variables”with , and with independent.
a. The likelihood. Conditioning on freezes , leaving only the noise:
b. The marginal . Rather than doing the integral, use the fact that is a sum of two independent Gaussians — and . By Equations 6.50 and 6.51 the first has mean and covariance ; by Equation 6.78 the sum adds both:
c. Then . Identically: , and
d. The posterior . Following the book’s hint, build the joint. The cross-covariance is Equation 6.52:
(the noise is independent of , so it contributes nothing). Now apply Gaussian conditioning, Equations 6.66 and 6.67:
This is Bayesian linear regression, and Chapter 9 is largely this formula.
Verified with , , against two million simulated draws:
| quantity | worst gap against simulation |
|---|---|
and the posterior trace fell , with having eigenvalues — positive semidefinite, with one direction unlearned because has only two components against ‘s three.
6.13 — The probability integral transform
Section titled “6.13 — The probability integral transform”Given continuous with cdf , show is uniform.
Take and compute the cdf of :
The middle step needs to be strictly monotonic so the inverse exists and applying it preserves the inequality. A random variable whose cdf equals on is uniform on , so .
Verified on three distributions:
| drawn from | mean of | variance | worst |
|---|---|---|---|
| Exponential | |||
| Gamma |
against a uniform’s and .
All thirteen, in one table
Section titled “All thirteen, in one table”| # | What it is | Key result |
|---|---|---|
| 6.1 | joint table | marginals by summing an axis; conditionals by dividing by a row or column total |
| 6.2 | the bimodal mixture | mean exactly; mode at ; bimodal, not |
| 6.3 | Bernoulli conjugacy | |
| 6.4 | two bags | |
| 6.5 | time series | the Kalman filter, from Equations 6.66/6.67/6.78/6.88 |
| 6.6 | proof | , by linearity |
| 6.7 | proof | pairwise sum is twice the raw-score variance; terms for terms’ worth |
| 6.8 | Bernoulli, exp family | , |
| 6.9 | Binomial, Beta, product | natural parameters add, which is conjugacy |
| 6.10 | product of Gaussians | two routes, same and ; precisions add |
| 6.11 | iterated expectations | product rule then sum rule, three lines |
| 6.12 | Gaussian linear model | Bayesian linear regression’s posterior |
| 6.13 | proof | is uniform, needing only strict monotonicity |
The complete check
Section titled “The complete check”"""Every exercise at the end of Chapter 6, checked numerically."""
import math
import numpy as np
np.set_printoptions(precision=6, suppress=True, linewidth=150)
print("########## ex_6_1")
# Rows are y1, y2, y3; columns x1..x5.
P = np.array([
[0.01, 0.02, 0.03, 0.10, 0.10],
[0.05, 0.10, 0.05, 0.07, 0.20],
[0.10, 0.05, 0.03, 0.05, 0.04],
])
print("joint p(x, y):")
print(P)
print(f" sums to {P.sum():.10f} -- a valid joint")
px = P.sum(axis=0)
py = P.sum(axis=1)
print(f"a. p(x) = {px} sums to {px.sum():.10f}")
print(f" p(y) = {py} sums to {py.sum():.10f}")
# b. p(x | Y = y1) is the y1 ROW divided by its total.
cond_x_y1 = P[0] / py[0]
# p(y | X = x3) is the x3 COLUMN divided by its total.
cond_y_x3 = P[:, 2] / px[2]
print(f"b. p(x | Y=y1) = {cond_x_y1}")
print(f" row total p(y1) = {py[0]:.4f}, conditional sums to {cond_x_y1.sum():.10f}")
print(f" p(y | X=x3) = {cond_y_x3}")
print(f" column total p(x3) = {px[2]:.4f}, conditional sums to {cond_y_x3.sum():.10f}")
print(" note which total divides which: conditioning on Y fixes a ROW.")
print()
print("########## ex_6_2")
W = np.array([0.4, 0.6])
MU = [np.array([10.0, 2.0]), np.array([0.0, 0.0])]
SIG = [np.array([[1.0, 0.0], [0.0, 1.0]]),
np.array([[8.4, 2.0], [2.0, 1.7]])]
print("a. marginals: a Gaussian's marginal is Gaussian (Eq 6.68), and a mixture")
print(" marginalises component by component, so each marginal is a 1-D mixture:")
for d, name in ((0, "x1"), (1, "x2")):
print(f" p({name}) = {W[0]} N({MU[0][d]:.1f}, {SIG[0][d,d]:.1f})"
f" + {W[1]} N({MU[1][d]:.1f}, {SIG[1][d,d]:.1f})")
grid = np.linspace(-14, 20, 400_001)
def norm1(t, m, v):
return np.exp(-0.5 * (t - m) ** 2 / v) / np.sqrt(2 * np.pi * v)
print()
print("b. per-marginal mean, mode and median:")
print(f" {'dim':>4} {'mean':>10} {'mode':>10} {'median':>10} {'modes found':>12}")
for d, name in ((0, "x1"), (1, "x2")):
dens = W[0] * norm1(grid, MU[0][d], SIG[0][d, d]) \
+ W[1] * norm1(grid, MU[1][d], SIG[1][d, d])
mass = np.trapezoid(dens, grid)
mean = np.trapezoid(grid * dens, grid) / mass
cdf = np.concatenate([[0.0], np.cumsum((dens[1:] + dens[:-1]) / 2 * np.diff(grid))])
cdf /= cdf[-1]
med = float(grid[int(np.searchsorted(cdf, 0.5))])
peaks = [i for i in range(1, len(dens) - 1)
if dens[i] > dens[i - 1] and dens[i] > dens[i + 1]
and dens[i] > 0.02 * dens.max()]
mode = float(grid[int(np.argmax(dens))])
print(f" {name:>4} {mean:>10.6f} {mode:>10.6f} {med:>10.6f} {len(peaks):>12}")
# The exact mean, by linearity.
print(f" exact mean by Eq 6.34: "
f"{W[0]*MU[0][d] + W[1]*MU[1][d]:.6f}")
print()
print("c. the two-dimensional mean and mode:")
mean2 = W[0] * MU[0] + W[1] * MU[1]
print(f" mean, exactly by linearity: {mean2}")
# The mode of the joint: search the density on a grid.
g1 = np.linspace(-9, 15, 1200)
g2 = np.linspace(-7, 9, 1200)
G1, G2 = np.meshgrid(g1, g2)
pts = np.stack([G1.ravel(), G2.ravel()], axis=1)
dens2 = np.zeros(pts.shape[0])
for w, m, S in zip(W, MU, SIG):
d = pts - m
Si = np.linalg.inv(S)
dens2 += w * np.exp(-0.5 * np.einsum("ij,jk,ik->i", d, Si, d)) \
/ (2 * np.pi * np.sqrt(np.linalg.det(S)))
i = int(np.argmax(dens2))
print(f" mode, by grid search: {pts[i]} density {dens2[i]:.6f}")
# Compare the density at the two component centres.
for w, m, S in zip(W, MU, SIG):
dd = np.zeros(1)
for w2, m2, S2 in zip(W, MU, SIG):
v = (m - m2)[None, :]
dd += w2 * np.exp(-0.5 * v @ np.linalg.inv(S2) @ v.T).ravel() \
/ (2 * np.pi * np.sqrt(np.linalg.det(S2)))
print(f" density at component centre {m}: {float(dd[0]):.6f}")
print(" the mode is NOT the mean, and the mean sits between the two humps.")
print()
print("########## ex_6_3")
# Conjugate prior for a Bernoulli likelihood is the Beta (Table 6.2).
print(" conjugate prior: Beta(alpha, beta). With N observations summing to s:")
print(" p(mu | x_1..x_N) = Beta(alpha + s, beta + N - s)")
rng = np.random.default_rng(3)
mu_true = 0.7
xs = (rng.random(50) < mu_true).astype(int)
a0, b0 = 2.0, 2.0
s = int(xs.sum())
N = xs.size
print(f" simulated: N = {N}, successes s = {s}, prior Beta({a0}, {b0})")
print(f" posterior Beta({a0 + s}, {b0 + N - s}) mean "
f"{(a0+s)/(a0+b0+N):.6f} (truth {mu_true})")
# Check against a grid.
gg = np.linspace(1e-6, 1 - 1e-6, 200_001)
lik = gg ** s * (1 - gg) ** (N - s)
pri = gg ** (a0 - 1) * (1 - gg) ** (b0 - 1)
post = lik * pri
post /= np.trapezoid(post, gg)
lc = math.lgamma(a0 + s + b0 + N - s) - math.lgamma(a0 + s) - math.lgamma(b0 + N - s)
closed = np.exp(lc + (a0 + s - 1) * np.log(gg) + (b0 + N - s - 1) * np.log1p(-gg))
print(f" grid posterior vs closed form: worst gap {np.abs(post - closed).max():.2e}")
print(" the compiler either works or it does not, and one Beta absorbs every run.")
print()
print("########## ex_6_4")
# Two bags, a biased coin, Bayes.
p_h = 0.6
p_mango_b1 = 4 / 6
p_mango_b2 = 4 / 8
p_mango = p_h * p_mango_b1 + (1 - p_h) * p_mango_b2
post_b2 = (1 - p_h) * p_mango_b2 / p_mango
print(f" P(heads) = {p_h}, so bag 1 with prob {p_h} and bag 2 with prob {1-p_h}")
print(f" P(mango | bag 1) = 4/6 = {p_mango_b1:.6f}")
print(f" P(mango | bag 2) = 4/8 = {p_mango_b2:.6f}")
print(f" evidence P(mango) = {p_h}*{p_mango_b1:.4f} + {1-p_h}*{p_mango_b2:.4f}"
f" = {p_mango:.6f}")
print(f" P(bag 2 | mango) = {1-p_h}*{p_mango_b2:.4f} / {p_mango:.6f} = "
f"{post_b2:.10f} = 1/3")
print(f" P(bag 1 | mango) = {1 - post_b2:.10f} = 2/3 (they sum to "
f"{post_b2 + (1-post_b2):.10f})")
# Simulate to confirm.
M = 4_000_000
heads = rng.random(M) < p_h
draw = rng.random(M)
is_mango = np.where(heads, draw < p_mango_b1, draw < p_mango_b2)
print(f" simulated P(bag 2 | mango) = "
f"{float((~heads & is_mango).sum() / is_mango.sum()):.6f}")
print(" note the prior favoured bag 1 (0.6) AND bag 1 is richer in mangos, so")
print(" seeing a mango pushes further toward bag 1: 0.4 prior -> 0.333 posterior.")
print()
print("########## ex_6_5_kalman")
# The time-series model. Everything stays Gaussian, so only means and
# covariances need propagating.
A = np.array([[0.9, 0.2], [-0.1, 0.95]])
Q = np.array([[0.30, 0.05], [0.05, 0.20]])
C = np.array([[1.0, 0.5]])
R = np.array([[0.25]])
mu_t = np.array([1.0, -0.5])
S_t = np.array([[0.40, 0.10], [0.10, 0.30]])
print("a. p(x_0, ..., x_T) is GAUSSIAN: x_{t+1} = A x_t + w is an affine map of a")
print(" Gaussian plus independent Gaussian noise, and Eq 6.78 and 6.88 say both")
print(" operations preserve Gaussianity. The joint of jointly-Gaussian variables")
print(" is Gaussian, so the whole trajectory is one big Gaussian.")
print()
# b1: the predictive.
mu_pred = A @ mu_t
S_pred = A @ S_t @ A.T + Q
print(f"b1. p(x_{{t+1}} | y_1..y_t) = N(A mu_t, A Sigma_t A^T + Q)")
print(f" mean {mu_pred}")
print(f" cov\n{S_pred}")
# b2: the joint of x_{t+1} and y_{t+1}.
mu_y = C @ mu_pred
S_yy = C @ S_pred @ C.T + R
S_xy = S_pred @ C.T
print(f"b2. the joint p(x_{{t+1}}, y_{{t+1}} | y_1..y_t) is Gaussian with")
print(f" mean_x {mu_pred} mean_y {mu_y}")
print(f" Sigma_xx\n{S_pred}")
print(f" Sigma_xy {S_xy.ravel()} Sigma_yy {S_yy.ravel()}")
# b3: condition on the observation. This is the Kalman update.
y_hat = np.array([0.8])
K = S_xy @ np.linalg.inv(S_yy)
mu_post = mu_pred + (K @ (y_hat - mu_y)).ravel()
S_post = S_pred - K @ S_xy.T
print(f"b3. observing y = {y_hat[0]}, Eq 6.66 and 6.67 give")
print(f" gain K = {K.ravel()}")
print(f" posterior mean {mu_post}")
print(f" posterior cov\n{S_post}")
print(f" trace fell {np.trace(S_pred):.6f} -> {np.trace(S_post):.6f}, and")
print(f" Sigma_pred - Sigma_post has eigenvalues "
f"{np.linalg.eigvalsh(S_pred - S_post)} -> PSD, as conditioning must be")
print(" that is exactly the Kalman filter: predict with 6.78/6.88, update with")
print(" 6.66/6.67. No new mathematics is needed.")
print()
print("########## ex_6_6_and_6_7")
d = np.array([2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0])
n = d.size
mu_d = d.mean()
lhs66 = float(np.mean((d - mu_d) ** 2))
rhs66 = float(np.mean(d ** 2) - mu_d ** 2)
print("6.6 E[(x-mu)^2] = E[x^2] - 2 mu E[x] + mu^2 = E[x^2] - mu^2")
print(f" measured: {lhs66:.12f} against {rhs66:.12f} gap {abs(lhs66-rhs66):.1e}")
lhs67 = float(np.sum((d[:, None] - d[None, :]) ** 2) / n ** 2)
print("6.7 expanding (x_i - x_j)^2 = x_i^2 - 2 x_i x_j + x_j^2 and summing over")
print(" both indices gives 2 N sum x_i^2 - 2 (sum x_i)^2, hence 2[E[x^2]-E[x]^2]")
print(f" measured: {lhs67:.12f} against 2 * {rhs66:.12f} = {2*rhs66:.12f}")
print(f" gap {abs(lhs67 - 2*rhs66):.1e} -- N^2 = {n**2} terms for what N = {n} give")
print()
print("########## ex_6_8_and_6_9")
print("6.8 Bernoulli: p(x|mu) = exp[x log(mu/(1-mu)) + log(1-mu)], so")
print(" phi(x) = x, theta = log(mu/(1-mu)), A(theta) = log(1+e^theta), h(x) = 1")
for m in (0.25, 0.6):
th = math.log(m / (1 - m))
for xv in (0, 1):
direct = m ** xv * (1 - m) ** (1 - xv)
expfam = math.exp(xv * th - math.log1p(math.exp(th)))
print(f" mu={m}, x={xv}: direct {direct:.10f} exp-family {expfam:.10f}"
f" gap {abs(direct-expfam):.1e}")
print()
print("6.9 Binomial(N, mu) at fixed N: phi(m) = m, theta = log(mu/(1-mu)),")
print(" A(theta) = N log(1+e^theta), h(m) = C(N, m)")
Nb, mub = 12, 0.35
thb = math.log(mub / (1 - mub))
worst = 0.0
for k in range(Nb + 1):
direct = math.comb(Nb, k) * mub ** k * (1 - mub) ** (Nb - k)
ef = math.comb(Nb, k) * math.exp(k * thb - Nb * math.log1p(math.exp(thb)))
worst = max(worst, abs(direct - ef))
print(f" worst gap over all {Nb+1} outcomes: {worst:.1e}")
print(" Beta(a,b): phi(mu) = [log mu, log(1-mu)], theta = [a-1, b-1],")
print(" A(theta) = log B(a,b), h(mu) = 1")
aa, bb = 3.0, 4.0
gm = np.linspace(1e-6, 1 - 1e-6, 20001)
lb = math.lgamma(aa) + math.lgamma(bb) - math.lgamma(aa + bb)
direct = np.exp(-lb + (aa - 1) * np.log(gm) + (bb - 1) * np.log1p(-gm))
phi_mu = np.stack([np.log(gm), np.log1p(-gm)], axis=1)
ef = np.exp(phi_mu @ np.array([aa - 1, bb - 1]) - lb)
print(f" worst gap over the grid: {np.abs(direct - ef).max():.1e}")
print(" and the PRODUCT of the two: adding exponents means adding natural")
print(" parameters, so the product is exponential-family with theta summed --")
print(" which is exactly why the Beta is conjugate for the Binomial.")
print()
print("########## ex_6_10_product_of_gaussians")
a_, A_ = np.array([1.0, -0.5]), np.array([[1.2, 0.3], [0.3, 0.8]])
b_, B_ = np.array([-0.4, 1.1]), np.array([[0.6, -0.2], [-0.2, 1.5]])
C_ = np.linalg.inv(np.linalg.inv(A_) + np.linalg.inv(B_))
c_ = C_ @ (np.linalg.solve(A_, a_) + np.linalg.solve(B_, b_))
sign, ld = np.linalg.slogdet(A_ + B_)
diff = a_ - b_
log_scale = -np.log(2 * np.pi) - 0.5 * ld - 0.5 * diff @ np.linalg.solve(A_ + B_, diff)
print("a. completing the square: the exponents are two quadratics in x; adding them")
print(" gives a single quadratic whose Hessian is A^-1 + B^-1, so C = (A^-1+B^-1)^-1")
print(" and whose linear term gives c = C(A^-1 a + B^-1 b). What is left over does")
print(" not involve x, and is the scaling constant.")
print("b. in exponential-family form: a Gaussian's natural parameters are")
print(" [Sigma^-1 mu, -Sigma^-1/2]. Multiplying densities ADDS natural parameters,")
print(" so Sigma_new^-1 = A^-1 + B^-1 and Sigma_new^-1 mu_new = A^-1 a + B^-1 b.")
print(" Both routes give the same C and c, which is the point of the exercise.")
print(f" C =\n{C_}")
print(f" c = {c_}")
print(f" log scaling constant = {log_scale:.10f}")
# Verify by integration.
gx = np.linspace(-8, 8, 1600)
X1, X2 = np.meshgrid(gx, gx)
pp = np.stack([X1.ravel(), X2.ravel()], axis=1)
def gauss(pts, m, S):
d0 = pts - m
Si = np.linalg.inv(S)
return np.exp(-0.5 * np.einsum("ij,jk,ik->i", d0, Si, d0)) \
/ (2 * np.pi * np.sqrt(np.linalg.det(S)))
prod = gauss(pp, a_, A_) * gauss(pp, b_, B_)
dxg = gx[1] - gx[0]
mass = prod.sum() * dxg * dxg
print(f" integral of the product: {mass:.10e} predicted {np.exp(log_scale):.10e}")
print(f" relative gap {abs(mass - np.exp(log_scale))/np.exp(log_scale):.2e}")
print()
print("########## ex_6_11_iterated_expectations")
# E_X[x] = E_Y[E_X[x|y]]. Check on a discrete joint.
J = np.array([[0.05, 0.10, 0.02],
[0.15, 0.08, 0.20],
[0.10, 0.20, 0.10]])
J = J / J.sum()
xv = np.array([-1.0, 0.5, 2.0]) # states of X, indexing columns
yv = np.array([0.0, 1.0, 2.0]) # states of Y, indexing rows
p_y = J.sum(axis=1)
E_x = float((J.sum(axis=0) * xv).sum())
E_x_given_y = (J / p_y[:, None]) @ xv
outer = float((p_y * E_x_given_y).sum())
print(" the proof: E_Y[E_X[x|y]] = sum_y p(y) sum_x x p(x|y) = sum_y sum_x x p(x,y)")
print(" = sum_x x sum_y p(x,y) = sum_x x p(x) = E_X[x]. The product rule is the")
print(" only step, and the sum rule collapses the inner sum.")
print(f" E[x] directly {E_x:.12f}")
print(f" E_Y[E_X[x|y]] {outer:.12f}")
print(f" gap {abs(E_x - outer):.1e}")
print(f" the inner conditional means are {E_x_given_y}, which differ a lot -- the")
print(" identity is about their weighted average, not about them individually.")
print()
print("########## ex_6_12_gaussian_manipulation")
D_, E_, F_ = 3, 2, 2
rngz = np.random.default_rng(12)
Araw = rngz.normal(size=(D_, D_))
Sx = Araw @ Araw.T + D_ * np.eye(D_)
mux = rngz.normal(size=D_)
Am = rngz.normal(size=(E_, D_))
bm = rngz.normal(size=E_)
Qm = np.diag(np.abs(rngz.normal(size=E_)) + 0.4)
print(f"a. p(y | x) = N(y | A x + b, Q) -- shapes A {Am.shape}, Q {Qm.shape}")
mu_y = Am @ mux + bm
S_y = Am @ Sx @ Am.T + Qm
print(f"b. mu_y = A mu_x + b = {mu_y}")
print(f" Sigma_y = A Sigma_x A^T + Q =\n{S_y}")
Cm = rngz.normal(size=(F_, E_))
Rm = np.diag(np.abs(rngz.normal(size=F_)) + 0.3)
mu_z = Cm @ mu_y
S_z = Cm @ S_y @ Cm.T + Rm
print(f"c. p(z | y) = N(z | C y, R); mu_z = C mu_y = {mu_z}")
print(f" Sigma_z = C Sigma_y C^T + R =\n{S_z}")
# d. the posterior p(x | y_hat), via the joint and Gaussian conditioning.
S_xy = Sx @ Am.T # Cov[x, y] = Sigma_x A^T
y_obs = mu_y + np.array([0.7, -0.4])
Kg = S_xy @ np.linalg.inv(S_y)
mu_post = mux + Kg @ (y_obs - mu_y)
S_post = Sx - Kg @ S_xy.T
print(f"d. Cov[x,y] = Sigma_x A^T, shape {S_xy.shape}")
print(f" posterior mean {mu_post}")
print(f" posterior cov\n{S_post}")
print(f" trace {np.trace(Sx):.6f} -> {np.trace(S_post):.6f}")
print(f" Sigma_x - Sigma_post eigenvalues {np.linalg.eigvalsh(Sx - S_post)} -> PSD")
# Confirm b and c by simulation.
M2 = 2_000_000
xsim = rngz.multivariate_normal(mux, Sx, M2)
ysim = xsim @ Am.T + bm + rngz.multivariate_normal(np.zeros(E_), Qm, M2)
zsim = ysim @ Cm.T + rngz.multivariate_normal(np.zeros(F_), Rm, M2)
print(f" simulated: mu_y gap {np.abs(ysim.mean(axis=0)-mu_y).max():.4f}, "
f"Sigma_y gap {np.abs(np.cov(ysim.T, bias=True)-S_y).max():.4f}")
print(f" mu_z gap {np.abs(zsim.mean(axis=0)-mu_z).max():.4f}, "
f"Sigma_z gap {np.abs(np.cov(zsim.T, bias=True)-S_z).max():.4f}")
print(f" Cov[x,y] gap "
f"{np.abs(np.cov(np.hstack([xsim, ysim]).T, bias=True)[:D_, D_:] - S_xy).max():.4f}")
print()
print("########## ex_6_13_probability_integral_transform")
print(" proof: let Y = F_x(X) and take u in [0,1]. Then")
print(" P(Y <= u) = P(F_x(X) <= u) = P(X <= F_x^-1(u)) = F_x(F_x^-1(u)) = u,")
print(" using strict monotonicity to invert. A cdf equal to u on [0,1] IS the")
print(" uniform, so Y ~ U[0,1].")
rngp = np.random.default_rng(13)
for name, smp, cdf in (
("N(0,1)", rngp.standard_normal(2_000_000),
lambda t: 0.5 * (1 + np.vectorize(math.erf)(t / math.sqrt(2)))),
("Exponential(1.5)", rngp.exponential(1 / 1.5, 2_000_000),
lambda t: 1 - np.exp(-1.5 * t)),
("Gamma(2, 1)", rngp.gamma(2.0, 1.0, 2_000_000),
lambda t: 1 - np.exp(-t) * (1 + t))):
u = cdf(smp)
srt = np.sort(u[:200_000])
ks = float(np.abs(srt - np.linspace(0, 1, srt.size)).max())
print(f" {name:>18} mean {u.mean():.6f} var {u.var():.6f} "
f"worst |F_emp - u| {ks:.6f}")
print(" a uniform has mean 0.5 and variance 0.083333.")########## ex_6_1
joint p(x, y):
[[0.01 0.02 0.03 0.1 0.1 ]
[0.05 0.1 0.05 0.07 0.2 ]
[0.1 0.05 0.03 0.05 0.04]]
sums to 1.0000000000 -- a valid joint
a. p(x) = [0.16 0.17 0.11 0.22 0.34] sums to 1.0000000000
p(y) = [0.26 0.47 0.27] sums to 1.0000000000
b. p(x | Y=y1) = [0.038462 0.076923 0.115385 0.384615 0.384615]
row total p(y1) = 0.2600, conditional sums to 1.0000000000
p(y | X=x3) = [0.272727 0.454545 0.272727]
column total p(x3) = 0.1100, conditional sums to 1.0000000000
note which total divides which: conditioning on Y fixes a ROW.
########## ex_6_2
a. marginals: a Gaussian's marginal is Gaussian (Eq 6.68), and a mixture
marginalises component by component, so each marginal is a 1-D mixture:
p(x1) = 0.4 N(10.0, 1.0) + 0.6 N(0.0, 8.4)
p(x2) = 0.4 N(2.0, 1.0) + 0.6 N(0.0, 1.7)
b. per-marginal mean, mode and median:
dim mean mode median modes found
x1 4.000008 9.998390 2.803905 2
exact mean by Eq 6.34: 4.000000
x2 0.800000 1.330770 0.868710 1
exact mean by Eq 6.34: 0.800000
c. the two-dimensional mean and mode:
mean, exactly by linearity: [4. 0.8]
mode, by grid search: [9.99583 1.994162] density 0.063734
density at component centre [10. 2.]: 0.063735
density at component centre [0. 0.]: 0.029783
the mode is NOT the mean, and the mean sits between the two humps.
########## ex_6_3
conjugate prior: Beta(alpha, beta). With N observations summing to s:
p(mu | x_1..x_N) = Beta(alpha + s, beta + N - s)
simulated: N = 50, successes s = 34, prior Beta(2.0, 2.0)
posterior Beta(36.0, 18.0) mean 0.666667 (truth 0.7)
grid posterior vs closed form: worst gap 8.79e-14
the compiler either works or it does not, and one Beta absorbs every run.
########## ex_6_4
P(heads) = 0.6, so bag 1 with prob 0.6 and bag 2 with prob 0.4
P(mango | bag 1) = 4/6 = 0.666667
P(mango | bag 2) = 4/8 = 0.500000
evidence P(mango) = 0.6*0.6667 + 0.4*0.5000 = 0.600000
P(bag 2 | mango) = 0.4*0.5000 / 0.600000 = 0.3333333333 = 1/3
P(bag 1 | mango) = 0.6666666667 = 2/3 (they sum to 1.0000000000)
simulated P(bag 2 | mango) = 0.333517
note the prior favoured bag 1 (0.6) AND bag 1 is richer in mangos, so
seeing a mango pushes further toward bag 1: 0.4 prior -> 0.333 posterior.
########## ex_6_5_kalman
a. p(x_0, ..., x_T) is GAUSSIAN: x_{t+1} = A x_t + w is an affine map of a
Gaussian plus independent Gaussian noise, and Eq 6.78 and 6.88 say both
operations preserve Gaussianity. The joint of jointly-Gaussian variables
is Gaussian, so the whole trajectory is one big Gaussian.
b1. p(x_{t+1} | y_1..y_t) = N(A mu_t, A Sigma_t A^T + Q)
mean [ 0.8 -0.575]
cov
[[0.672 0.1545 ]
[0.1545 0.45575]]
b2. the joint p(x_{t+1}, y_{t+1} | y_1..y_t) is Gaussian with
mean_x [ 0.8 -0.575] mean_y [0.5125]
Sigma_xx
[[0.672 0.1545 ]
[0.1545 0.45575]]
Sigma_xy [0.74925 0.382375] Sigma_yy [1.190438]
b3. observing y = 0.8, Eq 6.66 and 6.67 give
gain K = [0.62939 0.321205]
posterior mean [ 0.98095 -0.482653]
posterior cov
[[ 0.200429 -0.086163]
[-0.086163 0.332929]]
trace fell 1.127750 -> 0.533358, and
Sigma_pred - Sigma_post has eigenvalues [0. 0.594392] -> PSD, as conditioning must be
that is exactly the Kalman filter: predict with 6.78/6.88, update with
6.66/6.67. No new mathematics is needed.
########## ex_6_6_and_6_7
6.6 E[(x-mu)^2] = E[x^2] - 2 mu E[x] + mu^2 = E[x^2] - mu^2
measured: 4.000000000000 against 4.000000000000 gap 0.0e+00
6.7 expanding (x_i - x_j)^2 = x_i^2 - 2 x_i x_j + x_j^2 and summing over
both indices gives 2 N sum x_i^2 - 2 (sum x_i)^2, hence 2[E[x^2]-E[x]^2]
measured: 8.000000000000 against 2 * 4.000000000000 = 8.000000000000
gap 0.0e+00 -- N^2 = 64 terms for what N = 8 give
########## ex_6_8_and_6_9
6.8 Bernoulli: p(x|mu) = exp[x log(mu/(1-mu)) + log(1-mu)], so
phi(x) = x, theta = log(mu/(1-mu)), A(theta) = log(1+e^theta), h(x) = 1
mu=0.25, x=0: direct 0.7500000000 exp-family 0.7500000000 gap 0.0e+00
mu=0.25, x=1: direct 0.2500000000 exp-family 0.2500000000 gap 5.6e-17
mu=0.6, x=0: direct 0.4000000000 exp-family 0.4000000000 gap 0.0e+00
mu=0.6, x=1: direct 0.6000000000 exp-family 0.6000000000 gap 0.0e+00
6.9 Binomial(N, mu) at fixed N: phi(m) = m, theta = log(mu/(1-mu)),
A(theta) = N log(1+e^theta), h(m) = C(N, m)
worst gap over all 13 outcomes: 1.7e-16
Beta(a,b): phi(mu) = [log mu, log(1-mu)], theta = [a-1, b-1],
A(theta) = log B(a,b), h(mu) = 1
worst gap over the grid: 1.3e-15
and the PRODUCT of the two: adding exponents means adding natural
parameters, so the product is exponential-family with theta summed --
which is exactly why the Beta is conjugate for the Binomial.
########## ex_6_10_product_of_gaussians
a. completing the square: the exponents are two quadratics in x; adding them
gives a single quadratic whose Hessian is A^-1 + B^-1, so C = (A^-1+B^-1)^-1
and whose linear term gives c = C(A^-1 a + B^-1 b). What is left over does
not involve x, and is the scaling constant.
b. in exponential-family form: a Gaussian's natural parameters are
[Sigma^-1 mu, -Sigma^-1/2]. Multiplying densities ADDS natural parameters,
so Sigma_new^-1 = A^-1 + B^-1 and Sigma_new^-1 mu_new = A^-1 a + B^-1 b.
Both routes give the same C and c, which is the point of the exercise.
C =
[[0.376271 0.020339]
[0.020339 0.482567]]
c = [ 0.237288 -0.160533]
log scaling constant = -3.7048850193
integral of the product: 2.4603046081e-02 predicted 2.4603046081e-02
relative gap 2.82e-15
########## ex_6_11_iterated_expectations
the proof: E_Y[E_X[x|y]] = sum_y p(y) sum_x x p(x|y) = sum_y sum_x x p(x,y)
= sum_x x sum_y p(x,y) = sum_x x p(x) = E_X[x]. The product rule is the
only step, and the sum rule collapses the inner sum.
E[x] directly 0.530000000000
E_Y[E_X[x|y]] 0.530000000000
gap 1.1e-16
the inner conditional means are [0.235294 0.674419 0.5 ], which differ a lot -- the
identity is about their weighted average, not about them individually.
########## ex_6_12_gaussian_manipulation
a. p(y | x) = N(y | A x + b, Q) -- shapes A (2, 3), Q (2, 2)
b. mu_y = A mu_x + b = [-2.419224 2.837356]
Sigma_y = A Sigma_x A^T + Q =
[[ 17.57386 -13.710442]
[-13.710442 19.444137]]
c. p(z | y) = N(z | C y, R); mu_z = C mu_y = [ 0.877603 -0.221628]
Sigma_z = C Sigma_y C^T + R =
[[3.621597 1.61282 ]
[1.61282 4.471699]]
d. Cov[x,y] = Sigma_x A^T, shape (3, 2)
posterior mean [0.732067 0.171968 0.379045]
posterior cov
[[ 1.141729 0.108221 -2.077995]
[ 0.108221 1.413374 -0.873665]
[-2.077995 -0.873665 4.555614]]
trace 17.391171 -> 7.110717
Sigma_x - Sigma_post eigenvalues [0. 3.593818 6.686635] -> PSD
simulated: mu_y gap 0.0060, Sigma_y gap 0.0143
mu_z gap 0.0027, Sigma_z gap 0.0056
Cov[x,y] gap 0.0180
########## ex_6_13_probability_integral_transform
proof: let Y = F_x(X) and take u in [0,1]. Then
P(Y <= u) = P(F_x(X) <= u) = P(X <= F_x^-1(u)) = F_x(F_x^-1(u)) = u,
using strict monotonicity to invert. A cdf equal to u on [0,1] IS the
uniform, so Y ~ U[0,1].
N(0,1) mean 0.500063 var 0.083365 worst |F_emp - u| 0.001340
Exponential(1.5) mean 0.499978 var 0.083265 worst |F_emp - u| 0.002682
Gamma(2, 1) mean 0.500376 var 0.083258 worst |F_emp - u| 0.002350
a uniform has mean 0.5 and variance 0.083333.-
In 6.2, the mixture puts weight 0.4 on the component at (10, 2) and 0.6 on the one at (0, 0). Where is the mode?
Weight is not height. And the mean at (4.0, 0.8) sits in the low-density gap between the clusters — a point the distribution almost never produces, which is why a mean is a poor summary of a multimodal density.
pch.quizShowAnswer
B — At (10, 2) — the lighter component, because its covariance is the identity while the other's is much broader, so its mass concentrates into a taller peak. Measured densities: 0.063735 against 0.029783 — Weight is not height. And the mean at (4.0, 0.8) sits in the low-density gap between the clusters — a point the distribution almost never produces, which is why a mean is a poor summary of a multimodal density.
-
Exercise 6.5 asks for the form of the joint over a whole trajectory. What is the argument?
That is why the exercise says you need not compute the joint explicitly: the closure properties settle the form. Parts b1 to b3 then produce the Kalman filter from Equations 6.66 and 6.67 with no new mathematics.
pch.quizShowAnswer
B — It is Gaussian, because the dynamics apply only two operations — an affine map (Eq 6.88) and adding independent Gaussian noise (Eq 6.78) — and neither leaves the Gaussian family — That is why the exercise says you need not compute the joint explicitly: the closure properties settle the form. Parts b1 to b3 then produce the Kalman filter from Equations 6.66 and 6.67 with no new mathematics.
-
In 6.4 the prior on bag 2 is 0.4 and the posterior after seeing a mango is 1/3. Why did it go down?
Worth checking the direction of every Bayes update. Here P(mango) = 0.6 exactly, and the posterior 0.2/0.6 = 1/3 confirms it — simulated at 0.333517.
pch.quizShowAnswer
B — Because bag 1 is the mango-richer bag — 2/3 against 1/2 — so a mango is evidence FOR bag 1. Prior and likelihood point the same way and reinforce each other — Worth checking the direction of every Bayes update. Here P(mango) = 0.6 exactly, and the posterior 0.2/0.6 = 1/3 confirms it — simulated at 0.333517.
-
Exercise 6.9 asks you to show the product of a Beta and a Binomial is in the exponential family. What is the one-line reason?
It also explains WHY Beta-Binomial conjugacy holds, rather than just verifying it: theta_prior + theta_likelihood gives Beta(alpha+m, beta+N-m), which is Example 6.11's answer arrived at structurally.
pch.quizShowAnswer
B — Because both are exponentials of linear functions of the same sufficient statistics, so multiplying ADDS the exponents — which means adding the natural parameters, leaving the form unchanged. That is exactly conjugacy — It also explains WHY Beta-Binomial conjugacy holds, rather than just verifying it: theta_prior + theta_likelihood gives Beta(alpha+m, beta+N-m), which is Example 6.11's answer arrived at structurally.
-
In both 6.5 and 6.12 the covariance difference has a zero eigenvalue. What does that mean?
Equation 6.67 subtracts a positive-semidefinite term whose rank is at most the observation's dimension. A zero eigenvalue is the honest report that the unobserved direction is exactly as uncertain as it was before.
pch.quizShowAnswer
B — One direction in the state learned nothing, because the observation has fewer components than the state — a scalar y against a 2-D state in 6.5, a 2-D y against a 3-D x in 6.12 — Equation 6.67 subtracts a positive-semidefinite term whose rank is at most the observation's dimension. A zero eigenvalue is the honest report that the unobserved direction is exactly as uncertain as it was before.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – 6.1, marginals and conditionals
Section titled “Exercise 1 – 6.1, marginals and conditionals”Exercise 2 – 6.4, the mango
Section titled “Exercise 2 – 6.4, the mango”Exercise 3 – 6.5, one Kalman step
Section titled “Exercise 3 – 6.5, one Kalman step”Exercise 4 – 6.6 and 6.7, the two variance identities
Section titled “Exercise 4 – 6.6 and 6.7, the two variance identities”Exercise 5 – 6.13, the probability integral transform
Section titled “Exercise 5 – 6.13, the probability integral transform”Recall card
Section titled “Recall card”- 6.1: marginals sum an axis, conditionals divide by a total. Conditioning on Y fixes a ROW and divides by p(y); conditioning on X fixes a COLUMN. Each conditional must sum to 1 — the check that catches a transpose.
- 6.2: the mixture’s mean is exact by linearity, (4.0, 0.8), but its mode is at (10, 2) — the LIGHTER component, because its covariance is tighter and so its peak is taller. Measured 0.063735 against 0.029783.
- 6.2: marginalising can lose a mode. p(x1) is bimodal with peaks near 0 and 10; p(x2) is unimodal with its peak at 1.3308, at neither component mean.
- 6.3: the Beta is conjugate for a Bernoulli, so the posterior is Beta(alpha + s, beta + N - s). The whole run history collapses to two numbers.
- 6.4: P(bag 2 | mango) = 1/3. The prior on bag 2 was 0.4 and it FELL, because bag 1 is the mango-richer bag, so prior and likelihood reinforce.
- 6.5 is the Kalman filter, and it needs no new mathematics: predict with Eq 6.78 and 6.88, update with Eq 6.66 and 6.67. Measured: trace 1.1278 down to 0.5334 after one scalar observation.
- A scalar observation can only inform one direction. The covariance reduction had eigenvalues [0, 0.594392] — the zero is the unobserved direction, and it is honest rather than an error.
- 6.6: V[x] = E[x^2] - E[x]^2 follows from linearity of expectation plus the fact that mu is a constant.
- 6.7: the pairwise sum over N^2 terms equals TWICE the raw-score variance. The cross term factorises into the square of a sum, which is what collapses N^2 into N.
- 6.8 and 6.9: Bernoulli, Binomial and Beta are all exponential-family. The Binomial’s A(theta) is the Bernoulli’s scaled by N; the Beta’s sufficient statistics are (log mu, log(1-mu)).
- 6.9’s real content: multiplying exponential-family densities ADDS natural parameters. That is why the Beta is conjugate for the Binomial — conjugacy is structural, not lucky.
- 6.10: two derivations, one answer. Completing the square and adding natural parameters both give C = (A-inverse + B-inverse)-inverse. Precisions add.
- 6.11: iterated expectations is the product rule then the sum rule, three lines. The conditional means were 0.235, 0.674, 0.500 — the identity is about their weighted average.
- 6.12 is Bayesian linear regression. Cov[x,y] = Sigma_x A-transpose, then Gaussian conditioning. Verified against two million simulated draws to within 0.018.
- 6.13: F_x(x) is uniform, and the proof needs only strict monotonicity — enough to invert F and preserve the inequality. Verified on a Gaussian, an exponential and a Gamma.
Next: Chapter 6 Formula Sheet — every definition, theorem and identity in the chapter on one page.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading