In a previous post I compared several parameterizations for estimating the covariance parameter of latent exchangeable normal random variables using PyMC. These parameterizations were necesssary because for quite some time before the official release of PyMC version 4.0, the beta lacked support for multivariate random variables. Support for these distributions has since been added, rendering the workaround parameterizations in that post superfluous. Even so, after writing that post I remained curious about the closed-form solution for the Cholesky decomposition of the covariance matrix for exchangeable normal random variables, and eventually was able to derive it. This post presents that closed-form solution along with a numerical verification of its correctness.
Recall that a vector of random variables is exchangeable if every permutation of its entries has the same joint distribution. A set of exchangeable normal random variables, X1,…,XT∼N(μ,σ2) is parameterized by their marginal mean, μ, marginal scale, σ, and the Pearson correlation coefficient ρ=Cov(Xi,Xj)σ2.
In this post we will assume μ=0 and σ=1 for simplicity; the results are straightforward to generalize. With this assumption, the covariance matrix is
Σρ=(1ρ⋯ρρρ1⋯ρρρρ⋯1ρρρ⋯ρ1)∈RT×T.The previous post used SymPy to calculate the Cholesky decomposition of this matrix for small values of T. It was obvious that there was a pattern, but the we did not pursue it at the time, as the SymPy factorization could be lambdified and the result applied to Aesara tensors to facilitate inference in PyMC. This post will prove and numerically verify the following closed-form solution for the Cholesky decomposition of Σρ.
Let −1T−1≤ρ<1. Define d1=1 and ℓ1=ρ. For 1<j≤T, let dj=√d2j−1−ℓ2j−1
Finally, let LT=(d100⋯0ℓ1d20⋯0ℓ1ℓ2d3⋯0⋱ℓ1ℓ2ℓ3⋯dT)∈RT×T.
Claim ΣT=LTL⊤T.
Proof (by induction):
When T=1 or T=2, the claim is easily verified manually.
Assume, for all 1≤n≤T, that Σn=LnL⊤n.
It will be useful to block partition Ln as Ln=(Ln−10vndn),
Now, LT+1L⊤T+1=(ΣTLTv⊤T+1vT+1L⊤TvT+1v⊤T+1+d2T+1),
First, we have that LTv⊤T+1=(LT−10vTdT)(v⊤TℓT)=(LT−1v⊤TvTv⊤T+dT⋅ℓT).
Second, we have that vT+1v⊤T+1+d2T+1=vTv⊤T+ℓ2T+d2T+1=1−d2T+ℓ2T+(√d2T−ℓ2T)2=1.
QED
To validate these calculations numerically we will use Hypothesis, a Python library for generative testing. First we make the necessary Python imports.
from hypothesis import assume, given
from hypothesis.strategies import integers, floats
import numpy as np
The following function generates the covariance matrix, Σρ, for given values of ρ and T.
def get_cov_mat(ρ, T):
return np.eye(T) + ρ * (1 - np.eye(T))
The next function implements the closed-form solution for the Cholesky decomposition of Σρ presented above.
def get_cov_mat_chol(ρ, T):
diag = np.empty(T)
tril = np.empty(T)
diag[0] = 1
tril[0] = ρ
for j in range(1, T):
diag[j] = np.sqrt(diag[j - 1]**2 - tril[j - 1]**2)
tril[j] = (ρ - 1) / diag[j] + diag[j] if j < T - 1 else 0
return np.diag(diag) + np.tril(tril, k=-1)
This final function is a Hypothesis test that verifies that we have, in fact, computed the Cholesky decomposition of Σρ.
@given(floats(-0.5, 1), integers(2, 1000))
def test_cov_mat_chol(ρ, T):
assume(-1 / (T - 1) <= ρ < 1)
Σ = get_cov_mat(ρ, T)
chol = get_cov_mat_chol(ρ, T)
# check that chol is lower diagonal
assert (chol == np.tril(chol)).all()
# check that "chol is a factorization of Σ
assert np.allclose(Σ, chol @ chol.T)
The given
decorator tells Hypothesis to generate floating point values in the interval [−12,1] for ρ and integers between 2 and 1,000 for T. The upper bound on values generated for T is not necessary but ensures the test runs in a reasonable amount of time. The assume
call ensures that ρ is in the appropriate interval based on the size of the random vector. Given this guidance, Hypothesis will generate random values of ρ and T to run the test with.
test_cov_mat_chol()
The test passes, numerically validating our closed-form solution for the Cholesky decomposition.
This post is available as a Jupyter notebook here.
%load_ext watermark
%watermark -n -u -v -iv -p hypothesis
Last updated: Sun Sep 25 2022 Python implementation: CPython Python version : 3.10.6 IPython version : 8.4.0 hypothesis: 6.54.6 numpy: 1.23.2