Lesson 19: PCA Implementation

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

What this lesson covers

The lesson, slide by slide

1. PCA, Built From the Math Up

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.

2. By the end of this lesson you can

Objectives

  1. Center a data matrix and build its covariance C = XᶜᵀXᶜ/(n−1) entry by entry
  2. Find PCA's components by eigendecomposing C — characteristic polynomial, eigenvalues, eigenvectors — all by hand
  3. Get the identical answer the SVD way and prove σᵢ² = (n−1)λᵢ, so both routes give the same explained-variance ratio
  4. Choose k from the cumulative variance (95% ⇒ 29 of 64 on digits), project to k dims, and read the reconstruction error
  5. State PCA's linear-only limit, dodge the sign-ambiguity trap, and implement a PCA class that matches sklearn

3. What survived from Full Backpropagation?

Warm-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.

4. What PCA is for

Section

Part 1 of 8 — the goal

5. Too many features, most of them redundant

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.

6. Break it if you can: Too many features, most of them redundant

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.

7. Variance is the signal

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.

8. By analogy: Variance is the signal

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.

9. One running dataset for the whole lesson

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.

samplefeature 1feature 2
120
231
351
473
585

The two features clearly rise together — strongly correlated. PCA should find one dominant direction along that trend and a tiny second one across it.

10. Fill in: feature 1 for One running dataset for the whole lesson

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.

samplefeature 1feature 2
120
231
351
473
585

11. The plan: two routes to the same components

Concept

There are two equivalent ways to compute PCA, and we do both on this dataset so you trust the result:

  1. Eigen route: center X, form the covariance C = XᶜᵀXᶜ/(n−1), then eigendecompose C
  2. SVD route: center X, 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)λᵢ.

12. Teach it back: The plan: two routes to the same components

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)λᵢ.

13. Step 1: center

Section

Part 2 of 8 — subtract the mean

14. Why center first

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.

15. Guess the shape of the answer: Center the data, row by row

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.

16. Center the data, row by row

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 \]

rowf1 − 5f2 − 2
12 − 5 = −30 − 2 = −2
23 − 5 = −21 − 2 = −1
35 − 5 = 01 − 2 = −1
47 − 5 = +23 − 2 = +1
58 − 5 = +35 − 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.

17. What each one costs: Center the data, row by row

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.

rowf1 − 5f2 − 2
12 − 5 = −30 − 2 = −2
23 − 5 = −21 − 2 = −1
35 − 5 = 01 − 2 = −1
47 − 5 = +23 − 2 = +1
58 − 5 = +35 − 2 = +3

18. What has to be given first: Center it in NumPy

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.

19. Center it in NumPy

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 Xcvalue (verified)
row 1[−3, −2]
row 2[−2, −1]
row 3[ 0, −1]
row 4[ 2, 1]
row 5[ 3, 3]

20. Watch it run: Center it in NumPy

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?

  1. Step 1: row of Xc is row 1
  2. Step 2: row of Xc is row 2
  3. Step 3: row of Xc is row 3
  4. Step 4: row of Xc is row 4
  5. Step 5: row of Xc is row 5

21. Route A: covariance eigendecomposition

Section

Part 3 of 8 — by hand

22. The covariance matrix

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.

23. What has to happen first: Build XᶜᵀXᶜ entry by entry

Ranking

Put in order

Put the moves of Build XᶜᵀXᶜ entry by entry into the order they have to happen.

  1. Top-left = c₀·c₀ = 9+4+0+4+9 = 26
  2. Off-diagonal = c₀·c₁ = 6+2+0+2+9 = 19
  3. Bottom-right = c₁·c₁ = 4+1+1+1+9 = 16

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.

24. Build XᶜᵀXᶜ entry by entry

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} \]

entrydot productvalue
(0,0)c₀·c₀26
(0,1)=(1,0)c₀·c₁19
(1,1)c₁·c₁16

25. Decode the notation: Build XᶜᵀXᶜ entry by entry

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} \)

  • Sum of squared entries of centered feature 1.
  • (−3)(−2)+(−2)(−1)+(0)(−1)+(2)(1)+(3)(3) = 6+2+0+2+9 = 19. Appears in both off-diagonal slots by symmetry.
  • Sum of squared entries of centered feature 2.

