← all articles

SVD and PCA: The Same Thing, Twice

Principal Component Analysis is usually introduced via the covariance matrix, but the more numerically stable — and more revealing — route is through the Singular Value Decomposition of the data matrix directly.

The covariance route

Given a centered data matrix X∈Rn×pX \in \mathbb{R}^{n \times p} (rows are observations, columns are features, each column mean-zero), PCA diagonalizes the covariance matrix

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

Since CC is symmetric positive semi-definite, it admits an eigendecomposition C=VΛV⊤C = V \Lambda V^\top, where VV’s columns are the principal directions and Λ=diag(λ1,…,λp)\Lambda = \text{diag}(\lambda_1, \dots, \lambda_p) holds the variances explained by each component.

The SVD route

Instead, decompose XX directly:

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

where U∈Rn×nU \in \mathbb{R}^{n \times n} and V∈Rp×pV \in \mathbb{R}^{p \times p} are orthogonal, and Σ\Sigma is diagonal with singular values σ1≥σ2≥⋯≥0\sigma_1 \geq \sigma_2 \geq \cdots \geq 0.

Substituting into X⊤XX^\top X:

X⊤X=(UΣV⊤)⊤(UΣV⊤)=VΣ⊤U⊤UΣV⊤=VΣ2V⊤X^\top X = (U \Sigma V^\top)^\top (U \Sigma V^\top) = V \Sigma^\top U^\top U \Sigma V^\top = V \Sigma^2 V^\top

using U⊤U=IU^\top U = I. Comparing with C∝VΛV⊤C \propto V \Lambda V^\top, this means:

λi=σi2n−1\lambda_i = \frac{\sigma_i^2}{n - 1}

The right singular vectors VV are the principal directions — no need to ever form X⊤XX^\top X.

Why this matters in practice

Forming C=X⊤XC = X^\top X explicitly squares the condition number of the underlying problem: if XX has condition number κ\kappa, then CC has condition number κ2\kappa^2. For features with very different scales, this can meaningfully degrade numerical precision. Computing the SVD of XX directly avoids that squaring entirely, which is why sklearn.decomposition.PCA uses SVD under the hood rather than eigendecomposing the covariance matrix.

import numpy as np

def pca_via_svd(X, n_components):
    X_centered = X - X.mean(axis=0)
    U, S, Vt = np.linalg.svd(X_centered, full_matrices=False)
    components = Vt[:n_components]
    explained_variance = (S[:n_components] ** 2) / (X.shape[0] - 1)
    scores = X_centered @ components.T
    return scores, components, explained_variance

Explained variance ratio

The proportion of total variance captured by the first kk components is

EVR(k)=∑i=1kσi2∑i=1pσi2\text{EVR}(k) = \frac{\sum_{i=1}^{k} \sigma_i^2}{\sum_{i=1}^{p} \sigma_i^2}

which is exactly what explained_variance_ratio_ reports in scikit-learn — computed from singular values, never from an explicit covariance matrix.