Coverage for src/cvx/linalg/covariance/cov_to_corr.py: 100%
17 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 18:56 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 18:56 +0000
1"""Convert a covariance matrix to a correlation matrix."""
3from __future__ import annotations
5import numpy as np
7from ..core.types import Matrix
10def cov_to_corr(cov: Matrix, min_var: float = 1e-14) -> Matrix:
11 """Convert a covariance matrix to a correlation matrix.
13 Off-diagonal entries are symmetrised by averaging the upper and lower
14 triangles, so floating-point asymmetry in *cov* does not propagate.
15 Diagonal entries are set to ``1.0`` when the variance is above *min_var*
16 and to ``nan`` otherwise. All entries are clipped to ``[-1, 1]``.
18 Args:
19 cov: Square covariance matrix of shape ``(N, N)``.
20 min_var: Threshold below which a variance is treated as zero;
21 the corresponding row and column are filled with ``nan``.
22 Defaults to ``1e-14``.
24 Returns:
25 Symmetrised correlation matrix of shape ``(N, N)`` with diagonal
26 entries in ``{1.0, nan}``.
28 Example:
29 >>> import numpy as np
30 >>> from cvx.linalg import cov_to_corr
31 >>> cov = np.array([[4.0, 2.0], [2.0, 9.0]])
32 >>> corr = cov_to_corr(cov)
33 >>> np.allclose(np.diag(corr), [1.0, 1.0])
34 True
35 >>> float(round(corr[0, 1], 6))
36 0.333333
37 """
38 var = np.diag(cov)
39 denom = np.sqrt(np.outer(var, var))
40 with np.errstate(divide="ignore", invalid="ignore"):
41 corr = np.where(denom > min_var, cov / denom, np.nan)
42 corr = np.clip(corr, -1.0, 1.0)
43 n = len(var)
44 idx = np.arange(n)
45 corr[idx, idx] = np.where(var > min_var, 1.0, np.nan)
46 tril_i, tril_j = np.tril_indices(n, k=-1)
47 avg = 0.5 * (corr[tril_i, tril_j] + corr[tril_j, tril_i])
48 corr[tril_i, tril_j] = avg
49 corr[tril_j, tril_i] = avg
50 return corr