Problem
A dataset with 300 features is hard to plot, slow to search and full of redundancy: survey questions that measure the same attitude, pixels that move together, sensor channels that track each other. You want fewer numbers per row that keep most of the information, to visualize the data, to speed up a downstream model, to remove noise, or to make clustering and nearest-neighbour search work in fewer dimensions.
"Most of the information" needs a definition. PCA's answer: keep the k-dimensional linear subspace that preserves the most variance, which is the same as the subspace that reconstructs the points with the smallest squared error. That choice is cheap to compute and easy to invert, but it is linear and it is sensitive to the units of each feature, and both limits come up below.
Intuition
Picture a cloud of points shaped like a tilted cigar. Its long axis is the direction where points differ most; knowing only where a point sits along that axis already tells you most of where it is. That axis is the first principal component. The second is the longest axis at right angles to the first, and so on. PCA is a rotation of the coordinate system so that the axes line up with the cloud, sorted from widest to thinnest. Dropping the thin axes is the compression: every point is projected onto the wide ones.
Projecting onto PC1 turns each pair of numbers into one and loses only the short dashed offsets, the part of each point that lies along PC2.
Two things the picture hides. First, PCA only sees straight lines: if the data lies on a curve, no rotation lines an axis up with it, and points far apart along the curve can land on top of each other. Second, "widest" depends on units: measure one feature in grams and another as a fraction, and the grams axis wins whatever the data says. Both are worked through below.
Mechanics
Variance-maximizing directions
Stack n points of d features as rows of X and subtract the column means, giving the centred matrix Xc. The sample covariance is
C = Xcᵀ Xc / (n − 1)
The variance of the data projected onto a unit vector w is wᵀ C w. Maximizing it subject to ‖w‖ = 1 with a Lagrange multiplier λ gives C w = λ w: the best direction is an eigenvector of C, and the variance along it is its eigenvalue, so the first principal component is the eigenvector with the largest eigenvalue. Requiring the next direction to be orthogonal to the first gives the second eigenvector, and so on. Because C is symmetric, its eigenvectors are orthogonal and the eigenvalues are real and non-negative (see the linear algebra toolkit).
The same subspace also minimizes the squared reconstruction error. For any unit w, ‖x‖² = (wᵀx)² + ‖x − w wᵀx‖², so for centred data, maximizing the variance kept is the same as minimizing what is thrown away. That is why "PCA" can be introduced either way.
PCA via the SVD
Forming C squares the condition number, so libraries usually skip it. Take the singular value decomposition Xc = U S Vᵀ. Then
C = V S² Vᵀ / (n − 1)
so the rows of Vᵀ are the principal directions (scikit-learn's components_), the eigenvalues are λj = Sj² / (n − 1) (explained_variance_), and the coordinates of the points in the new basis, the scores, are Xc V = U S.
Eigenvectors are only defined up to sign, so the same data could return +v or −v. scikit-learn's svd_flip fixes the sign so that the largest-magnitude entry of each row of Vᵀ is positive, which makes results reproducible across runs and LAPACK builds.
scikit-learn's PCA picks among four solvers when svd_solver="auto": "covariance_eigh" (form the d × d covariance and diagonalize it) when there are at most 1,000 features and at least ten times as many rows as features; otherwise "full" (an exact LAPACK SVD) when the larger dimension is at most 500 or n_components="mle"; otherwise "randomized" when 1 ≤ k < 0.8 · min(n, d); and "full" in the remaining cases.
The covariance route is fast for tall, thin data and is safe there because the d × d matrix is small; it clips tiny negative eigenvalues caused by rounding to zero.
The randomized solver (Halko, Martinsson and Tropp) multiplies Xc by a random d × (k + p) Gaussian matrix, with oversampling p (n_oversamples=10), sharpens the result with a few power iterations XcXcᵀ, orthonormalizes it into a basis Q, and takes the exact SVD of the small matrix Qᵀ Xc.
When the spectrum decays, the top k components come out accurate at a fraction of the cost. PyTorch's torch.pca_lowrank does the same: it centres the columns, uses q = min(6, m, n) by default and 2 power iterations, and returns U, S, V, leaving the S² / (m − 1) step to you.
Explained variance and choosing k
The share of the total variance that component j accounts for is
ratioj = λj / Σi λi
A scree plot shows these ratios in order; the cumulative sum tells you how much a k-component projection keeps. Common ways to pick k:
- A variance threshold. Pass a float such as
n_components=0.9, and scikit-learn keeps the smallest k whose cumulative ratio is strictly greater than 0.9 (asearchsorted(..., side="right") + 1on the cumulative sum). - The elbow of the scree plot: the point where the ratios flatten into a floor of noise.
- Minka's MLE (
n_components="mle"): treat the data as a low-rank signal plus isotropic Gaussian noise and pick the rank with the highest approximate Bayesian evidence. It needs n ≥ d. - The downstream task. If a classifier on top is what matters, cross-validate k like any other hyperparameter.
After fitting with k components, scikit-learn stores noise_variance_, the mean of the dropped eigenvalues. It is the noise estimate of probabilistic PCA (Tipping and Bishop), which treats each point as W z + μ + ε with z ~ N(0, I) and ε ~ N(0, σ² I); that model is what score and get_covariance use.
Projection and reconstruction
Keeping the first k rows Vkᵀ of Vᵀ, a point x maps to k scores and back:
z = Vkᵀ (x − μ) x̂ = μ + Vk z
Over the training set, the total squared reconstruction error Σ‖xi − x̂i‖² is exactly the sum of the dropped squared singular values, Sk+1² + … + Sr², or (n − 1) times the dropped eigenvalues: Eckart–Young says no other rank-k projection does better. Reconstruction is also how PCA denoises: noise spreads its variance over every direction, while signal concentrates in a few, so projecting onto the top components keeps most of the signal and removes most of the noise. A point whose reconstruction error is far above the training points' is unlike them, a cheap anomaly score.
Whitening
Whitening divides each score by the standard deviation along its component, so that the new features are uncorrelated and all have variance 1:
zj = vjᵀ (x − μ) / √λj
The covariance of whitened data is the identity. That helps models that assume isotropic, equally scaled inputs, such as RBF-kernel SVMs, k-means or ICA. The cost: low-variance directions, which are often mostly noise, get blown up to the same size as the signal, and inverse_transform has to undo the scaling.
In scikit-learn, whiten=True changes transform but not components_; the division uses √λ clipped below at machine epsilon so a zero-variance direction does not divide by zero.
Scaling comes first
Variance has units. A feature in grams with a spread of tens has a variance thousands of times larger than a fraction that varies in the second decimal, and PC1 will simply be "the grams feature". Unless features already share a unit and a meaningful scale (pixel intensities, say), standardize them first with a StandardScaler. PCA on standardized data is PCA of the correlation matrix rather than the covariance matrix.
Nonlinear alternatives: t-SNE and UMAP
When the data lies on a curved surface, any linear projection flattens it by squashing, which can put far-apart points on top of each other. Neighbour-embedding methods drop the goal of a global linear map and instead try to keep each point's nearest neighbours near it in 2 or 3 dimensions.
t-SNE (van der Maaten and Hinton) turns distances in the original space into neighbour probabilities with a Gaussian around each point, whose width is tuned so that every point has the same perplexity (roughly, an effective number of neighbours, 30 by default), and symmetrizes them into pij.
In the embedding it uses a heavy-tailed Student-t kernel, qij ∝ (1 + ‖yi − yj‖²)−1 for a 2-D output, and moves the points y by gradient descent to minimize KL(P ‖ Q).
The heavy tail is the trick: a 2-D map has too little room for all the moderately distant points of a high-dimensional space (the "crowding problem"), and the t kernel lets them sit much farther apart than a Gaussian would allow.
scikit-learn's TSNE runs the first 250 iterations with the pij multiplied by an early exaggeration of 12, which pulls clusters together; uses the Barnes-Hut approximation with about 3 × perplexity neighbours per point by default; starts from a PCA projection scaled down so PC1 has standard deviation 10⁻⁴; and sets the learning rate to max(n / 12 / 4, 50).
UMAP (McInnes, Healy and Melville) builds a weighted k-nearest-neighbour graph (n_neighbors, 15 by default in the reference implementation) whose edge weights fall off with distance past each point's nearest neighbour, then lays out a low-dimensional graph to minimize the cross-entropy between the two, with negative sampling. min_dist sets how tightly points may pack.
It is usually faster than t-SNE, keeps somewhat more of the global arrangement, and can embed new points with transform; it is a separate package (umap-learn), not part of scikit-learn.
Caveats for both, which matter more than the differences:
- Cluster sizes mean nothing. Each point's neighbourhood is rescaled to the same effective size, so dense and sparse clusters come out about equally wide.
- Distances between clusters mean little. The loss barely penalizes moving far-apart groups, so the gap between two islands is not a measure of how different they are.
- The hyperparameters change the picture. Different perplexities or
n_neighborsvalues give different-looking maps, and random noise at low perplexity can show apparent clusters. - They are stochastic and, for t-SNE, non-parametric. Different seeds give different maps; scikit-learn's
TSNEhas notransformfor new points. - They are for looking, not for features. Use the coordinates as model inputs only with care; PCA or a learned embedding is usually the better pipeline step, and running PCA to about 50 dimensions before t-SNE is standard to cut noise and cost.
| Component | Shape/Params | Meaning | Source |
|---|---|---|---|
PCA |
n_components None, whiten False, svd_solver "auto", tol 0, iterated_power "auto", n_oversamples 10; components_ (k, d), explained_variance_ (k,), mean_ (d,) | The estimator. n_components may be an int, a float in (0, 1) for a variance threshold, or "mle". | _pca.py 113-852, 399-421 |
PCA._fit |
X (n, d) | Validates the input and picks the solver with the "auto" rule above. | _pca.py 481-542 |
PCA._fit_full |
"full" or "covariance_eigh" | Centres, runs an exact SVD (or eigh of the covariance), flips signs, sets explained_variance_ = S² / (n − 1), resolves a float or "mle" n_components, and sets noise_variance_ to the mean of the dropped eigenvalues. | _pca.py 544-694 |
PCA._fit_truncated |
"arpack" or "randomized" | Computes only k components; noise_variance_ comes from the total variance minus the kept part. | _pca.py 696-788 |
PCA.fit_transform |
→ (n, k) | Returns U · S, or U · √(n − 1) when whitening, without a second pass over the data. | _pca.py 444-479 |
_assess_dimension, _infer_dimension |
spectrum (d,), n | Minka's evidence for each rank; the argmax is n_components="mle". | _pca.py 25-96, 99-110 |
transform, inverse_transform |
(n, d) ↔ (n, k) | (x − μ) Vₖ, divided by √λ when whitening; the inverse multiplies back and adds μ. | _base.py 125-175, 177-214 |
get_covariance |
→ (d, d) | The probabilistic-PCA covariance Vₖ diag(λ − σ²) Vₖᵀ + σ² I. | _base.py 34-60 |
svd_flip |
u_based_decision False for PCA | Makes the largest-magnitude entry of each row of Vᵀ positive. | extmath.py 924-982 |
randomized_svd, randomized_range_finder |
n_oversamples 10, n_iter "auto" | Random projection, power iterations, QR, then an exact SVD of the small matrix. | extmath.py 402-557, 241-310 |
torch.pca_lowrank, get_approximate_basis |
q = min(6, m, n), center True, niter 2; returns U, S, V | Halko's randomized range finder and SVD on tensors, dense or sparse. | _lowrank.py 183-293, 11-82 |
TSNE |
n_components 2, perplexity 30, early_exaggeration 12, learning_rate "auto", max_iter 1000, init "pca", method "barnes_hut", angle 0.5 | The t-SNE estimator; fit_transform only. | _t_sne.py 557-1174, 807-840 |
_joint_probabilities, _kl_divergence |
P (n·(n−1)/2,), embedding (n, 2) | Symmetrized Gaussian neighbour probabilities; the KL objective with a Student-t kernel and its gradient. | _t_sne.py 35-65, 125-199 |
The rows of Vᵀ from the SVD of the centred data are the principal directions. Why must you centre first?
Without centring, the SVD finds directions that maximize the mean of (wᵀx)², the second moment about the origin, not the variance. If the data sits far from the origin, the first "component" just points at the mean, and every later direction is forced orthogonal to it. Subtracting μ makes XᵀX / (n − 1) the covariance, which is what the variance argument needs.
Worked example
Components from the SVD
Data. 8 points: (1.0, 1.5), (2.0, 1.8), (3.0, 3.4), (4.0, 3.6), (5.0, 5.3), (2.5, 3.0), (3.5, 2.6), (4.5, 4.9). Their mean is (3.1875, 3.2625) and the sample covariance is
C = [[1.7812, 1.6652], [1.6652, 1.8170]]
The SVD of the centred 8 × 2 matrix gives singular values S = (4.9245, 0.9679) and directions v₁ = (0.7033, 0.7109), v₂ = (0.7109, −0.7033). Dividing S² by n − 1 = 7 gives the explained variances 3.4644 and 0.1338: the eigenvalues of C, and they sum to its trace, 3.5982. The ratios are 0.9628 and 0.0372. v₁ is almost the 45° diagonal because the two features have nearly equal variances and a strong covariance.
Power iteration on C from (1, 0) finds the same direction without an SVD, by repeatedly computing v ← C v / ‖C v‖ and reading off the variance vᵀ C v:
| Step | v | vᵀ C v |
|---|---|---|
| 1 | (0.730507, 0.682905) | 3.4593111 |
| 2 | (0.704377, 0.709826) | 3.4643739 |
| 3 | (0.703347, 0.710847) | 3.4643814 |
| 4 | (0.703307, 0.710886) | 3.4643815 |
| 5 | (0.703305, 0.710888) | 3.4643815 |
The direction's error shrinks by about λ₂ / λ₁ = 0.0386 per step and the variance's by its square, so the variance is right to 7 digits after 4 steps. Randomized solvers use the same idea on blocks of vectors.
Projecting, reconstructing and whitening
The new point (3, 2) has scores z = Vᵀ(x − μ) = (−1.0294, 0.7546). Keeping only PC1 and mapping back gives μ + z₁ v₁ = (2.4635, 2.5307), the nearest point on the PC1 line. Doing that for all 8 training points loses a total squared error of 0.9368, which is exactly S₂² = 0.9368.
Whitening divides the scores by √λ = (1.8613, 0.3658). The first point's scores (−2.7914, −0.3155) become (−1.4997, −0.8624): its small PC2 offset is now more than half the size of its PC1 coordinate. The covariance of the 8 whitened points is [[1.0000, 0.0000], [0.0000, 1.0000]], the identity.
Why scaling matters
Six fruits with weight (g) and sugar fraction: (120, 0.10), (150, 0.12), (180, 0.11), (140, 0.14), (200, 0.13), (160, 0.09). The standard deviations are 26.09 g and 0.0171.
| Input | PC1 | Ratio PC1 | Ratio PC2 |
|---|---|---|---|
| raw | (1.00000, 0.00013) | 0.9999996 | 0.0000004 |
| standardized | (0.70711, 0.70711) | 0.6029 | 0.3971 |
On raw data PC1 is the weight axis, carrying all but 0.00004% of the variance: the sugar feature is invisible because its numbers are small, not because it is uninformative. After standardizing, both features count equally and PC1 is the 45° diagonal with 60.3% of the variance; with two standardized features the components are always ±45°, and only the ratio says how correlated they are.
Choosing k on a survey
30 answers to 6 questions, generated (seed 11) from two hidden factors: questions 1-3 load mostly on the first, 4-5 on the second, 6 on both, plus independent noise with standard deviation 0.25 (variance 0.0625). PCA does not know any of that.
| k | λk | Ratio | Cumulative | Squared error keeping k | noise_variance_ |
|---|---|---|---|---|---|
| 0 | — | — | 0 | 193.638 | 1.1129 |
| 1 | 4.9408 | 0.7400 | 0.7400 | 50.354 | 0.3473 |
| 2 | 1.4472 | 0.2167 | 0.9567 | 8.386 | 0.0723 |
| 3 | 0.1375 | 0.0206 | 0.9773 | 4.400 | 0.0506 |
| 4 | 0.0668 | 0.0100 | 0.9873 | 2.461 | 0.0424 |
| 5 | 0.0456 | 0.0068 | 0.9941 | 1.138 | 0.0392 |
| 6 | 0.0392 | 0.0059 | 1.0000 | 0.000 | — |
Two components keep 95.7% of the variance, so n_components=0.9 keeps k = 2: one component (74.0%) is not strictly above 0.9. The scree plot has a clear elbow at 2 as well, and the four dropped eigenvalues sit in a flat floor between 0.0392 and 0.1375.
Their mean, the noise estimate at k = 2, is 0.0723, close to the true noise variance 0.0625. Each squared error equals the sum of the dropped S², and with all 6 kept it is zero.
The two kept directions are v₁ = (0.57, 0.54, 0.39, 0.26, 0.07, 0.38) and v₂ = (−0.21, −0.10, −0.32, 0.64, 0.62, 0.22). They span the same plane as the two factors but are not the factors themselves: PCA only promises orthogonal directions of decreasing variance, so v₁ mixes in every question. Recovering interpretable factors needs a rotation such as varimax (factor analysis) or a different method such as ICA.
Two big components, then a flat floor of noise: the elbow and the 0.9 threshold agree on k = 2 here. On real data the floor is rarely this flat.
Where a linear projection fails
13 points evenly spaced along an arc of radius 2.0 from −45° to 225°, three quarters of a circle. The arc is symmetric about the vertical axis, so PCA's components are the coordinate axes, and PC1 (the horizontal axis) keeps 62.3% of the variance. Along PC1, the points at −45° and 45° both land at 1.414: a gap of 0.000, though they are 2.828 apart in the plane and 3.142 apart along the arc. Four such pairs collide. A neighbour embedding unrolls the arc instead, because it only asks that each point keep its two neighbours close.
The t-SNE kernel. How much affinity a pair keeps at distance d in the map, with a Gaussian kernel exp(−d²/2) and t-SNE's Student-t kernel 1 / (1 + d²):
| d | Gaussian | Student-t | t / Gaussian |
|---|---|---|---|
| 0.0 | 1.000000 | 1.0000 | 1.00 |
| 0.5 | 0.882497 | 0.8000 | 0.91 |
| 1.0 | 0.606531 | 0.5000 | 0.82 |
| 2.0 | 0.135335 | 0.2000 | 1.48 |
| 3.0 | 0.011109 | 0.1000 | 9.00 |
| 5.0 | 0.000004 | 0.0385 | 10321 |
At d = 3 the t kernel still gives 0.10 where the Gaussian has fallen to 0.0111, 9.0 times more. So a modest affinity pij can be matched by points placed much farther apart in the map than a Gaussian would allow, which opens up the gaps between clusters. It is also why those gaps should not be read as distances.
Points 0 and 4 are a quarter of the circle apart along the arc, yet PCA gives them the same coordinate; so do 1 and 3, 9 and 11, and 8 and 12.
Implementation
import numpy as np
from sklearn.decomposition import PCA
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
pca = make_pipeline(StandardScaler(), PCA(n_components=0.9))
Z = pca.fit_transform(X) # (n, k), k chosen by the 0.9 threshold
p = pca[-1]
p.components_, p.explained_variance_ratio_ # (k, d) rows of Vᵀ, shares of variance
X_back = pca.inverse_transform(Z) # reconstruction in the original units
# The same thing from the SVD:
def pca_svd(X, k):
mu = X.mean(0)
U, S, Vt = np.linalg.svd(X - mu, full_matrices=False)
flip = np.sign(Vt[np.arange(len(Vt)), np.abs(Vt).argmax(1)])
U, Vt = U * flip, Vt * flip[:, None] # svd_flip: largest entry of each row positive
var = S**2 / (len(X) - 1) # explained_variance_
return mu, Vt[:k], var[:k], (U * S)[:, :k] # mean, components, variances, scores
# PyTorch, randomized, on a GPU tensor:
# U, S, V = torch.pca_lowrank(A, q=k, center=True, niter=2)
# scores = A_centred @ V[:, :k]; variances = S**2 / (A.shape[0] - 1)
Fit the scaler and PCA on the training split only and reuse them for validation and test data, or information leaks across the split; a pipeline does that automatically. For data that does not fit in memory, IncrementalPCA updates the components batch by batch; for sparse matrices such as TF-IDF, TruncatedSVD skips the centring, which would make the matrix dense.
For a 2-D picture of high-dimensional data, reduce to about 50 dimensions with PCA first, then run t-SNE or UMAP on that, and try more than one perplexity or n_neighbors and seed before trusting any structure you see.
Tradeoffs
| Method | Map | Preserves | New points | Cost | Use for |
|---|---|---|---|---|---|
| PCA (full SVD) | linear, orthogonal | global variance; exact best rank-k reconstruction | yes, a matrix product | O(n·d·min(n, d)) | compression, denoising, a preprocessing step, a first look |
| Randomized PCA | linear, approximate | top k components when the spectrum decays | yes | about O(n·d·k) | large matrices, k ≪ d |
| Kernel PCA | nonlinear via a kernel | variance in the kernel's feature space | yes, through kernel evaluations | O(n²) memory, O(n³) eigensolve | small curved datasets |
| t-SNE | nonlinear, non-parametric | local neighbourhoods | no (refit) | O(n log n) per iteration with Barnes-Hut | 2-D or 3-D visualization |
| UMAP | nonlinear | local neighbourhoods, somewhat more global layout | yes, approximately | approximate k-NN graph, then fast SGD | visualization, sometimes as features |
- PCA is the default because it is deterministic, fast, invertible and has one knob. Its components are hard to name, since each mixes every feature; for interpretable parts, sparse PCA or non-negative matrix factorization trade some variance for sparser loadings.
- Variance is not importance. The directions that separate classes can have small variance; PCA ignores labels, so a supervised method (LDA, or simply the model's own features) can beat it as preprocessing.
- Outliers dominate variance. One extreme point can rotate PC1 toward itself; robust PCA variants or clipping help.
- t-SNE and UMAP make persuasive pictures. Treat clusters in them as hypotheses to check in the original space, not as findings.
You whiten before k-means and the clusters get worse. Why might that happen?
Whitening scales every component to variance 1, including the low-variance ones that hold mostly noise. k-means uses Euclidean distance, so after whitening a noise direction counts as much as the direction that separated the clusters. Keeping only the top components first, or not whitening, keeps the noise from drowning the signal.
A t-SNE plot shows two tight clusters far apart and one large diffuse cluster. What can you conclude?
Only that the points within each blob are near neighbours of each other in the original space. The relative sizes are not trustworthy, because t-SNE equalizes neighbourhood sizes, and the gaps are not trustworthy, because the heavy-tailed kernel exaggerates them. Check the clusters' spread and separation in the original space or in a PCA projection, and rerun with other perplexities and seeds.
Connections
Read first
Read next
No page builds on this one yet.
The linear algebra toolkit supplies the eigen-decomposition, the SVD and power iteration this page is built from; feature preprocessing supplies the standardization PCA needs first; and PCA is the usual step before clustering or nearest-neighbour search in many dimensions. An autoencoder with linear layers and squared loss learns the same subspace as PCA, and with nonlinear layers it is a nonlinear generalization.
References
- Pearson. On Lines and Planes of Closest Fit to Systems of Points in Space. Philosophical Magazine 1901. doi:10.1080/14786440109462720
- Hotelling. Analysis of a Complex of Statistical Variables into Principal Components. Journal of Educational Psychology 1933. doi:10.1037/h0071325
- Eckart and Young. The Approximation of One Matrix by Another of Lower Rank. Psychometrika 1936. doi:10.1007/BF02288367
- Tipping and Bishop. Probabilistic Principal Component Analysis. Journal of the Royal Statistical Society B 1999.
- Minka. Automatic Choice of Dimensionality for PCA. NIPS 2000.
- Halko, Martinsson and Tropp. Finding Structure with Randomness: Probabilistic Algorithms for Constructing Approximate Matrix Decompositions. SIAM Review 2011. arXiv:0909.4061
- van der Maaten and Hinton. Visualizing Data using t-SNE. Journal of Machine Learning Research 2008.
- McInnes, Healy and Melville. UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction. 2018. arXiv:1802.03426
- Wattenberg, Viégas and Johnson. How to Use t-SNE Effectively. Distill 2016. doi:10.23915/distill.00002
- scikit-learn 1.9.1:
PCA,transformandinverse_transform,TSNE. PyTorch v2.15.0:pca_lowrank.