BRAGA

02 Writing02-02 Notes02-02-132

Cholesky Decomposition (for PD matrix A)

Published 16 Apr 20267 min read
Contents
  1. 01Deep Breakdown
  2. 02Flowchart
  3. 03Real-world use
  4. 04The formula, piece by piece
  5. 05The formula in words
  6. 06Critical Concepts
  7. 07Step-by-step calculation (tiny example)
  8. 08Code examples
A=LLTA = LL^T
(1)

where LL is lower triangular

Deep Breakdown

What it tells me (Situation Awareness): Cholesky decomposition is what you reach for when you have a symmetric positive definite (PD) matrix that you need to use repeatedly, most often a covariance matrix. In DA/DS this appears in multivariate normal models, Gaussian processes, Kalman filters, and any likelihood that needs a log-determinant plus a fast way to solve Ax=bAx = b without forming A−1A^{-1}. Operationally, it replaces an expensive and unstable inverse with two cheap triangular solves, and it gives you a clean way to simulate correlated outcomes, for example correlated channel ROI scenarios.

Flowchart

YesNoYesNoInput matrix A(covariance orHessian)Check: A issymmetric (Aequals Atranspose)Is A positivedefinite?Compute L(lowertriangular) suchthat A = L * LtransposeAdd small jitterto diagonal oruse alternativefactorizationUse L for fastsolves: forwardsolve L y = b,then L transposex = yComputelogdet(A) = 2 *sum(log(diag(L)))Simulatecorrelatedsamples: x = mu+ L z, z ~ N(0, I)Verify: L * Ltransposeapproximates A?Proceed withmodeling orsimulationChecksymmetry,scaling, andnumericaltolerance

Real-world use

Where it is used

You use Cholesky when your pipeline includes a covariance matrix, for example in Bayesian marketing mix models (MMM), multivariate A/B test lifts across segments, or anomaly detection with a Gaussian assumption. It is also useful in optimization when AA is a PD Hessian or normal-equation matrix and you need stable linear solves.

Problem it solves

Without Cholesky, people often compute an explicit matrix inverse or do generic decomposition work they do not need. That is slower and numerically brittle, especially when features are correlated and AA is ill-conditioned. You also cannot efficiently compute log⁡(det⁡(A))\log(\det(A)) or draw correlated samples at scale without a stable factorization.

How it solves the problem

Cholesky exploits the PD property to factor AA into LLTLL^T where LL is triangular. Triangular structure matters: solving Ly=bLy = b and then LTx=yL^T x = y is O(n2)O(n^2) per right-hand side, which is much cheaper than O(n3)O(n^3) inversion. The diagonal of LL also gives log⁡(det⁡(A))\log(\det(A)) directly, which is a core term in multivariate normal log-likelihoods.

Example in a business

Imagine you model weekly incremental revenue lifts for three channels as correlated: Search, Social, and TV. You estimate mean lifts of [€12k, €8k, €20k] with covariance (in €k²): A=[[25,10,5],[10,16,4],[5,4,36]]A = [[25, 10, 5], [10, 16, 4], [5, 4, 36]]. Cholesky gives LL so you can generate 10,000 simulated weeks x=μ+Lzx = \mu + Lz and answer business questions like "What is P(total lift>€50k)P(\text{total lift} > \text{€50k})?" or "How often does TV underperform while Search overperforms?" This turns a sensitivity discussion into a reproducible Monte Carlo risk view.

The formula, piece by piece

  • AA: a symmetric positive definite matrix (n×nn \times n), often a covariance matrix or Hessian
  • LL: a lower triangular matrix (n×nn \times n) with positive diagonal entries
  • LTL^T: the transpose of LL (upper triangular)
  • LLTLL^T: matrix multiplication that reconstructs AA
  • "positive definite": for any non-zero vector vv, vTAv>0v^T A v > 0, which guarantees the factorization exists and is unique Full formula: A=LLTA = LL^T

