Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Intermediate

Eigenvalues and Eigenvectors

The mathematical foundation of PCA, spectral clustering, and PageRank — built from plain English first, then intuition, then math, then code.

26–34 min March 2026
Before any formula — what problem does this solve?

Imagine spinning a blob of data. Most directions stretch and rotate. A few special directions only stretch — never rotate. Those are eigenvectors.

You have a dataset of 10,000 DoorDash customers. Each customer is described by 20 numbers — order frequency, average order value, preferred cuisine, delivery distance preference, time of day, and so on. That dataset is a cloud of 10,000 points in 20-dimensional space.

Now imagine squashing that cloud into 2 dimensions so you can plot it. You need to pick which 2 directions to keep out of the original 20. If you pick randomly, you lose most of the interesting structure. But there exist specific directions in the data — the directions along which the cloud is most spread out — that capture the most information. Finding those directions is exactly what eigenvalues and eigenvectors do.

Those special directions are the eigenvectors. How much the data spreads along each direction is the eigenvalue. The eigenvector with the largest eigenvalue is the single most informative direction in your entire dataset.

🧠 Analogy — read this first

Think of a shadow. You hold an odd-shaped object under a light. As you rotate the object, its shadow changes shape — sometimes long and thin, sometimes short and wide. There is one specific angle where the shadow is longest. That angle is like an eigenvector — the direction that reveals the most about the object's structure. The length of that longest shadow is like the eigenvalue.

PCA (Principal Component Analysis) uses exactly this idea. It finds the directions in your data where the shadow is longest — where the data varies the most — and uses those as the new axes. Module 33 covers PCA in full. This module gives you the mathematical foundation to understand how it works.

🎯 Pro Tip
You do not need to memorise how to compute eigenvalues by hand. NumPy does that in one line. What you need to understand is what they mean — because once you understand that, PCA, spectral clustering, and Google's PageRank all become obvious.
The foundation — what matrices do to vectors

A matrix is a transformation — it stretches, rotates, and reflects vectors

From Module 04 you know that matrix multiplication transforms vectors. When you multiply a matrix by a vector, the result is a new vector — usually pointing in a different direction and having a different length. The matrix is performing a geometric operation on the vector.

Most vectors get both rotated AND stretched when a matrix is applied to them. The direction changes and the length changes. But some special vectors only get stretched — they keep pointing in exactly the same direction. Those special vectors are eigenvectors.

What a matrix does to different vectors
BEFORE transformationv₁e (eigenvec)two vectors before matrix A
AFTER transformation (A × v)A·v₁rotated + stretchedA·e = λesame direction, just longer

The orange vector gets rotated AND stretched — its direction changed. The purple eigenvector only gets stretched — same direction, longer length. The amount it stretches is the eigenvalue λ (lambda).

The eigenvector definition — in one sentence

A vector e is an eigenvector of matrix A if multiplying A by e gives back e scaled by a number λ:

A · e = λ · e
ethe eigenvector — the special direction that does not rotate
Athe matrix — the transformation being applied
λthe eigenvalue — how much the vector stretches (or shrinks)
A·e = λ·eapplying A to e is the same as just scaling e by λ
Building intuition with small numbers

A concrete example — verify the definition yourself

The best way to understand eigenvectors is to verify the definition by hand on a small example. We will use a 2×2 matrix so everything is visible. Once you see it work once, the concept locks in permanently.

Verifying A · e = λ · e with real numbers
Matrix A (a simple transformation):
[
31
13
]
Eigenvector 1: e₁ = [1, 1] Eigenvalue: λ₁ = 4
A · e₁ = [3×1 + 1×1, 1×1 + 3×1] = [4, 4]
λ₁ · e₁ = 4 × [1, 1] = [4, 4]
✓ A · e₁ = λ₁ · e₁ → [4, 4] = [4, 4] ← same direction, 4× longer
Eigenvector 2: e₂ = [1, -1] Eigenvalue: λ₂ = 2
A · e₂ = [3×1 + 1×(-1), 1×1 + 3×(-1)] = [2, -2]
λ₂ · e₂ = 2 × [1, -1] = [2, -2]
✓ A · e₂ = λ₂ · e₂ → [2, -2] = [2, -2] ← same direction, 2× longer
Matrix A has two eigenvectors in 2D. They are always perpendicular to each other (for symmetric matrices). e₁ = [1,1] stretches 4× (larger eigenvalue = more important direction). e₂ = [1,-1] stretches 2× (smaller eigenvalue = less important direction).
python
import numpy as np

