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 and a vector outside it, which point of is closest to ?
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.
What you’ll learn
Section titled “What you’ll learn”- Definition 3.10 and the property that defines a projection.
- Projection onto a line (§3.8.1): and .
- Projection onto a general subspace (§3.8.2): the normal equation, the pseudo-inverse, and the projection matrix .
- 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.
Intuition: the shadow at noon
Section titled “Intuition: the shadow at noon”A projection is a shadow cast by a light directly overhead. The pencil is , the floor is , and the shadow is .
Three properties of that shadow give you the whole of §3.8 before any algebra.
- The shadow is on the floor. — that is what makes the problem finite: instead of ambient coordinates you need only of them.
- The shadow of a shadow is itself. Something already flat on the floor casts itself. That is , 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.
flowchart TD Q["which point of U is closest to x?"] Q --> COND["the displacement x − π(x) must be
orthogonal to EVERY basis vector of U"] COND --> WHY["because sliding along U from the foot
of a perpendicular only adds a squared term
— Pythagoras, §3.6"] COND --> ONE["ONE direction b: λ = bᵀx / ‖b‖²
Eq 3.40"] COND --> MANY["m directions: BᵀBλ = Bᵀx
the NORMAL EQUATION, Eq 3.56"] ONE --> PL["Pπ = b bᵀ / bᵀb
Eq 3.46"] MANY --> PM["Pπ = B(BᵀB)⁻¹Bᵀ
Eq 3.59"] PM --> PSEUDO["(BᵀB)⁻¹Bᵀ is the PSEUDO-INVERSE"] PM --> ONB["if B is orthonormal it collapses to BBᵀ
Eq 3.65"] PM --> LS["Ax = b unsolvable ⟹ project b
onto the column space: LEAST SQUARES, Ch 9"] PM --> PCA["minimise the projection error
‖x − π(x)‖: PCA, Ch 10"] PM --> GS["use it constructively: GRAM-SCHMIDT
§3.8.3"] PM --> AFF["shift, project, shift back:
affine subspaces §3.8.4 → hyperplanes Ch 12"]
The math
Section titled “The math”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 , 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 be a line through the origin. Since , it is for some scalar , and there is exactly one unknown.
Step 1 — find . The displacement must be orthogonal to :
Step 2 — the projection.
Its length has a clean form (Equations 3.43 and 3.44):
so the projection is the original length scaled by the absolute cosine of the angle — which is when is already on the line and when it is perpendicular to it.
Step 3 — the projection matrix. Step 2 is linear in , so rewrite it as a matrix acting on :
Note the reordering in the numerator — moving to the left of the scalar turns a vector-times-scalar into a matrix-times-vector, and that is the whole trick. is an outer product: an matrix of rank . So is always symmetric and always of rank here.
Projection onto a general subspace
Section titled “Projection onto a general subspace”Now , with an ordered basis stacked as the columns of . The same three steps.
Step 1 — the coordinates. , and “closest” means the displacement is orthogonal to all basis vectors, giving simultaneous conditions:
Stack them:
This is the normal equation. Because are a basis and therefore linearly independent, is invertible, so
The matrix is the pseudo-inverse of . It exists for non-square ; it requires only that be positive definite, which holds when has full column rank.
Step 2 — the projection.
Step 3 — the projection matrix.
Two special cases fall out immediately. If then is a scalar and Equation 3.59 becomes — Equation 3.46 exactly, so §3.8.1 was never a separate result. And if the basis is orthonormal, and everything collapses:
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
and this is the quantity PCA minimises.
The ridge, and where it comes from
Section titled “The ridge, and where it comes from”The book adds a note worth carrying: in practice one often adds a “jitter term” to 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.
Gram-Schmidt
Section titled “Gram-Schmidt”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.
Projection onto an affine subspace
Section titled “Projection onto an affine subspace”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.
Worked example by hand
Section titled “Worked example by hand”Example 3.10 — projection onto a line
Section titled “Example 3.10 — projection onto a line”Find for the line through the origin spanned by .
, and the outer product is
Now project . Row by row:
| row | working | result |
|---|---|---|
| 1 | ||
| 2 | ||
| 3 |
which is , and indeed . Applying again changes nothing, as Definition 3.10 requires. Measured: the eigenvalues of are , the rank is and the trace is .
Example 3.11 — projection onto a plane
Section titled “Example 3.11 — projection onto a plane”First, check the generating set is a basis. The two vectors are not multiples of each other, so they are linearly independent, and
Second, form the two pieces of the normal equation:
Third, solve . Subtracting the first row from the second gives , so , and then , so :
Fourth, the projection:
Fifth, the projection error:
Sixth, the projection matrix:
Measured checks: exactly, , the rank is , the trace is , and the eigenvalues are .
That last pattern is not a coincidence, and it is the cleanest way to recognise a projection matrix: forces every eigenvalue to satisfy , so , 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.
Example 3.12 — Gram-Schmidt
Section titled “Example 3.12 — Gram-Schmidt”, .
, and
and . The projection matrix is worth a second look: it keeps the first coordinate and discards the second, which is exactly what projecting onto the -axis should do.
An affine projection
Section titled “An affine projection”with and ; project .
| step | working | result |
|---|---|---|
| shift | ||
| project onto | ||
| shift back | ||
| distance | ||
| the same, via Eq 3.73b |
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.
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))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.618802Every number in Examples 3.10 and 3.11 reproduces digit for digit, including the projection error to sixteen figures and the error being exactly zero rather than .
See it move
Section titled “See it move”The first sketch is §3.8.1 with everything draggable, and it shows the equality live.
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.
Now the stepped versions of both book examples. First the line, Example 3.10:
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:
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.
From scratch
Section titled “From scratch”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}")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-02The first block is Example 3.11, verified through three independent routes that agree to
. Note trace printing as rather than : 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 , where — not an
extreme figure for a polynomial fit — the normal-equation projection is 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, 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.
On real data
Section titled “On real data”Reading the plot
Section titled “Reading the plot”From the regression figure. The fitted line is over twenty points, with a squared error of . But the number to read is .
That is the normal equation, measured. Fitting a line by least squares and projecting onto the column space of 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 renamed .
The projection matrix is rank with trace — one for the intercept column and one for the slope column — and . 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:
| mean squared error | variance kept | |
|---|---|---|
| 1 | ||
| 4 | ||
| 12 | ||
| 40 | ||
| 61 |
The error hits exactly zero at , not at , because the centred data has rank — which the Orthogonal Complement page traced to three pixels that are zero in all images. Beyond there is nothing left to capture, so the curve does not merely flatten; it terminates.
And components carry 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. implies for every eigenvalue, so — measured, three exact ones and five exact zeros. Three consequences follow for free:
- , since the rank is the number of nonzero eigenvalues.
- , since the trace is their sum. Measured: .
- is not invertible unless is the whole space. It throws information away, which is what a projection is for.
That third point is the one people trip over. has an inverse only when , in which case and the projection was doing nothing.
Pitfalls
Section titled “Pitfalls”Compare
Section titled “Compare”| situation | formula | cost | when to use |
|---|---|---|---|
| line, | one direction; always safe | ||
| general basis | derivations; small well-conditioned | ||
| orthonormal basis | whenever you can get one — no solve, no conditioning | ||
| in practice | np.linalg.lstsq(B, x) | numerically the right default; tolerates rank deficiency | |
| affine | as above | offsets, hyperplanes, bias terms | |
| with a ridge | as general | deliberate shrinkage; not a projection |
-
What single condition determines the projection, and where does it come from?
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.
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.
-
A projection matrix has trace 3. What do you know?
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.
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.
-
Why does the from-scratch code warn against forming B-transpose B?
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.
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.
-
The digits projection error reaches exactly zero at k = 61 rather than k = 64. Why?
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.
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.
-
Which matrix is a projection but NOT an orthogonal projection?
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.
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.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Example 3.10 from scratch
Section titled “Exercise 1 – Example 3.10 from scratch”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”Recall card
Section titled “Recall card”- 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.coffeeCtapch.feedbackHeading
pch.feedbackSubheading