26. Predict the next row: Divide by n−1 to get C

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 entrymeaningvalue
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.

27. Divide by n−1 to get C

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 entrymeaningvalue
C[0,0]var(feature 1)6.5
C[1,1]var(feature 2)4.0
C[0,1]cov(f1, f2)4.75

28. Work backwards from the answer: Divide by n−1 to get C

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:

29. PCA = eigenvectors of C

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.

30. What an eigenvector of C means here

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.

31. Characteristic polynomial of C

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 \]

quantityvalue
trace(C) = C₀₀ + C₁₁10.5
det(C) = C₀₀C₁₁ − C₀₁²3.4375

32. Draw the shape of it: Characteristic polynomial of C

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.

33. What has to happen first: Solve the quadratic for the eigenvalues

Ranking

Put in order

Put the moves of Solve the quadratic for the eigenvalues into the order they have to happen.

  1. Discriminant = tr² − 4·det = 10.5² − 4(3.4375) = 110.25 − 13.75 = 96.5
  2. λ₁ = (10.5 + 9.8234)/2 = 10.1617
  3. λ₂ = (10.5 − 9.8234)/2 = 0.3383

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.

34. Solve the quadratic for the eigenvalues

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 ✓.

eigenvaluevalue= variance along PC
λ₁10.1617PC1
λ₂0.3383PC2
λ₁ + λ₂10.5 (= trace ✓)total variance

35. Draw the shape of it: Solve the quadratic for the eigenvalues

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.

36. The top eigenvector, by hand

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} \]

stepvalue
λ₁ − 6.53.6617
unnormalized v(4.75, 3.6617)
‖v‖5.997
unit v₁(0.7920, 0.6105)

37. Say it in words: The top eigenvector, by hand

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.

38. Restore the missing line: Confirm the eigendecomposition in NumPy

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.

39. Confirm the eigendecomposition in NumPy

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.

outputvalue (verified)
lam[10.1617, 0.3383]
V[:,0] (PC1)[−0.7920, −0.6105]
V[:,1] (PC2)[+0.6105, −0.7920]

40. Something is wrong here: an eigenvector's sign is not fixed

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.

41. Trap: an eigenvector's sign is not fixed

Trap

The 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.

The fix

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.

42. Complete the line: Explained-variance ratio from the eigenvalues

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.

43. Explained-variance ratio from the eigenvalues

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.0

EVR = [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.

componentEVRcumulative
PC10.96780.9678
PC20.03221.0000

44. Route B: SVD

Section

Part 4 of 8 — same answer, better numerics

45. SVD skips forming C

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).

46. SVD of the centered data

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.

outputvalue (verified)
S (σ₁, σ₂)[6.3755, 1.1632]
Vt[0] (PC1)[0.7920, 0.6105]
Vt[1] (PC2)[0.6105, −0.7920]

47. Guess the shape of the answer: Prove the two routes are one: σᵢ² = (n−1)λᵢ

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.

48. Prove the two routes are one: σᵢ² = (n−1)λᵢ

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
140.64694 × 10.1617 = 40.6469✓
21.35314 × 0.3383 = 1.3531✓

49. Decode the notation: Prove the two routes are one: σᵢ² = (n−1)λᵢ

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 \)

  • 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).
  • σ₂² = 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.

50. EVR from singular values equals EVR from eigenvalues

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.

routeEVR (verified)
eigenvalues of C[0.967783, 0.032217]
singular values of Xᶜ[0.967783, 0.032217]

51. Fill in: EVR (verified) for EVR from singular values equals EVR from…

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.

routeEVR (verified)
eigenvalues of C[0.967783, 0.032217]
singular values of Xᶜ[0.967783, 0.032217]

52. Something is wrong here: 'eig and SVD are different methods'

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.

53. Trap: 'eig and SVD are different methods'

Trap

The 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.

The fix

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.

54. Break it on purpose: 'eig and SVD are different methods'

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.

55. Step 2: project

Section

Part 5 of 8 — scores & reconstruction

56. Projecting onto the components

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.