# Define a simple 2×2 matrix
A = np.array([
    [3, 1],
    [1, 3],
])

# ── Manually verify the eigenvector definition ─────────────────────────
e1 = np.array([1, 1])   # eigenvector 1
e2 = np.array([1, -1])  # eigenvector 2

print("Verifying A · e = λ · e for eigenvector 1:")
Ae1  = A @ e1
lam1 = 4
print(f"  A · e1        = {Ae1}")
print(f"  λ₁ × e1 (×4) = {lam1 * e1}")
print(f"  Are they equal? {np.allclose(Ae1, lam1 * e1)}")  # True

print("\nVerifying A · e = λ · e for eigenvector 2:")
Ae2  = A @ e2
lam2 = 2
print(f"  A · e2        = {Ae2}")
print(f"  λ₂ × e2 (×2) = {lam2 * e2}")
print(f"  Are they equal? {np.allclose(Ae2, lam2 * e2)}")  # True

# ── NumPy computes eigenvalues and eigenvectors in one call ────────────
eigenvalues, eigenvectors = np.linalg.eig(A)

print(f"\nNumPy computed eigenvalues:  {eigenvalues}")
print(f"NumPy computed eigenvectors:\n{eigenvectors}")
# Each COLUMN of the eigenvectors matrix is one eigenvector
# Column 0 corresponds to eigenvalues[0], Column 1 to eigenvalues[1]

print(f"\nEigenvector 0 (column 0): {eigenvectors[:, 0].round(4)}")
print(f"Eigenvector 1 (column 1): {eigenvectors[:, 1].round(4)}")
# Note: NumPy returns unit vectors (length=1), so [1,1] becomes [0.707, 0.707]
# They point in the same direction — same eigenvector, just normalised

# ── A random vector is NOT an eigenvector ─────────────────────────────
random_vec = np.array([3, 1])   # not an eigenvector
print(f"\nRandom vector [3, 1]:")
print(f"  A · v = {A @ random_vec}  — direction changed, not an eigenvector")
Interpreting eigenvalues

Eigenvalues tell you how important each eigenvector is

Every square matrix has as many eigenvalue-eigenvector pairs as it has dimensions. A 20×20 matrix has 20 pairs. Each pair is a direction (eigenvector) and a number (eigenvalue). The eigenvalue tells you how much the transformation stretches the data in that direction.

In the context of data analysis: if you compute the covariance matrix of your dataset (a matrix that describes how features vary together), its eigenvectors point in the directions of maximum variance in the data. The eigenvalues tell you how much variance each direction captures.

How to read eigenvalues — what the numbers mean
λ is large (e.g. 45.2)The data spreads a lot in this direction. This eigenvector captures important variation. Keep it.
λ is small (e.g. 0.3)The data barely varies in this direction. This eigenvector is mostly noise. Can discard it.
λ = 0The data has no variation in this direction at all. The dimension is redundant — it can be removed entirely.
λ is negativeThe transformation flips the vector (points in opposite direction) AND scales it. Occurs in non-covariance matrices.

Explained variance — turning eigenvalues into percentages

The most useful thing you can do with eigenvalues in ML is convert them to "explained variance" — what percentage of the total information does each direction capture? This tells you how many dimensions to keep when reducing the dimensionality of your data.

python
import numpy as np

np.random.seed(42)

# ── Simulate a DoorDash customer dataset: 1000 customers, 5 features ────
# Features: order_frequency, avg_order_value, delivery_distance,
#           night_orders_pct, weekend_orders_pct
# Features are correlated — frequent orderers tend to spend more

n = 1000
order_freq      = np.random.normal(15, 5, n)
avg_value       = 150 + 8 * order_freq + np.random.normal(0, 30, n)  # correlated
delivery_dist   = np.random.normal(4, 1.5, n)
night_pct       = np.random.normal(0.3, 0.1, n).clip(0, 1)
weekend_pct     = np.random.normal(0.4, 0.1, n).clip(0, 1)

X = np.column_stack([order_freq, avg_value, delivery_dist,
                     night_pct, weekend_pct])

