Skip to content

Orthogonal Projections

This is the section the chapter exists for. Everything before it — norms, inner products, angles, orthogonality, complements — was assembled so that this question could be answered:

Given a subspace UU and a vector x\mathbf{x} outside it, which point of UU is closest to x\mathbf{x}?

The answer is a three-line recipe, and it is the most reused computation in the remainder of the book. Least squares in Chapter 9 is this computation. PCA in Chapter 10 minimises the error this computation leaves behind. The margin of a support vector machine in Chapter 12 is the affine version of it. Learn it once here and those chapters become applications rather than new material.

  • Definition 3.10 and the property π2=π\pi^2 = \pi that defines a projection.
  • Projection onto a line (§3.8.1): λ=bxb2\lambda = \dfrac{\mathbf{b}^\top\mathbf{x}}{\lVert\mathbf{b}\rVert^2} and Pπ=bbbb\mathbf{P}_\pi = \dfrac{\mathbf{b}\mathbf{b}^\top}{\mathbf{b}^\top\mathbf{b}}.
  • Projection onto a general subspace (§3.8.2): the normal equation, the pseudo-inverse, and the projection matrix B(BB)1B\mathbf{B}(\mathbf{B}^\top\mathbf{B})^{-1}\mathbf{B}^\top.
  • Gram-Schmidt (§3.8.3) as projection used constructively.
  • Projection onto an affine subspace (§3.8.4): shift, project, shift back.
  • Why least squares is a projection, and why PCA is the projection error minimised — both measured on real data.

A projection is a shadow cast by a light directly overhead. The pencil is x\mathbf{x}, the floor is UU, and the shadow is πU(x)\pi_U(\mathbf{x}).

Three properties of that shadow give you the whole of §3.8 before any algebra.

  • The shadow is on the floor. πU(x)U\pi_U(\mathbf{x}) \in U — that is what makes the problem finite: instead of nn ambient coordinates you need only dimU\dim U of them.
  • The shadow of a shadow is itself. Something already flat on the floor casts itself. That is π2=π\pi^2 = \pi, and it is the definition of a projection.
  • The line from the pencil to its shadow is vertical. Perpendicular to the floor — which is why “closest” and “perpendicular displacement” are the same condition, by Pythagoras.
diagram Diagram mermaid

Idempotence is the whole definition. It says nothing about orthogonality — an oblique projection is idempotent too, and it is a projection. What the rest of this section adds is the extra requirement that the displacement be perpendicular to UU, which is what makes the result the closest point rather than merely some point.

Projection onto a one-dimensional subspace

Section titled “Projection onto a one-dimensional subspace”

Let U=span[b]U = \mathrm{span}[\mathbf{b}] be a line through the origin. Since πU(x)U\pi_U(\mathbf{x}) \in U, it is λb\lambda\mathbf{b} for some scalar λ\lambda, and there is exactly one unknown.

Step 1 — find λ\lambda. The displacement must be orthogonal to b\mathbf{b}:

b, xλb=0    bxλbb=0    λ=bxbb=bxb2(3.40)\langle \mathbf{b},\ \mathbf{x} - \lambda\mathbf{b}\rangle = 0 \;\Longleftrightarrow\; \mathbf{b}^\top\mathbf{x} - \lambda\,\mathbf{b}^\top\mathbf{b} = 0 \;\Longleftrightarrow\; \lambda = \frac{\mathbf{b}^\top\mathbf{x}}{\mathbf{b}^\top\mathbf{b}} = \frac{\mathbf{b}^\top\mathbf{x}}{\lVert\mathbf{b}\rVert^2} \tag{3.40}

Step 2 — the projection.

πU(x)=λb=bxb2b(3.42)\pi_U(\mathbf{x}) = \lambda\mathbf{b} = \frac{\mathbf{b}^\top\mathbf{x}}{\lVert\mathbf{b}\rVert^2}\,\mathbf{b} \tag{3.42}

Its length has a clean form (Equations 3.43 and 3.44):

πU(x)=λb=cosωx\lVert\pi_U(\mathbf{x})\rVert = \lvert\lambda\rvert\,\lVert\mathbf{b}\rVert = \lvert\cos\omega\rvert\,\lVert\mathbf{x}\rVert

so the projection is the original length scaled by the absolute cosine of the angle — which is 11 when x\mathbf{x} is already on the line and 00 when it is perpendicular to it.

Step 3 — the projection matrix. Step 2 is linear in x\mathbf{x}, so rewrite it as a matrix acting on x\mathbf{x}:

πU(x)=b(bx)b2=bbb2xPπ=bbbb(3.46)\pi_U(\mathbf{x}) = \frac{\mathbf{b}\,(\mathbf{b}^\top\mathbf{x})}{\lVert\mathbf{b}\rVert^2} = \frac{\mathbf{b}\mathbf{b}^\top}{\lVert\mathbf{b}\rVert^2}\,\mathbf{x} \qquad\Longrightarrow\qquad \mathbf{P}_\pi = \frac{\mathbf{b}\mathbf{b}^\top}{\mathbf{b}^\top\mathbf{b}} \tag{3.46}

