solow_stats/
correlation_tools.rs1use ndarray::{Array1, Array2};
14use solow_core::error::{Error, Result};
15use solow_linalg::eigh;
16
17pub fn cov2corr(cov: &Array2<f64>) -> Array2<f64> {
21 let (corr, _) = cov2corr_std(cov);
22 corr
23}
24
25pub fn cov2corr_std(cov: &Array2<f64>) -> (Array2<f64>, Array1<f64>) {
28 let k = cov.nrows();
29 let std: Array1<f64> = Array1::from_iter((0..k).map(|i| cov[[i, i]].sqrt()));
30 let mut corr = Array2::<f64>::zeros((k, k));
31 for i in 0..k {
32 for j in 0..k {
33 corr[[i, j]] = cov[[i, j]] / (std[i] * std[j]);
34 }
35 }
36 (corr, std)
37}
38
39pub fn corr2cov(corr: &Array2<f64>, std: &Array1<f64>) -> Array2<f64> {
42 let k = corr.nrows();
43 let mut cov = Array2::<f64>::zeros((k, k));
44 for i in 0..k {
45 for j in 0..k {
46 cov[[i, j]] = corr[[i, j]] * std[i] * std[j];
47 }
48 }
49 cov
50}
51
52fn clip_evals(x: &Array2<f64>, value: f64) -> Result<(Array2<f64>, bool)> {
55 let (w, v) = eigh(x)?;
56 let clipped = w.iter().any(|&e| e < value);
57 let k = w.len();
58 let mut scaled = v.clone(); for j in 0..k {
61 let ev = w[j].max(value);
62 for i in 0..k {
63 scaled[[i, j]] *= ev;
64 }
65 }
66 let x_new = scaled.dot(&v.t());
67 Ok((x_new, clipped))
68}
69
70pub fn corr_clipped(corr: &Array2<f64>, threshold: f64) -> Result<Array2<f64>> {
76 let (x_new, clipped) = clip_evals(corr, threshold)?;
77 if !clipped {
78 return Ok(corr.clone());
79 }
80 let k = x_new.nrows();
82 let d: Vec<f64> = (0..k).map(|i| x_new[[i, i]].sqrt()).collect();
83 let mut out = Array2::<f64>::zeros((k, k));
84 for i in 0..k {
85 for j in 0..k {
86 out[[i, j]] = x_new[[i, j]] / (d[i] * d[j]);
87 }
88 }
89 Ok(out)
90}
91
92pub fn corr_nearest(corr: &Array2<f64>, threshold: f64, n_fact: usize) -> Result<Array2<f64>> {
100 let k = corr.nrows();
101 if corr.ncols() != k {
102 return Err(Error::Shape("matrix is not square".into()));
103 }
104 let mut diff = Array2::<f64>::zeros((k, k));
105 let mut x_new = corr.clone();
106 let max_iter = k * n_fact;
107 for _ in 0..max_iter {
108 let x_adj = &x_new - &diff;
109 let (x_psd, clipped) = clip_evals(&x_adj, threshold)?;
110 if !clipped {
111 x_new = x_psd;
112 break;
113 }
114 diff = &x_psd - &x_adj;
115 x_new = x_psd;
116 for i in 0..k {
117 x_new[[i, i]] = 1.0;
118 }
119 }
120 Ok(x_new)
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum NearestMethod {
126 Clipped,
128 Nearest,
130}
131
132pub fn cov_nearest(
139 cov: &Array2<f64>,
140 method: NearestMethod,
141 threshold: f64,
142 n_fact: usize,
143) -> Result<Array2<f64>> {
144 let (corr, std) = cov2corr_std(cov);
145 let corr_fixed = match method {
146 NearestMethod::Clipped => corr_clipped(&corr, threshold)?,
147 NearestMethod::Nearest => corr_nearest(&corr, threshold, n_fact)?,
148 };
149 Ok(corr2cov(&corr_fixed, &std))
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use ndarray::array;
156
157 #[test]
158 fn cov2corr_roundtrip() {
159 let cov = array![[4.0, 2.0, 0.0], [2.0, 9.0, -3.0], [0.0, -3.0, 16.0]];
160 let (corr, std) = cov2corr_std(&cov);
161 assert!((corr[[0, 0]] - 1.0).abs() < 1e-12);
162 assert!((corr[[0, 1]] - 2.0 / (2.0 * 3.0)).abs() < 1e-12);
163 let back = corr2cov(&corr, &std);
164 for i in 0..3 {
165 for j in 0..3 {
166 assert!((back[[i, j]] - cov[[i, j]]).abs() < 1e-12);
167 }
168 }
169 }
170
171 #[test]
172 fn corr_nearest_makes_psd() {
173 let corr = array![[1.0, 0.9, -0.9], [0.9, 1.0, 0.9], [-0.9, 0.9, 1.0]];
175 let fixed = corr_nearest(&corr, 1e-7, 100).unwrap();
176 let (w, _) = eigh(&fixed).unwrap();
177 assert!(w[0] >= -1e-8, "smallest eigenvalue {} negative", w[0]);
178 for i in 0..3 {
179 assert!((fixed[[i, i]] - 1.0).abs() < 1e-6);
180 }
181 }
182
183 #[test]
184 fn corr_clipped_psd_passthrough() {
185 let corr = array![[1.0, 0.2], [0.2, 1.0]];
186 let fixed = corr_clipped(&corr, 1e-7).unwrap();
187 for i in 0..2 {
189 for j in 0..2 {
190 assert!((fixed[[i, j]] - corr[[i, j]]).abs() < 1e-15);
191 }
192 }
193 }
194}