Principal Component Analysis and Factor Extraction › PCA Applied to Yield Curves

Michael BrenndoerferDecember 12, 20259 min read
Quantitative FinanceAll chapters
Part 8 of 15Econometrics and Statistical Modeling
Chapter 44Principal Component Analysis and Factor Extraction › PCA Applied to Yield Curves
Reading section 5 of 13 · PCA Applied to Yield Curves · 9 minSection 5 of 13 · 9 min

Book contents

Section 5 of 13 · 9 min read

Principal Component Analysis and Factor Extraction › PCA Applied to Yield CurvesOne section · Sections in this chapter5 / 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

The term structure of interest rates is a standard application of PCA in finance. As we discussed in Part II when covering bond pricing and the term structure, yield curves across maturities move together in systematic ways. PCA extracts the dominant patterns of yield curve movements.

Simulating Yield Curve Data

We'll simulate realistic yield curve data based on the well-documented empirical patterns of level, slope, and curvature movements:

In[13]:
Code
import numpy as np
import pandas as pd

np.random.seed(123)

# Define maturities (in years)
maturities = np.array([0.25, 0.5, 1, 2, 3, 5, 7, 10, 15, 20, 30])
n_maturities = len(maturities)
n_days = 500

# Simulate three factors: level, slope, curvature
level_factor = np.cumsum(np.random.randn(n_days) * 0.05)  # Random walk
slope_factor = np.cumsum(np.random.randn(n_days) * 0.03)
curvature_factor = np.cumsum(np.random.randn(n_days) * 0.02)

# Define factor loadings that create realistic yield curve shapes
# Level: affects all maturities equally
level_loadings = np.ones(n_maturities)

# Slope: increases with maturity (negative at short end, positive at long end)
slope_loadings = np.log(maturities + 1) - np.log(maturities + 1).mean()

# Curvature: peaks at intermediate maturities
curvature_loadings = -((maturities - 5) ** 2) / 400 + 0.5

# Generate yield curves
base_curve = 2 + 0.5 * np.log(maturities + 1)  # Upward sloping base curve

yields = np.zeros((n_days, n_maturities))
for t in range(n_days):
    yields[t, :] = (
        base_curve
        + level_factor[t] * level_loadings
        + slope_factor[t] * slope_loadings
        + curvature_factor[t] * curvature_loadings
        + np.random.randn(n_maturities) * 0.003
    )  # Idiosyncratic noise

# Store as DataFrame
yield_df = pd.DataFrame(yields, columns=[f"{m}Y" for m in maturities])

Let's visualize several yield curves from our simulated data:

Out[14]:
Visualization
Line chart of yield curves across maturities for several randomly selected days.
Sample yield curves from simulated data showing typical shapes and variations across different days. The curves exhibit the characteristic upward slope with variations in level, steepness, and curvature.

The yield curves show the typical upward slope with variations in their overall level, steepness, and curvature. This is exactly what our three factors produce.

Applying PCA to Yield Changes

In practice, we usually apply PCA to yield changes rather than yield levels because changes are more stationary. This follows the same logic as our discussion of returns versus prices in the time series chapter.

In[15]:
Code
# Compute daily yield changes
yield_changes = np.diff(yields, axis=0)

# Center the changes (mean is already close to zero for stationary data)
yield_changes_centered = yield_changes - yield_changes.mean(axis=0)

# Compute covariance matrix of yield changes
cov_yields = np.cov(yield_changes_centered, rowvar=False)

# Eigendecomposition
eigenvalues_yield, eigenvectors_yield = np.linalg.eigh(cov_yields)

# Sort descending
sort_idx = np.argsort(eigenvalues_yield)[::-1]
eigenvalues_yield = eigenvalues_yield[sort_idx]
eigenvectors_yield = eigenvectors_yield[:, sort_idx]