Note the reordering in the numerator — moving b\mathbf{b} to the left of the scalar bx\mathbf{b}^\top\mathbf{x} turns a vector-times-scalar into a matrix-times-vector, and that is the whole trick. bb\mathbf{b}\mathbf{b}^\top is an outer product: an n×nn \times n matrix of rank 11. So Pπ\mathbf{P}_\pi is always symmetric and always of rank 11 here.

Now dimU=m1\dim U = m \geq 1, with an ordered basis (b1,,bm)(\mathbf{b}_1, \dots, \mathbf{b}_m) stacked as the columns of BRn×m\mathbf{B} \in \mathbb{R}^{n \times m}. The same three steps.

Step 1 — the coordinates. πU(x)=iλibi=Bλ\pi_U(\mathbf{x}) = \sum_i \lambda_i\mathbf{b}_i = \mathbf{B}\boldsymbol{\lambda}, and “closest” means the displacement is orthogonal to all mm basis vectors, giving mm simultaneous conditions:

b1(xBλ)=0,,bm(xBλ)=0\mathbf{b}_1^\top(\mathbf{x} - \mathbf{B}\boldsymbol{\lambda}) = 0,\quad\dots,\quad \mathbf{b}_m^\top(\mathbf{x} - \mathbf{B}\boldsymbol{\lambda}) = 0

Stack them:

B(xBλ)=0     BBλ=Bx (3.56)\mathbf{B}^\top(\mathbf{x} - \mathbf{B}\boldsymbol{\lambda}) = \mathbf{0} \;\Longleftrightarrow\; \boxed{\ \mathbf{B}^\top\mathbf{B}\,\boldsymbol{\lambda} = \mathbf{B}^\top\mathbf{x}\ } \tag{3.56}

This is the normal equation. Because b1,,bm\mathbf{b}_1,\dots,\mathbf{b}_m are a basis and therefore linearly independent, BBRm×m\mathbf{B}^\top\mathbf{B} \in \mathbb{R}^{m\times m} is invertible, so

λ=(BB)1Bx(3.57)\boldsymbol{\lambda} = (\mathbf{B}^\top\mathbf{B})^{-1}\mathbf{B}^\top\mathbf{x} \tag{3.57}

The matrix (BB)1B(\mathbf{B}^\top\mathbf{B})^{-1}\mathbf{B}^\top is the pseudo-inverse of B\mathbf{B}. It exists for non-square B\mathbf{B}; it requires only that BB\mathbf{B}^\top\mathbf{B} be positive definite, which holds when B\mathbf{B} has full column rank.

Step 2 — the projection.

πU(x)=Bλ=B(BB)1Bx(3.58)\pi_U(\mathbf{x}) = \mathbf{B}\boldsymbol{\lambda} = \mathbf{B}(\mathbf{B}^\top\mathbf{B})^{-1}\mathbf{B}^\top\mathbf{x} \tag{3.58}

Step 3 — the projection matrix.

Pπ=B(BB)1B(3.59)\mathbf{P}_\pi = \mathbf{B}(\mathbf{B}^\top\mathbf{B})^{-1}\mathbf{B}^\top \tag{3.59}

Two special cases fall out immediately. If m=1m = 1 then BB\mathbf{B}^\top\mathbf{B} is a scalar and Equation 3.59 becomes bb/bb\mathbf{b}\mathbf{b}^\top / \mathbf{b}^\top\mathbf{b} — Equation 3.46 exactly, so §3.8.1 was never a separate result. And if the basis is orthonormal, BB=I\mathbf{B}^\top\mathbf{B} = \mathbf{I} and everything collapses:

πU(x)=BBx,λ=Bx(3.65, 3.66)\pi_U(\mathbf{x}) = \mathbf{B}\mathbf{B}^\top\mathbf{x}, \qquad \boldsymbol{\lambda} = \mathbf{B}^\top\mathbf{x} \tag{3.65, 3.66}

which is what §3.5 promised and why every numerical routine hands you an orthonormal basis.

The projection error (Equation 3.63), also called the reconstruction error, is

xπU(x)\lVert \mathbf{x} - \pi_U(\mathbf{x})\rVert

and this is the quantity PCA minimises.

The book adds a note worth carrying: in practice one often adds a “jitter term” εI\varepsilon\mathbf{I} to BB\mathbf{B}^\top\mathbf{B} to guarantee numerical stability and positive definiteness. That is exactly ridge regularisation, and the book says it can be rigorously derived by Bayesian inference — see Chapter 9.

This is §3.8.2 used constructively rather than as an answer — the same projection, applied to build a basis instead of to answer a question about one. The numerical hazards are on the Orthonormal Basis page, and they are severe.

Three moves: subtract the support point so the problem becomes a subspace problem, project, add the support point back. Nothing new is computed — the distance to an affine subspace is literally the distance from the shifted vector to the direction space. Chapter 12 uses this to derive the separating hyperplane.