# ── Step 1: Standardise the data (zero mean, unit variance) ───────────
# This is CRITICAL before computing eigenvalues
# Without it, features with larger values (like avg_value in dollars)
# will dominate simply because their numbers are bigger
X_mean = X.mean(axis=0)
X_std  = X.std(axis=0)
X_std_data = (X - X_mean) / X_std
print(f"Data shape: {X_std_data.shape}  (1000 customers × 5 features)")

# ── Step 2: Compute covariance matrix ────────────────────────────────
# Covariance matrix tells us: how do features vary together?
# Shape: (5, 5) — every feature pair has a covariance value
cov_matrix = np.cov(X_std_data.T)   # .T because np.cov expects features as rows
print(f"Covariance matrix shape: {cov_matrix.shape}")

# ── Step 3: Compute eigenvalues and eigenvectors ────────────────────
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
# np.linalg.eigh for symmetric matrices (covariance is always symmetric)
# Returns eigenvalues sorted ASCENDING — we want DESCENDING
idx          = np.argsort(eigenvalues)[::-1]
eigenvalues  = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]

# ── Step 4: Explained variance ────────────────────────────────────────
total_variance     = eigenvalues.sum()
explained_variance = eigenvalues / total_variance

feature_names = ['order_freq', 'avg_value', 'dist', 'night_pct', 'weekend_pct']

print(f"\nEigenvalues and explained variance:")
print(f"{'PC':<5} {'Eigenvalue':<14} {'Explained':<12} {'Cumulative':<12}")
print("─" * 46)
cumulative = 0
for i, (ev, pct) in enumerate(zip(eigenvalues, explained_variance)):
    cumulative += pct
    bar = '█' * int(pct * 40)
    print(f"  PC{i+1}  {ev:<14.4f} {pct*100:<10.1f}%  {cumulative*100:.1f}%  {bar}")

print(f"\nConclusion:")
print(f"  PC1 alone explains {explained_variance[0]*100:.1f}% of all variation")
print(f"  PC1 + PC2 explain  {(explained_variance[:2].sum())*100:.1f}% — good enough for most tasks")
print(f"  To preserve 95% variance, keep {np.searchsorted(np.cumsum(explained_variance), 0.95)+1} components")

# ── What does the first principal component 'mean'? ──────────────────
print(f"\nPC1 loadings (how much each feature contributes):")
pc1 = eigenvectors[:, 0]
for name, loading in zip(feature_names, pc1):
    bar = '█' * int(abs(loading) * 20)
    sign = '+' if loading > 0 else '-'
    print(f"  {name:<18}: {sign}{bar} {loading:.4f}")
Where this immediately applies

PCA from scratch using eigenvalues — the complete picture

PCA (Principal Component Analysis) is the most widely used dimensionality reduction technique in ML. It reduces a dataset from many dimensions to fewer dimensions while preserving as much information as possible. The entire algorithm is just four steps — all of which you now understand.

1
Standardise the data

Subtract the mean and divide by standard deviation for each feature. This puts all features on the same scale so no single feature dominates the eigenvalue calculation because of its units.

2
Compute the covariance matrix

The covariance matrix captures how every pair of features varies together. If two features always go up and down together, they have high covariance. This matrix is always square and symmetric.

3
Find the eigenvectors and eigenvalues

The eigenvectors of the covariance matrix are the principal components — the directions of maximum variance. The eigenvalues tell you how much variance each direction captures. Sort both by eigenvalue, largest first.

4
Project the data onto the top k eigenvectors

Multiply the original data by the top k eigenvectors. The result is a new dataset with only k dimensions, but those k dimensions capture the most important variation in the original data.

python
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

np.random.seed(42)

# Same DoorDash customer dataset
n = 1000
order_freq    = np.random.normal(15, 5, n)
avg_value     = 150 + 8 * order_freq + np.random.normal(0, 30, n)
delivery_dist = np.random.normal(4, 1.5, n)
night_pct     = np.random.normal(0.3, 0.1, n).clip(0, 1)
weekend_pct   = np.random.normal(0.4, 0.1, n).clip(0, 1)
X = np.column_stack([order_freq, avg_value, delivery_dist, night_pct, weekend_pct])
feature_names = ['order_freq', 'avg_value', 'dist', 'night_pct', 'weekend_pct']

