← 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 XRn×pX \in \mathbb{R}^{n \times p} (rows are observations, columns are features, each column mean-zero), PCA diagonalizes the covariance matrix

C=1n1XXC = \frac{1}{n-1} X^\top X

Since CC is symmetric positive semi-definite, it admits an eigendecomposition C=VΛVC = 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ΣVX = U \Sigma V^\top

where URn×nU \in \mathbb{R}^{n \times n} and VRp×pV \in \mathbb{R}^{p \times p} are orthogonal, and Σ\Sigma is diagonal with singular values σ1σ20\sigma_1 \geq \sigma_2 \geq \cdots \geq 0.

Substituting into XXX^\top X:

XX=(UΣV)(UΣV)=VΣUUΣV=VΣ2VX^\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 UU=IU^\top U = I. Comparing with CVΛVC \propto V \Lambda V^\top, this means:

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

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

Why this matters in practice

Forming C=XXC = 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σi2i=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.