Linear Algebra for Quantitative Finance › Matrix Decompositions

Michael BrenndoerferOctober 23, 202516 min read
Quantitative FinanceAll chapters
Part 1 of 15Mathematical Foundations
Chapter 5Linear Algebra for Quantitative Finance › Matrix Decompositions
Reading section 5 of 9 · Matrix Decompositions · 16 minSection 5 of 9 · 16 min

Book contents

Section 5 of 9 · 16 min read

Linear Algebra for Quantitative Finance › Matrix DecompositionsOne section · Sections in this chapter5 / 9 ↓
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

Matrix decompositions break a matrix into simpler components, exposing structure that's hidden in the raw numbers. In quantitative finance, decompositions help us study risk directions, enable dimensionality reduction through truncation, and improve numerical stability.

Think of decomposition as a kind of mathematical X-ray. A covariance matrix appears as a dense array of numbers, but its eigendecomposition reveals modes of variation, the basic ways in which assets tend to move together. A return matrix might contain hundreds of thousands of numbers, but its singular value decomposition exposes dominant patterns in the observed variation. These mathematical components can support financial interpretation after their loadings, scores, and stability have been examined; the decomposition itself does not assign economic meaning.

Eigenvalue Decomposition

A diagonalizable square matrix A\mathbf{A} has a complete eigenvector basis and can be expressed as:

A=VΛV−1\mathbf{A} = \mathbf{V}\mathbf{\Lambda}\mathbf{V}^{-1}

where:

  • V\mathbf{V}: an n×nn \times n matrix whose columns are the eigenvectors v1,v2,…,vn\mathbf{v}_1, \mathbf{v}_2, \ldots, \mathbf{v}_n
  • Λ\mathbf{\Lambda}: a diagonal matrix with eigenvalues λ1,λ2,…,λn\lambda_1, \lambda_2, \ldots, \lambda_n on the diagonal
  • V−1\mathbf{V}^{-1}: the inverse of the eigenvector matrix, which "undoes" the coordinate transformation

This decomposition shows that A\mathbf{A} acts as: (1) changing into the eigenvector coordinate system via V−1\mathbf{V}^{-1}, (2) scaling each coordinate by the corresponding eigenvalue via Λ\mathbf{\Lambda}, and (3) changing back via V\mathbf{V}. These changes of basis are rotations or reflections only when V\mathbf{V} is orthogonal, as it can be for a real symmetric covariance matrix. In that covariance setting, the decomposition separates uncorrelated directions of variation.

The power of this decomposition lies in the simplicity of the diagonal matrix Λ\mathbf{\Lambda}. A diagonal matrix just scales each coordinate independently, with no mixing between directions. All the complexity of A\mathbf{A} is absorbed into finding the right coordinate system (the eigenvectors) in which the matrix action becomes simple scaling.

Eigenvalues and Eigenvectors

An eigenvector is a nonzero vector v\mathbf{v} satisfying Av=λv\mathbf{A}\mathbf{v} = \lambda\mathbf{v} for matrix A\mathbf{A}, where λ\lambda is the eigenvalue. Its span is an invariant line: a nonzero λ\lambda preserves that line, with an orientation reversal when λ<0\lambda < 0, while λ=0\lambda = 0 maps the eigenvector to the zero vector. A real symmetric matrix has real eigenvalues and admits an orthonormal eigenbasis. Eigenvectors associated with distinct eigenvalues are orthogonal; within a repeated eigenspace, an orthonormal basis can be chosen.

For a covariance matrix, the eigenvectors represent the principal directions of variation in the data, and the eigenvalues represent the variance along each direction. Among unit-length directions v\mathbf{v}, the leading eigenvector maximizes vTΣv\mathbf{v}^T\Sigma\mathbf{v} and the maximum equals the largest eigenvalue. This describes the highest-variance direction in the asset universe; the contribution to a particular portfolio's variance also depends on that portfolio's weights.

In[29]:
Code
# Eigendecomposition of the covariance matrix
eigenvalues, eigenvectors = np.linalg.eigh(sample_cov)

# Sort by eigenvalue (descending)
sort_idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[sort_idx]
eigenvectors = eigenvectors[:, sort_idx]

# Calculate variance explained by each eigenvector
total_variance = np.sum(eigenvalues)
variance_explained = eigenvalues / total_variance
cumulative_variance = np.cumsum(variance_explained)
Out[30]:
Console
Eigenvalues of covariance matrix:
  λ1 = 0.00063233 (57.0% of variance)
  λ2 = 0.00036008 (32.5% of variance)
  λ3 = 0.00011670 (10.5% of variance)

Total variance: 0.00110911

First eigenvector (principal direction):
  [0.43878792 0.32747683 0.83679393]

