PCA From Scratch in Python: The Math of Eigenvectors and Dimensionality Reduction Explained
Principal Component Analysis (PCA) is an unsupervised linear dimensionality reduction technique that transforms correlated features into a set of linearly uncorrelated orthogonal axes called principal components. These components align with the directions of maximum variance in the data, derived mathematically as the eigenvectors of the feature covariance matrix. The corresponding eigenvalues quantify the exact variance preserved along each axis, allowing high-dimensional data to be compressed into fewer dimensions with minimal reconstruction loss.
Principal Component Analysis (PCA) is the foundational workhorse of unsupervised machine learning and multivariate statistics. Whenever tabular tables grow to hundreds of columns, computer vision models ingest thousands of raw pixel intensities, or genomics pipelines process tens of thousands of gene expression levels, data scientists face the notorious curse of dimensionality. Distances between points collapse, predictive models overfit, training times balloon, and human visualization becomes impossible beyond three dimensions.
1. Key Concepts & Mathematical Notation Glossary
Before establishing the mathematical derivations, review the standard notation used throughout multivariate statistics, linear algebra, and machine learning literature:
| Symbol | Mathematical Concept | Dimensionality | Interpretation / Role in PCA |
|---|---|---|---|
| Raw Data Matrix | observations (samples) across original input features. | ||
| Empirical Feature Mean Vector | Mean vector used to center the feature cloud at the origin. | ||
| Centered Data Matrix | Zero-mean data ; ensures variance is measured relative to the centroid. | ||
| Sample Covariance Matrix | Symmetric matrix capturing pairwise feature covariances. | ||
| Eigenvector / Principal Axis | Orthogonal unit direction vector () pointing along an axis of data variance. | ||
| Eigenvalue | Scalar | Variance magnitude of the data projected onto eigenvector . | |
| Projection Matrix | Matrix formed by concatenating the top principal eigenvectors as columns. | ||
| or | Transformed / Latent Matrix | Compressed low-dimensional coordinates: . | |
| Explained Variance Ratio | Scalar | Fraction of total variance accounted for by component : . | |
| Singular Value Decomposition (SVD) | Matrices | Decomposition used in production PCA for numerical stability. |
2. Why PCA Is Needed: Overcoming the Curse of Dimensionality
In modern data science, more features do not always yield better models. As the feature count grows relative to the sample count , three critical problems emerge:
- Multicollinearity and Redundant Information: In financial and customer analytics, variables such as monthly income, annual tax bracket, and home value are highly correlated. Retaining all of them destabilizes regression coefficients, inflates standard errors, and adds computational weight without introducing novel signal.
- Distance Concentration in High Dimensions: In high-dimensional Euclidean space, the ratio between the distance to the nearest neighbor and the distance to the farthest neighbor approaches 1. Distance-based algorithms (such as KNN, K-Means, and SVM with RBF kernels) degrade because every point appears virtually equidistant from every other point.
- Storage, Throughput, and Human Interpretability: Processing raw 1024-dimensional embeddings or 4096-pixel images incurs severe GPU memory and latency penalties. Compressing features down to an informative low-dimensional subspace accelerates downstream gradient descent and enables 2D/3D scatter visualization.
3. Geometric Intuition: The Long Axis of the Data Cloud
To visualize the geometry of PCA, picture a two-dimensional scatter plot displaying two correlated student metrics: hours studied on the horizontal axis and final exam score on the vertical axis. Because students who study more generally score higher, the cloud of points forms an elongated, tilted ellipse — reminiscent of a cigar angled at roughly 45 degrees.
Now ask the central question of dimensionality reduction: If you are allowed to draw only ONE straight line through this 2D cloud, which line preserves the most information?
- If you project every dot horizontally onto the exam score axis, you discard how many hours they studied.
- If you project every dot vertically onto the study hours axis, you discard their exam score.
- However, if you draw a line straight through the longest axis of the ellipse and drop perpendicular shadows (orthogonal projections) onto it, the points that were far apart remain far apart. The variation along that diagonal captures the underlying academic commitment story.
That longest axis is the first principal component (PC1). It is not an original feature; it is a linear combination of both features (). The second principal component (PC2) is then constrained to be strictly orthogonal (perpendicular) to PC1, capturing whatever residual dispersion remains. In dimensions, PCA discovers orthogonal axes ordered strictly by the variance of the data along them.
4. Complete Mathematical Derivation of PCA
PCA can be derived from two equivalent mathematical perspectives: maximizing the projected variance or minimizing the orthogonal reconstruction error. Here, we present the classic variance maximization derivation using the method of Lagrange multipliers.
Step 1: Centering and Standardizing the Data
Let be the data matrix with rows and columns. Before computing directional spread, we must center each column around zero so the coordinate origin coincides with the data centroid:
When features have disparate units (for example, age in years ranging from 18 to 80, alongside annual income in dollars ranging from 20,000 to 500,000), features with massive numerical ranges would artificially dominate the variance. In such cases, we standardize each feature by dividing by its sample standard deviation: .
Step 2: The Sample Covariance Matrix
The empirical sample covariance matrix summarizes the joint dispersion between all pairs of features:
Notice key structural properties of :
- Symmetry: because , so .
- Positive Semi-Definite: For any vector , . Consequently, all eigenvalues of are real and non-negative ().
- Diagonal Entries: represents the sample variance of feature alone.
Step 3: Variance Maximization via Lagrange Multipliers
Let be a candidate unit direction vector (). The projection of a centered sample point onto is the scalar dot product . The sample variance of these projected scalars across all points is:
We wish to find the direction that maximizes the projected variance , subject to the constraint that is a unit vector ():
We formulate the Lagrangian function with Lagrange multiplier :
Taking the vector derivative of with respect to and setting it to zero:
Step 4: Subsequent Components and the Projection Matrix
For the second principal component , we maximize subject to two constraints: and orthogonality to the first component (). By the spectral theorem for symmetric matrices, this yields the eigenvector corresponding to the second largest eigenvalue .
We sort the eigenvalues in descending order: . Selecting the top eigenvectors forms the projection matrix :
The dimensionally reduced coordinates and the approximate reconstruction are given by:
Step 5: Explained Variance Ratio and Cumulative Variance
Because the covariance matrix is symmetric, the sum of its diagonal elements (the total feature variance) equals the sum of its eigenvalues (the trace invariance property: ). The fraction of total information preserved by component is:
5. Grounded Step-by-Step Hand-Worked Numerical Walkthrough
To eliminate all abstraction, let us execute the entire PCA pipeline on a verified -sample, -feature dataset with zero hand-waving. Consider the following pre-centered matrix :
Verification of zero mean: Feature 1 mean is . Feature 2 mean is .
Sub-step 5.1: Compute the Sample Covariance Matrix C
With , degrees of freedom :
Sub-step 5.2: Calculate the Eigenvalues
We find scalar values where the characteristic polynomial :
Applying the quadratic formula :
Trace Check: Total variance .
A single principal component retains over 86% of the dataset's total variance!
Sub-step 5.3: Compute the First Eigenvector v1
We solve for vector :
From row 1: . Imposing unit norm :
Sub-step 5.4: Project the Data Points onto PC1
Projecting each 2D observation onto ():
- Sample 1 :
- Sample 2 :
- Sample 3 :
6. Production Engineering: Why Scikit-Learn Uses SVD Instead of Covariance
If PCA is defined by the eigenvectors of , why does production code in sklearn.decomposition.PCA bypass computing entirely and run Singular Value Decomposition (SVD) on ?
- Condition Number Squaring: The condition number measures numerical sensitivity to floating-point rounding errors. Forming the product squares the condition number: . If has an ill-conditioned ratio of singular values , has , causing severe loss of numerical precision.
- Memory Footprint when : In genomic or NLP applications where features but patients, forming the covariance matrix requires allocating a massive float matrix (10 GB RAM). SVD decomposes directly without allocating .
Recall the thin SVD decomposition of centered data: , where , is diagonal with singular values , and . Substituting this into the covariance matrix formulation:
Because , the right singular vectors of are identically the eigenvectors of , and the eigenvalues relate directly to the singular values by: Modern libraries compute SVD on directly using LAPACK routines (gesdd), achieving runtime with superior numerical stability.
7. Python Implementation From Scratch
Here is a complete, production-grade object-oriented implementation of PCA built using pure NumPy. It includes zero-mean centering, covariance computation, symmetric eigendecomposition via np.linalg.eigh, variance ratios, projection, and inverse reconstruction.
import numpy as np
class PCAFromScratch:
"""
Principal Component Analysis (PCA) implemented from first principles.
Parameters
----------
n_components : int or float
If int, the number of top principal components to retain.
If float between 0.0 and 1.0, the fraction of cumulative variance to preserve.
"""
def __init__(self, n_components=2):
self.n_components = n_components
self.components_ = None # Shape: (d, k) projection vectors
self.explained_variance_ = None # Retained eigenvalues
self.explained_variance_ratio_ = None # Fraction of variance per component
self.mean_ = None # Empirical feature means
self.n_features_in_ = None
def fit(self, X):
X = np.asarray(X, dtype=np.float64)
n_samples, n_features = X.shape
self.n_features_in_ = n_features
# Step 1: Center the data (mean = 0)
self.mean_ = np.mean(X, axis=0)
X_centered = X - self.mean_
# Step 2: Sample covariance matrix C (d x d)
# rowvar=False denotes columns are features, rows are samples
cov_matrix = np.cov(X_centered, rowvar=False)
if cov_matrix.ndim == 0: # scalar fallback for 1D edge-case
cov_matrix = np.array([[cov_matrix]])
# Step 3: Eigendecomposition of symmetric matrix C
# np.linalg.eigh is optimized for Hermitian/symmetric matrices
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
# Step 4: Sort eigenvalues and eigenvectors in descending order
idx_desc = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx_desc]
eigenvectors = eigenvectors[:, idx_desc]
# Zero out tiny negative eigenvalues caused by floating point precision
eigenvalues = np.maximum(eigenvalues, 0.0)
total_variance = np.sum(eigenvalues)
variance_ratio = eigenvalues / total_variance if total_variance > 0 else np.zeros_like(eigenvalues)
# Step 5: Select number of components
if isinstance(self.n_components, float) and 0.0 < self.n_components <= 1.0:
cumulative = np.cumsum(variance_ratio)
k = int(np.searchsorted(cumulative, self.n_components) + 1)
else:
k = int(min(self.n_components, n_features))
self.components_ = eigenvectors[:, :k]
self.explained_variance_ = eigenvalues[:k]
self.explained_variance_ratio_ = variance_ratio[:k]
return self
def transform(self, X):
X = np.asarray(X, dtype=np.float64)
X_centered = X - self.mean_
return np.dot(X_centered, self.components_)
def fit_transform(self, X):
return self.fit(X).transform(X)
def inverse_transform(self, Z):
"""Reconstruct high-dimensional approximations from latent scores: Z @ W.T + mean."""
Z = np.asarray(Z, dtype=np.float64)
return np.dot(Z, self.components_.T) + self.mean_
# --- Verification on the 3-sample hand-worked dataset ---
if __name__ == "__main__":
X_demo = np.array([
[ 2.0, 1.0],
[ 0.0, -1.0],
[-2.0, 0.0]
])
# 1. Run From-Scratch PCA
pca_scratch = PCAFromScratch(n_components=1)
Z_scratch = pca_scratch.fit_transform(X_demo)
X_reconstructed = pca_scratch.inverse_transform(Z_scratch)
print("=== PCA FROM SCRATCH VERIFICATION ===")
print(f"Eigenvalue (Variance) lambda_1: {pca_scratch.explained_variance_[0]:.4f}")
print(f"Explained Variance Ratio: {pca_scratch.explained_variance_ratio_[0]:.4%}")
print(f"Principal Axis Direction (v1): {pca_scratch.components_[:, 0]}")
print("Transformed 1D Coordinates (Z):")
print(np.round(Z_scratch, 4))
print("Reconstructed Approximation (X_hat):")
print(np.round(X_reconstructed, 4))
# 2. Compare Against Scikit-Learn
from sklearn.decomposition import PCA
pca_sklearn = PCA(n_components=1)
Z_sklearn = pca_sklearn.fit_transform(X_demo)
print("\n=== SCIKIT-LEARN VALIDATION ===")
print(f"Sklearn Explained Variance: {pca_sklearn.explained_variance_[0]:.4f}")
print(f"Sklearn Variance Ratio: {pca_sklearn.explained_variance_ratio_[0]:.4%}")
print("Sklearn Transformed (Z):")
print(np.round(Z_sklearn, 4))
# Check absolute value congruence
np.testing.assert_allclose(np.abs(Z_scratch), np.abs(Z_sklearn), rtol=1e-5)
print("\n[SUCCESS] From-scratch implementation matches scikit-learn within 1e-5 precision!")Executing this test harness confirms exact mathematical congruence with our manual calculations:
=== PCA FROM SCRATCH VERIFICATION ===
Eigenvalue (Variance) lambda_1: 4.3028
Explained Variance Ratio: 86.0555%
Principal Axis Direction (v1): [-0.95709203 -0.28978415]
Transformed 1D Coordinates (Z):
[[-2.204 ]
[ 0.2898]
[ 1.9142]]
Reconstructed Approximation (X_hat):
[[ 2.1094 0.6387]
[-0.2774 -0.084 ]
[-1.832 -0.5547]]
=== SCIKIT-LEARN VALIDATION ===
Sklearn Explained Variance: 4.3028
Sklearn Variance Ratio: 86.0555%
Sklearn Transformed (Z):
[[ 2.204 ]
[-0.2898]
[-1.9142]]
[SUCCESS] From-scratch implementation matches scikit-learn within 1e-5 precision!8. Selecting the Optimal Number of Principal Components
In exploratory data analysis and ML preprocessing, how do practitioners systematically decide how many components to retain? Three standard criteria are used:
- Cumulative Explained Variance Threshold (e.g., 90% or 95%): Plot cumulative explained variance against . Choose the smallest where the curve surpasses 0.90 or 0.95. This guarantees that 95% of information is preserved while discarding the noise tail.
- The Scree Plot Elbow Method: Plot individual eigenvalues in descending order. The graph typically plunges steeply and then flattens out into an 'elbow.' The components before the elbow represent primary structural signal, while points after the elbow represent random experimental noise.
- The Kaiser-Guttman Criterion: When PCA is applied to standardized data (-scores, where each variable has variance 1), any principal component with an eigenvalue contains less variance than a single original feature. The Kaiser rule states: retain only components where .
9. Common Pitfalls, Edge Cases, and Failure Modes
- Failing to Standardize Varied Scales: If a real estate dataset pairs square footage () with bedroom count (), the raw variance of square footage is tens of thousands of times greater. Without standard scaling, PC1 will align 99.9% with square footage simply due to units of measurement.
- Data Leakage Across Train/Test Splits: Never call
fit_transform()on the entire dataset prior to splitting. Centering and computing covariance across test samples leaks future variance distributions into training. Always runpca.fit(X_train)and thenpca.transform(X_test)using the training mean and components. - Failure on Non-Linear Manifolds: PCA is strictly a linear projection technique. If data lies on a curved manifold — such as concentric spheres, a Swiss roll, or non-linear parabolas — PCA flattens the geometry across chords, collapsing separate clusters on top of each other. In such domains, non-linear techniques (Kernel PCA, t-SNE, or UMAP) must be utilized.
- Sensitivity to Extreme Outliers: Because variance minimizes squared deviations (), a single distant outlier can pull the principal component axis heavily toward itself, warping the coordinate frame for all normal points.
- Sparse Text Data Collapse: In NLP TF-IDF matrices (which are 99% sparse), centering () fills millions of zeros with non-zero mean values, immediately converting a sparse matrix into a dense matrix that consumes terabytes of memory. For sparse data, use
TruncatedSVD, which decomposes without zero-centering.
10. Dimensionality Reduction Comparison Matrix
Understanding where PCA sits in relation to supervised, non-linear, and manifold learning methods:
| Algorithm | Supervised / Unsupervised | Linear / Non-linear | Primary Objective | Best Use Case |
|---|---|---|---|---|
| PCA | Unsupervised | Linear | Maximizes global variance, minimizes reconstruction error | Fast preprocessing, feature compression, noise reduction, multicollinearity removal |
| Kernel PCA | Unsupervised | Non-linear | Projects data into high-dimensional Hilbert space via kernel trick () | Separating non-linear geometric structures (circles, curves) for downstream linear models |
| LDA | Supervised (uses labels ) | Linear | Maximizes between-class variance relative to within-class variance () | Dimensionality reduction for linear classification (e.g., face recognition, cancer subtyping) |
| t-SNE | Unsupervised | Non-linear | Preserves local neighborhood probabilities using Student-t distributions | 2D/3D visualization of complex multi-cluster datasets (e.g., single-cell RNA, image embeddings) |
| UMAP | Unsupervised (or Semi-supervised) | Non-linear | Preserves local and global fuzzy simplicial set topology | High-performance non-linear visualization and general clustering preprocessing |
| Autoencoder | Self-supervised Neural Net | Non-linear (with non-linear activations) | Minimizes non-linear bottleneck reconstruction loss | Complex multimodal compression (images, audio, latent generative spaces) |
11. Hands-On Practice & Curriculum Roadmap
To consolidate your mastery of PCA and linear algebra in machine learning, tackle these practical challenges:
- Implement PCA with SVD: Refactor the from-scratch class to compute
np.linalg.svd(X_centered, full_matrices=False)and demonstrate that right singular vectors match the eigenvectors of the covariance matrix within numerical tolerance. - The 95% Variance MNIST Compression: Load Scikit-Learn's digits dataset ( dimensions). Plot the cumulative explained variance curve. Determine how many components are required to capture 95% of the variance, reconstruct the images, and visually compare the compressed digits against the originals.
- Explore Non-Linear Manifold Breakdown: Generate a 3D Swiss roll using
sklearn.datasets.make_swiss_roll. Apply standard PCA to project it to 2D, then compare the output against t-SNE and UMAP to visually witness why linear PCA fails on folded manifolds.