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

1"""Convert a covariance matrix to a correlation matrix.""" 

2 

3from __future__ import annotations 

4 

5import numpy as np 

6 

7from ..core.types import Matrix 

8 

9 

10def cov_to_corr(cov: Matrix, min_var: float = 1e-14) -> Matrix: 

11 """Convert a covariance matrix to a correlation matrix. 

12 

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]``. 

17 

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``. 

23 

24 Returns: 

25 Symmetrised correlation matrix of shape ``(N, N)`` with diagonal 

26 entries in ``{1.0, nan}``. 

27 

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