57. Restore the missing line: Compute the scores

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.

58. Compute the scores

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.1617

var(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'.

samplePC1 scorePC2 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

59. Watch it run: Compute the scores

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?

  1. Step 1: sample is 1
  2. Step 2: sample is 2
  3. Step 3: sample is 3
  4. Step 4: sample is 4
  5. Step 5: sample is 5

60. Scores are the data in new coordinates

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.

61. Reconstruction: how much did we lose?

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.

62. Finish it with less help: Reconstruct from k = 1 and measure the loss

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).

63. Reconstruct from k = 1 and measure the loss

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.3531

Reconstruction 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.

quantityvalue (verified)
reconstruction SSE (k=1)1.3531
σ₂² = (n−1)λ₂1.3531
variance kept (EVR₁)96.78%

64. Work backwards from the answer: Reconstruct from k = 1 and measure the loss

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 λ₂:

65. Choosing k at scale

Section

Part 6 of 8 — the digits dataset

66. Scaling up: 64-feature digits

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?

67. Two ways to pick k

Concept

The threshold is the defensible, automatable choice: it turns 'how many dimensions?' into one number from the cumulative-variance curve.

68. Finish it with less help: EVR of the digits data

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.

69. EVR of the digits data

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.

componentEVRcumulative
PC10.14890.1489
PC20.13620.2851
PC30.11790.4030
PC40.08410.4871
PC50.05780.5450

70. What happens as it grows: EVR of the digits data

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?

  1. Step 1: component is PC1
  2. Step 2: component is PC2
  3. Step 3: component is PC3
  4. Step 4: component is PC4
  5. Step 5: component is PC5

71. Total variance is the trace

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.

72. Restore the missing line: The scree curve: where does it flatten?

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.

73. The scree curve: where does it flatten?

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.

componentEVRdrop from previous
PC10.1489—
PC20.13620.0127
PC30.11790.0183
PC40.08410.0338
PC50.05780.0263

74. Watch it run: The scree curve: where does it flatten?

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?

  1. Step 1: component is PC1
  2. Step 2: component is PC2
  3. Step 3: component is PC3
  4. Step 4: component is PC4
  5. Step 5: component is PC5

75. What has to be given first: How many components reach 95%?

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.

76. How many components reach 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 64

29 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 targetcomponents needed (verified)
50%5
90%21
95%29
99%41
100%64 (all)

77. What happens as it grows: How many components reach 95%?

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?

  1. Step 1: cumulative target is 50%
  2. Step 2: cumulative target is 90%
  3. Step 3: cumulative target is 95%
  4. Step 4: cumulative target is 99%
  5. Step 5: cumulative target is 100%

78. Visualization & the limit

Section

Part 7 of 8

79. PCA for 2D visualization

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.

80. Guess the shape of the answer: Project digits to 2D

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.

81. Project digits to 2D

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.

samplePC1PC2
11.26−21.27
2−7.9620.77
3−6.999.96

82. Watch it run: Project digits to 2D

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?

  1. Step 1: sample is 1
  2. Step 2: sample is 2
  3. Step 3: sample is 3

83. The hard limit: PCA is linear

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.

84. Why a spiral breaks it

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.

85. Teach it back: Why a spiral breaks it

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.

86. Something is wrong here: expecting PCA to find nonlinear structure

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.

87. Trap: expecting PCA to find nonlinear structure

Trap

The 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.

The fix

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.

88. Which of these survive contact with Lesson 19: PCA Implementation?

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.

Holds up
The trick: rotate to axes along which the data varies the most. Those axes are the principal components.; 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.; The two features clearly rise together — strongly correlated. PCA should find one dominant direction along that trend and a tiny second one across it.
Breaks
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.
sound
These are stated as this lesson states them — each one survives the edge cases Lesson 19: PCA Implementation puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

89. Rebuild the recipe: The PCA recipe

Ranking

Put in order