Find Pπ\mathbf{P}_\pi for the line through the origin spanned by b=(1,2,2)\mathbf{b} = (1,2,2)^\top.

bb=1+4+4=9\mathbf{b}^\top\mathbf{b} = 1 + 4 + 4 = 9, and the outer product is

bb=[122][122]=[122244244]\mathbf{b}\mathbf{b}^\top = \begin{bmatrix}1\\2\\2\end{bmatrix}\begin{bmatrix}1 & 2 & 2\end{bmatrix} = \begin{bmatrix}1 & 2 & 2\\ 2 & 4 & 4\\ 2 & 4 & 4\end{bmatrix} Pπ=19[122244244](3.47)\mathbf{P}_\pi = \frac{1}{9}\begin{bmatrix}1 & 2 & 2\\ 2 & 4 & 4\\ 2 & 4 & 4\end{bmatrix} \tag{3.47}

Now project x=(1,1,1)\mathbf{x} = (1,1,1)^\top. Row by row:

rowworkingresult
11+2+21 + 2 + 255
22+4+42 + 4 + 41010
32+4+42 + 4 + 41010
πU(x)=19[51010]span ⁣[[122]](3.48)\pi_U(\mathbf{x}) = \frac{1}{9}\begin{bmatrix}5\\10\\10\end{bmatrix} \in \mathrm{span}\!\left[\begin{bmatrix}1\\2\\2\end{bmatrix}\right] \tag{3.48}

which is 59b\tfrac{5}{9}\mathbf{b}, and indeed λ=bx/bb=5/9=0.555556\lambda = \mathbf{b}^\top\mathbf{x}/\mathbf{b}^\top\mathbf{b} = 5/9 = 0.555556. Applying Pπ\mathbf{P}_\pi again changes nothing, as Definition 3.10 requires. Measured: the eigenvalues of Pπ\mathbf{P}_\pi are (0,0,1)(0, 0, 1), the rank is 11 and the trace is 11.

U=span ⁣[[111],[012]]R3,x=[600]U = \mathrm{span}\!\left[\begin{bmatrix}1\\1\\1\end{bmatrix}, \begin{bmatrix}0\\1\\2\end{bmatrix}\right] \subseteq \mathbb{R}^3, \qquad \mathbf{x} = \begin{bmatrix}6\\0\\0\end{bmatrix}

First, check the generating set is a basis. The two vectors are not multiples of each other, so they are linearly independent, and

B=[101112].\mathbf{B} = \begin{bmatrix}1 & 0\\ 1 & 1\\ 1 & 2\end{bmatrix} .

Second, form the two pieces of the normal equation:

BB=[111012][101112]=[3335],Bx=[111012][600]=[60](3.60)\mathbf{B}^\top\mathbf{B} = \begin{bmatrix}1&1&1\\0&1&2\end{bmatrix}\begin{bmatrix}1&0\\1&1\\1&2\end{bmatrix} = \begin{bmatrix}3&3\\3&5\end{bmatrix}, \qquad \mathbf{B}^\top\mathbf{x} = \begin{bmatrix}1&1&1\\0&1&2\end{bmatrix}\begin{bmatrix}6\\0\\0\end{bmatrix} = \begin{bmatrix}6\\0\end{bmatrix} \tag{3.60}

Third, solve [3335]λ=[60]\begin{bmatrix}3&3\\3&5\end{bmatrix}\boldsymbol{\lambda} = \begin{bmatrix}6\\0\end{bmatrix}. Subtracting the first row from the second gives 2λ2=62\lambda_2 = -6, so λ2=3\lambda_2 = -3, and then 3λ1=63(3)=153\lambda_1 = 6 - 3(-3) = 15, so λ1=5\lambda_1 = 5:

λ=[53](3.61)\boldsymbol{\lambda} = \begin{bmatrix}5\\-3\end{bmatrix} \tag{3.61}

Fourth, the projection:

πU(x)=Bλ=5[111]3[012]=[521](3.62)\pi_U(\mathbf{x}) = \mathbf{B}\boldsymbol{\lambda} = 5\begin{bmatrix}1\\1\\1\end{bmatrix} - 3\begin{bmatrix}0\\1\\2\end{bmatrix} = \begin{bmatrix}5\\2\\-1\end{bmatrix} \tag{3.62}

Fifth, the projection error:

xπU(x)=[121]=1+4+1=62.449490(3.63)\lVert\mathbf{x} - \pi_U(\mathbf{x})\rVert = \left\lVert\begin{bmatrix}1\\-2\\1\end{bmatrix}\right\rVert = \sqrt{1+4+1} = \sqrt{6} \approx 2.449490 \tag{3.63}

Sixth, the projection matrix:

Pπ=B(BB)1B=16[521222125](3.64)\mathbf{P}_\pi = \mathbf{B}(\mathbf{B}^\top\mathbf{B})^{-1}\mathbf{B}^\top = \frac{1}{6}\begin{bmatrix}5&2&-1\\2&2&2\\-1&2&5\end{bmatrix} \tag{3.64}