# ── PCA from scratch — using eigenvalues ──────────────────────────────
def pca_from_scratch(X, n_components):
    """
    Full PCA implementation using eigenvalue decomposition.
    Every step is explicit so you can see exactly what is happening.
    """
    # Step 1: Standardise
    mean  = X.mean(axis=0)
    std   = X.std(axis=0)
    X_std = (X - mean) / std

    # Step 2: Covariance matrix
    cov = np.cov(X_std.T)                       # shape (n_features, n_features)

    # Step 3: Eigendecomposition
    eigenvalues, eigenvectors = np.linalg.eigh(cov)
    # Sort descending
    idx          = np.argsort(eigenvalues)[::-1]
    eigenvalues  = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]

    # Step 4: Project data onto top n_components eigenvectors
    components   = eigenvectors[:, :n_components]     # shape (n_features, n_components)
    X_projected  = X_std @ components                  # shape (n_samples, n_components)

    explained_var = eigenvalues[:n_components] / eigenvalues.sum()
    return X_projected, explained_var, components

X_pca, exp_var, components = pca_from_scratch(X, n_components=2)

print(f"Original data shape:   {X.shape}")
print(f"Projected data shape:  {X_pca.shape}")
print(f"Variance explained:    PC1={exp_var[0]*100:.1f}%  PC2={exp_var[1]*100:.1f}%")
print(f"Total captured:        {exp_var.sum()*100:.1f}% of original information")

# ── sklearn PCA — one line, same result ───────────────────────────────
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

pca = PCA(n_components=2)
X_pca_sk = pca.fit_transform(X_scaled)

print(f"\nsklearn PCA result:")
print(f"  Explained variance ratio: {pca.explained_variance_ratio_.round(3)}")
print(f"  Cumulative: {pca.explained_variance_ratio_.cumsum().round(3)}")

# ── How many components to keep? ──────────────────────────────────────
pca_full = PCA()
pca_full.fit(X_scaled)
cumulative = np.cumsum(pca_full.explained_variance_ratio_)

print(f"\nHow many components to keep (95% threshold):")
for k, cum in enumerate(cumulative, 1):
    bar = '█' * int(cum * 30)
    flag = ' ← keep this many' if cum >= 0.95 and cumulative[k-2] < 0.95 else ''
    print(f"  k={k}: {bar} {cum*100:.1f}%{flag}")

# ── Real use: compress and reconstruct data ───────────────────────────
k = 2   # use only 2 dimensions
pca2 = PCA(n_components=k)
X_compressed   = pca2.fit_transform(X_scaled)   # 1000×5 → 1000×2
X_reconstructed = pca2.inverse_transform(X_compressed)  # 1000×2 → 1000×5

# Reconstruction error — how much did we lose?
reconstruction_error = np.mean((X_scaled - X_reconstructed) ** 2)
print(f"\nCompression: 5 features → {k} components")
print(f"Reconstruction MSE: {reconstruction_error:.4f}")
print(f"Information preserved: {pca2.explained_variance_ratio_.sum()*100:.1f}%")

When to use PCA vs when not to

Good use cases ✓
Visualising high-dimensional data in 2D or 3D
Removing correlated features before linear models
Reducing memory / compute when you have 1000+ features
Denoising — small eigenvalue components often carry noise
Preprocessing for KNN (curse of dimensionality)
Avoid PCA when ✗
You need to interpret feature importance (components mix features)
Tree models — they handle high dimensions natively
Features are already independent (no correlation to remove)
You have few features (< 10) — overhead not worth it
Non-linear structure — PCA only finds linear directions
Where eigenvalues appear in production ML

Three algorithms you will use regularly — all built on eigenvalues

Eigenvalues are not just a PCA concept. They are the mathematical core of three widely deployed algorithms. Once you see the connection, these algorithms stop being black boxes.

1. PCA — Dimensionality reduction

Eigenvectors of the covariance matrix. Already covered in full above. Used everywhere: image compression, customer segmentation visualisation, noise removal from sensor data.

sklearn: PCA(n_components=k).fit_transform(X)
2. Spectral Clustering — finding groups in complex shapes

Regular K-Means fails when clusters are not round blobs. Spectral Clustering builds a graph where similar data points are connected, then finds eigenvalues of the graph's Laplacian matrix. The eigenvectors reveal cluster structure that K-Means can never find. Used at Amazon for discovering customer communities, at DoorDash for identifying restaurant "neighbourhoods".

sklearn: SpectralClustering(n_clusters=k).fit_predict(X)
3. Google PageRank — ranking web pages by importance

