solow_stats/
correlation.rs1use solow_core::{Error, Result};
5
6#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct CorrelationResult {
9 pub statistic: f64,
11 pub pvalue: f64,
13}
14
15pub fn pearsonr(x: &[f64], y: &[f64]) -> Result<CorrelationResult> {
17 let n = x.len();
18 if n < 3 || y.len() != n {
19 return Err(Error::Value("pearsonr: need n ≥ 3 and matched lengths".into()));
20 }
21 let mean_x: f64 = x.iter().sum::<f64>() / n as f64;
22 let mean_y: f64 = y.iter().sum::<f64>() / n as f64;
23 let mut sxy = 0.0_f64;
24 let mut sxx = 0.0_f64;
25 let mut syy = 0.0_f64;
26 for i in 0..n {
27 let dx = x[i] - mean_x;
28 let dy = y[i] - mean_y;
29 sxy += dx * dy;
30 sxx += dx * dx;
31 syy += dy * dy;
32 }
33 let denom = (sxx * syy).sqrt();
34 if denom < 1e-300 {
35 return Err(Error::Value("pearsonr: at least one column has zero variance".into()));
36 }
37 let r = (sxy / denom).clamp(-1.0, 1.0);
38 let pvalue = if r.abs() >= 1.0 - 1e-14 {
40 0.0
41 } else {
42 let df = (n - 2) as f64;
43 let t = r * (df / (1.0 - r * r)).sqrt();
44 2.0 * student_t_survival(t.abs(), df)
45 };
46 Ok(CorrelationResult { statistic: r, pvalue })
47}
48
49pub fn spearmanr(x: &[f64], y: &[f64]) -> Result<CorrelationResult> {
51 if x.len() != y.len() || x.len() < 3 {
52 return Err(Error::Value("spearmanr: need n ≥ 3 and matched lengths".into()));
53 }
54 let rx = ranks_with_ties(x);
55 let ry = ranks_with_ties(y);
56 pearsonr(&rx, &ry)
57}
58
59pub fn kendalltau(x: &[f64], y: &[f64]) -> Result<CorrelationResult> {
61 let n = x.len();
62 if y.len() != n || n < 3 {
63 return Err(Error::Value("kendalltau: need n ≥ 3 and matched lengths".into()));
64 }
65 let mut concordant = 0_i64;
66 let mut discordant = 0_i64;
67 let mut ties_x = 0_i64;
68 let mut ties_y = 0_i64;
69 for i in 0..n {
70 for j in (i + 1)..n {
71 let dx = x[i] - x[j];
72 let dy = y[i] - y[j];
73 let sx = dx.signum();
74 let sy = dy.signum();
75 if dx == 0.0 && dy == 0.0 {
76 } else if dx == 0.0 {
78 ties_x += 1;
79 } else if dy == 0.0 {
80 ties_y += 1;
81 } else if sx == sy {
82 concordant += 1;
83 } else {
84 discordant += 1;
85 }
86 }
87 }
88 let n0 = n as f64 * (n as f64 - 1.0) / 2.0;
89 let tau_b = (concordant - discordant) as f64
90 / (((n0 - ties_x as f64) * (n0 - ties_y as f64)).sqrt().max(1e-300));
91 let var = (2.0 * (2.0 * n as f64 + 5.0)) / (9.0 * n as f64 * (n as f64 - 1.0));
93 let z = tau_b / var.sqrt();
94 let pvalue = 2.0 * standard_normal_survival(z.abs());
95 Ok(CorrelationResult { statistic: tau_b, pvalue })
96}
97
98fn ranks_with_ties(x: &[f64]) -> Vec<f64> {
99 let n = x.len();
100 let mut idx: Vec<usize> = (0..n).collect();
101 idx.sort_by(|&a, &b| x[a].partial_cmp(&x[b]).unwrap());
102 let mut ranks = vec![0.0_f64; n];
103 let mut i = 0;
104 while i < n {
105 let mut j = i;
106 while j + 1 < n && x[idx[j + 1]] == x[idx[i]] {
107 j += 1;
108 }
109 let avg = ((i + j) as f64 + 2.0) / 2.0; for k in i..=j {
111 ranks[idx[k]] = avg;
112 }
113 i = j + 1;
114 }
115 ranks
116}
117
118fn standard_normal_survival(z: f64) -> f64 {
119 0.5 * erfc(z / std::f64::consts::SQRT_2)
120}
121
122fn erfc(x: f64) -> f64 {
123 let a1 = 0.254_829_592;
125 let a2 = -0.284_496_736;
126 let a3 = 1.421_413_741;
127 let a4 = -1.453_152_027;
128 let a5 = 1.061_405_429;
129 let p = 0.327_591_1;
130 let sign = if x < 0.0 { -1.0 } else { 1.0 };
131 let ax = x.abs();
132 let t = 1.0 / (1.0 + p * ax);
133 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-ax * ax).exp();
134 1.0 - sign * y
135}
136
137fn student_t_survival(t: f64, df: f64) -> f64 {
138 if t <= 0.0 {
142 return 0.5;
143 }
144 let x = df / (df + t * t);
145 0.5 * regularised_incomplete_beta(x, df / 2.0, 0.5)
146}
147
148fn regularised_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
149 if x <= 0.0 {
150 return 0.0;
151 }
152 if x >= 1.0 {
153 return 1.0;
154 }
155 let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
156 let front = ((a * x.ln() + b * (1.0 - x).ln()) - ln_beta).exp() / a;
157 if x < (a + 1.0) / (a + b + 2.0) {
158 front * betacf(x, a, b)
159 } else {
160 1.0 - front * betacf(1.0 - x, b, a) * (front / front).max(1.0)
161 }
162}
163
164fn betacf(x: f64, a: f64, b: f64) -> f64 {
165 let mut c = 1.0_f64;
166 let qab = a + b;
167 let qap = a + 1.0;
168 let qam = a - 1.0;
169 let mut d = 1.0 - qab * x / qap;
170 if d.abs() < 1e-300 {
171 d = 1e-300;
172 }
173 d = 1.0 / d;
174 let mut h = d;
175 for m in 1..200 {
176 let mf = m as f64;
177 let two_m = 2.0 * mf;
178 let mut aa = mf * (b - mf) * x / ((qam + two_m) * (a + two_m));
179 d = 1.0 + aa * d;
180 if d.abs() < 1e-300 {
181 d = 1e-300;
182 }
183 c = 1.0 + aa / c;
184 if c.abs() < 1e-300 {
185 c = 1e-300;
186 }
187 d = 1.0 / d;
188 h *= d * c;
189 aa = -(a + mf) * (qab + mf) * x / ((a + two_m) * (qap + two_m));
190 d = 1.0 + aa * d;
191 if d.abs() < 1e-300 {
192 d = 1e-300;
193 }
194 c = 1.0 + aa / c;
195 if c.abs() < 1e-300 {
196 c = 1e-300;
197 }
198 d = 1.0 / d;
199 let delta = d * c;
200 h *= delta;
201 if (delta - 1.0).abs() < 3e-15 {
202 break;
203 }
204 }
205 h
206}
207
208fn ln_gamma(x: f64) -> f64 {
209 let g = 7.0;
211 let cof = [
212 0.999_999_999_999_809_93,
213 676.520_368_121_885_1,
214 -1_259.139_216_722_402_8,
215 771.323_428_777_653_13,
216 -176.615_029_162_140_59,
217 12.507_343_278_686_905,
218 -0.138_571_095_265_720_12,
219 9.984_369_578_019_571_5e-6,
220 1.505_632_735_149_311_6e-7,
221 ];
222 if x < 0.5 {
223 std::f64::consts::PI.ln()
224 - (std::f64::consts::PI * x).sin().ln()
225 - ln_gamma(1.0 - x)
226 } else {
227 let x = x - 1.0;
228 let mut a = cof[0];
229 let t = x + g + 0.5;
230 for (i, &c) in cof.iter().enumerate().skip(1) {
231 a += c / (x + i as f64);
232 }
233 0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + a.ln()
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn pearsonr_recovers_perfect_positive_correlation() {
243 let x = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0];
244 let y = vec![2.0_f64, 4.0, 6.0, 8.0, 10.0];
245 let r = pearsonr(&x, &y).unwrap();
246 assert!((r.statistic - 1.0).abs() < 1e-12);
247 assert!(r.pvalue < 1e-6);
248 }
249
250 #[test]
251 fn spearmanr_handles_ties_correctly() {
252 let x = vec![1.0_f64, 2.0, 2.0, 3.0, 4.0];
253 let y = vec![1.0_f64, 3.0, 3.0, 5.0, 7.0];
254 let r = spearmanr(&x, &y).unwrap();
255 assert!(r.statistic > 0.9);
256 }
257
258 #[test]
259 fn kendalltau_returns_a_value_in_the_valid_range() {
260 let x = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0];
261 let y = vec![5.0_f64, 4.0, 3.0, 2.0, 1.0];
262 let r = kendalltau(&x, &y).unwrap();
263 assert!((r.statistic - (-1.0)).abs() < 1e-12);
264 assert!(r.pvalue < 0.1);
265 }
266}