Index Ai Academy
Phase 2 · Math, when you need it · Linear algebra

SVD and PCA

Compress data to its most important directions.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

A spreadsheet of customers might have 4 columns. An embedding, the list of numbers an AI model uses to stand for a sentence, can have 1,536. Many of those columns rise and fall together, so most of the information sits in a few directions. PCA finds those directions. With it you can shrink data, clean out noise, or squash 1,536 numbers down to 2 and plot your embeddings to see the clusters. The tool underneath, the singular value decomposition (SVD), also explains LoRA, the cheap way to fine-tune big models that you'll meet in Phase 6.

What you'll be able to do

  • Explain PCA as finding the directions in which data varies most
  • Run PCA with NumPy's SVD and read the explained variance
  • Project high-dimensional data to 2D to look at it
  • Use a low-rank approximation to compress a matrix, and connect it to LoRA

What it covers

  1. Many columns, few real stories
  2. The recipe in four steps
  3. SVD: turn, stretch, turn
  4. PCA on 120 customers
  5. Worked example: PCA on four customers
  6. Seeing it: from 4 numbers to 2
  7. Compression: keep the big stretches

Many columns, few real stories

Think about describing a person's body size. You could record height, arm length, leg length and shoe size. That's four numbers. But they rise and fall together. Tall people tend to have long arms, long legs and big feet. One number, "overall size", tells most of the story. A second number, such as "long legs for their height", adds a little more.

Shop data works the same way. Say every customer has four features: orders per year, average order value, discount use and email clicks. Bargain hunters order often, use discounts and click a lot. Premium buyers order less but spend more each time. So the four columns mostly tell one story: bargain hunter or premium?

Principal component analysis (PCA) finds these hidden stories for you. Each one is called a principal component. A component is a direction. Think of it as a recipe that mixes the original features with weights, such as "some orders, plus some clicks, minus some order value". It is not one of the original columns.

How does PCA pick the directions? By spread. Picture each customer as a dot, so all of them form a cloud. Along some directions the cloud is long. Along others it's thin. The long directions are where customers differ most, so they carry the most information. Statisticians call this spread variance. You'll measure it properly in a later lesson.

In plain words: PCA looks at a table with many columns. It finds the few directions where the rows differ most, and then describes each row with only those few numbers.

Quick check
How does PCA choose its directions?
  1. In the order of the columns
  2. At random
  3. By spread: it starts with the direction where the points vary most
  4. By which column has the largest name
Show the answer

By spread: it starts with the direction where the points vary most

A direction where everyone looks alike tells you little. The one with the most spread keeps the most information.

The recipe in four steps

  1. Center the data. Subtract each column's average, so the cloud sits around zero. For example, orders of 10, 20 and 30 have an average of 20. They become −10, 0 and 10.
  2. Standardize, usually. Divide each column by its standard deviation: the typical distance of a value from its average. Now dollars and percentages share one scale. No column wins only because its numbers are bigger.
  3. Find the longest direction of the cloud. That's the first principal component. Then find the next-longest direction at right angles to it (mathematicians say perpendicular). Repeat for as many as you want.
  4. Project each point onto the top few components. A customer then has, say, 2 numbers instead of 4.

Projecting is the dot product you already know. Multiply each centered value by the direction's weight, then add up. Take the centered point [3, 4] and the direction [0.6, 0.8]. You get 3 × 0.6 + 4 × 0.8 = 1.8 + 3.2 = 5. So this customer sits 5 steps along that direction.

Try it now

Turn the line to find the direction that keeps the most spread. That's PCA's first component.

Quick check
A column of weekly orders is [4, 6, 8]. What does it look like after centering?
  1. [0, 2, 4]
  2. [4, 6, 8]: centering only moves the other columns
  3. [-2, 0, 2]
  4. [0.5, 0.75, 1]
Show the answer

[-2, 0, 2]

The average is 6. Subtract 6 from each value: 4 − 6 = −2, 6 − 6 = 0, 8 − 6 = 2.

SVD: turn, stretch, turn

How does a computer find the longest directions? It uses the singular value decomposition, or SVD. It works on any table of numbers, of any shape.

You've seen that a matrix moves points around. SVD says every matrix makes that move in three simple stages. It turns the space, stretches it along a few directions at right angles, then turns it again. (A "turn" here may also include a mirror flip.)

X=UΣV⊤X = U \Sigma V^\top

Here XX is your data table, with one row per customer. Read the right-hand side from right to left. V⊤V^\top ("V transposed") is the first turn. Its rows are the directions. For centered data, they are the principal components.

Next comes Σ\Sigma, a capital sigma. It is the stretch. It holds the singular values, one per direction, sorted from biggest to smallest. We write them σ1,σ2\sigma_1, \sigma_2 and so on, with a small sigma. Last, UU is the final turn.

In plain words: any table of numbers can be rebuilt from a turn, a set of stretches and another turn. For centered data, the biggest stretch points along the direction where your data spreads most.

Which share of the spread?

Each direction's share of the spread comes from its singular value. This works on centered data, as in step 1. Number the directions with ii: 1 for the first, 2 for the second. Square every singular value. A direction's share is its square divided by the total of all the squares:

sharei=σi2σ12+σ22+⋯\text{share}_i = \frac{\sigma_i^2}{\sigma_1^2 + \sigma_2^2 + \cdots}

In plain words: square the stretches, then ask what fraction of the total each square is. This fraction is called the explained variance ratio. All the shares add up to 1.

You'll also see the bottom written as ∑σ2\sum \sigma^2. The big ∑\sum is another sigma, used to mean "add them all up".

Here is a small example to practice the formula. The matrix below stretches the page 3 times across and 1 time up. It isn't a centered data table, but the arithmetic is the same. No turning is needed, so the singular values are 3 and 1. Square them: 9 and 1, which total 10. The shares are 9 ÷ 10 = 0.9 and 1 ÷ 10 = 0.1. The first direction gets 90% of the total.

import numpy as np

X = np.array([[3.0, 0.0],
              [0.0, 1.0]])
U, S, Vt = np.linalg.svd(X)
print("singular values:", S)
print("shares:", (S ** 2 / np.sum(S ** 2)).round(2))
It runs right here in your browser, with nothing to install.

It prints [3. 1.] and [0.9 0.1], the numbers you worked out by hand.

PCA on 120 customers

Now the real thing. The code below invents 120 customers with the four features from earlier. About half are bargain hunters and half are premium buyers. It standardizes the table, runs SVD and prints each component's share of the spread. It also prints the first component's weights.

import numpy as np

rng = np.random.default_rng(7)
n = 120
segment = rng.integers(0, 2, size=n)                 # 0 = bargain hunter, 1 = premium
orders = np.where(segment == 0, 14, 5) + rng.normal(0, 2, n)
value = np.where(segment == 0, 40, 120) + rng.normal(0, 12, n)
discount = np.where(segment == 0, 0.6, 0.1) + rng.normal(0, 0.08, n)
clicks = 0.8 * orders + rng.normal(0, 2, n)
X = np.column_stack([orders, value, discount, clicks])

Z = (X - X.mean(axis=0)) / X.std(axis=0)             # standardize
U, S, Vt = np.linalg.svd(Z, full_matrices=False)
explained = S ** 2 / np.sum(S ** 2)
print("explained variance ratio:", explained.round(3))
print("first component (weights on the 4 features):", Vt[0].round(2))

The first component alone explains about 90% of the spread. Its weights show what it measures. Orders, discounts and clicks pull one way. Order value pulls the other. That's the "bargain hunter or premium" story. PCA found it without ever being told which customer was which. (Your signs may all come out flipped. An arrow and its reverse lie on the same line, so the meaning is the same.)

Go deeper Where eigenvectors come in

Take the centered table XX, with nn rows. Build this square table:

C=1n−1X⊤XC = \frac{1}{n-1} X^\top X

Read it as: "C is X transposed times X, divided by n minus 1." CC is called the covariance matrix. It records how each pair of features moves together.

The principal components are the eigenvectors of CC. Each eigenvalue is the variance along its component. The SVD gives the same answer: the variance along component ii is σi2/(n−1)\sigma_i^2 / (n-1). In practice people use the SVD, because it is more accurate on a computer.

Quick check
The first component's weights are [0.51, -0.5, 0.5, 0.48] on orders, order value, discount use and clicks. What is this component?
  1. A mix of all four features, with order value pulling the opposite way
  2. The orders column, renamed
  3. The average customer
  4. The two features with the largest numbers
Show the answer

A mix of all four features, with order value pulling the opposite way

A principal component is a direction: a weighted recipe of every original feature, not one of the original columns.

Worked example

Worked example: PCA on four customers

Four customers, two features: orders [2, 4, 6, 8] and clicks [3, 4, 8, 9]. The two columns rise together, so one direction should tell most of the story. Center the table, run SVD, then project each customer onto the first direction.

import numpy as np

orders = np.array([2.0, 4.0, 6.0, 8.0])
clicks = np.array([3.0, 4.0, 8.0, 9.0])
X = np.column_stack([orders, clicks])

centered = X - X.mean(axis=0)
print("centered:")
print(centered)

U, S, Vt = np.linalg.svd(centered, full_matrices=False)
direction = Vt[0] * np.sign(Vt[0, 0])      # make the arrow point the positive way
print("direction:", direction.round(2))
print("share of spread:", (S ** 2 / np.sum(S ** 2)).round(2))
print("one number per customer:", (centered @ direction).round(1))
In the lesson you can change this and run it.

The averages are 5 orders and 6 clicks, so the first customer becomes [−3, −3]. The direction [0.66, 0.75] means "some orders plus a bit more clicks". It carries 98% of the spread. Projecting is a dot product: −3 × 0.66 + −3 × 0.75 ≈ −4.2 for the first customer. Four customers now need one number each, and almost nothing is lost. We skipped standardizing because both columns are small counts on similar scales. The np.sign line only makes the arrow point the positive way, since SVD may hand it back reversed.

Seeing it: from 4 numbers to 2