The original Google algorithm that made Google worth a trillion dollars. The web is a graph: pages are nodes, links are edges. PageRank builds a matrix representing all links and finds its principal eigenvector. Pages that appear strongly in that eigenvector are the most "important" pages — the ones that many important pages link to. Every search result you see is influenced by this eigenvector.

Core computation: eigenvector of the page transition matrix
python
import numpy as np
from sklearn.cluster import SpectralClustering

# ── Simplified PageRank demo using eigenvalues ─────────────────────────
# Imagine a tiny web with 5 pages: [home, products, about, blog, cart]
# Entry [i][j] = 1 means page i links to page j

link_matrix = np.array([
    # home  prod  about  blog  cart
    [  0,    1,    1,    1,    0  ],   # home links to products, about, blog
    [  1,    0,    0,    1,    1  ],   # products links to home, blog, cart
    [  1,    0,    0,    0,    0  ],   # about links only to home
    [  1,    1,    0,    0,    0  ],   # blog links to home, products
    [  1,    1,    0,    0,    0  ],   # cart links to home, products
], dtype=float)

page_names = ['home', 'products', 'about', 'blog', 'cart']

# Build transition matrix: normalise each row so probabilities sum to 1
row_sums           = link_matrix.sum(axis=1, keepdims=True)
transition_matrix  = link_matrix / row_sums

# PageRank is the principal eigenvector of the TRANSPOSE of transition matrix
eigenvalues, eigenvectors = np.linalg.eig(transition_matrix.T)

# Find the eigenvector with eigenvalue closest to 1 (the stationary distribution)
idx        = np.argmin(np.abs(eigenvalues - 1.0))
page_rank  = np.real(eigenvectors[:, idx])
page_rank  = np.abs(page_rank) / np.abs(page_rank).sum()  # normalise to sum=1

print("PageRank scores (importance of each page):")
for name, score in sorted(zip(page_names, page_rank), key=lambda x: x[1], reverse=True):
    bar = '█' * int(score * 100)
    print(f"  {name:<12}: {bar} {score:.4f}")
print("\n'home' and 'products' score highest because many pages link to them.")

# ── Spectral clustering demo ───────────────────────────────────────────
np.random.seed(42)
# Create two concentric ring clusters — K-Means would fail on these
from sklearn.datasets import make_circles
X_circles, y_circles = make_circles(n_samples=300, noise=0.05, factor=0.4)

# K-Means fails on ring-shaped clusters
from sklearn.cluster import KMeans
km_labels = KMeans(n_clusters=2, random_state=42, n_init=10).fit_predict(X_circles)
km_accuracy = max(
    (km_labels == y_circles).mean(),
    (km_labels != y_circles).mean()
)

# Spectral clustering handles them correctly using eigenvectors
sc_labels = SpectralClustering(
    n_clusters=2, affinity='rbf', random_state=42
).fit_predict(X_circles)
sc_accuracy = max(
    (sc_labels == y_circles).mean(),
    (sc_labels != y_circles).mean()
)

print(f"\nRing-shaped clusters:")
print(f"  K-Means accuracy:         {km_accuracy:.1%}  ← fails, thinks linearly")
print(f"  Spectral Clustering:       {sc_accuracy:.1%}  ← succeeds via eigenvectors")
Errors you will hit

Every common eigenvalue error — explained and fixed

Complex eigenvalues appear — eigenvalues contain imaginary numbers like (2.3+0.4j)
Why it happens

np.linalg.eig() returns complex numbers when the matrix is not symmetric. For asymmetric matrices (like a transition matrix), some eigenvalues genuinely are complex — the transformation involves rotation, not just stretching. But if you passed in a covariance matrix and got complex values, your covariance matrix is slightly non-symmetric due to floating-point errors.

Fix

For covariance matrices and any symmetric matrix, always use np.linalg.eigh() instead of np.linalg.eig(). The 'h' stands for Hermitian (symmetric). It guarantees real eigenvalues and is also faster and more numerically stable. If you must use eig(), extract the real part: eigenvalues = np.real(eigenvalues).

PCA components explain only 20% of variance — something is wrong
Why it happens

You forgot to standardise your data before PCA. A feature with values in the thousands (like price in dollars) has a variance millions of times larger than a feature between 0 and 1 (like a proportion). The covariance matrix is dominated by the high-variance feature, and all principal components end up aligned with that one feature.

Fix

Always apply StandardScaler before PCA: scaler = StandardScaler(); X_scaled = scaler.fit_transform(X). Or use PCA with whiten=True which standardises automatically. Standardisation ensures every feature contributes equally to the covariance structure, so eigenvalues reflect genuine data patterns rather than unit differences.

Eigenvectors change sign or order between runs — results are not reproducible
Why it happens

Eigenvectors are only defined up to sign and scale — if e is an eigenvector, so is -e. Different implementations or different runs may return the eigenvector pointing in the positive or negative direction. NumPy and sklearn do not guarantee consistent sign conventions across versions or random seeds.

Fix

This is mathematically correct and not a bug. To make results reproducible for downstream tasks, enforce a consistent sign convention: flip each eigenvector so its largest absolute component is positive. In sklearn PCA this is handled automatically. For raw np.linalg.eigh(), apply: for i in range(V.shape[1]): if V[np.argmax(np.abs(V[:,i])), i] < 0: V[:,i] *= -1

ValueError: array must not contain infs or NaNs when calling np.linalg.eig
Why it happens

Your matrix contains NaN or infinite values. This usually happens because the covariance matrix was computed on data with missing values (NaN propagates through all arithmetic), or because you divided by zero when normalising a zero-variance feature (a column that is constant has zero standard deviation).

Fix

Check for NaN: print(np.isnan(X).sum()). Check for constant columns: print((X.std(axis=0) == 0)). Remove or impute NaN values before computing the covariance matrix. Drop constant columns before PCA — they carry no information and cause division by zero in standardisation. Use SimpleImputer for missing values.

What this looks like at work

When to actually reach for eigenvalues — and when a neural net is the better call

Section 6 above already walked through PCA, spectral clustering, and PageRank as production algorithms built on eigenvalues. What that section didn't cover is the judgment call: data scientists and ML engineers hit this decision constantly during feature engineering and model debugging, and eigen-decomposition is not always the right tool even when dimensionality is the problem. Here is the framework for making that call, plus the incident that shows up most often in practice — an unstable recurrent model traced back to eigenvalues of its own weight matrix.

Reach for eigen-decomposition when ✓
You need a linear, interpretable transformation (PCA loadings map back to real features)
The data fits in memory and is at most a few thousand features wide
You need explained variance to justify how many dimensions to keep
The structure you are after is genuinely linear (correlated features, linear separability)
You need a deterministic, reproducible result for a report or a regulator
Reach for something else when ✗
The structure is non-linear — an autoencoder or UMAP will separate it, PCA will not
The matrix is huge and sparse (millions of features) — use randomized SVD or truncated SVD
You need visualisation quality, not variance preservation — t-SNE/UMAP look better for that
You just want "fewer features, roughly" and interpretability does not matter — feature hashing is cheaper
The data streams in continuously — full eigendecomposition requires the whole matrix at once
The 2am page — 'the model's loss just went to NaN after 40,000 steps'

An RNN or a deep network trains fine for hours, then the loss spikes to NaN with no obvious cause in the data. One of the first things an experienced engineer checks: the eigenvalues of the recurrent weight matrix (or, for very deep feedforward nets, the spectral norm of consecutive weight matrices). If the largest eigenvalue's magnitude is consistently above 1, the network is multiplying activations by a factor greater than 1 at every time step or layer — the exact mechanism behind exploding gradients. If it's consistently below 1, you get vanishing gradients instead.

This is the same A·e = λ·e relationship from Section 2, just applied to a weight matrix instead of a covariance matrix — the eigenvalues tell you how much repeated multiplication by that matrix stretches or shrinks a signal, which is exactly what happens to gradients flowing backward through many time steps or many layers.

python
import numpy as np

np.random.seed(42)

# ── Diagnosing RNN instability via the eigenvalues of the recurrent weight ──
# This is the check an ML engineer runs when a recurrent model trains fine
# for a while, then the loss suddenly explodes to NaN

def check_recurrent_stability(W_hidden):
    """
    W_hidden: the hidden-to-hidden weight matrix in a vanilla RNN cell.
    Its eigenvalues determine whether repeated multiplication (one per
    time step) makes signals grow, shrink, or stay roughly stable.
    """
    eigenvalues = np.linalg.eigvals(W_hidden)
    magnitudes  = np.abs(eigenvalues)
    spectral_radius = magnitudes.max()   # largest |eigenvalue|

    print(f"Spectral radius (max |λ|): {spectral_radius:.4f}")
    if spectral_radius > 1.05:
        print("  ⚠ > 1 — repeated multiplication over time steps will EXPLODE")
        print("     fix: gradient clipping, or switch to LSTM/GRU gating")
    elif spectral_radius < 0.95:
        print("  ⚠ < 1 — repeated multiplication over time steps will VANISH")
        print("     fix: orthogonal initialisation, or switch to LSTM/GRU")
    else:
        print("  ✓ ≈ 1 — signal magnitude roughly preserved across time steps")
    return spectral_radius

# A poorly initialised recurrent weight — this is what causes the 2am page
W_unstable = np.random.randn(64, 64) * 0.5   # random scale, no care taken
check_recurrent_stability(W_unstable)

# A properly initialised recurrent weight — orthogonal initialisation
# guarantees every eigenvalue has magnitude exactly 1
from scipy.stats import ortho_group
W_orthogonal = ortho_group.rvs(64, random_state=42)
print()
check_recurrent_stability(W_orthogonal)

# This is exactly why orthogonal initialisation (nn.init.orthogonal_ in
# PyTorch) is a standard recommendation for vanilla RNNs — it directly
# controls the eigenvalues of the recurrent weight matrix at the start
# of training, before anything has had a chance to explode or vanish.
Misconceptions

Five things people get wrong about eigenvalues and eigenvectors

Myth: Eigenvalues and eigenvectors are basically a PCA-only concept

PCA is the most commonly taught example, but it's one of at least half a dozen production uses: spectral clustering (graph Laplacian eigenvectors), PageRank (principal eigenvector of a link matrix), recommender systems (matrix factorisation is built on eigen/singular decomposition), and stability analysis of recurrent and very deep networks, where the eigenvalues of a weight matrix directly predict exploding or vanishing gradients. Treating this as "the PCA math" makes every one of those other appearances look unrelated when they're the exact same underlying tool.

Myth: Every square matrix has a full set of eigenvectors

Some matrices — called defective matrices — do not have enough independent eigenvectors to form a full basis for their dimension, which breaks algorithms that assume diagonalisability. The reason covariance matrices are so convenient is the spectral theorem: every real symmetric matrix is guaranteed to have a full set of real eigenvalues and orthogonal eigenvectors. That guarantee is precisely why np.linalg.eigh (for symmetric matrices) is both safer and faster than the general-purpose np.linalg.eig — it can rely on a property that doesn't hold for arbitrary matrices.

Myth: You should always keep however many components explain 'enough' variance, like a fixed 95% rule

Explained variance is a useful heuristic, not a universal rule. The right number of components depends on the downstream task: for visualisation you often want exactly 2 or 3 regardless of variance captured, and for anomaly detection the small-eigenvalue directions that a 95%-variance cutoff would discard are sometimes exactly where anomalies show up, since anomalies are often defined as points that violate the low-variance structure of normal data. Picking a threshold without checking it against actual downstream performance is a common way PCA preprocessing quietly hurts a model.

Myth: A matrix's eigenvectors are unique and fixed

Eigenvectors are only defined up to sign and scale — if e is an eigenvector, so is −e and so is 5e, all satisfying A·e = λ·e equally well. Worse, when a matrix has a repeated eigenvalue, any vector in the entire subspace spanned by the eigenvectors for that eigenvalue is a valid eigenvector, so there is no single "correct" choice at all — just a convention. This is exactly why PCA results can flip sign between library versions or random seeds without being wrong; it's a documented consequence of the math, not a bug to chase down.

Myth: Computing eigenvalues is cheap, so it scales fine to any dataset size

A full eigendecomposition of an n×n matrix costs roughly O(n³) — fine for a 50-feature covariance matrix, prohibitive for a 50,000-dimensional one. This is exactly why production systems almost never call np.linalg.eig directly on large matrices: they use randomized SVD, truncated SVD, or iterative methods like the power method or Lanczos algorithm, which approximate only the top few eigenvalues/eigenvectors needed without ever forming or decomposing the full matrix. sklearn's PCA switches to a randomized solver automatically once the data gets large enough.

Interview prep

Eigenvalues and eigenvectors — 5 questions interviewers actually ask

Q1 — Explain what an eigenvector is to someone with no linear algebra background

Most vectors change direction when you multiply them by a matrix — the matrix rotates and stretches them at the same time. An eigenvector is one of the special directions a specific matrix doesn't rotate at all; it only stretches or shrinks it, by a factor called the eigenvalue. Every square matrix has its own small set of these special directions, and they describe the "natural axes" the matrix operates along — which is why finding them tells you something fundamental about what the matrix does to any input.

Q2 — Why do we compute eigenvectors of the covariance matrix in PCA, and what do the eigenvalues tell us?

The covariance matrix encodes how every pair of features varies together. Its eigenvectors point in the directions along which the data spreads out the most — the directions of maximum variance — and its eigenvalues tell you exactly how much variance each of those directions captures. Sorting by eigenvalue and keeping the top few eigenvectors gives you a lower-dimensional representation that preserves as much of the original variation as possible for the number of dimensions you're willing to keep.

Q3 — What's the difference between np.linalg.eig and np.linalg.eigh, and when would you use each?

eigh assumes the input is symmetric (or Hermitian) and exploits that guarantee to return real eigenvalues, use a numerically more stable algorithm, and run faster. eig makes no such assumption and can return complex eigenvalues even from a matrix that's supposed to be symmetric but has tiny floating-point asymmetries. In practice: use eigh for covariance matrices, graph Laplacians, and anything you know is symmetric by construction; use eig only for genuinely asymmetric matrices, like a Markov transition matrix in PageRank.

Q4 — How does PageRank use eigenvectors to rank web pages?

PageRank models the web as a transition matrix — each entry is the probability of moving from one page to another by following a link. A page's long-run importance is the stationary distribution of a random walk over this graph, and that stationary distribution is exactly the eigenvector of the transition matrix with eigenvalue 1. Pages that many important pages link to accumulate more of that eigenvector's weight, which is why PageRank captures something closer to "importance" than simply counting incoming links.

Q5 — You're training an RNN and the loss explodes after a few hundred steps. How do eigenvalues relate to this, and what would you check?

An RNN applies the same recurrent weight matrix repeatedly, once per time step, so a signal (or a gradient flowing backward) gets multiplied by that matrix over and over. The eigenvalues of that matrix determine what happens under repeated multiplication: if the largest eigenvalue's magnitude is above 1, the signal grows exponentially with sequence length and eventually overflows to NaN; if it's below 1, the signal vanishes instead. I'd check the spectral radius of the recurrent weight directly, and consider orthogonal initialisation, gradient clipping, or switching to an LSTM/GRU, whose gating mechanisms are specifically designed to control this.

What comes next

You now understand how ML finds structure in data. Next: how it learns from it.

Eigenvalues help you find the important directions in a dataset before training. The next module — Derivatives, Gradients and the Chain Rule — explains how ML models actually improve during training. Specifically: how do you adjust millions of model parameters to make predictions better? The answer is gradient descent — the engine behind every neural network, logistic regression, and linear regression you will ever train.

Next — Module 07 · Math Foundations
Derivatives, Gradients and the Chain Rule

The mathematical engine behind every learning algorithm. Understand gradient descent before you ever run model.fit().

read →

🎯 Key Takeaways

  • An eigenvector is a special vector that does not rotate when a matrix transformation is applied to it — it only stretches or shrinks. The amount it stretches is the eigenvalue. The equation is A·e = λ·e.
  • Most vectors change direction when multiplied by a matrix. Eigenvectors are the exception — they are the "natural axes" of the transformation, the directions the matrix fundamentally operates along.
  • For a covariance matrix of your dataset, eigenvectors point in the directions of maximum variance in the data. The eigenvalue tells you how much variance that direction captures. Large eigenvalue = important direction. Small eigenvalue = noise.
  • PCA is four steps: standardise → covariance matrix → eigendecomposition → project onto top-k eigenvectors. The result is a lower-dimensional dataset that preserves the most important variation from the original.
  • Always use np.linalg.eigh() (not eig()) for covariance matrices — it guarantees real eigenvalues, is faster, and is numerically more stable. Always standardise data before computing eigenvalues.
  • Eigenvalues appear in three major production algorithms: PCA (covariance matrix eigenvectors), Spectral Clustering (graph Laplacian eigenvectors for non-convex clusters), and Google PageRank (principal eigenvector of the web link matrix).
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...