The formula in words

"Cholesky factorization rewrites a valid covariance-like matrix AA as a lower-triangular matrix LL times its transpose, so you can solve systems, compute log-determinants, and simulate correlated values efficiently." Tiny numeric example: if A=[4223]A = \begin{bmatrix}4 & 2 \\ 2 & 3\end{bmatrix}, then L=[2012]≈[2011.4142]L = \begin{bmatrix}2 & 0 \\ 1 & \sqrt{2}\end{bmatrix} \approx \begin{bmatrix}2 & 0 \\ 1 & 1.4142\end{bmatrix} because LLT=[4223]=ALL^T = \begin{bmatrix}4 & 2 \\ 2 & 3\end{bmatrix} = A.

Critical Concepts

  • PD requirement: Cholesky exists only if AA is symmetric and positive definite; small numerical negatives on eigenvalues can break it.
  • Jitter trick: in practice you may add εI\varepsilon I (for example ε=10−8\varepsilon = 10^{-8} to 10−610^{-6}) to make a near-PD covariance usable.
  • Log-determinant shortcut: log⁡(det⁡(A))=2∑ilog⁡(Lii)\log(\det(A)) = 2 \sum_i \log(L_{ii}) is stable and fast, crucial in Gaussian log-likelihoods.
  • Solving beats inverting: compute xx via two triangular solves, do not form A−1A^{-1} unless you have a strong reason.
  • Cost profile: factorization is O(n3)O(n^3), but each additional solve or simulation is O(n2)O(n^2), so it pays off when reused.
  • PSD case: if AA is only positive semidefinite (rank-deficient), use an SVD or an LDLTLDL^T or pivoted Cholesky variant.

Step-by-step calculation (tiny example)

Step 1: Compute the first diagonal entry of L Compute l11=a11=4=2l_{11} = \sqrt{a_{11}} = \sqrt{4} = 2. So the first column of LL starts with l11=2l_{11} = 2, all entries below it follow.

Step 2: Compute the remaining entries of L column by column Compute l21=a21/l11=2/2=1l_{21} = a_{21} / l_{11} = 2 / 2 = 1. Now compute l22=a22−l212=3−12=2≈1.4142l_{22} = \sqrt{a_{22} - l_{21}^2} = \sqrt{3 - 1^2} = \sqrt{2} \approx 1.4142. So L=[2011.4142]L = \begin{bmatrix}2 & 0 \\ 1 & 1.4142\end{bmatrix}.

Step 3: Verify by reconstructing A = L L^T LLT=[2011.4142][2101.4142]LL^T = \begin{bmatrix}2 & 0 \\ 1 & 1.4142\end{bmatrix}\begin{bmatrix}2 & 1 \\ 0 & 1.4142\end{bmatrix} - (1,1)(1,1): 2⋅2+0⋅0=42 \cdot 2 + 0 \cdot 0 = 4

  • (1,2)(1,2): 2⋅1+0⋅1.4142=22 \cdot 1 + 0 \cdot 1.4142 = 2
  • (2,1)(2,1): 1⋅2+1.4142⋅0=21 \cdot 2 + 1.4142 \cdot 0 = 2
  • (2,2)(2,2): 1⋅1+1.4142⋅1.4142=1+2=31 \cdot 1 + 1.4142 \cdot 1.4142 = 1 + 2 = 3 We recover AA exactly (up to rounding), sanity check passes.

Code examples

Beginner example (Python)

