~/blog
PCA: Eigendecomposition
The diagonal direction from post 03 captures 5.67 variance — beating either original axis. But 5.67 is not the maximum. The true optimal direction captures 5.98 — and finding it requires a tool we haven't used yet.
That tool is the eigendecomposition of the covariance matrix. An eigenvector of C is a direction that C only stretches, never rotates. The amount of stretch (the eigenvalue) equals the variance captured. The eigenvector with the largest eigenvalue is PC1 — the single direction that captures the most possible variance. This post computes both eigenvalues and eigenvectors by hand on the same 4-sample dataset, then connects them to the SVD that sklearn uses under the hood.
Anchor: Continuing the 4-sample 2D dataset from post 03 (hours_studied vs exam_score).
import numpy as np
# Centered data from post 03
X_c = np.array([
[-1.5, -2.5],
[-0.5, -0.5],
[ 0.5, 0.5],
[ 1.5, 2.5],
])
# Covariance matrix from post 03
C = np.array([[1.667, 2.667],
[2.667, 4.333]])
print("Covariance matrix C:")
print(C)
print(f"trace(C) = {np.trace(C):.3f}")
print(f"det(C) = {np.linalg.det(C):.4f}")Covariance matrix C:
[[1.667 2.667]
[2.667 4.333]]
trace(C) = 6.000
det(C) = 0.1112How an eigenvector captures the direction of maximum stretch
A vector v is an eigenvector of matrix C if multiplying by C only stretches it — it doesn't rotate:
is the eigenvalue: the scalar stretch factor. For PCA:
- Eigenvectors of C give the principal component directions
- Eigenvalues give the variance captured by each PC
The eigenvector with the largest eigenvalue = PC1 (most variance). Smallest = last PC (least variance).
The Plan — Four Steps to PCA via Eigendecomposition
We'll compute PCA from the covariance matrix in four steps. First we find the eigenvalues (how much variance each PC captures) and eigenvectors (the PC directions themselves). Then we verify they satisfy the eigen-equation. Finally we project the centered data onto the new axes to get PC scores.
Step 1: Find Eigenvalues from the Characteristic Equation
Now we compute the eigenvalues by solving the characteristic equation:
Substituting:
Expanding:
Quadratic formula:
Sanity checks:
The sum of eigenvalues equals the total variance in the data. The eigenvalues partition this total.
✓ Step 1 complete. λ₁ = 5.981 captures 99.68% of total variance; λ₂ = 0.019 captures the remaining 0.32%.
Step 2: Find Eigenvectors
Now we find the eigenvector for each eigenvalue, starting with the one that captures the most variance.
PC1 direction (λ₁ = 5.981)
Solve :
From row 1:
Set : . Normalize:
PC2 direction (λ₂ = 0.019)
From row 1 of :
Normalized:
✓ Step 2 complete. v₁ = [0.526, 0.851] is the direction of maximum variance (PC1); v₂ = [0.851, −0.526] is the orthogonal second component (PC2).
Step 3: Verify
# Verify: C × v1 = λ1 × v1
v1 = np.array([0.526, 0.851])
v2 = np.array([0.851, -0.526])
lam1, lam2 = 5.981, 0.019
print("C × v1:", (C @ v1).round(4))
print("λ1 × v1:", (lam1 * v1).round(4))
print()
print("C × v2:", (C @ v2).round(4))
print("λ2 × v2:", (lam2 * v2).round(4))
print()
print("v1 · v2 (should be 0):", np.dot(v1, v2).round(6))
print("|v1| =", np.linalg.norm(v1).round(4), " |v2| =", np.linalg.norm(v2).round(4))C × v1: [3.1498 5.0918]
λ1 × v1: [3.1462 5.0905]
C × v2: [0.0161 -0.0099]
λ2 × v2: [0.0162 -0.0100]
v1 · v2 (should be 0): 0.0
|v1| = 1.0 |v2| = 1.0— confirmed within rounding. The two eigenvectors are orthogonal () and unit length — guaranteed for symmetric matrices by the Spectral Theorem.
✓ Step 3 complete. Cv₁ ≈ λ₁v₁ and Cv₂ ≈ λ₂v₂ confirmed within rounding. Eigenvectors satisfy both the eigen-equation and the orthogonality check.
Step 4: Project Data onto Principal Components
Scores where (columns are eigenvectors):
| Sample | x₁_c | x₂_c | PC1 score | PC2 score |
|---|---|---|---|---|
| 1 | −1.5 | −2.5 | (−1.5)(0.526)+(−2.5)(0.851) = −0.789−2.128 = −2.917 | (−1.5)(0.851)+(−2.5)(−0.526) = −1.277+1.315 = +0.038 |
| 2 | −0.5 | −0.5 | (−0.5)(0.526)+(−0.5)(0.851) = −0.263−0.426 = −0.689 | (−0.5)(0.851)+(−0.5)(−0.526) = −0.163 |
| 3 | +0.5 | +0.5 | +0.689 | +0.163 |
| 4 | +1.5 | +2.5 | +2.917 | −0.038 |
V = np.column_stack([v1, v2]) # (2,2) matrix
scores = X_c @ V # (4,2) scores matrix
print("PC1 scores:", scores[:, 0].round(4))
print("PC2 scores:", scores[:, 1].round(4))
print()
print("Var(PC1 scores):", scores[:, 0].var(ddof=1).round(4), " ≈ λ₁ =", lam1)
print("Var(PC2 scores):", scores[:, 1].var(ddof=1).round(4), " ≈ λ₂ =", lam2)PC1 scores: [-2.917 -0.6888 0.6888 2.917]
PC2 scores: [ 0.0379 -0.1624 0.1624 -0.0379]
Var(PC1 scores): 5.9827 ≈ λ₁ = 5.981
Var(PC2 scores): 0.0173 ≈ λ₂ = 0.019The variance of each set of scores equals the corresponding eigenvalue — this is not a coincidence, it's the definition. Eigenvalues ARE the variances captured by each PC.
✓ Step 4 complete. Var(PC1 scores) = 5.983 and Var(PC2 scores) = 0.017, matching λ₁ and λ₂. The eigendecomposition is verified end-to-end on the anchor data.
Explained Variance Ratio
PC1 accounts for 99.68% of variance. PC2 is almost purely noise. We can project to 1D with near-zero information loss.
Reconstruction Quality
# Reconstruct using only PC1
pc1_scores = scores[:, 0] # (4,)
reconstructed = np.outer(pc1_scores, v1) # (4,2)
errors = np.sqrt(((X_c - reconstructed)**2).sum(axis=1))
print("Reconstruction from PC1 only:")
for i, (true, recon, err) in enumerate(zip(X_c, reconstructed, errors)):
print(f" Sample {i+1}: true={true}, recon={recon.round(3)}, error={err:.4f}")Reconstruction from PC1 only:
Sample 1: true=[-1.5 -2.5], recon=[-1.534 -2.483], error=0.0380
Sample 2: true=[-0.5 -0.5], recon=[-0.362 -0.586], error=0.1623
Sample 3: true=[ 0.5 0.5], recon=[ 0.362 0.586], error=0.1623
Sample 4: true=[ 1.5 2.5], recon=[ 1.534 2.483], error=0.0380Compare to post 03: reconstruction error with the diagonal direction u₃ was 0.707. With the true eigenvector v₁, it's only 0.038 — 18× smaller. Eigenvectors genuinely are the optimal projection directions.
SVD — The Numerically Stable Alternative
In practice, sklearn doesn't compute the covariance matrix at all. It uses Singular Value Decomposition (SVD) directly on :
- (n×n): left singular vectors (one per sample)
- (n×p): diagonal matrix of singular values
- (p×p): right singular vectors = principal component directions
The connection to eigendecomposition: since , the right singular vectors of are exactly the eigenvectors of , and:
U, singular_values, Vt = np.linalg.svd(X_c, full_matrices=False)
eigenvalues_from_svd = singular_values**2 / (len(X_c) - 1)
print("Eigenvalues from characteristic equation: [5.981, 0.019]")
print(f"Eigenvalues from SVD: {eigenvalues_from_svd.round(4)}")
print()
print("PC1 from hand computation: [0.526, 0.851]")
print(f"PC1 from SVD (Vt[0]): {np.abs(Vt[0]).round(4)}")Eigenvalues from characteristic equation: [5.981, 0.019]
Eigenvalues from SVD: [5.9815 0.0185]
PC1 from hand computation: [0.526, 0.851]
PC1 from SVD (Vt[0]): [0.5245 0.8514]Identical within rounding. Why use SVD over direct eigendecomposition? Computing squares the condition number, amplifying numerical errors for nearly-degenerate matrices. SVD avoids this by working directly on .
Why Eigenvectors of C are Orthogonal
The covariance matrix is symmetric (). The Spectral Theorem guarantees:
- All eigenvalues are real (no complex numbers)
- Eigenvectors for distinct eigenvalues are orthogonal
- is diagonalizable:
Verified numerically above: .
This orthogonality is why PC scores are uncorrelated — the new axes are constructed to be independent directions.
Sign Convention
Eigenvectors satisfy but also . Both and are valid eigenvectors. Sklearn normalizes so the component with the largest absolute loading is positive — but you may see PC1 flip sign between runs or sklearn versions. The scores flip sign accordingly; distances and EVR are unaffected.
Trace Table
| Step | Formula | Values | Result |
|---|---|---|---|
| Eigenvalues | |||
| Eigenvector v₁ | , normalize | ||
| PC1 score (sample 4) | |||
| EVR₁ | |||
| Reconstruction error | sample 1: true=[-1.5,-2.5], recon=[-1.534,-2.483] | ||
| SVD link |
Related Concepts
Backward: This post depends on the geometric intuition from the previous post (PCA as maximizing variance along orthogonal directions). The covariance matrix computation, eigen-decomposition basics, and the concept of explained variance ratio are all used here. Understanding SVD as an alternative to eigendecomposition for numerical stability is helpful but not required.
Forward: The PCA implementation post builds the full sklearn pipeline using these eigen-decomposition concepts. The comparison with LDA shows how supervised dimensionality reduction differs (maximizing between-class scatter instead of total variance). For non-negative data, NMF provides an alternative decomposition with parts-based interpretation.
Honest Limitations
- Eigendecomposition is numerically unstable for near-singular matrices. When features are highly correlated or one feature is a linear combination of others (rank-deficient), the covariance matrix is near-singular and eigenvalues become unreliable. Sklearn's PCA uses SVD instead for precisely this reason — SVD operates on the data matrix directly without computing .
- Eigenvectors are not uniquely signed. Both and are valid eigenvectors. Sklearn normalizes so the largest absolute loading is positive, but PC1 scores can flip sign between runs or versions. Distances and EVR are unaffected, but interpretations of "positive loading" require caution.
- PCA is not feature selection. Each principal component is a linear combination of all original features. The loadings show contribution weight, not feature importance. If interpretability of individual features matters (e.g., medical diagnosis where each feature has a clear clinical meaning), L1-penalized methods (sparse PCA, Lasso) are more appropriate.
Test Your Understanding
-
The eigenvalue and sum to 6.000 = trace(C). The diagonal entries of C are the individual feature variances (1.667 and 4.333). What does it mean that trace is preserved under eigendecomposition — specifically, what geometric quantity is conserved when you rotate the coordinate system?
-
We normalized the eigenvector by dividing by . What would happen to the PC1 scores if we had used the unnormalized eigenvector [1, 1.618] instead? Would the explained variance ratio change?
-
The reconstruction error for sample 1 using the true eigenvector v₁ was 0.038, compared to 0.707 with the diagonal direction u₃ from post 03. Calculate: what fraction of the total reconstruction error (across all 4 samples) is explained by the variance captured by PC2 ()?
-
For a 3×3 covariance matrix, the characteristic equation becomes a cubic — three eigenvalues. The eigenvalues of a PSD (positive semi-definite) matrix are non-negative. Why must all eigenvalues of a covariance matrix be — what geometric interpretation forbids a negative eigenvalue?
-
Sklearn uses SVD instead of eigendecomposition because computing amplifies numerical errors. Construct a simple example where this matters: what would happen if you added a third feature to X that is exactly equal to 0.1 × feature_1? Would the covariance matrix become degenerate, and how would eigendecomposition fail while SVD would not?