Multiply the centered data by the top two components. You get two numbers per customer. Two numbers make a point on a flat chart, so you can plot it:

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(7)
n = 120
segment = rng.integers(0, 2, size=n)
orders = np.where(segment == 0, 14, 5) + rng.normal(0, 2, n)
value = np.where(segment == 0, 40, 120) + rng.normal(0, 12, n)
discount = np.where(segment == 0, 0.6, 0.1) + rng.normal(0, 0.08, n)
clicks = 0.8 * orders + rng.normal(0, 2, n)
Z = np.column_stack([orders, value, discount, clicks])
Z = (Z - Z.mean(axis=0)) / Z.std(axis=0)

U, S, Vt = np.linalg.svd(Z, full_matrices=False)
P = Z @ Vt[:2].T                                     # (120, 2): coordinates on PC1 and PC2
for seg, label in [(0, "bargain hunters"), (1, "premium")]:
    plt.scatter(P[segment == seg, 0], P[segment == seg, 1], s=18, label=label)
plt.xlabel("first principal component")
plt.ylabel("second principal component")
plt.title("120 customers, 4 features, shown in 2D")
plt.legend()

Two clear groups appear. PCA never saw the segment labels. The colors are added afterwards, only so you can check the result.

People plot embeddings the same way. They take 1,536 numbers down to 2, which is often enough to see topics cluster. Two directions can't hold everything, though. For pictures like this, nonlinear methods such as UMAP often separate clusters more clearly. PCA is the straight-line, exact and fast starting point.

Quick check
PCA on 50 features gives explained variance ratios [0.62, 0.21, 0.05, ...]. What can you conclude?
  1. PCA failed
  2. Only 2 of the 50 original features matter
  3. The data has exactly 2 dimensions
  4. The first two components capture about 83% of the variation
Show the answer

The first two components capture about 83% of the variation

0.62 + 0.21 = 0.83. Each component mixes all the original features, so it isn't 'two features'.

Compression: keep the big stretches

Saving a photo at lower quality keeps the big shapes and drops the fine detail. SVD can do the same to any table. Because the singular values are sorted, you can keep only the first kk of them, where kk is a number you choose. Throw the rest away. The result is the best possible rank-kk approximation: the closest copy of the table you can build from kk directions. (The rank of a table is how many truly separate directions it holds.)

X≈UkΣkVk⊤X \approx U_k \Sigma_k V_k^\top

In plain words: keep the top kk stretches and their directions, drop the rest, and multiply back. The sign ≈\approx means "roughly equal". The small kk means "only the first k pieces".

The savings can be huge. A 1,000 × 1,000 table holds a million numbers. A rank-10 copy needs 1,000 × 10 = 10,000 numbers for UkU_k, 10 singular values, and 10 × 1,000 = 10,000 numbers for Vk⊤V_k^\top. That's about 20,000 numbers in all, roughly 2% of the original.

The code below builds a 200 × 50 table that secretly has only 3 directions. Then it adds a little noise. It rebuilds the table from 1, 2, 3 and 5 directions. The relative error is how far each copy is from the original, as a share of the original's size. 0 would be a perfect copy.

import numpy as np

rng = np.random.default_rng(0)
low_rank = rng.normal(size=(200, 3)) @ rng.normal(size=(3, 50))   # secretly rank 3
X = low_rank + 0.05 * rng.normal(size=(200, 50))                  # plus a little noise
U, S, Vt = np.linalg.svd(X, full_matrices=False)
for k in [1, 2, 3, 5]:
    approx = U[:, :k] * S[:k] @ Vt[:k]
    error = np.linalg.norm(X - approx) / np.linalg.norm(X)
    print(f"rank {k}: relative error {error:.3f}")

The error falls from 0.750 at rank 1 to almost nothing at rank 3. After that it barely moves. The data really only had three directions. What's left over is the noise.

For later: LoRA, the most common way to fine-tune large models cheaply, assumes the change to a huge weight matrix is low-rank. It learns that change as two thin matrices instead of the full one. Same idea as above, applied to training.

Quick check
What does this print?
sizes = [8.0, 1.5, 0.5]
kept = sum(s * s for s in sizes[:1]) / sum(s * s for s in sizes)
print(round(kept, 2))
  1. 8.0
  2. 0.5
  3. 0.96
  4. 0.8
Show the answer

0.96

The first stretch squared is 64, and all three squared add up to 64 + 2.25 + 0.25 = 66.5. So 64 / 66.5 rounds to 0.96. Variation grows with the square of each stretch, so one big stretch can hold nearly everything.

Quick check
In the compression output, rank 3 gives a relative error of 0.028 and rank 5 gives 0.027. What does that tell you?
  1. Directions after the third add almost nothing: the table has about 3 real directions
  2. The table has 5 columns
  3. The SVD stopped working after rank 3
  4. Rank 5 is a much better copy than rank 3
Show the answer

Directions after the third add almost nothing: the table has about 3 real directions

Going from 3 to 5 directions only shaves off 0.001. The first three directions hold the real pattern; the rest is mostly noise.

Practice what you just read

  • 5 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

What does the first principal component of a dataset represent?

  1. The direction along which the data varies most
  2. The feature with the largest values in it
  3. The first column of the data, after sorting
  4. The average data point, found across all rows

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson