Principal component analysis

Variance maximization and low-rank approximation turn out to be the same problem.

The question PCA answers

You record from 100 neurons over 500 time bins. The data is a 500-by-100 matrix XX (centered: each neuron's mean has been subtracted). You believe the population's activity is approximately low-dimensional, say confined near a 5-dimensional subspace of the 100-dimensional neuron space.

You want to find that subspace. But which 5-dimensional subspace? There are infinitely many 5-dimensional subspaces of R100\mathbb{R}^{100}. You need a criterion, a way to rank one subspace as better than another.

There are two natural criteria, and they look quite different.

Criterion 1: variance. Find the 5-dimensional subspace along which the data spread out the most. The data are informative in directions where they vary, and uninformative in directions where they sit still. Capture the most variance with the fewest axes.

Criterion 2: reconstruction. Find the 5-dimensional subspace such that projecting the data onto it and then lifting back to 100 dimensions loses as little as possible. Minimize the reconstruction error.

These seem like different problems. The first is about keeping what moves. The second is about approximating the original data. But they turn out to have the same answer. That answer is PCA.

Maximize variance

Start with one direction. Find the unit vector ww in R100\mathbb{R}^{100} such that the variance of the data projected onto ww is as large as possible.

From Post 5, the variance of the data along a unit direction ww is the quadratic form wCww^\top C w, where C=1TXXC = \tfrac{1}{T} X^\top X is the covariance matrix. So we want:

maxw  wCwsubject tow=1\max_{w}\; w^\top C\, w \qquad \text{subject to}\quad \|w\| = 1
(1)

This is a constrained optimization. Use a Lagrange multiplier λ\lambda for the constraint ww=1w^\top w = 1. The Lagrangian is L=wCwλ(ww1)L = w^\top C w - \lambda(w^\top w - 1). Set the gradient to zero:

Lw=2Cw2λw=0Cw=λw\frac{\partial L}{\partial w} = 2Cw - 2\lambda w = 0 \qquad \Longrightarrow \qquad Cw = \lambda w
(2)

That is an eigenvalue equation. The direction that maximizes variance is an eigenvector of the covariance matrix. Which one? Multiply both sides by ww^\top: wCw=λww=λw^\top C w = \lambda w^\top w = \lambda. The variance along ww equals the eigenvalue. To maximize variance, pick the eigenvector with the largest eigenvalue.

That eigenvector is the first principal component.

wvar = 1.86θ = 95°drag handle to rotate directionvariance vs angle45°90°135°180°wCwPC1PC2
Drag the direction vector around the unit circle. The right panel shows variance as a function of angle. The maximum coincides with the first eigenvector of the covariance matrix.

Now find the second. We want the unit direction of maximum variance that is orthogonal to the first. Add the constraint ww1=0w^\top w_1 = 0 and repeat. The same argument gives another eigenvalue equation, and the maximum is the second-largest eigenvalue. Its eigenvector is the second principal component. Continue: the kk-th principal component is the eigenvector with the kk-th largest eigenvalue.Each successive component is orthogonal to all previous ones, because the eigenvectors of a symmetric matrix are orthogonal (the spectral theorem from Post 5). The orthogonality constraint does not need to be imposed separately. It follows from the spectral theorem.

Minimize reconstruction error

Now the second criterion. We want to approximate each 100-dimensional data vector by its projection onto a kk-dimensional subspace, and choose the subspace that makes the approximation as good as possible.

From Post 2, if the subspace is spanned by orthonormal vectors w1,,wkw_1, \ldots, w_k, the projection of a data point xtx_t is:

x^t=j=1k(xtwj)wj\hat{x}_t = \sum_{j=1}^{k} (x_t \cdot w_j)\, w_j
(3)

The reconstruction error for one data point is xtx^t2\|x_t - \hat{x}_t\|^2: the squared distance between the original and its projection. Average over all time bins:

error=1Tt=1Txtx^t2\text{error} = \frac{1}{T} \sum_{t=1}^{T} \|x_t - \hat{x}_t\|^2
(4)

We want to minimize this. Expand the squared norm. Since x^t\hat{x}_t is the projection onto the subspace and xtx^tx_t - \hat{x}_t is perpendicular to it (that was the point of projection, from Post 4), Pythagoras gives:

xt2=x^t2+xtx^t2\|x_t\|^2 = \|\hat{x}_t\|^2 + \|x_t - \hat{x}_t\|^2
(5)

The total length of the data point splits into the part captured by the projection and the part lost. Average over all tt:

1Ttxt2total variance=1Ttx^t2captured variance+1Ttxtx^t2error\underbrace{\frac{1}{T}\sum_t \|x_t\|^2}_{\text{total variance}} = \underbrace{\frac{1}{T}\sum_t \|\hat{x}_t\|^2}_{\text{captured variance}} + \underbrace{\frac{1}{T}\sum_t \|x_t - \hat{x}_t\|^2}_{\text{error}}
(6)

The total variance (left side) is fixed. It does not depend on the choice of subspace. So minimizing the error (third term) is the same as maximizing the captured variance (second term). The two criteria are not just similar. They are exactly equivalent, connected by Pythagoras.

Why both give the same answer

Let's verify this. The captured variance is 1Ttx^t2\tfrac{1}{T}\sum_t \|\hat{x}_t\|^2. Expanding x^t\hat{x}_t using equation (3):

1Ttx^t2=1Ttj=1k(xtwj)2=j=1k1Tt(xtwj)2wjCwj\frac{1}{T}\sum_t \|\hat{x}_t\|^2 = \frac{1}{T}\sum_t \sum_{j=1}^{k} (x_t \cdot w_j)^2 = \sum_{j=1}^{k} \underbrace{\frac{1}{T}\sum_t (x_t \cdot w_j)^2}_{w_j^\top C w_j}
(7)

Each term wjCwjw_j^\top C w_j is the variance along direction wjw_j. So the total captured variance is the sum of variances along the kk subspace directions. To maximize this sum, you want each wjw_j to point along a direction of high variance, subject to mutual orthogonality. The solution: the top kk eigenvectors of CC. The maximum captured variance is λ1+λ2++λk\lambda_1 + \lambda_2 + \cdots + \lambda_k.

The reconstruction error is the leftover: λk+1+λk+2++λn\lambda_{k+1} + \lambda_{k+2} + \cdots + \lambda_n. Maximize the first sum or minimize the second — same answer either way.

PC1variance captured: 74.2%drag green handle to rotate subspace
Drag the green line to rotate the projection subspace. The captured variance and reconstruction error update in real time. The dashed line shows the optimal direction (the first principal component).

PCA via the SVD

The derivation above says: eigendecompose the covariance matrix, take the top eigenvectors. That is mathematically correct, but it is not how you should compute PCA.

From the SVD post, the centered data matrix has SVD X=UΣVX = U\Sigma V^\top, and:

C=1TXX=VΣ2TVC = \frac{1}{T}\,X^\top X = V\,\frac{\Sigma^2}{T}\,V^\top
(8)

The columns of VV are the eigenvectors of CC, i.e., the principal components. The eigenvalues are σi2/T\sigma_i^2 / T. You never need to form CC at all. In practice, PCA is always computed via the SVD of the data matrix, because it is faster and more numerically stable.Forming XXX^\top X squares the condition number of the problem. If the ratio of the largest to smallest singular value is κ\kappa, the ratio of the largest to smallest eigenvalue of XXX^\top X is κ2\kappa^2. The SVD avoids this squaring.

The recipe: center the data, compute the (thin) SVD, and read off the principal components from the columns of VV. That is PCA in three steps.

Scores, loadings, and reconstruction

PCA produces two objects, and the terminology is often confused.

The loadings are the principal component directions: the columns of VV. Each loading is a unit vector in R100\mathbb{R}^{100} (neuron space). It tells you the "recipe" for one component: how much each neuron contributes to that direction.

The scores are the coordinates of the data in the principal component basis. For data point xtx_t, the score on component jj is the dot product xtvjx_t \cdot v_j. The full score matrix is Z=XVZ = XV, a 500-by-100 matrix (or 500-by-kk if you keep only kk components). Each row is a time bin. Each column is a component.

From the SVD, the scores have a clean form: Z=XV=UΣZ = XV = U\Sigma. The left singular vectors UU, scaled by the singular values, are the PC scores. The structure is visible: UU gives the temporal patterns, Σ\Sigma tells you how important each one is, VV tells you how each neuron contributes.

Reconstruction from kk components:

X^=ZkVk=UkΣkVk\hat{X} = Z_k V_k^\top = U_k \Sigma_k V_k^\top
(9)

This is the rank-kk truncated SVD from the previous post. The best rank-kk approximation to the data matrix. PCA and the truncated SVD are the same thing, seen from different angles.

Neuron basisPCA basisN1N2PC1PC2rotate axesN1N2PC1PC2
The same neural trajectory in two bases. Left: neuron-space axes. Right: principal component axes. The trajectory has not changed; only the coordinates have.

How many components to keep

The eigenvalues tell you the variance captured by each component. The fraction of total variance explained by the first kk components is:

explained variance=λ1++λkλ1++λn\text{explained variance} = \frac{\lambda_1 + \cdots + \lambda_k}{\lambda_1 + \cdots + \lambda_n}
(10)

Plot this as a function of kk and you get the cumulative explained variance curve. Plot the individual eigenvalues and you get the scree plot. Both help you choose kk.

Common heuristics: keep enough components to explain 90% or 95% of the variance. Or look for an "elbow" in the scree plot where the eigenvalues drop off sharply. Or use cross-validation: hold out some data, project using kk components, measure reconstruction error on the held-out data, and pick the kk that minimizes it.The 90% or 95% threshold is a convention, not a theorem. For some datasets, 5 components explain 95% of variance and the remaining dimensions are noise. For others, the variance is spread more evenly and 95% requires 50 components, which may not feel like dimensionality reduction. The right kk depends on the question, not on a fixed threshold.

For neural data, the scree plot often shows a smooth decay without an obvious elbow. This happens because neural noise is correlated (nearby neurons share noise sources), which inflates the variance along many dimensions. Methods like factor analysis and GPFA handle this by explicitly modeling shared signal variance separately from private noise [4].

When PCA misleads

PCA finds directions of maximum variance. That is useful when variance corresponds to the structure you care about. But it can mislead in several ways.

Forgetting to center. If you run PCA on uncentered data, the first component may simply point toward the mean firing rate rather than capturing the most variable direction. This is the most common PCA mistake in practice. Always subtract the mean of each neuron first.

High variance is not the same as importance. The direction with the most variance might be a nuisance: a global gain fluctuation, a slow drift across the session, or a motion artifact. PCA does not know what is scientifically relevant. It finds what moves the most, and what moves the most might not be what you care about.This is exactly the limitation that motivates PSID. PCA captures variance regardless of behavioral relevance. PSID finds the subspace that is maximally predictive of behavior, which may have less variance than the top PCA components but more scientific relevance.

Nonlinear structure. PCA finds linear subspaces. If the data lie on a curved surface (a nonlinear manifold), PCA will approximate it with a flat sheet. The approximation may be poor, and the number of components needed may be much larger than the intrinsic dimensionality of the manifold. Nonlinear methods like UMAP, t-SNE, and autoencoders address this, at the cost of losing PCA's clean guarantees.

Sensitivity to scaling. If one neuron fires at rates in the hundreds and another fires at rates in the single digits, PCA will be dominated by the high-rate neuron. This is a consequence of the L2L^2 norm from Post 1. If the variables have different units or very different scales, you may need to standardize (divide each neuron's activity by its standard deviation) before running PCA. This changes the geometry, and the PCA results will differ.

Rotational indeterminacy within a subspace. PCA gives you a specific set of orthogonal axes within the best subspace. But any rotation of those axes within the subspace captures the same total variance. The individual components are not unique. They depend on the ordering (by decreasing variance) and the orthogonality constraint. Methods like varimax rotation and ICA address this by imposing additional criteria.

PC1 follows the major axis of variance.PC1PC2PC1 explains 93.8%neuron 1neuron 2
Toggle between failure modes: uncentered data, high-variance noise, and slow drift. Watch PC1 shift to follow the nuisance instead of the structure.
N1N2N1N2variance
Click Change Basis to rotate from neuron-space to PC-space. The low-dimensional structure becomes visible.

What comes next

PCA finds the best subspace for representing a single dataset. But PCA's decoder — the least-squares readout from PC scores back to neural activity — has a problem: the best-fitting decoder is usually the worst one to use on new data. It overfits. The coefficients blow up whenever two dimensions are correlated, which in neural data they almost always are.

The fix is regularization. Ridge regression, the lasso, and their relatives add a penalty on the size of the coefficients. The result is a decoder that fits slightly worse in-sample but generalizes far better out-of-sample. The geometry of regularization — why it shrinks some directions and not others, how the penalty interacts with the covariance structure of the data — is the subject of the next post.

Implementation

PCA via the SVD is a few lines of NumPy. Center the data, compute the thin SVD, read off the principal components from the columns of VV, and the scores from UΣU\Sigma.

import numpy as np

def pca(X, k=None):
    """
    PCA via the thin SVD of the centered data matrix.

    Parameters
    ----------
    X : array, shape (n_time, n_neurons)
        Raw data matrix (rows = observations).
    k : int or None
        Number of components. If None, keep all.

    Returns
    -------
    scores : array, shape (n_time, k)
        PC scores (projections onto principal components).
    loadings : array, shape (n_neurons, k)
        PC loadings (principal component directions).
    explained : array, shape (k,)
        Fraction of variance explained by each component.
    """
    # Center
    X_centered = X - X.mean(axis=0)

    # Thin SVD
    U, s, Vt = np.linalg.svd(X_centered, full_matrices=False)

    # Explained variance
    var_explained = s**2 / np.sum(s**2)

    # Truncate
    if k is None:
        k = len(s)
    U_k = U[:, :k]
    s_k = s[:k]
    Vt_k = Vt[:k, :]

    scores = U_k * s_k[np.newaxis, :]   # n_time x k
    loadings = Vt_k.T                    # n_neurons x k

    return scores, loadings, var_explained[:k]


# ── Example: PCA on simulated neural data ──
rng = np.random.default_rng(42)

# 500 time bins, 100 neurons, ~5 latent dimensions
n_time, n_neurons, true_rank = 500, 100, 5
latent = rng.standard_normal((n_time, true_rank))
weights = rng.standard_normal((true_rank, n_neurons))
noise = 0.3 * rng.standard_normal((n_time, n_neurons))
X = latent @ weights + noise

scores, loadings, explained = pca(X, k=10)

print("Explained variance (first 10 PCs):")
print(explained.round(3))
print(f"Cumulative: {explained.cumsum().round(3)}")
# First 5 PCs capture ~95% — matches the true rank

# Reconstruct from k components
X_approx = scores @ loadings.T + X.mean(axis=0)
error = np.linalg.norm(X - X_approx) / np.linalg.norm(X - X.mean(axis=0))
print(f"Relative reconstruction error (k=10): {error:.3f}")

References

  1. Strang, G. Introduction to Linear Algebra, 6th ed. Wellesley-Cambridge Press, 2023.
  2. 3Blue1Brown. "Essence of Linear Algebra" video series, 2016.
  3. Churchland, M. M., Cunningham, J. P., Kaufman, M. T., et al. "Neural population dynamics during reaching," Nature, vol. 487, pp. 51-56, 2012.
  4. Cunningham, J. P. and Yu, B. M. "Dimensionality reduction for large-scale neural recordings," Nature Neuroscience, vol. 17, pp. 1500-1509, 2014.
  5. Axler, S. Linear Algebra Done Right, 4th ed. Springer, 2024.
  6. Strang, G. "The fundamental theorem of linear algebra," The American Mathematical Monthly, vol. 100, no. 9, pp. 848-855, 1993.
  7. Safaie, M., Chang, J. C., Park, J., et al. "Preserved neural dynamics across animals performing similar behaviour," Nature, vol. 623, pp. 765-771, 2023.
  8. Jolliffe, I. T. Principal Component Analysis, 2nd ed. Springer, 2002.
← Back to home