Measured checks: B(xπU(x))=(0,0)\mathbf{B}^\top(\mathbf{x} - \pi_U(\mathbf{x})) = (0, 0) exactly, Pπ2=Pπ\mathbf{P}_\pi^2 = \mathbf{P}_\pi, the rank is 22, the trace is 22, and the eigenvalues are (0,1,1)(0, 1, 1).

That last pattern is not a coincidence, and it is the cleanest way to recognise a projection matrix: P2=P\mathbf{P}^2 = \mathbf{P} forces every eigenvalue to satisfy λ2=λ\lambda^2 = \lambda, so λ{0,1}\lambda \in \{0, 1\}, and the number of ones is the dimension of the subspace. Since the trace is the sum of the eigenvalues, the trace of a projection matrix equals the dimension it projects onto.

b1=(2,0)\mathbf{b}_1 = (2,0)^\top, b2=(1,1)\mathbf{b}_2 = (1,1)^\top.

u1=b1=(2,0)\mathbf{u}_1 = \mathbf{b}_1 = (2,0)^\top, and

u2=b2u1u1u12b2=[11][1000][11]=[11][10]=[01](3.71)\mathbf{u}_2 = \mathbf{b}_2 - \frac{\mathbf{u}_1\mathbf{u}_1^\top}{\lVert\mathbf{u}_1\rVert^2}\mathbf{b}_2 = \begin{bmatrix}1\\1\end{bmatrix} - \begin{bmatrix}1&0\\0&0\end{bmatrix}\begin{bmatrix}1\\1\end{bmatrix} = \begin{bmatrix}1\\1\end{bmatrix} - \begin{bmatrix}1\\0\end{bmatrix} = \begin{bmatrix}0\\1\end{bmatrix} \tag{3.71}

and u1u2=0\mathbf{u}_1^\top\mathbf{u}_2 = 0. The projection matrix u1u1/u12=diag(1,0)\mathbf{u}_1\mathbf{u}_1^\top/\lVert\mathbf{u}_1\rVert^2 = \mathrm{diag}(1,0) is worth a second look: it keeps the first coordinate and discards the second, which is exactly what projecting onto the x1x_1-axis should do.

L=x0+UL = \mathbf{x}_0 + U with x0=(1,2,1)\mathbf{x}_0 = (1,2,-1)^\top and U=span[(1,1,0),(0,1,1)]U = \mathrm{span}[(1,1,0)^\top, (0,1,1)^\top]; project x=(4,0,2)\mathbf{x} = (4,0,2)^\top.

stepworkingresult
shiftxx0\mathbf{x} - \mathbf{x}_0(3,2,3)(3, -2, 3)^\top
project onto UUπU(3,2,3)\pi_U(3,-2,3)(0.333333, 0.666667, 0.333333)(0.333333,\ 0.666667,\ 0.333333)^\top
shift backx0+πU()\mathbf{x}_0 + \pi_U(\cdot)(1.333333, 2.666667, 0.666667)(1.333333,\ 2.666667,\ -0.666667)^\top
distancexπL(x)\lVert\mathbf{x} - \pi_L(\mathbf{x})\rVert4.6188024.618802
the same, via Eq 3.73bd(xx0, πU(xx0))d(\mathbf{x}-\mathbf{x}_0,\ \pi_U(\mathbf{x}-\mathbf{x}_0))4.6188024.618802

The two distances agree exactly, which is the content of Equation 3.73: translation does not change distances, so the affine problem never needed its own formula.

book_examples.py
import numpy as np
 
# --- Example 3.10: projection onto a line ---------------------------------
b = np.array([1.0, 2.0, 2.0])
P = np.outer(b, b) / float(b @ b)
print("9 * P =\n", 9 * P)
x = np.array([1.0, 1.0, 1.0])
print("lambda =", float(b @ x) / float(b @ b))
print("9 * pi(x) =", 9 * (P @ x))
print("P^2 = P:", np.allclose(P @ P, P), " rank", np.linalg.matrix_rank(P),
      " trace", round(float(np.trace(P)), 12), " eig", np.round(np.linalg.eigvalsh(P), 12))
 
# --- Example 3.11: projection onto a plane --------------------------------
B = np.array([[1.0, 0.0], [1.0, 1.0], [1.0, 2.0]])
x = np.array([6.0, 0.0, 0.0])
G, c = B.T @ B, B.T @ x
print("B^T B =\n", G, "\nB^T x =", c)
lam = np.linalg.solve(G, c)
pi = B @ lam
err = x - pi
Pm = B @ np.linalg.inv(G) @ B.T
print("lambda =", lam, " pi(x) =", pi)
print("error =", err, " norm =", np.linalg.norm(err), " sqrt(6) =", np.sqrt(6))
print("6 * P =\n", np.round(6 * Pm, 10))
print("B^T error =", np.round(B.T @ err, 12))
print("P^2 = P:", np.allclose(Pm @ Pm, Pm), " rank", np.linalg.matrix_rank(Pm),
      " trace", round(float(np.trace(Pm)), 12), " eig", np.round(np.linalg.eigvalsh(Pm), 12))
 
# --- Affine: shift, project, shift back -----------------------------------
x0 = np.array([1.0, 2.0, -1.0])
Ba = np.stack([np.array([1.0, 1.0, 0.0]), np.array([0.0, 1.0, 1.0])], axis=1)
Q, _ = np.linalg.qr(Ba)
xa = np.array([4.0, 0.0, 2.0])
shifted = xa - x0
pu = Q @ (Q.T @ shifted)
pl = x0 + pu
print("pi_L(x) =", np.round(pl, 6))
print("d(x, L) =", round(float(np.linalg.norm(xa - pl)), 6),
      " via Eq 3.73b =", round(float(np.linalg.norm(shifted - pu)), 6))
output
9 * P =
 [[1. 2. 2.]
 [2. 4. 4.]
 [2. 4. 4.]]
lambda = 0.5555555555555556
9 * pi(x) = [ 5. 10. 10.]
P^2 = P: True  rank 1  trace 1.0  eig [0. 0. 1.]
B^T B =
 [[3. 3.]
 [3. 5.]] 
B^T x = [6. 0.]
lambda = [ 5. -3.]  pi(x) = [ 5.  2. -1.]
error = [ 1. -2.  1.]  norm = 2.449489742783178  sqrt(6) = 2.449489742783178
6 * P =
 [[ 5.  2. -1.]
 [ 2.  2.  2.]
 [-1.  2.  5.]]
B^T error = [0. 0.]
P^2 = P: True  rank 2  trace 2.0  eig [-0.  1.  1.]
pi_L(x) = [ 1.333333  2.666667 -0.666667]
d(x, L) = 4.618802  via Eq 3.73b = 4.618802

Every number in Examples 3.10 and 3.11 reproduces digit for digit, including the 6\sqrt{6} projection error to sixteen figures and the B\mathbf{B}^\top error being exactly zero rather than 101610^{-16}.

The first sketch is §3.8.1 with everything draggable, and it shows the equality πU(x)=cosωx\lVert\pi_U(\mathbf{x})\rVert = \lvert\cos\omega\rvert\lVert\mathbf{x}\rVert live.

sketch Projection onto a line, with the cosine identity live p5.js
Drag x and drag b. The green arrow is the projection, the red dashed segment is the error, and the small square marks the right angle at the foot. The readout gives lambda, both routes to the projection's length, and the check that the error is orthogonal to b — which is the condition the formula was derived from.

The second sketch drops a point cloud onto a line and shows what PCA is choosing between: rotate the line and watch the total squared error and the total projected spread trade off exactly.

sketch Rotate the line: what PCA is actually optimising p5.js
Twenty points, a line through their mean, and every point's projection onto it. Drag the angle knob. The two bars are the total squared projection error and the variance of the projected coordinates, and their sum is constant — so minimising the error and maximising the spread are the same problem, which is the whole of Chapter 10 in one control.

Now the stepped versions of both book examples. First the line, Example 3.10:

vectorExample 3.10, step by stepprojection onto a line, §3.8.1
b1x
dim U 1ambient R^3
setupThe line U is everything of the form λb with b = (1, 2, 2), and x = (1, 1, 1) is not on it. The question is which point of U is closest to x — and "closest" is going to turn out to mean one specific thing.
1/7

One direction, so one unknown and no system to solve. The last two frames multiply out the projection matrix and check that squaring it changes nothing.

Then the plane, Example 3.11 — with the normal equation formed and solved on screen:

vectorExample 3.11, step by stepprojection onto a subspace, §3.8.2
b1b2x
dim U 2ambient R^3
setupThe subspace U is spanned by (1, 1, 1) and (0, 1, 2), and x = (6, 0, 0) sticks out of it. We want the point of U closest to x, expressed in the basis of U.
1/8

Watch the Gram matrix appear, get inverted, and produce lambda = (5, -3). The verification frame measures the inner product of the displacement against each basis vector, and both are exactly zero.

projection_from_scratch.py
import numpy as np
 
