Principal Component Analysis and Factor Extraction › Implementing PCA from Scratch

Michael BrenndoerferDecember 12, 20255 min read
Quantitative FinanceAll chapters
Part 8 of 15Econometrics and Statistical Modeling
Chapter 44Principal Component Analysis and Factor Extraction › Implementing PCA from Scratch
Reading section 4 of 13 · Implementing PCA from Scratch · 5 minSection 4 of 13 · 5 min

Book contents

Section 4 of 13 · 5 min read

Principal Component Analysis and Factor Extraction › Implementing PCA from ScratchOne section · Sections in this chapter4 / 13 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement PCA step by step to solidify the mathematical concepts. We'll use a simple example before moving to financial applications.

In[3]:
Code
import numpy as np

np.random.seed(42)

# Generate correlated data: two underlying factors drive three observed variables
n_obs = 200
factor1 = np.random.randn(n_obs)
factor2 = np.random.randn(n_obs)

# Create three observed variables as combinations of factors plus noise
x1 = 2 * factor1 + 0.5 * factor2 + 0.3 * np.random.randn(n_obs)
x2 = 1.5 * factor1 - 0.8 * factor2 + 0.3 * np.random.randn(n_obs)
x3 = 1.8 * factor1 + 0.3 * factor2 + 0.3 * np.random.randn(n_obs)

# Stack into data matrix
X = np.column_stack([x1, x2, x3])

Now we center the data and compute the covariance matrix:

In[4]:
Code
# Center the data by subtracting column means
X_centered = X - X.mean(axis=0)

# Compute sample covariance matrix
cov_matrix = np.cov(X_centered, rowvar=False)
Out[5]:
Console
Sample Covariance Matrix:
[[3.8158 2.1197 3.2931]
 [2.1197 2.5623 2.0789]
 [3.2931 2.0789 3.0186]]

The covariance matrix shows strong positive correlations among all three variables, consistent with a common factor structure.

Next, we compute eigenvalues and eigenvectors:

In[6]:
Code
# Compute eigendecomposition
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)

# Sort by decreasing eigenvalue (eigh returns in ascending order)
sort_idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[sort_idx]
eigenvectors = eigenvectors[:, sort_idx]

# Calculate variance explained
total_var = eigenvalues.sum()
explained_variance_pct = 100 * eigenvalues / total_var
cumulative_variance_pct = np.cumsum(explained_variance_pct)
Out[7]:
Console
Eigenvalues (variance of each PC):
  PC1: 8.2744
  PC2: 1.0416
  PC3: 0.0806

Variance explained by each component:
  PC1: 88.1%
  PC2: 11.1%
  PC3: 0.9%

Cumulative variance explained:
  First 1 PCs: 88.1%
  First 2 PCs: 99.1%
  First 3 PCs: 100.0%

The first principal component captures the dominant common movement across all three variables, explaining about 85% of total variance. The second captures most of the remaining variance, leaving very little for the third. This confirms that our three observed variables are effectively driven by two underlying factors.

Out[8]:
Visualization
Bar chart of eigenvalues for three principal components, showing a steep drop after PC1.
Scree plot showing eigenvalues for each principal component. The steep drop after PC1 indicates that the first component captures the dominant source of variation.
Line plot of cumulative variance explained rising steeply to near 100% after two components.
Cumulative variance explained by principal components. Two components together capture nearly all information in the three-variable example, consistent with the two-factor data-generating process.

The eigenvectors (loading vectors) tell us how each original variable contributes to each component:

Out[9]:
Console
Principal Component Loadings:

            PC1      PC2      PC3
  x1:  -0.6585   0.4351  -0.6141
  x2:  -0.4609  -0.8782  -0.1279
  x3:  -0.5949   0.1988   0.7788

The first component has positive loadings on all variables, representing their common factor. The second component has loadings of opposite signs, capturing the differential between certain variables. This corresponds to the second factor in our data-generating process.

Finally, we project the data onto the principal components:

In[10]:
Code
# Project data onto principal components
pc_scores = X_centered @ eigenvectors

# Calculate statistics for verification
pc_means = pc_scores.mean(axis=0)
pc_stds = pc_scores.std(axis=0)
pc_corr = np.corrcoef(pc_scores, rowvar=False)
Out[11]:
Console
Principal Component Statistics:
  PC1 - Mean: 0.000000, Std: 2.8693
  PC2 - Mean: 0.000000, Std: 1.0180
  PC3 - Mean: -0.000000, Std: 0.2832

Correlation between PCs:
  Corr(PC1, PC2): -0.000000
  Corr(PC1, PC3): -0.000000
  Corr(PC2, PC3): 0.000000

The principal components are uncorrelated by construction, and their standard deviations equal the square roots of the corresponding eigenvalues.

Out[12]:
Visualization
Scatter plot of 200 observations in the space of the first two principal components, showing no linear relationship between the axes.
Data projected onto the first two principal components. The horizontal axis (PC1) captures the dominant source of variation, while the vertical axis (PC2) captures secondary variation. The uncorrelated nature of principal components is evident from the lack of any linear relationship between the two axes.