These are the steps of The PCA recipe, scrambled. Put them back in order before the next slide shows you.

  1. Center: subtract the column means → Xᶜ (never skip this)
  2. Decompose: SVD of Xᶜ (preferred) — or eigendecompose C = XᶜᵀXᶜ/(n−1); both give the same V
  3. EVR: σᵢ²/Σσⱼ² (= λᵢ/Σλⱼ); accumulate with cumsum for variance kept
  4. Choose k: scree elbow, or smallest k with cumulative EVR ≥ target (95% ⇒ 29 of 64 on digits)
  5. Project: Z = Xᶜ V[:, :k]; reconstruction error = Σⱼ₌ₖ₊₁ σⱼ². Remember it is linear-only

Why: 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.

90. The PCA recipe

Pattern

  1. Center: subtract the column means → Xᶜ (never skip this)
  2. Decompose: SVD of Xᶜ (preferred) — or eigendecompose C = XᶜᵀXᶜ/(n−1); both give the same V
  3. EVR: σᵢ²/Σσⱼ² (= λᵢ/Σλⱼ); accumulate with cumsum for variance kept
  4. Choose k: scree elbow, or smallest k with cumulative EVR ≥ target (95% ⇒ 29 of 64 on digits)
  5. Project: Z = Xᶜ V[:, :k]; reconstruction error = Σⱼ₌ₖ₊₁ σⱼ². Remember it is linear-only

91. Where does it stop working: The PCA recipe

Edge 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:

  1. Center: subtract the column means → Xᶜ (never skip this)
  2. Decompose: SVD of Xᶜ (preferred) — or eigendecompose C = XᶜᵀXᶜ/(n−1); both give the same V
  3. EVR: σᵢ²/Σσⱼ² (= λᵢ/Σλⱼ); accumulate with cumsum for variance kept
  4. Choose k: scree elbow, or smallest k with cumulative EVR ≥ target (95% ⇒ 29 of 64 on digits)
  5. Project: Z = Xᶜ V[:, :k]; reconstruction error = Σⱼ₌ₖ₊₁ σⱼ². Remember it is linear-only

92. Rule out three: Check yourself — explained variance

Elimination

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.

  • A. σᵢ² / Σⱼ σⱼ²
  • B. σᵢ / Σⱼ σⱼ
  • C. σᵢ² (the raw squared singular value)
  • D. 1 / k for each of the k kept components

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].

93. Check yourself — explained variance

Check

What exactly do the singular values encode?

Check your understanding

The explained-variance ratio of principal component i is:

  • A. σᵢ² / Σⱼ σⱼ² (correct)
  • B. σᵢ / Σⱼ σⱼ
  • C. σᵢ² (the raw squared singular value)
  • D. 1 / k for each of the k kept components

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].

Why B tempts people
Variance scales with the SQUARE of the singular value, not the singular value itself. Using σ instead of σ² gives the wrong shares (and they wouldn't correspond to variance at all).
Why C tempts people
σᵢ² is the unnormalized variance (times n−1). The RATIO divides by the total so the shares sum to 1 and are comparable across datasets.
Why D tempts people
Equal 1/k shares would only hold for perfectly isotropic data. Real components capture unequal variance — PC1 held 96.8% on the toy, not 50%.

94. Answer it before you see the options: Check yourself — eig vs SVD

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.

95. Check yourself — eig vs SVD

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)?

  • A. σᵢ² = (n−1)·λᵢ, so both routes give identical explained-variance ratios (correct)
  • B. σᵢ = λᵢ exactly
  • C. They are unrelated — eig and SVD are different algorithms with different answers
  • D. λᵢ = (n−1)·σᵢ²

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.

Why B tempts people
Off by a square and a factor: σ is a singular value of Xᶜ, λ is an eigenvalue of the covariance. σ₁ = 6.3755 but λ₁ = 10.1617 — clearly not equal.
Why C tempts people
They are the SAME decomposition. C = VΣ²Vᵀ/(n−1), so C's eigenvectors are V's columns. Verified equal to 6 decimals in the lesson.
Why D tempts people
The (n−1) and the square are on the wrong sides. It is σᵢ² that equals (n−1)λᵢ, not the reverse.

96. Answer it before you see the options: Check yourself — choosing k

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.

97. Check yourself — choosing k

Check

Read the cumulative-variance result carefully.

Check your understanding