python
import math   # -- Input: 2x2 positive definite covariance matrix -------------------
COV_11 = 4.0
COV_12 = 2.0
COV_21 = 2.0
COV_22 = 3.0   # Step 1: compute L(1,1)
l_11 = math.sqrt(COV_11)   # Step 2: compute L(2,1) and L(2,2)
l_21 = COV_21 / l_11
l_22 = math.sqrt(COV_22 - l_21 ** 2)
L = [ [l_11, 0.0], [l_21, l_22],
]
print(f"L[0] = {L[0]}")
print(f"L[1] = {L[1]}")   # Step 3: verify L * L^T reconstructs A
a11_hat = L[0][0] * L[0][0] + L[0][1] * L[0][1]
a12_hat = L[0][0] * L[1][0] + L[0][1] * L[1][1]
a22_hat = L[1][0] * L[1][0] + L[1][1] * L[1][1]
print(f"\nReconstructed A = [[{a11_hat:.4f}, {a12_hat:.4f}], [{a12_hat:.4f}, {a22_hat:.4f}]]")
print("Expected A = [[4.0000, 2.0000], [2.0000, 3.0000]]")

Beginner example (R)

r
# -- Input: 2x2 positive definite covariance matrix -------------------
cov_11 <- 4
cov_21 <- 2
cov_22 <- 3  # Step 1: compute l_11
l_11 <- sqrt(cov_11)  # Step 2: compute l_21 and l_22
l_21 <- cov_21 / l_11
l_22 <- sqrt(cov_22 - l_21^2)
L <- matrix(c(l_11, l_21, 0, l_22), nrow = 2, byrow = FALSE)
cat("L =\n")
print(L)  # Step 3: verify
a_hat <- L %*% t(L)
cat("\nL %*% t(L) =\n")
print(a_hat)
cat(sprintf("\nExpected: [[4, 2], [2, 3]]\n"))

Professional example (Python)

python
import numpy as np
import pandas as pd
np.random.seed(42)   # -- Business scenario: simulate correlated weekly channel lifts -------
N_SIM = 10_000
TARGET_TOTAL_LIFT_EUR = 50_000
JITTER = 1e-8
channels = ["search", "social", "tv"]   # Mean incremental lift per week (EUR)
mu = np.array([12_000.0, 8_000.0, 20_000.0])   # Covariance of lifts (EUR^2), must be symmetric and positive definite
cov = np.array( [ [25_000_000.0, 10_000_000.0, 5_000_000.0], [10_000_000.0, 16_000_000.0, 4_000_000.0], [ 5_000_000.0, 4_000_000.0, 36_000_000.0], ], dtype=float,
)   # Guard: small jitter ensures numerical positive definiteness
cov_stable = cov + JITTER * np.eye(len(channels))   # Cholesky factorization: cov = L L^T
L = np.linalg.cholesky(cov_stable)   # Simulate: draw z ~ N(0, I), transform to x = mu + L z
z = np.random.normal(loc=0.0, scale=1.0, size=(N_SIM, len(channels)))
sim_lifts = mu + z @ L.T
df = pd.DataFrame(sim_lifts, columns=channels)
df["total_lift"] = df.sum(axis=1)
prob_hit_target = (df["total_lift"] > TARGET_TOTAL_LIFT_EUR).mean()   # Diagnostics: empirical covariance should resemble input covariance
emp_cov = df[channels].cov().to_numpy()
print("Cholesky L (lower triangular):")
print(L.round(2))
print()
print(f"P(total_lift > {TARGET_TOTAL_LIFT_EUR:,.0f} EUR) = {prob_hit_target:.3f}")
print()
print("Empirical covariance from simulations (EUR^2):")
print(pd.DataFrame(emp_cov, index=channels, columns=channels).round(0))
print(f"\nInterpretation: the weekly lift target is hit about {prob_hit_target:.1%} of the time.")

Source: MML1

#linear-algebra#advanced-linear-algebra-and-decompositions#decompositions-svd-cholesky-qr

Keyboard shortcuts

Single keys, ignored while typing in a field or with Cmd, Ctrl or Alt held.

0
Home
1 2 3 4
Work, Writing, Tools, About
j / k
Next / previous section on this page
n / p
Next / previous project or essay
u
Up one level
l
Deutsch
t
Light or dark theme
?
This list
Esc
Close