def project(x, B, ridge=0.0):
    """Project x onto the column span of B, three ways, and check they agree.
 
    `ridge` is the book's jitter term: adding eps*I to B^T B guarantees positive
    definiteness. It changes the answer, which is the point of the pitfall below.
    """
    B = np.atleast_2d(np.asarray(B, dtype=float))
    if B.shape[0] == 1:
        B = B.T
    m = B.shape[1]
 
    # Route 1: the normal equation, as stated in Equation 3.56.
    G = B.T @ B + ridge * np.eye(m)
    lam = np.linalg.solve(G, B.T @ x)
    p1 = B @ lam
 
    # Route 2: the projection matrix of Equation 3.59, formed explicitly.
    P = B @ np.linalg.inv(G) @ B.T
    p2 = P @ x
 
    # Route 3: an orthonormal basis, where everything collapses to B B^T.
    Q, _ = np.linalg.qr(B)
    p3 = Q @ (Q.T @ x)
 
    return {
        "lambda": lam,
        "projection": p1,
        "P": P,
        "error": x - p1,
        "error_norm": float(np.linalg.norm(x - p1)),
        "route_1_vs_2": float(np.linalg.norm(p1 - p2)),
        "route_1_vs_3": float(np.linalg.norm(p1 - p3)),
        "idempotent": float(np.linalg.norm(P @ P - P)),
        "symmetric": float(np.linalg.norm(P - P.T)),
        "orthogonal_residual": float(np.abs(B.T @ (x - p1)).max()),
        "rank": int(np.linalg.matrix_rank(P)),
        "trace": float(np.trace(P)),
    }
 
B = np.array([[1.0, 0.0], [1.0, 1.0], [1.0, 2.0]])
x = np.array([6.0, 0.0, 0.0])
r = project(x, B)
for k in ("lambda", "projection", "error", "error_norm", "route_1_vs_2", "route_1_vs_3",
          "idempotent", "symmetric", "orthogonal_residual", "rank", "trace"):
    v = r[k]
    print(f"{k:22}", np.round(v, 12) if isinstance(v, np.ndarray) else v)
 
# The three routes agree here, but only one of them survives an ill-conditioned
# basis. A polynomial design matrix is the standard way to produce one: the
# monomials 1, t, t^2, ... are very nearly parallel on a short interval.
print()
n = 40
t = np.linspace(0.0, 1.0, n)
rng = np.random.default_rng(5)
y = rng.normal(size=n)
for degree in (5, 7, 9, 11):
    Bd = np.vander(t, degree + 1, increasing=True)
    G = Bd.T @ Bd
    p_normal = Bd @ np.linalg.solve(G, Bd.T @ y)
    Qb, _ = np.linalg.qr(Bd)
    p_qr = Qb @ (Qb.T @ y)
    print(f"degree {degree:3}   cond(B) {np.linalg.cond(Bd):9.2e}   "
          f"cond(B^T B) {np.linalg.cond(G):9.2e}   "
          f"|normal-eq minus QR| {np.linalg.norm(p_normal - p_qr):.3e}")
output
lambda                 [ 5. -3.]
projection             [ 5.  2. -1.]
error                  [ 1. -2.  1.]
error_norm             2.449489742783178
route_1_vs_2           6.280369834735101e-16
route_1_vs_3           6.661338147750939e-16
idempotent             2.6910028968576024e-16
symmetric              0.0
orthogonal_residual    0.0
rank                   2
trace                  1.9999999999999998
 
degree   5   cond(B)  3.48e+03   cond(B^T B)  1.21e+07   |normal-eq minus QR| 7.567e-11
degree   7   cond(B)  1.09e+05   cond(B^T B)  1.18e+10   |normal-eq minus QR| 5.842e-08
degree   9   cond(B)  3.52e+06   cond(B^T B)  1.24e+13   |normal-eq minus QR| 4.811e-06
degree  11   cond(B)  1.18e+08   cond(B^T B)  1.34e+16   |normal-eq minus QR| 4.807e-02

The first block is Example 3.11, verified through three independent routes that agree to 6.7×10166.7\times10^{-16}. Note trace printing as 1.99999999999999981.9999999999999998 rather than 22: the trace of a projection matrix is exactly the dimension mathematically, and two units in the last place numerically.

The second block is the warning, and it is worth reading column by column. cond(B^T B) is the square of cond(B) in every row, so forming the normal equation doubles the digits you lose. The consequence is in the last column: at degree 1111, where κ(B)=1.2×108\kappa(\mathbf{B}) = 1.2\times10^{8} — not an extreme figure for a polynomial fit — the normal-equation projection is 0.0480.048 away from the QR answer. That is not a rounding difference, it is a visible error in the fitted values.

A caution about how to demonstrate this, because it is easy to get wrong: if you build a test basis with orthogonal columns and merely shrink them, BB\mathbf{B}^\top\mathbf{B} comes out diagonal and solving it loses nothing at all, no matter how large the condition number is. The damage comes from nearly-parallel columns, which is why the demonstration above uses a monomial basis rather than a scaled orthogonal one.