Interpretation: This is the direction in asset-space along which
returns vary most. Its economic meaning must be checked in this sample.
Out[31]:
Visualization
Bar chart showing the percentage of variance explained by each principal component.
Individual variance contribution by each principal component. The first component captures the majority of total variance.
Line chart showing cumulative variance explained as principal components are added, with a dashed 95% threshold line.
Cumulative variance explained as components are added. The first two explain 89.5% in this seeded three-asset example; all three are needed to cross the displayed 95% threshold.

Principal Component Analysis (PCA)

PCA uses eigendecomposition to transform correlated variables into uncorrelated principal components. The full projection preserves the original dimension; dimensionality reduction occurs only when we retain k<Nk<N leading components. Whether a small number explains most variation depends on the assets, sample period, return frequency, scaling, and preprocessing.

The principal components are projections of the data onto the eigenvectors:

Z=XV\mathbf{Z} = \mathbf{X}\mathbf{V}

where:

  • Z\mathbf{Z}: the full T×NT \times N matrix of principal component scores; retaining its first k<Nk<N columns gives a reduced T×kT \times k representation
  • X\mathbf{X}: the T×NT \times N centered data matrix (each row is an observation, each column is a variable minus its mean)
  • V\mathbf{V}: the N×NN \times N matrix of eigenvectors (principal component loadings)

Each column of Z\mathbf{Z} represents a principal component, a new synthetic variable that captures a specific pattern of co-movement in the original data. The first principal component captures the most variance, the second captures the most remaining variance while being uncorrelated with the first, and so on. Analysts may assign economic labels such as market, sector, or style factors after inspecting the loadings and scores, but PCA does not guarantee that a component has a clean economic interpretation.

Why does this step produce uncorrelated components? Let Σ\Sigma be the covariance matrix of the centered data and let the columns of V\mathbf{V} be its orthonormal eigenvectors. Then

Cov⁡(Z)=Cov⁡(XV)=VTΣV=Λ.\operatorname{Cov}(\mathbf{Z}) = \operatorname{Cov}(\mathbf{X}\mathbf{V}) = \mathbf{V}^T\Sigma\mathbf{V} = \mathbf{\Lambda}.

Because Λ\mathbf{\Lambda} is diagonal, distinct principal-component scores have zero sample covariance. Orthogonal axes alone would not guarantee this for arbitrary data; the result holds because PCA chooses axes that diagonalize Σ\Sigma. The eigenvalues give the variances along those axes, so ordering them places the main variance direction first.

In[32]:
Code
# Apply PCA to return data
from sklearn.decomposition import PCA

# Center the returns
centered_returns = returns_data - returns_data.mean(axis=0)

# Fit PCA
pca = PCA()
principal_components = pca.fit_transform(centered_returns)
reduced_scores = principal_components[:, :2]
# Examine results
pc_variance = pca.explained_variance_ratio_
loadings = pca.components_  # Each row is an eigenvector
Out[33]:
Console
PCA Results:
--------------------------------------------------
PC1: 57.0% variance explained
PC2: 32.5% variance explained
PC3: 10.5% variance explained

Cumulative variance: [0.57012852 0.8947838  1.        ]
Reduced score shape: (252, 2); variance retained: 89.5%

Factor loadings (how each PC relates to original assets):
          Asset1    Asset2    Asset3
PC1:    +0.4388    +0.3275    +0.8368
PC2:    +0.7356    +0.4040    -0.5438
PC3:    -0.5162    +0.8541    -0.0636
Out[34]:
Visualization
Grouped bar chart of principal component loadings for three PCs across three assets.
Principal component loadings for the seeded three-asset sample. PC1 has same-sign loadings under the displayed sign convention, while PC2 and PC3 contrast assets within this sample.

In this seeded sample, the first principal component has same-sign loadings and can be read as a broad co-movement direction. The later components have mixed signs and contrast assets. These interpretations are sample-dependent, and multiplying any loading vector by -1 describes the same principal axis, so loading signs are a convention rather than an economic direction by themselves.

PCA in Practice: Interest Rate Curves

PCA is often applied to yield-curve movements because rates at different maturities tend to co-move. In the synthetic example below, the first three components explain 92.3% of sample variation and their loading shapes resemble three commonly used descriptions:

  1. Level (PC1): A broad same-direction shift resembling a level change; loading magnitudes need not be equal across maturities.
  2. Slope (PC2): Steepening or flattening. Short and long rates move in opposite directions.
  3. Curvature (PC3): Bending. The middle of the curve moves relative to the ends.

Retaining the first three components below produces a 500×3500 \times 3 score matrix instead of the original 500×8500 \times 8 data matrix. This reduces the dimension, but the variance retained and the economic labels depend on the sample and preprocessing. A fixed-income risk analysis can track these component exposures alongside maturity-specific risks that the reduced representation leaves out.

