USAAIO Lesson 19, from Week 7, fully worked. PCA is derived and implemented end to end on one hand-computable five-point 2D dataset. Centering is shown row by row, the 2x2 covariance matrix is built entry by entry, its eigenvalues are found from the characteristic polynomial with the quadratic formula, and the top eigenvector is solved by hand. The SVD route is done in parallel and proven identical via sigma^2 = (n-1)*lambda. It then covers the explained-variance ratio and the cumulative curve, choosing k at a 95% threshold on the 64-feature digits data, which gives k=29, the 2D projection, reconstruction error, and the limitation that PCA is linear only. It ends with a from-scratch PCA class verified equal to sklearn. Every snippet runs standalone in a fresh interpreter, and every number came from real execution. The lesson runs to 60 slides.
Subject: Machine Learning · 113 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
USAAIO · Lesson 19 · Week 7 (Dimensionality Reduction)
We take one 5-point dataset and run PCA on it two ways — covariance eigendecomposition and SVD — with every centering, every 2×2 eigenvalue, and every projection shown by hand, then prove the two routes are the same math and match sklearn.
Objectives
C = XᶜᵀXᶜ/(n−1) entry by entryC — characteristic polynomial, eigenvalues, eigenvectors — all by handσᵢ² = (n−1)λᵢ, so both routes give the same explained-variance ratiok from the cumulative variance (95% ⇒ 29 of 64 on digits), project to k dims, and read the reconstruction errorsklearnWarm-up
Discussion prompt
Before we open Lesson 19: PCA Implementation: without looking back, what was the main idea of Full Backpropagation, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
the complete forward and backward pass through a multi-layer network with matrix shapes, the computational-graph view, vanishing and exploding gradients with clipping and initialization — capped by training an MLP on the digits dataset to ~97% accuracy.
Section
Part 1 of 8 — the goal
Concept
Real data has many features, but they overlap: height in cm and height in inches carry the same information twice. PCA finds a smaller set of new axes that hold almost all the spread, so we can compress, plot, and denoise.
The trick: rotate to axes along which the data varies the most. Those axes are the principal components.
Counterexample
Discussion prompt
The trick: rotate to axes along which the data varies the most. Those axes are the principal components.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Intuition
PCA's core bet: the directions where the data spreads out carry the structure, and the directions where it barely moves are mostly noise or redundancy.
So the first component is the single axis capturing the most variance. The second is the best remaining axis perpendicular to the first. And so on — each new axis mops up the most variance left over.
Keep the first few, drop the rest, and you have thrown away little spread — that is dimensionality reduction.
Analogy
Discussion prompt
Explain Variance is the signal by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
PCA's core bet: the directions where the data spreads out carry the structure, and the directions where it barely moves are mostly noise or redundancy.
Concept
Everything below uses these five points in 2D. Small enough to eigendecompose by hand, real enough to show every step. Think of two correlated measurements on five samples.
| sample | feature 1 | feature 2 |
|---|---|---|
| 1 | 2 | 0 |
| 2 | 3 | 1 |
| 3 | 5 | 1 |
| 4 | 7 | 3 |
| 5 | 8 | 5 |
The two features clearly rise together — strongly correlated. PCA should find one dominant direction along that trend and a tiny second one across it.
Comparison
Comparison matrix
From One running dataset for the whole lesson: refill the feature 1 column from what you know. The rest of the table is as it appeared.
| sample | feature 1 | feature 2 |
|---|---|---|
| 1 | 2 | 0 |
| 2 | 3 | 1 |
| 3 | 5 | 1 |
| 4 | 7 | 3 |
| 5 | 8 | 5 |
Concept
There are two equivalent ways to compute PCA, and we do both on this dataset so you trust the result:
X, form the covariance C = XᶜᵀXᶜ/(n−1), then eigendecompose CX, take its SVD Xᶜ = UΣVᵀ — the rows of Vᵀ are the components (Lesson 16)They are not two different methods — they are the same linear algebra seen from two sides. Part 4 proves it with σᵢ² = (n−1)λᵢ.
Explain it
Discussion prompt
Explain The plan: two routes to the same components to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
They are not two different methods — they are the same linear algebra seen from two sides. Part 4 proves it with σᵢ² = (n−1)λᵢ.
Section
Part 2 of 8 — subtract the mean
Concept
PCA is about variance — spread around the center. If we skip centering, the largest 'direction' is just the arrow from the origin to the data's center of mass, which says nothing about structure.
So step one is always: subtract each column's mean, moving the cloud so its center sits at the origin. Only then do directions of spread mean anything.
Estimation
Predict first
Column means: feature 1 averages (2+3+5+7+8)/5 = 5; feature 2 averages (0+1+1+3+5)/5 = 2. Subtract [5, 2] from every row:
Commit before you compute: what does Center the data, row by row come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Each centered column now sums to 0
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Column 0: −3−2+0+2+3 = 0. Column 1: −2−1−1+1+3 = 0.
Worked example
Column means: feature 1 averages (2+3+5+7+8)/5 = 5; feature 2 averages (0+1+1+3+5)/5 = 2. Subtract [5, 2] from every row:
\[ \bar x = \begin{bmatrix} 5 & 2 \end{bmatrix}, \qquad X_c = X - \bar x \]
| row | f1 − 5 | f2 − 2 |
|---|---|---|
| 1 | 2 − 5 = −3 | 0 − 2 = −2 |
| 2 | 3 − 5 = −2 | 1 − 2 = −1 |
| 3 | 5 − 5 = 0 | 1 − 2 = −1 |
| 4 | 7 − 5 = +2 | 3 − 2 = +1 |
| 5 | 8 − 5 = +3 | 5 − 2 = +3 |
Each centered column now sums to 0
Why: Column 0: −3−2+0+2+3 = 0. Column 1: −2−1−1+1+3 = 0. That is the definition of 'centered' — the mean has been removed, so the cloud is centered on the origin.
Trade off
Comparison matrix
From Center the data, row by row: every row here is a choice with a cost. Fill the f1 − 5 column, then say which row you would actually pick and what you give up for it.
| row | f1 − 5 | f2 − 2 |
|---|---|---|
| 1 | 2 − 5 = −3 | 0 − 2 = −2 |
| 2 | 3 − 5 = −2 | 1 − 2 = −1 |
| 3 | 5 − 5 = 0 | 1 − 2 = −1 |
| 4 | 7 − 5 = +2 | 3 − 2 = +1 |
| 5 | 8 − 5 = +3 | 5 − 2 = +3 |
Missing information
Discussion prompt
X.mean(0) averages down each column; broadcasting subtracts that row-vector from every row. This block runs on its own:
What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.
Hint: Anything you would have to invent to get started is a thing the problem must supply.
Answer:
The centered matrix has zero column means, exactly matching the by-hand table. From here on we work entirely with Xc.
Worked example
X.mean(0) averages down each column; broadcasting subtracts that row-vector from every row. This block runs on its own:
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0) # subtract column means [5, 2]
print(Xc)
print(Xc.mean(0)) # -> [0. 0.] (centered)Xc.mean(0) prints [0. 0.] — confirmed centered
Why: The centered matrix has zero column means, exactly matching the by-hand table. From here on we work entirely with Xc.
| row of Xc | value (verified) |
|---|---|
| row 1 | [−3, −2] |
| row 2 | [−2, −1] |
| row 3 | [ 0, −1] |
| row 4 | [ 2, 1] |
| row 5 | [ 3, 3] |
Pattern
Step through it
Step through Center it in NumPy one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 3 of 8 — by hand
Concept
The 2×2 covariance summarizes how the two centered features spread and co-vary. With n samples:
\[ C = \frac{1}{n-1}\, X_c^\top X_c \]
The diagonal holds each feature's variance; the off-diagonal holds their covariance. We divide by n−1 (Bessel's correction) so it is an unbiased estimate — the same convention sklearn uses.
Ranking
Put in order
Put the moves of Build XᶜᵀXᶜ entry by entry into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Sum of squared entries of centered feature 1.
Worked example
(XᶜᵀXᶜ)ⱼₖ is centered column j dotted with centered column k. Columns are c₀ = [−3,−2,0,2,3] and c₁ = [−2,−1,−1,1,3]. Three distinct entries (it is symmetric):
Top-left = c₀·c₀ = 9+4+0+4+9 = 26
Why: Sum of squared entries of centered feature 1.
Off-diagonal = c₀·c₁ = 6+2+0+2+9 = 19
Why: (−3)(−2)+(−2)(−1)+(0)(−1)+(2)(1)+(3)(3) = 6+2+0+2+9 = 19. Appears in both off-diagonal slots by symmetry.
Bottom-right = c₁·c₁ = 4+1+1+1+9 = 16
Why: Sum of squared entries of centered feature 2.
\[ X_c^\top X_c = \begin{bmatrix} 26 & 19 \\ 19 & 16 \end{bmatrix} \]
| entry | dot product | value |
|---|---|---|
| (0,0) | c₀·c₀ | 26 |
| (0,1)=(1,0) | c₀·c₁ | 19 |
| (1,1) | c₁·c₁ | 16 |
Notation
Annotate
From Build XᶜᵀXᶜ entry by entry — read this one piece at a time. What is each part doing?
On: \( X_c^\top X_c = \begin{bmatrix} 26 & 19 \\ 19 & 16 \end{bmatrix} \)
Pattern
Predict first
The table runs: C[0,0] | var(feature 1) | 6.5 · C[1,1] | var(feature 2) | 4.0
In Divide by n−1 to get C, given the rows so far: what is the next one — the row where C entry is C[0,1]?
Correct: C[0,1] | cov(f1, f2) | 4.75
| C entry | meaning | value |
|---|---|---|
| C[0,0] | var(feature 1) | 6.5 |
| C[1,1] | var(feature 2) | 4.0 |
| C[0,1] | cov(f1, f2) | 4.75 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Both diagonal variances are positive and the covariance 4.75 is large relative to them — the features are strongly correlated, exactly as the scatter suggested.
Worked example
Five samples, so n − 1 = 4. Divide every entry of XᶜᵀXᶜ by 4:
\[ C = \frac{1}{4}\begin{bmatrix} 26 & 19 \\ 19 & 16 \end{bmatrix} = \begin{bmatrix} 6.5 & 4.75 \\ 4.75 & 4.0 \end{bmatrix} \]
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0)
C = Xc.T @ Xc / (len(X) - 1) # n - 1 = 4
print(Xc.T @ Xc) # [[26 19] [19 16]]
print(C) # [[6.5 4.75] [4.75 4.]]C = [[6.5, 4.75], [4.75, 4.0]]
Why: Both diagonal variances are positive and the covariance 4.75 is large relative to them — the features are strongly correlated, exactly as the scatter suggested.
| C entry | meaning | value |
|---|---|---|
| C[0,0] | var(feature 1) | 6.5 |
| C[1,1] | var(feature 2) | 4.0 |
| C[0,1] | cov(f1, f2) | 4.75 |
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
C = [[6.5, 4.75], [4.75, 4.0]]
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
Five samples, so n − 1 = 4. Divide every entry of XᶜᵀXᶜ by 4:
Concept
The principal components are the eigenvectors of C, and each eigenvalue is the variance captured along its eigenvector. Biggest eigenvalue ⇒ first principal component.
\[ C v = \lambda v \]
Because C is symmetric, its eigenvectors are orthogonal — the new axes are perpendicular, as promised. Now we find λ and v by hand.
Intuition
Cv = λv says: applying the covariance to direction v gives back the same direction, only scaled. Those special directions are the axes the data 'wants' to spread along.
The scale factor λ is the amount of spread along that axis. So the eigenvector with the largest λ is literally the direction of maximum variance — the first principal component — with no optimization loop needed.
For our strongly-correlated cloud, expect one big λ (along the trend) and one tiny λ (across it). The numbers will bear that out.
Worked example
Eigenvalues solve det(C − λI) = 0. For a 2×2 this is a quadratic in λ built from the trace and determinant:
trace(C) = 6.5 + 4.0 = 10.5
Why: Sum of the diagonal — it will equal λ₁ + λ₂.
det(C) = 6.5·4.0 − 4.75·4.75 = 26 − 22.5625 = 3.4375
Why: Product of the diagonal minus the off-diagonal squared — it will equal λ₁·λ₂.
\[ \det(C - \lambda I) = \lambda^2 - \operatorname{tr}(C)\,\lambda + \det(C) = \lambda^2 - 10.5\,\lambda + 3.4375 = 0 \]
| quantity | value |
|---|---|
| trace(C) = C₀₀ + C₁₁ | 10.5 |
| det(C) = C₀₀C₁₁ − C₀₁² | 3.4375 |
Blank canvas
Draw it
Draw what Characteristic polynomial of C just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Ranking
Put in order
Put the moves of Solve the quadratic for the eigenvalues into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Positive, so two distinct real eigenvalues — as it must be for a symmetric matrix.
Worked example
Quadratic formula on λ² − 10.5λ + 3.4375 = 0. Compute the discriminant first:
Discriminant = tr² − 4·det = 10.5² − 4(3.4375) = 110.25 − 13.75 = 96.5
Why: Positive, so two distinct real eigenvalues — as it must be for a symmetric matrix.
\[ \lambda = \frac{10.5 \pm \sqrt{96.5}}{2} = \frac{10.5 \pm 9.8234}{2} \]
λ₁ = (10.5 + 9.8234)/2 = 10.1617
Why: The larger root — variance along the first principal component.
λ₂ = (10.5 − 9.8234)/2 = 0.3383
Why: The smaller root — variance across it. Check: λ₁+λ₂ = 10.5 = trace ✓ and λ₁λ₂ ≈ 3.4375 = det ✓.
| eigenvalue | value | = variance along PC |
|---|---|---|
| λ₁ | 10.1617 | PC1 |
| λ₂ | 0.3383 | PC2 |
| λ₁ + λ₂ | 10.5 (= trace ✓) | total variance |
Blank canvas
Draw it
Draw what Solve the quadratic for the eigenvalues just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Worked example
Solve (C − λ₁I)v = 0 for v. The top row gives (6.5 − λ₁)v₀ + 4.75 v₁ = 0, i.e. (−3.6617)v₀ + 4.75 v₁ = 0:
Pick v ∝ (4.75, 3.6617)
Why: From (6.5 − λ₁)v₀ + 4.75 v₁ = 0, a solution is v = (4.75, λ₁ − 6.5) = (4.75, 3.6617). Any nonzero multiple is also an eigenvector — direction is what matters.
\[ v \;\propto\; \begin{bmatrix} 4.75 \\ 3.6617 \end{bmatrix}, \qquad \lVert v \rVert = \sqrt{4.75^2 + 3.6617^2} = 5.997 \]
Normalize to unit length: v₁ = (0.7920, 0.6105)
Why: Divide by the norm 5.997. Components are always reported as unit vectors so the scores' variance equals λ exactly.
\[ v_1 = \begin{bmatrix} 0.7920 \\ 0.6105 \end{bmatrix} \]
| step | value |
|---|---|
| λ₁ − 6.5 | 3.6617 |
| unnormalized v | (4.75, 3.6617) |
| ‖v‖ | 5.997 |
| unit v₁ | (0.7920, 0.6105) |
Translation
\( v \;\propto\; \begin{bmatrix} 4.75 \\ 3.6617 \end{bmatrix}, \qquad \lVert v \rVert = \sqrt{4.75^2 + 3.6617^2} = 5.997 \)
Draw it
Translate both ways. First write the expression above as a sentence with no symbols in it at all. Then cover it, and write your sentence back as notation. If the two versions disagree, the disagreement is the thing to fix.
Fill the middle
Fill in the blanks
From Confirm the eigendecomposition in NumPy — one line has had its right-hand side removed. Put it back.
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0)
C = Xc.T @ Xc / (len(X) - 1)
lam, V = np.linalg.eigh(C) # ascending
order = np.argsort(lam)[::-1] # -> descending
lam, V = lam[order], V[:, order]
print(lam.round(4)) # [10.1617 0.3383]
print(V.round(4)) # columns are the components
Why: C is what everything below it consumes, so the wrong expression here fails later and somewhere else. The eigenvector column printed as [-0.792, -0.6105]: the SAME direction as our (0.792, 0.6105), just flipped in sign.
Worked example
np.linalg.eigh (for symmetric matrices) returns eigenvalues ascending, so we reverse to get descending order — PC1 first:
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0)
C = Xc.T @ Xc / (len(X) - 1)
lam, V = np.linalg.eigh(C) # ascending
order = np.argsort(lam)[::-1] # -> descending
lam, V = lam[order], V[:, order]
print(lam.round(4)) # [10.1617 0.3383]
print(V.round(4)) # columns are the componentsλ = [10.1617, 0.3383] — matches the hand quadratic exactly
Why: The eigenvector column printed as [-0.792, -0.6105]: the SAME direction as our (0.792, 0.6105), just flipped in sign. That sign freedom is the next trap.
| output | value (verified) |
|---|---|
| lam | [10.1617, 0.3383] |
| V[:,0] (PC1) | [−0.7920, −0.6105] |
| V[:,1] (PC2) | [+0.6105, −0.7920] |
Anomaly
Predict first
A student writes this, and it looks reasonable:
np.linalg.eigh returned PC1 as [−0.792, −0.6105], but I computed [+0.792, +0.6105] by hand — one of us made an error and the answers disagree.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: There is no error. If v is a unit eigenvector then so is −v: C(−v) = −Cv = −λv = λ(−v).
Treat v and −v as the same component — a principal axis is a line, not an arrow.
Why: There is no error. If v is a unit eigenvector then so is −v: C(−v) = −Cv = −λv = λ(−v). Solvers pick the sign by an internal convention; libraries even disagree with each other.
Trap
np.linalg.eigh returned PC1 as [−0.792, −0.6105], but I computed [+0.792, +0.6105] by hand — one of us made an error and the answers disagree.
Declare a mismatch and 'fix' the code
Why: There is no error. If v is a unit eigenvector then so is −v: C(−v) = −Cv = −λv = λ(−v). Solvers pick the sign by an internal convention; libraries even disagree with each other.
Treat v and −v as the same component — a principal axis is a line, not an arrow.
Compare directions (or |v|), never raw signs
Why: The eigenvalue 10.1617, the explained-variance ratio, and every reconstruction are identical either way. A flipped sign only mirrors the projected scores; it never changes the variance captured.
Fill the middle
Fill in the blanks
From Explained-variance ratio from the eigenvalues — finish the line. Write what belongs on the right of the equals sign before you look.
\text\frac{\lambda_i}{\sum_j \lambda_j}_i = ___
Why: Producing the right-hand side unprompted is the difference between recognising this line and being able to use it. 10.1617/10.5 = 0.9678. The five points really do live almost entirely along one line, so 2D → 1D loses only 3.2% of the spread.
Worked example
Each component's share of the total variance is its eigenvalue over the sum of eigenvalues. Total variance = λ₁ + λ₂ = 10.5:
\[ \text{EVR}_i = \frac{\lambda_i}{\sum_j \lambda_j} \]
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0)
lam = np.linalg.eigvalsh(Xc.T @ Xc / (len(X)-1))[::-1]
evr = lam / lam.sum()
print(evr.round(4)) # [0.9678 0.0322]
print(evr.sum()) # 1.0EVR = [0.9678, 0.0322] — PC1 alone holds 96.8% of the variance
Why: 10.1617/10.5 = 0.9678. The five points really do live almost entirely along one line, so 2D → 1D loses only 3.2% of the spread.
| component | EVR | cumulative |
|---|---|---|
| PC1 | 0.9678 | 0.9678 |
| PC2 | 0.0322 | 1.0000 |
Section
Part 4 of 8 — same answer, better numerics
Concept
The SVD factors the centered data directly: Xᶜ = UΣVᵀ. The rows of Vᵀ are the principal components, and the singular values σᵢ on Σ's diagonal encode the variance.
\[ X_c = U\,\Sigma\,V^\top, \qquad \text{components} = \text{rows of } V^\top \]
This never builds XᶜᵀXᶜ. Forming that product squares the condition number, amplifying round-off — so on real data the SVD route is the numerically safer default (and what sklearn uses).
Worked example
full_matrices=False gives the economy SVD — Vᵀ is 2×2 here. Read the singular values and components:
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
print(S.round(4)) # [6.3755 1.1632]
print(Vt.round(4)) # rows are the componentsσ = [6.3755, 1.1632]; PC1 row of Vt = [0.7920, 0.6105]
Why: That PC1 is our hand eigenvector on the nose (this time with the + sign). The SVD produced the same principal axis without ever forming the covariance.
| output | value (verified) |
|---|---|
| S (σ₁, σ₂) | [6.3755, 1.1632] |
| Vt[0] (PC1) | [0.7920, 0.6105] |
| Vt[1] (PC2) | [0.6105, −0.7920] |
Estimation
Predict first
Substitute Xᶜ = UΣVᵀ into the covariance. Since U is orthonormal (UᵀU = I), the U's cancel:
Commit before you compute: what does Prove the two routes are one: σᵢ² = (n−1)λᵢ come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Numeric check: σ₁² = 6.3755² = 40.6469, and (n−1)λ₁ = 4·10.1617 = 40.6469 ✓
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. σ₂² = 1.1632² = 1.3531 = 4·0.3383 too.
Worked example
Substitute Xᶜ = UΣVᵀ into the covariance. Since U is orthonormal (UᵀU = I), the U's cancel:
C = XᶜᵀXᶜ/(n−1) = V Σ² Vᵀ /(n−1)
Why: XᶜᵀXᶜ = (UΣVᵀ)ᵀ(UΣVᵀ) = VΣᵀUᵀUΣVᵀ = VΣ²Vᵀ. So C's eigenvectors ARE V's columns, and its eigenvalues are σᵢ²/(n−1).
\[ \lambda_i = \frac{\sigma_i^2}{\,n-1\,} \quad\Longleftrightarrow\quad \sigma_i^2 = (n-1)\,\lambda_i \]
Numeric check: σ₁² = 6.3755² = 40.6469, and (n−1)λ₁ = 4·10.1617 = 40.6469 ✓
Why: σ₂² = 1.1632² = 1.3531 = 4·0.3383 too. The singular values squared are exactly (n−1) times the covariance eigenvalues — the two routes are algebraically identical.
| i | σᵢ² | (n−1)·λᵢ | match |
|---|---|---|---|
| 1 | 40.6469 | 4 × 10.1617 = 40.6469 | ✓ |
| 2 | 1.3531 | 4 × 0.3383 = 1.3531 | ✓ |
Notation
Annotate
From Prove the two routes are one: σᵢ² = (n−1)λᵢ — read this one piece at a time. What is each part doing?
On: \( \lambda_i = \frac{\sigma_i^2}{\,n-1\,} \quad\Longleftrightarrow\quad \sigma_i^2 = (n-1)\,\lambda_i \)
Worked example
Because σᵢ² = (n−1)λᵢ, the (n−1) cancels in the ratio — so EVR is the same whether you use σᵢ² or λᵢ:
\[ \text{EVR}_i = \frac{\sigma_i^2}{\sum_j \sigma_j^2} = \frac{(n-1)\lambda_i}{(n-1)\sum_j \lambda_j} = \frac{\lambda_i}{\sum_j \lambda_j} \]
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0); n = len(X)
lam = np.linalg.eigvalsh(Xc.T @ Xc / (n-1))[::-1]
S = np.linalg.svd(Xc, full_matrices=False)[1]
print('eig EVR:', (lam/lam.sum()).round(6))
print('svd EVR:', (S**2/np.sum(S**2)).round(6))Both print [0.967783, 0.032217] — identical to 6 decimals
Why: Confirmed by execution: eig-route and SVD-route EVR agree. Use SVD for numerics, eig for intuition; the reported variance shares are the same number.
| route | EVR (verified) |
|---|---|
| eigenvalues of C | [0.967783, 0.032217] |
| singular values of Xᶜ | [0.967783, 0.032217] |
Comparison
Comparison matrix
From EVR from singular values equals EVR from eigenvalues: refill the EVR (verified) column from what you know. The rest of the table is as it appeared.
| route | EVR (verified) |
|---|---|
| eigenvalues of C | [0.967783, 0.032217] |
| singular values of Xᶜ | [0.967783, 0.032217] |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Eigendecomposing C and taking the SVD of Xᶜ are separate algorithms, so I should expect different components and different explained variance.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Wrong premise. C = VΣ²Vᵀ/(n−1), so C's eigenvectors are exactly V's columns and its eigenvalues are σᵢ²/(n−1).
They are the same decomposition. Pick the route by numerical stability, not by expecting a different answer.
Why: Wrong premise. C = VΣ²Vᵀ/(n−1), so C's eigenvectors are exactly V's columns and its eigenvalues are σᵢ²/(n−1). The outputs are the same up to sign.
Trap
Eigendecomposing C and taking the SVD of Xᶜ are separate algorithms, so I should expect different components and different explained variance.
Report two different sets of principal components
Why: Wrong premise. C = VΣ²Vᵀ/(n−1), so C's eigenvectors are exactly V's columns and its eigenvalues are σᵢ²/(n−1). The outputs are the same up to sign.
They are the same decomposition. Pick the route by numerical stability, not by expecting a different answer.
Same components, same EVR — SVD just avoids forming C
Why: Verified equal to 6 decimals. SVD works on Xᶜ directly (never squares the condition number); eig is cheaper to reason about by hand. Choose the tool, not the answer.
Break the constraint
Discussion prompt
The rule this trap just fixed:
Verified equal to 6 decimals. SVD works on Xᶜ directly (never squares the condition number); eig is cheaper to reason about by hand. Choose the tool, not the answer.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
Wrong premise. C = VΣ²Vᵀ/(n−1), so C's eigenvectors are exactly V's columns and its eigenvalues are σᵢ²/(n−1). The outputs are the same up to sign.
Section
Part 5 of 8 — scores & reconstruction
Concept
To reduce to k dimensions, project the centered data onto the top-k components. The result Z (the scores) is the data rewritten in the new axes:
\[ Z = X_c\, V_{[:,\,:k]} \quad\in\; \mathbb{R}^{\,n \times k} \]
Column j of Z is every sample's coordinate along PCj. Keep k = 1 and each point collapses to a single number on the dominant line.
Fill the middle
Fill in the blanks
From Compute the scores — one line has had its right-hand side removed. Put it back.
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
Z = Xc @ Vt.T # scores: cols = PC1, PC2
print(Z.round(4))
print('var PC1 =', round(Z[:,0].var(ddof=1), 4)) # 10.1617
print('lambda1 =', round(S[0]**2/(len(X)-1), 4)) # 10.1617
Why: Z is what everything below it consumes, so the wrong expression here fails later and somewhere else. The variance of the projected data along PC1 IS the first eigenvalue — the whole reason unit-length components matter.
Worked example
Multiply centered data by the components. Column 0 of Z is the PC1 score of each of the five samples:
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
Xc = X - X.mean(0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
Z = Xc @ Vt.T # scores: cols = PC1, PC2
print(Z.round(4))
print('var PC1 =', round(Z[:,0].var(ddof=1), 4)) # 10.1617
print('lambda1 =', round(S[0]**2/(len(X)-1), 4)) # 10.1617var(PC1 scores) = 10.1617 = λ₁ exactly
Why: The variance of the projected data along PC1 IS the first eigenvalue — the whole reason unit-length components matter. This is the numeric definition of 'variance captured'.
| sample | PC1 score | PC2 score |
|---|---|---|
| 1 | −3.5970 | −0.2476 |
| 2 | −2.1945 | −0.4291 |
| 3 | −0.6105 | +0.7920 |
| 4 | +2.1945 | +0.4291 |
| 5 | +4.2076 | −0.5444 |
Pattern
Step through it
Step through Compute the scores one row at a time. What is driving the change, and what would the row after the last one be?
Intuition
Nothing was thrown away when we computed all the scores Z — we just re-described each point using PC-axes instead of the original features. It's the same cloud seen from a rotated chair.
The saving comes only when we drop columns of Z. Keeping just the PC1 column flattens each 2D point to one number along the trend line — and because PC2 held only 3.2% of the spread, that flattening barely moves anything.
Concept
Rebuild an approximation from the top k components: X̂ = x̄ + Z_k V_kᵀ. The squared error you incur is exactly the variance in the dropped directions.
\[ \sum_i \lVert x_i - \hat x_i \rVert^2 = (n-1)\!\!\sum_{j>k}\lambda_j = \sum_{j>k}\sigma_j^2 \]
So keeping k components and losing the rest is not hand-waving — the error is a precise sum of discarded eigenvalues. That is why the EVR curve tells you exactly what a truncation costs.
Faded example
Fill in the blanks
Reconstruct from k = 1 and measure the loss, with the scaffolding fading: two lines are gone now — fill both.
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
mu = X.mean(0); Xc = X - mu
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
v1 = Vt[0] # top component
z1 = Xc @ v1 # PC1 scores
Xrec = mu + np.outer(z1, v1) # rebuild from 1 component
print('SSE =', round(np.sum((X - Xrec)2), 4)) # 1.3531
print('sigma2^2 =', round(S[1]2, 4)) # 1.3531
Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. The error of dropping PC2 equals its squared singular value (= (n−1)λ₂ = 4·0.3383).
Worked example
Keep only PC1, rebuild, and compare to the discarded eigenvalue λ₂:
import numpy as np
X = np.array([[2.,0.],[3.,1.],[5.,1.],[7.,3.],[8.,5.]])
mu = X.mean(0); Xc = X - mu
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
v1 = Vt[0] # top component
z1 = Xc @ v1 # PC1 scores
Xrec = mu + np.outer(z1, v1) # rebuild from 1 component
print('SSE =', round(np.sum((X - Xrec)**2), 4)) # 1.3531
print('sigma2^2 =', round(S[1]**2, 4)) # 1.3531Reconstruction SSE = 1.3531 = σ₂² exactly
Why: The error of dropping PC2 equals its squared singular value (= (n−1)λ₂ = 4·0.3383). Since EVR₂ = 3.2%, we kept 96.8% of the variance — the theorem made concrete.
| quantity | value (verified) |
|---|---|
| reconstruction SSE (k=1) | 1.3531 |
| σ₂² = (n−1)λ₂ | 1.3531 |
| variance kept (EVR₁) | 96.78% |
Reverse engineer
Discussion prompt
Work backwards. The example finished here:
Reconstruction SSE = 1.3531 = σ₂² exactly
What was it asked to do, and what must it have been given? Reconstruct the problem from its answer.
Hint: Every quantity in the result had to enter somewhere. Account for each one.
Answer:
Keep only PC1, rebuild, and compare to the discarded eigenvalue λ₂:
Section
Part 6 of 8 — the digits dataset
Concept
Our 2D toy makes k trivial. Real problems have dozens of features. sklearn's digits dataset is 1797 handwritten digits, each an 8×8 image flattened to 64 features.
The exact same recipe — center, SVD, EVR — now answers a real question: how many of the 64 dimensions do we actually need?
Concept
k whose cumulative EVR reaches a target such as 95%The threshold is the defensible, automatable choice: it turns 'how many dimensions?' into one number from the cumulative-variance curve.
Faded example
Fill in the blanks
EVR of the digits data, with the scaffolding fading: two lines are gone now — fill both.
import numpy as np
from sklearn.datasets import load_digits
X = load_digits().data # (1797, 64)
Xc = X - X.mean(0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
evr = S2 / np.sum(S2)
print(evr[:5].round(4)) # [0.1489 0.1362 0.1179 0.0841 0.0578]
Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. PC1 captures ~15% of the variance; the spectrum then falls off gradually — a typical natural-image profile with no single dominant axis.
Worked example
Center the 64 features, SVD, and read the top explained-variance ratios. Runnable as written:
import numpy as np
from sklearn.datasets import load_digits
X = load_digits().data # (1797, 64)
Xc = X - X.mean(0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
evr = S**2 / np.sum(S**2)
print(evr[:5].round(4)) # [0.1489 0.1362 0.1179 0.0841 0.0578]EVR (top 5) = [0.1489, 0.1362, 0.1179, 0.0841, 0.0578]
Why: PC1 captures ~15% of the variance; the spectrum then falls off gradually — a typical natural-image profile with no single dominant axis.
| component | EVR | cumulative |
|---|---|---|
| PC1 | 0.1489 | 0.1489 |
| PC2 | 0.1362 | 0.2851 |
| PC3 | 0.1179 | 0.4030 |
| PC4 | 0.0841 | 0.4871 |
| PC5 | 0.0578 | 0.5450 |
Scale up
Step through it
Step through EVR of the digits data and watch the numbers move. Now imagine the input ten times bigger: which column is the one that stops this being practical?
Concept
The denominator Σⱼσⱼ² — the total variance PCA can explain — equals the sum of the feature variances, i.e. trace(XᶜᵀXᶜ). Rotating axes never creates or destroys total spread; it only redistributes it onto the components.
That is why the EVRs must sum to 1: every component's share is a slice of one fixed pie. Choosing k is just deciding how much of that pie to keep.
Fill the middle
Fill in the blanks
From The scree curve: where does it flatten? — one line has had its right-hand side removed. Put it back.
import numpy as np
from sklearn.datasets import load_digits
X = load_digits().data
Xc = X - X.mean(0)
S = np.linalg.svd(Xc, full_matrices=False)[1]
evr = S2 / np.sum(S2)
for i in range(8):
print(i+1, round(evr[i], 4))
Why: Xc is what everything below it consumes, so the wrong expression here fails later and somewhere else. 0.149 -> 0.136 -> 0.118 are close, then 0.084, 0.058, and the tail flattens.
Worked example
A scree plot is EVR vs component index. The 'elbow' is where the curve stops dropping steeply. Print the first several EVRs and their gaps to find it numerically:
import numpy as np
from sklearn.datasets import load_digits
X = load_digits().data
Xc = X - X.mean(0)
S = np.linalg.svd(Xc, full_matrices=False)[1]
evr = S**2 / np.sum(S**2)
for i in range(8):
print(i+1, round(evr[i], 4))The drop shrinks after PC3-4, then crawls
Why: 0.149 -> 0.136 -> 0.118 are close, then 0.084, 0.058, and the tail flattens. There is no sharp cliff here, which is why a variance threshold (next slide) is the more decisive rule for this dataset.
| component | EVR | drop from previous |
|---|---|---|
| PC1 | 0.1489 | — |
| PC2 | 0.1362 | 0.0127 |
| PC3 | 0.1179 | 0.0183 |
| PC4 | 0.0841 | 0.0338 |
| PC5 | 0.0578 | 0.0263 |
Pattern
Step through it
Step through The scree curve: where does it flatten? one row at a time. What is driving the change, and what would the row after the last one be?
Missing information
Discussion prompt
Accumulate the EVR with cumsum, then find the first index that crosses the threshold. searchsorted gives the insertion point; add 1 for a count:
What do you need to know — or decide — before the first line can be written? List everything the problem has to hand you.
Hint: Anything you would have to invent to get started is a thing the problem must supply.
Answer:
Fewer than half the dimensions hold almost all the signal — a >2× compression at 5% variance cost. cum[28] ≈ 0.9548, the first crossing of 0.95.
Worked example
Accumulate the EVR with cumsum, then find the first index that crosses the threshold. searchsorted gives the insertion point; add 1 for a count:
import numpy as np
from sklearn.datasets import load_digits
X = load_digits().data
Xc = X - X.mean(0)
S = np.linalg.svd(Xc, full_matrices=False)[1]
evr = S**2 / np.sum(S**2)
cum = np.cumsum(evr)
k95 = int(np.searchsorted(cum, 0.95) + 1)
print(k95, 'of', X.shape[1]) # 29 of 6429 of 64 components retain 95% of the variance
Why: Fewer than half the dimensions hold almost all the signal — a >2× compression at 5% variance cost. cum[28] ≈ 0.9548, the first crossing of 0.95.
| cumulative target | components needed (verified) |
|---|---|
| 50% | 5 |
| 90% | 21 |
| 95% | 29 |
| 99% | 41 |
| 100% | 64 (all) |
Scale up
Step through it
Step through How many components reach 95%? and watch the numbers move. Now imagine the input ten times bigger: which column is the one that stops this being practical?
Section
Part 7 of 8
Concept
Set k = 2 and every sample becomes an (x, y) point you can scatter-plot. It is the fastest way to eyeball whether high-dimensional data has cluster structure.
\[ Z = X_c\, V_{[:,\,:2]} \quad\in\; \mathbb{R}^{\,n \times 2} \]
On digits, this 2D projection already pulls several digit classes into visibly separated blobs — a quick sanity check that the classes are linearly separable enough to matter.
Estimation
Predict first
Keep the first two component rows of Vᵀ and project. The result is (1797, 2) — ready to scatter by label:
Commit before you compute: what does Project digits to 2D come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Z is (1797, 2) — 64D compressed to a plottable plane
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Each row is one digit's coordinates on PC1/PC2.
Worked example
Keep the first two component rows of Vᵀ and project. The result is (1797, 2) — ready to scatter by label:
import numpy as np
from sklearn.datasets import load_digits
X = load_digits().data
Xc = X - X.mean(0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
Z = Xc @ Vt[:2].T # (1797, 2)
print(Z.shape)
print(Z[:3].round(2))Z is (1797, 2) — 64D compressed to a plottable plane
Why: Each row is one digit's coordinates on PC1/PC2. Plotting these colored by the true label shows clusters — structure that existed in 64D, now visible in 2D.
| sample | PC1 | PC2 |
|---|---|---|
| 1 | 1.26 | −21.27 |
| 2 | −7.96 | 20.77 |
| 3 | −6.99 | 9.96 |
Pattern
Step through it
Step through Project digits to 2D one row at a time. What is driving the change, and what would the row after the last one be?
Concept
PCA can only rotate and project — it takes linear combinations of features. So it captures linear structure and nothing else.
Data on a curved manifold — a spiral, a Swiss roll — defeats it: no rotation flattens a curl. For nonlinear structure reach for t-SNE or UMAP, which preserve local neighborhoods. PCA is still the fast, deterministic first look.
Intuition
Picture a spiral drawn on paper. To 'unroll' it you must bend the paper differently at every point — a nonlinear warp.
PCA is allowed exactly one rigid move: pick a straight axis and flatten onto it. Do that to a spiral and far-apart arms of the curl land on top of each other. The variance is maximized, but the structure is mangled.
Explain it
Discussion prompt
Explain Why a spiral breaks it to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Picture a spiral drawn on paper. To 'unroll' it you must bend the paper differently at every point — a nonlinear warp.
Anomaly
Predict first
A student writes this, and it looks reasonable:
PCA is a powerful reducer, so it will uncover whatever structure is present — curved or not. Just run PCA on the Swiss roll to unroll it.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: PCA only takes linear combinations of features.
Use PCA for linear structure and quick looks; switch tools when the manifold is curved.
Why: PCA only takes linear combinations of features. A curved manifold can't be flattened by rotation + projection, so PCA overlaps the folds and destroys the very structure you wanted.
Trap
PCA is a powerful reducer, so it will uncover whatever structure is present — curved or not. Just run PCA on the Swiss roll to unroll it.
Use PCA to flatten a curved manifold
Why: PCA only takes linear combinations of features. A curved manifold can't be flattened by rotation + projection, so PCA overlaps the folds and destroys the very structure you wanted.
Use PCA for linear structure and quick looks; switch tools when the manifold is curved.
PCA for linear/fast; t-SNE or UMAP for nonlinear embeddings
Why: Each has its place: PCA is fast, deterministic, and interpretable; t-SNE/UMAP reveal local neighborhoods on curved manifolds that PCA's straight projection can never recover.
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
np.linalg.eigh returned PC1 as [−0.792, −0.6105], but I computed [+0.792, +0.6105] by hand — one of us made an error and the answers disagree.; Eigendecomposing C and taking the SVD of Xᶜ are separate algorithms, so I should expect different components and different explained variance.Ranking
Put in order
These are the steps of The PCA recipe, scrambled. Put them back in order before the next slide shows you.
Xᶜ (never skip this)Xᶜ (preferred) — or eigendecompose C = XᶜᵀXᶜ/(n−1); both give the same Vσᵢ²/Σσⱼ² (= λᵢ/Σλⱼ); accumulate with cumsum for variance keptk with cumulative EVR ≥ target (95% ⇒ 29 of 64 on digits)Z = Xᶜ V[:, :k]; reconstruction error = Σⱼ₌ₖ₊₁ σⱼ². Remember it is linear-onlyWhy: This is the order the recipe itself gives. Recalling the sequence without the slide in front of you is the difference between recognising the method and being able to run it — most of what goes wrong in practice is a step done out of turn.
Pattern
Xᶜ (never skip this)Xᶜ (preferred) — or eigendecompose C = XᶜᵀXᶜ/(n−1); both give the same Vσᵢ²/Σσⱼ² (= λᵢ/Σλⱼ); accumulate with cumsum for variance keptk with cumulative EVR ≥ target (95% ⇒ 29 of 64 on digits)Z = Xᶜ V[:, :k]; reconstruction error = Σⱼ₌ₖ₊₁ σⱼ². Remember it is linear-onlyEdge cases
Discussion prompt
The PCA recipe works on the cases you have just seen. Push it to the edge: what is the most degenerate input it still handles — empty, zero, one item, everything equal — and what is the first case where it stops being true? Name the case, not just "it breaks".
Hint: Try the smallest legal input, then the largest, then the one where two things collide. Methods are specified at their edges; the middle takes care of itself.
Answer:
Xᶜ (never skip this)Xᶜ (preferred) — or eigendecompose C = XᶜᵀXᶜ/(n−1); both give the same Vσᵢ²/Σσⱼ² (= λᵢ/Σλⱼ); accumulate with cumsum for variance keptk with cumulative EVR ≥ target (95% ⇒ 29 of 64 on digits)Z = Xᶜ V[:, :k]; reconstruction error = Σⱼ₌ₖ₊₁ σⱼ². Remember it is linear-onlyElimination
Eliminate the wrong options
The explained-variance ratio of principal component i is:
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: Variance along PC i is proportional to σᵢ² (since λᵢ = σᵢ²/(n−1)), so its share of the total is σᵢ²/Σⱼσⱼ². These ratios sum to 1 — on our toy they were [0.9678, 0.0322].
Check
What exactly do the singular values encode?
Check your understanding
The explained-variance ratio of principal component i is:
Answer: A
Why: Variance along PC i is proportional to σᵢ² (since λᵢ = σᵢ²/(n−1)), so its share of the total is σᵢ²/Σⱼσⱼ². These ratios sum to 1 — on our toy they were [0.9678, 0.0322].
Prediction
Predict first
How do the covariance eigenvalues λᵢ relate to the singular values σᵢ of the centered data Xᶜ (n samples)?
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: σᵢ² = (n−1)·λᵢ, so both routes give identical explained-variance ratios
Why: Xᶜ = UΣVᵀ gives C = XᶜᵀXᶜ/(n−1) = VΣ²Vᵀ/(n−1), so λᵢ = σᵢ²/(n−1), i.e. σᵢ² = (n−1)λᵢ. On the toy: σ₁² = 40.6469 = 4·10.1617. The (n−1) cancels in the EVR, so both routes report the same shares.
Check
You computed both routes on the same data. Recall the relationship.
Check your understanding
How do the covariance eigenvalues λᵢ relate to the singular values σᵢ of the centered data Xᶜ (n samples)?
Answer: A
Why: Xᶜ = UΣVᵀ gives C = XᶜᵀXᶜ/(n−1) = VΣ²Vᵀ/(n−1), so λᵢ = σᵢ²/(n−1), i.e. σᵢ² = (n−1)λᵢ. On the toy: σ₁² = 40.6469 = 4·10.1617. The (n−1) cancels in the EVR, so both routes report the same shares.
Prediction
Predict first
On digits, 29 of 64 components reach 95% cumulative explained variance. This means:
Answer it in your own words, now, with nothing to choose from. The options are on the next slide — and picking the right one off a list is an easier skill than producing it.
Correct: Projecting to 29 dims keeps 95% of the total variance — a >2× compression
Why: The CUMULATIVE EVR of the top 29 components is ≈0.9548, the first value ≥ 0.95. So a 64→29 projection retains 95% of the variance while more than halving the dimension.
Check
Read the cumulative-variance result carefully.
Check your understanding
On digits, 29 of 64 components reach 95% cumulative explained variance. This means:
Answer: A
Why: The CUMULATIVE EVR of the top 29 components is ≈0.9548, the first value ≥ 0.95. So a 64→29 projection retains 95% of the variance while more than halving the dimension.
Elimination
Eliminate the wrong options
Your data lies on a curved 2-D manifold (a Swiss roll) embedded in 3-D. Running PCA down to 2-D will:
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: PCA only takes linear projections — rotate and flatten. Unrolling a curved manifold is a nonlinear operation it cannot perform, so far-apart folds collapse onto each other. Use t-SNE/UMAP for curved manifolds.
Check
When does PCA break down?
Check your understanding
Your data lies on a curved 2-D manifold (a Swiss roll) embedded in 3-D. Running PCA down to 2-D will:
Answer: A
Why: PCA only takes linear projections — rotate and flatten. Unrolling a curved manifold is a nonlinear operation it cannot perform, so far-apart folds collapse onto each other. Use t-SNE/UMAP for curved manifolds.
Section
Part 8 of 8 — the project
Concept
Implement PCA with the SVD route, expose explained_variance_ratio_, and prove it matches sklearn on digits — then find k for 95%. You've derived every piece; now assemble it.
| # | requirement | tool |
|---|---|---|
| 1 | Center the data and SVD it | X − X.mean(0); np.linalg.svd |
| 2 | explained_variance_ratio_ | S2 / np.sum(S2) |
| 3 | Verify vs sklearn; find k for 95% | PCA; np.cumsum, np.searchsorted |
Build rules: type every line yourself, always center before decomposing, and compare your EVR to sklearn with np.allclose — not by eyeballing.
Analogy
Discussion prompt
Explain Project: a PCA class from scratch by analogy to something with no Machine Learning in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Implement PCA with the SVD route, expose explained_variance_ratio_, and prove it matches sklearn on digits — then find k for 95%. You've derived every piece; now assemble it.
Worked example
Your turn: center digits and take the economy SVD. Predict the length of the singular-value vector before you print it.
Hint: Xc = X - X.mean(0); U, S, Vt = np.linalg.svd(Xc, full_matrices=False). With 64 features, S has 64 entries.
import numpy as np
from sklearn.datasets import load_digits
X = load_digits().data
Xc = X - X.mean(0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
print(S.shape, S[:3].round(2))| object | value (verified) |
|---|---|
| S.shape | (64,) |
| top 3 singular values | [567.01, 542.25, 504.63] |
| Vt.shape | (64, 64) |
Worked example
Your turn: compute the EVR and confirm it sums to 1. Predict PC1's share before printing.
Hint: evr = S**2 / np.sum(S**2); then evr.sum() should round to 1.0.
import numpy as np
from sklearn.datasets import load_digits
X = load_digits().data
Xc = X - X.mean(0)
S = np.linalg.svd(Xc, full_matrices=False)[1]
evr = S**2 / np.sum(S**2)
print(evr[:5].round(4))
print(round(evr.sum(), 6))| quantity | value (verified) |
|---|---|
| EVR top 5 | [0.1489, 0.1362, 0.1179, 0.0841, 0.0578] |
| Σ EVR | 1.0 |
Worked example
Your turn: confirm your EVR matches sklearn, then find k for 95%. Predict whether it is under half of 64.
Hint: PCA().fit(X).explained_variance_ratio_ for the reference; np.searchsorted(np.cumsum(evr), 0.95) + 1 for k.
import numpy as np
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
X = load_digits().data
Xc = X - X.mean(0)
S = np.linalg.svd(Xc, full_matrices=False)[1]
evr = S**2 / np.sum(S**2)
sk = PCA().fit(X).explained_variance_ratio_
print(np.allclose(evr[:5], sk[:5], atol=1e-6))
print(int(np.searchsorted(np.cumsum(evr), 0.95) + 1))| check | value (verified) |
|---|---|
| EVR matches sklearn | True |
| k for 95% | 29 of 64 |
Trade off
Comparison matrix
From Milestone 3 — verify & choose k: every row here is a choice with a cost. Fill the value (verified) column, then say which row you would actually pick and what you give up for it.
| check | value (verified) |
|---|---|
| EVR matches sklearn | True |
| k for 95% | 29 of 64 |
Pattern
Predict first
The table runs: EVR top 3 | [0.1489, 0.1362, 0.1179] · k @ 95% | 29
In The full PCA class, given the rows so far: what is the next one — the row where output is 2D shape?
Correct: 2D shape | (1797, 2)
| output | value (verified) |
|---|---|
| EVR top 3 | [0.1489, 0.1362, 0.1179] |
| k @ 95% | 29 |
| 2D shape | (1797, 2) |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. fit() stores the mean and components; transform() re-centers with the SAME mean and projects onto k components.
Concept
import numpy as np
class PCA:
def fit(self, X):
self.mean_ = X.mean(0)
Xc = X - self.mean_
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
self.components_ = Vt
self.explained_variance_ratio_ = S**2 / np.sum(S**2)
return self
def transform(self, X, k):
return (X - self.mean_) @ self.components_[:k].T
from sklearn.datasets import load_digits
X = load_digits().data
p = PCA().fit(X)
print(p.explained_variance_ratio_[:3].round(4))
cum = np.cumsum(p.explained_variance_ratio_)
print('k@95% =', int(np.searchsorted(cum, 0.95) + 1))
print('2D shape =', p.transform(X, 2).shape)The whole algorithm is: center, SVD, ratio — that's PCA
Why: fit() stores the mean and components; transform() re-centers with the SAME mean and projects onto k components. No covariance matrix is ever formed.
| output | value (verified) |
|---|---|
| EVR top 3 | [0.1489, 0.1362, 0.1179] |
| k @ 95% | 29 |
| 2D shape | (1797, 2) |
If your class reproduces sklearn's ratios and finds 29 components for 95% — you built PCA from the linear algebra up.
Comparison
Comparison matrix
From The full PCA class: refill the value (verified) column from what you know. The rest of the table is as it appeared.
| output | value (verified) |
|---|---|
| EVR top 3 | [0.1489, 0.1362, 0.1179] |
| k @ 95% | 29 |
| 2D shape | (1797, 2) |
Concept
Slides closed, out loud: explain (1) why we center before decomposing, (2) why the SVD route avoids forming XᶜᵀXᶜ, and (3) what σᵢ² = (n−1)λᵢ says about the two routes being one.
Stretch (homework): add a transform to 2D and scatter-plot digits by label to see the clusters; then flip the sign of a component and confirm the EVR and reconstruction error are unchanged. PCA returns in unsupervised learning (Week 22) and autoencoder latent spaces (Week 43).
Counterexample
Discussion prompt
Slides closed, out loud: explain (1) why we center before decomposing, (2) why the SVD route avoids forming XᶜᵀXᶜ, and (3) what σᵢ² = (n−1)λᵢ says about the two routes being one.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — What PCA is for · Step 1: center · Route A: covariance eigendecomposition · Route B: SVD · Step 2: project · Choosing k at scale. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
C = XᶜᵀXᶜ/(n−1) entry by entry, and eigendecompose the 2×2 by hand: λ = [10.1617, 0.3383], v₁ = [0.792, 0.6105]σᵢ² = (n−1)λᵢ, so both routes share one EVR = [0.9678, 0.0322]Σⱼ₌ₖ₊₁ σⱼ²sklearn| move | the one thing to remember |
|---|---|
| center first | PCA is about spread around the mean — subtract it |
| two routes | C = VΣ²Vᵀ/(n−1): eig of C = SVD of Xᶜ |
| EVR | σᵢ²/Σσⱼ² = λᵢ/Σλⱼ, sums to 1 |
| choosing k | cumulative variance ≥ threshold (95% → 29/64) |
| limitation | linear only → t-SNE/UMAP for curved manifolds |
Want this taught 1-on-1? Alexander tutors Machine Learning — $55/session, free consultation.