On digits, 29 of 64 components reach 95% cumulative explained variance. This means:

  • A. Projecting to 29 dims keeps 95% of the total variance — a >2× compression (correct)
  • B. Each of the 29 components individually explains 95% of the variance
  • C. The data has exactly 29 true dimensions
  • D. 29 components are needed to reach 100% of the variance

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.

Why B tempts people
Each component explains its own small share (PC1 only ~15%). It is the SUM of the top 29 shares that reaches 95%, not each one.
Why C tempts people
Variance is continuous, not a hard rank. 95% is a chosen threshold; at 99% you'd keep 41, at 90% only 21. There is no single 'true' 29.
Why D tempts people
100% needs all 64 components. 29 reaches 95%; the remaining 35 hold the final 5% of variance.

98. Rule out three: Check yourself — the limitation

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.

  • A. Fail to unroll it — PCA captures only linear structure and overlaps the folds
  • B. Perfectly unroll the manifold into a flat sheet
  • C. Cluster the points by their class label
  • D. Return 3 components instead of 2

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.

99. Check yourself — the limitation

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:

  • A. Fail to unroll it — PCA captures only linear structure and overlaps the folds (correct)
  • B. Perfectly unroll the manifold into a flat sheet
  • C. Cluster the points by their class label
  • D. Return 3 components instead of 2

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.

Why B tempts people
Unrolling a curl is inherently NONLINEAR; a single straight projection can't do it. This is exactly the case PCA fails on.
Why C tempts people
PCA is unsupervised and label-agnostic — it maximizes variance, it does not know or use class labels to cluster.
Why D tempts people
Projecting to 2-D returns 2 components by construction. The problem is the quality of that projection, not the number of components.

100. Your turn: build PCA

Section

Part 8 of 8 — the project

101. Project: a PCA class from scratch

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.

#requirementtool
1Center the data and SVD itX − X.mean(0); np.linalg.svd
2explained_variance_ratio_S2 / np.sum(S2)
3Verify 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.

102. By analogy: Project: a PCA class from scratch

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.

103. Milestone 1 — center & decompose

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))
objectvalue (verified)
S.shape(64,)
top 3 singular values[567.01, 542.25, 504.63]
Vt.shape(64, 64)

104. Milestone 2 — explained variance

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))
quantityvalue (verified)
EVR top 5[0.1489, 0.1362, 0.1179, 0.0841, 0.0578]
Σ EVR1.0

105. Milestone 3 — verify & choose k

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))
checkvalue (verified)
EVR matches sklearnTrue
k for 95%29 of 64

106. What each one costs: Milestone 3 — verify & choose k

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.

checkvalue (verified)
EVR matches sklearnTrue
k for 95%29 of 64

107. Predict the next row: The full PCA class

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)

outputvalue (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.

108. The full PCA class

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.

outputvalue (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.

109. Fill in: value (verified) for The full PCA class

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.

outputvalue (verified)
EVR top 3[0.1489, 0.1362, 0.1179]
k @ 95%29
2D shape(1797, 2)

110. Show it off

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).

111. Break it if you can: Show it off

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.

112. Connect it up: Lesson 19: PCA Implementation

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.

113. What you can do now

Recap

movethe one thing to remember
center firstPCA is about spread around the mean — subtract it
two routesC = VΣ²Vᵀ/(n−1): eig of C = SVD of Xᶜ
EVRσᵢ²/Σσⱼ² = λᵢ/Σλⱼ, sums to 1
choosing kcumulative variance ≥ threshold (95% → 29/64)
limitationlinear only → t-SNE/UMAP for curved manifolds

Sources

  1. USAAIO Year-Long Master Lesson Plan, Lesson 19 (Week 7 — PCA Implementation) — Barron · USAAIO Round 2 Preparation, 2026
  2. scikit-learn PCA (explained_variance_ratio_, components_, SVD-based)
  3. NumPy linalg.svd / eigh
  4. Every covariance, eigenvalue, singular value, EVR, and k-threshold produced by real execution — numpy 2.2.6 + scikit-learn 1.9, standalone-block verification, July 2026

Want this taught 1-on-1? Alexander tutors Machine Learning — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108