# Calculate variance metrics
total_var_yield = eigenvalues_yield.sum()
explained_pct_yield = 100 * eigenvalues_yield / total_var_yield
cumulative_pct_yield = np.cumsum(explained_pct_yield)
Out[16]:
Console
Variance Explained by Principal Components:
---------------------------------------------
PC1:  70.38%  (Cumulative:  70.38%)
PC2:  28.19%  (Cumulative:  98.56%)
PC3:   1.08%  (Cumulative:  99.65%)
PC4:   0.06%  (Cumulative:  99.70%)
PC5:   0.05%  (Cumulative:  99.75%)

The first three principal components explain over 99% of yield curve variation, confirming the well-known empirical result that yield curves have very low effective dimensionality. This finding has practical implications: despite having 11 maturities, we can capture virtually all yield curve dynamics with just three factors.

Out[17]:
Visualization
Bar chart of variance explained percentage for each of six principal components of yield curve changes, dominated by PC1.
Individual variance explained by each principal component of yield curve changes. The first component dominates, with rapidly diminishing contributions from subsequent components.
Line plot of cumulative variance explained for yield curve changes, rising steeply to near 100% by PC3.
Cumulative variance explained for yield curve changes. Three components reach near 100%, confirming the remarkably low dimensionality of the term structure.

Interpreting the Principal Components

The loading vectors reveal the economic interpretation of each component:

Out[18]:
Visualization
Line chart showing three principal component loading curves plotted against maturity.
Principal component loadings for yield curve changes. PC1 is a level shift with uniform loadings, while PC2 and PC3 capture slope and curvature through opposing signs at maturity extremes and peaks at intermediate points. These three components explain most yield curve movements and match the usual level, slope, and curvature interpretation.

The three components have clear economic interpretations:

  • PC1 (Level): Nearly constant loadings across maturities. When this component increases, all yields move up by similar amounts, representing a parallel shift of the entire curve.

  • PC2 (Slope): Loadings change monotonically with maturity, positive at one end and negative at the other. A positive shock to this component steepens the curve (short rates fall while long rates rise, or vice versa).

  • PC3 (Curvature): Loadings peak at intermediate maturities with opposite signs at the extremes. This component captures "twist" movements where the middle of the curve moves differently from the ends.

These interpretations are consistent across different countries, time periods, and yield curve datasets. The level factor typically explains 80-90% of variance, slope explains 5-10%, and curvature explains most of the remainder.

Reconstructing Yield Curves

We can reconstruct yield curve changes using only the first few principal components. This demonstrates how dimension reduction works in practice:

In[19]:
Code
# Project yield changes onto first 3 PCs
pc_scores_yield = yield_changes_centered @ eigenvectors_yield[:, :3]

# Reconstruct yield changes using only first 3 PCs
reconstructed_changes = pc_scores_yield @ eigenvectors_yield[:, :3].T

# Add back the mean
reconstructed_changes += yield_changes.mean(axis=0)

# Calculate reconstruction error
reconstruction_error = yield_changes - reconstructed_changes
rmse = np.sqrt((reconstruction_error**2).mean())
max_error = np.abs(reconstruction_error).max()
Out[20]:
Console
Reconstruction Quality (using 3 PCs):
  RMSE: 0.003538
  Max absolute error: 0.012394
  Original data std: 0.059524
  Error as % of std: 5.94%

The reconstruction error is tiny compared to the magnitude of yield changes, confirming that three components capture the essential dynamics.

Out[21]:
Visualization
Line chart comparing original and reconstructed yield curve changes across maturities for one sample day, with the reconstructed curve following the original pattern and small residual gaps.
Original versus three-PC reconstructed yield curve changes for a single day. The reconstructed curve tracks the original pattern across maturities, while the small residual gaps show the information discarded by dimension reduction.
Histogram of reconstruction errors in basis points, showing a tight bell-shaped distribution centered near zero.
Distribution of reconstruction errors across all days and maturities. The narrow spread centered on zero confirms that three principal components suffice to represent yield curve changes with minimal loss.