The synthetic daily yield changes and their marginal standard deviations are measured in percentage points: 0.05 percentage points is 5 basis points. This fixes the financial scale without changing the PCA loading directions or explained-variance ratios.

In[35]:
Code
# Simulate yield curve data (simplified example)
np.random.seed(456)
n_days = 500
maturities = [1, 2, 3, 5, 7, 10, 20, 30]  # Years

# Generate correlated yield changes using an illustrative index-distance kernel
# Correlation decays equally with the number of maturity-grid steps between rates
n_rates = len(maturities)
base_corr = np.eye(n_rates)
for i in range(n_rates):
    for j in range(n_rates):
        base_corr[i, j] = np.exp(-0.15 * abs(i - j))

# Daily yield-change standard deviations in percentage points (0.05 pp = 5 bp)
# These stipulated volatilities decrease slightly with maturity
vols = 0.05 * np.array([1.0, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65])
cov_matrix = np.outer(vols, vols) * base_corr

# Generate yield changes
yield_changes = np.random.multivariate_normal(np.zeros(n_rates), cov_matrix, n_days)

# Apply PCA
pca_yields = PCA()
yield_reduced_scores = pca_yields.fit_transform(yield_changes)[:, :3]
Out[36]:
Console
Yield Curve PCA Results:
--------------------------------------------------
Variance explained by each component:
  PC1: 72.8%
  PC2: 14.4%
  PC3: 5.2%
  PC4: 2.7%
  PC5: 1.8%

First 3 PCs explain 92.3% of variance; reduced score shape: (500, 3)
Out[37]:
Visualization
Line chart showing three principal component loadings across bond maturities from 1 to 30 years.
Principal component loadings in the synthetic yield-curve example. Under the displayed sign convention, PC1 resembles a level shift, PC2 contrasts short and long maturities, and PC3 contrasts the middle with the ends.

In this synthetic sample, PC1 has same-sign loadings across maturities and resembles a level shift. PC2 contrasts short and long maturities, while PC3 contrasts the middle with the ends and resembles curvature. The signs could all be reversed without changing the components. These loading patterns can be used to describe and hedge component exposures, subject to the fit and stability of the estimated PCA model.

Singular Value Decomposition (SVD)

While eigendecomposition only applies to square matrices, Singular Value Decomposition works for any matrix. For an m×nm \times n matrix A\mathbf{A}:

A=UΣVT\mathbf{A} = \mathbf{U}\mathbf{\Sigma}\mathbf{V}^T

where:

  • U\mathbf{U}: an m×mm \times m orthogonal matrix whose columns are the left singular vectors. The vectors associated with non-zero singular values span the column space of A\mathbf{A}; for a time-by-asset return matrix, their coordinates run across observations such as days.
  • Σ\mathbf{\Sigma}: an m×nm \times n diagonal matrix with non-negative singular values σ1≥σ2≥⋯≥0\sigma_1 \geq \sigma_2 \geq \cdots \geq 0 on the diagonal
  • V\mathbf{V}: an n×nn \times n orthogonal matrix whose columns are the right singular vectors. The vectors associated with non-zero singular values span the row space of A\mathbf{A}; for a time-by-asset return matrix, their coordinates run across assets.

The code below requests NumPy's reduced SVD. With k=min⁡(m,n)k=\min(m,n), full_matrices=False returns U with shape m×km \times k, singular_values as a length-kk vector, and Vt with shape k×nk \times n. NumPy returns the diagonal entries rather than an explicit Σ\mathbf{\Sigma} matrix; U @ np.diag(singular_values) @ Vt reconstructs A\mathbf{A}.

The singular values measure the "importance" of each component. For a return matrix with days as rows and assets as columns, U\mathbf{U} shows when certain patterns occurred, V\mathbf{V} shows which assets participated in each pattern, and Σ\Sigma shows how strong each pattern was.

The strength of SVD lies in its universality and interpretability. Any real matrix, not just square symmetric ones, can be decomposed into orthogonal changes of basis, including rotations or reflections, and a rectangular diagonal map with nonnegative scalings. The singular values are always non-negative and ordered by magnitude, and they provide a natural ranking by component strength. Truncating the SVD by keeping only the largest singular values gives the best low-rank approximation to the original matrix in terms of Frobenius norm, a result known as the Eckart-Young theorem. This makes SVD the mathematical foundation for dimensionality reduction and data compression.

SVD is useful for handling non-square return matrices (different numbers of days vs. assets) and for computing the pseudoinverse used in least squares solutions.

In[38]:
Code
# SVD of return matrix
U, singular_values, Vt = np.linalg.svd(centered_returns, full_matrices=False)