figure A regression fit is a projection matplotlib
A scatter of twenty points with a fitted line, each point joined to the line by a short vertical segment, and a panel giving the fitted coefficients, the squared error, the measured X-transpose residual, and the idempotence and rank of the projection matrix. A scatter of twenty points with a fitted line, each point joined to the line by a short vertical segment, and a panel giving the fitted coefficients, the squared error, the measured X-transpose residual, and the idempotence and rank of the projection matrix.
The fitted values are P y for the projection matrix onto the column space of the design matrix. X-transpose r comes out at 8e-15 rather than zero only because of floating point: the residual is orthogonal to both columns, which is the normal equation. Rank 2, trace 2.
figure The projection error is the objective PCA minimises matplotlib
A curve of mean squared projection error against subspace dimension for 1797 digit images, falling to exactly zero at dimension 61, with four reconstructed digits beneath at dimensions 1, 4, 12 and 40. A curve of mean squared projection error against subspace dimension for 1797 digit images, falling to exactly zero at dimension 61, with four reconstructed digits beneath at dimensions 1, 4, 12 and 40.
Each point is the mean squared error of projecting all 1797 images onto their best k-dimensional subspace. The error reaches exactly zero at k = 61, which is the rank of the centred data, and 21 components suffice for 90% of the variance. The reconstructions underneath are the same projection applied to one image.
figure A projection matrix has only two eigenvalues matplotlib
A stem plot of the eight eigenvalues of a projection matrix onto a three-dimensional subspace, three of them at exactly one and five at exactly zero, with dashed reference lines and a measured panel. A stem plot of the eight eigenvalues of a projection matrix onto a three-dimensional subspace, three of them at exactly one and five at exactly zero, with dashed reference lines and a measured panel.
P squared equals P forces every eigenvalue to satisfy lambda squared equals lambda, so each is 0 or 1. Three of them are exactly 1 — the dimension of the subspace — and the trace is 3.000000000000, which is why the trace of a projection matrix tells you the dimension it projects onto.

From the regression figure. The fitted line is 0.5066+1.2691x-0.5066 + 1.2691x over twenty points, with a squared error of 2.3218912.321891. But the number to read is Xr=(8.0×1015, 7.6×1015)\mathbf{X}^\top\mathbf{r} = (8.0\times10^{-15},\ -7.6\times10^{-15}).

That is the normal equation, measured. Fitting a line by least squares and projecting y\mathbf{y} onto the column space of X\mathbf{X} are not analogous operations, they are the same operation: the condition that determines the fit is that the residual be orthogonal to every column of the design matrix, which is Equation 3.56 with B\mathbf{B} renamed X\mathbf{X}.

The projection matrix is rank 22 with trace 22 — one for the intercept column and one for the slope column — and P2P=3.6×1016\lVert\mathbf{P}^2 - \mathbf{P}\rVert = 3.6\times10^{-16}. In regression this matrix has its own name, the hat matrix, and its diagonal entries are the leverages. Same object, different vocabulary.

From the digits curve. Four measurements worth having:

kkmean squared errorvariance kept
11022.57141022.571414.89%14.89\%
4616.1911616.191148.71%48.71\%
12258.7058258.705878.47%78.47\%
4014.174214.174298.82%98.82\%
610.00000.0000100%100\%

The error hits exactly zero at k=61k = 61, not at k=64k = 64, because the centred data has rank 6161 — which the Orthogonal Complement page traced to three pixels that are zero in all 17971797 images. Beyond k=61k = 61 there is nothing left to capture, so the curve does not merely flatten; it terminates.

And 2121 components carry 90%90\% of the variance. Sixty-four numbers per image compressed to twenty-one, at a tenth of the squared error — which is a claim about a projection, computed with Equation 3.59 and nothing else. Chapter 10 will spend a chapter on which subspace is best; the fact that projecting onto a subspace is what you are doing is settled here.

From the spectrum. P2=P\mathbf{P}^2 = \mathbf{P} implies λ2=λ\lambda^2 = \lambda for every eigenvalue, so λ{0,1}\lambda \in \{0,1\} — measured, three exact ones and five exact zeros. Three consequences follow for free:

  • rank(P)=dimU\mathrm{rank}(\mathbf{P}) = \dim U, since the rank is the number of nonzero eigenvalues.
  • trace(P)=dimU\mathrm{trace}(\mathbf{P}) = \dim U, since the trace is their sum. Measured: 3.0000000000003.000000000000.
  • P\mathbf{P} is not invertible unless UU is the whole space. It throws information away, which is what a projection is for.

That third point is the one people trip over. P\mathbf{P} has an inverse only when dimU=n\dim U = n, in which case P=I\mathbf{P} = \mathbf{I} and the projection was doing nothing.