# Singular values relate to eigenvalues of covariance matrix
# For centered data X (T×N), T>1, covariance eigenvalues are σ²/(T-1)
# Add zero eigenvalues if the reduced SVD returns fewer than N singular values
n_obs = centered_returns.shape[0]
eigenvalue_estimate = singular_values**2 / (n_obs - 1)
Out[39]:
Console
Singular values of return matrix:
  σ1 = 0.3984
  σ2 = 0.3006
  σ3 = 0.1711

Relationship to eigenvalues:
  Eigenvalues of cov matrix: [0.00063233 0.00036008 0.0001167 ]
  σ²/(T-1):                  [0.00063233 0.00036008 0.0001167 ]

The close match between eigenvalues and σ²/(T-1) confirms the mathematical relationship between SVD and eigendecomposition. The largest singular value corresponds to the dominant variance pattern in the return matrix. That pattern may be interpreted as a market factor only after inspecting and validating its asset loadings and time scores; SVD alone does not assign an economic label. This relationship is why PCA can be computed efficiently via SVD, which is numerically more stable than computing the eigendecomposition of the covariance matrix directly.

Cholesky Decomposition

For positive definite matrices, including covariance matrices that are strictly positive definite, Cholesky decomposition provides a "square root":

Σ=LLT\mathbf{\Sigma} = \mathbf{L}\mathbf{L}^T

where:

  • L\mathbf{L}: a lower triangular matrix (all entries above the diagonal are zero)
  • LT\mathbf{L}^T: the transpose of L\mathbf{L} (an upper triangular matrix)

For a dense n×nn \times n matrix, Cholesky factorization requires about n3/3n^3/3 floating-point operations. Dense LU factorization requires about 2n3/32n^3/3, so both are O(n3)O(n^3) but Cholesky has roughly half the leading operation count when its positive-definite assumptions hold.

When the diagonal entries of L\mathbf{L} are required to be positive, this lower-triangular Cholesky factor is unique. The factor can be applied efficiently to independent draws to simulate correlated variables, a common step in Monte Carlo derivatives-pricing and risk workflows.

The lower triangular structure of L\mathbf{L} gives a sequential construction relative to the chosen variable ordering. The first simulated component uses one independent shock, the second mixes that shock with a second one, and the third mixes the first two shocks with a third. This is a computational representation of the target covariance, not a causal claim that later-listed assets depend on earlier-listed assets. Reordering the assets generally changes the factor L\mathbf{L} while preserving the appropriately reordered covariance matrix; permutation-invariant cases, such as the identity matrix, can leave the factor unchanged.

In[40]:
Code
# Cholesky decomposition for generating correlated random samples
L = np.linalg.cholesky(sample_cov)

# Generate uncorrelated standard normal samples
n_simulations = 10000
uncorrelated = np.random.standard_normal((n_simulations, 3))

# Transform to correlated samples
correlated = uncorrelated @ L.T

# Verify the correlation structure
simulated_cov = np.cov(correlated, rowvar=False)
Out[41]:
Console
Original covariance matrix (×10,000):
[[3.4765 1.4642 0.9198]
 [1.4642 2.1172 0.8783]
 [0.9198 0.8783 5.4973]]

Simulated covariance matrix (×10,000):
[[3.5015 1.4471 0.8961]
 [1.4471 2.1432 0.8691]
 [0.8961 0.8691 5.6059]]

Cholesky factor L (lower triangular, ×100):
[[1.8645 0.     0.    ]
 [0.7853 1.225  0.    ]
 [0.4933 0.4008 2.2569]]
Out[42]:
Visualization
First 1,000 simulated decimal-return points for Asset 1 versus Asset 2, with correlation from all 10,000 draws annotated.
Asset 1 vs Asset 2 decimal returns show positive correlation. Points show the first 1,000 of 10,000 simulated draws; the annotated correlation uses all 10,000.
First 1,000 simulated decimal-return points for Asset 1 versus Asset 3, with correlation from all 10,000 draws annotated.
Asset 1 vs Asset 3 decimal returns show weak positive correlation in this simulation. Points show the first 1,000 of 10,000 draws; the annotated correlation uses all 10,000.
First 1,000 simulated decimal-return points for Asset 2 versus Asset 3, with correlation from all 10,000 draws annotated.
Asset 2 vs Asset 3 decimal returns show weak positive correlation in this simulation. Points show the first 1,000 of 10,000 draws; the annotated correlation uses all 10,000.

The simulated covariance matrix closely matches the original, confirming that our Cholesky-based sampling reproduces the target covariance up to Monte Carlo error. For the chosen asset order, the lower triangular factor mixes successive independent shocks into each simulated component. This triangular construction is efficient, but it does not imply a causal ordering among the assets.

The Cholesky decomposition provides one efficient way to generate correlated random returns for Monte Carlo pricing and risk calculations, including some path-dependent derivative and Value-at-Risk workflows. It is not required: alternative matrix square roots, copulas, historical simulation, and other scenario methods can produce dependence in different ways.