situationformulacostwhen to use
line, dimU=1\dim U = 1bbbbx\dfrac{\mathbf{b}\mathbf{b}^\top}{\mathbf{b}^\top\mathbf{b}}\mathbf{x}O(n)O(n)one direction; always safe
general basis B\mathbf{B}B(BB)1Bx\mathbf{B}(\mathbf{B}^\top\mathbf{B})^{-1}\mathbf{B}^\top\mathbf{x}O(nm2+m3)O(nm^2 + m^3)derivations; small well-conditioned mm
orthonormal basis Q\mathbf{Q}QQx\mathbf{Q}\mathbf{Q}^\top\mathbf{x}O(nm)O(nm)whenever you can get one — no solve, no conditioning
in practicenp.linalg.lstsq(B, x)O(nm2)O(nm^2)numerically the right default; tolerates rank deficiency
affine L=x0+UL = \mathbf{x}_0 + Ux0+πU(xx0)\mathbf{x}_0 + \pi_U(\mathbf{x}-\mathbf{x}_0)as aboveoffsets, hyperplanes, bias terms
with a ridgeB(BB+εI)1Bx\mathbf{B}(\mathbf{B}^\top\mathbf{B}+\varepsilon\mathbf{I})^{-1}\mathbf{B}^\top\mathbf{x}as generaldeliberate shrinkage; not a projection
pch.quizTag Check your understanding
  1. What single condition determines the projection, and where does it come from?

    pch.quizShowAnswer

    B — That the displacement x minus pi(x) is orthogonal to every basis vector of U — because sliding along U from the foot of a perpendicular adds a squared term by Pythagoras, so it can only increase the distance — Stacking that condition over the m basis vectors gives B-transpose (x minus B lambda) equals zero, which is the normal equation. Everything else on the page is bookkeeping on that one line.

  2. A projection matrix has trace 3. What do you know?

    pch.quizShowAnswer

    B — It projects onto a 3-dimensional subspace, because P squared equals P forces every eigenvalue to be 0 or 1 and the trace is their sum — The measured example on this page is 8 by 8 with trace 3.000000000000, projecting onto a 3-dimensional subspace. The rank equals the trace for the same reason.

  3. Why does the from-scratch code warn against forming B-transpose B?

    pch.quizShowAnswer

    B — Its condition number is the square of B's, so the normal equation loses twice as many digits — measured, on a degree-11 monomial design with cond(B) = 1.2e8 the normal-equation projection differs from the QR one by 0.048 — It is also the reason classical Gram-Schmidt and the augmented-matrix route to an ONB lose accuracy. The fix is the same in all three cases: work on B directly with a QR factorisation.

  4. The digits projection error reaches exactly zero at k = 61 rather than k = 64. Why?

    pch.quizShowAnswer

    B — The centred data has rank 61 — three pixels are zero in all 1797 images — so beyond dimension 61 there is nothing left for a larger subspace to capture — The Orthogonal Complement page identifies those three pixels as 0, 32 and 39, and shows that they span the orthogonal complement of the data exactly. 61 + 3 = 64.

  5. Which matrix is a projection but NOT an orthogonal projection?

    pch.quizShowAnswer

    B — [[1, 1], [0, 0]] — it is idempotent, so it is a projection by Definition 3.10, but it is not symmetric, so its displacement is not perpendicular to the target subspace and its output is not the closest point — Orthogonal projections are exactly the matrices that are both idempotent and symmetric. Definition 3.10 asks only for idempotence, which is why oblique projections exist and why the from-scratch code checks symmetry separately.

Exercise 2 – Example 3.11, all five steps

Section titled “Exercise 2 – Example 3.11, all five steps”

Exercise 3 – A projection matrix has only two eigenvalues

Section titled “Exercise 3 – A projection matrix has only two eigenvalues”

Exercise 4 – The normal equation loses twice the digits

Section titled “Exercise 4 – The normal equation loses twice the digits”

Exercise 5 – The book’s Exercise 3.5: spanning set, not a basis

Section titled “Exercise 5 – The book’s Exercise 3.5: spanning set, not a basis”
  • A projection is any linear map with pi squared equal to pi — Definition 3.10. Idempotence alone permits oblique projections; orthogonal projections are the idempotent maps that are also symmetric.
  • The condition that determines the answer is that the displacement be orthogonal to every basis vector of the subspace, which is Pythagoras: sliding along the subspace from the foot of a perpendicular only adds a squared term.
  • On a line: lambda is b-transpose x over b-transpose b, and the projection matrix is the outer product b b-transpose divided by b-transpose b. It is symmetric and has rank one.
  • In general: the normal equation B-transpose B lambda equals B-transpose x, so lambda is the pseudo-inverse applied to x and the projection matrix is B times the inverse Gram matrix times B-transpose.
  • With an orthonormal basis everything collapses to lambda equals Q-transpose x and the projector equals Q Q-transpose — no solve and no conditioning problem.
  • A projection matrix has eigenvalues zero and one only, so its rank and its trace both equal the dimension of the subspace, and it is never invertible unless it is the identity.
  • Gram-Schmidt is this projection used constructively, and the affine case is shift, project, shift back — with the distance unchanged by the shift.
  • Least squares is a projection: the residual is orthogonal to every column of the design matrix, measured at eight times ten to the minus fifteen. The projection matrix is the hat matrix.
  • PCA minimises the projection error of Equation 3.63. On the digit data the error reaches exactly zero at dimension 61, the rank of the centred data, and twenty-one components carry ninety percent of the variance.
  • Never form B-transpose B in production code: its condition number is the square of B’s, and on a degree-11 polynomial design the measured gap between the normal equation and QR reaches 0.048.

Next: Rotations — the transformations that move data without distorting any of the geometry this chapter built.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading