qualia_core_db/solvers/statistics/
correlation.rs1use super::descriptive::mean;
8use super::distributions::students_t;
9
10pub fn pearson(x: &[f64], y: &[f64]) -> Option<f64> {
14 let n = x.len();
15 if n != y.len() || n < 2 {
16 return None;
17 }
18 let mx = mean(x)?;
19 let my = mean(y)?;
20 let mut num = 0.0;
21 let mut dx2 = 0.0;
22 let mut dy2 = 0.0;
23 let mut i = 0;
24 while i < n {
25 let dx = x[i] - mx;
26 let dy = y[i] - my;
27 num += dx * dy;
28 dx2 += dx * dx;
29 dy2 += dy * dy;
30 i += 1;
31 }
32 let denom = (dx2 * dy2).sqrt();
33 if denom == 0.0 {
34 return Some(0.0);
35 }
36 Some(num / denom)
37}
38
39pub fn rank_into(values: &[f64], idx_scratch: &mut [usize], ranks_out: &mut [f64]) -> Option<()> {
43 let n = values.len();
44 if idx_scratch.len() != n || ranks_out.len() != n {
45 return None;
46 }
47 for i in 0..n {
48 idx_scratch[i] = i;
49 }
50 idx_scratch.sort_unstable_by(|&a, &b| {
51 values[a]
52 .partial_cmp(&values[b])
53 .unwrap_or(core::cmp::Ordering::Equal)
54 });
55
56 let mut i = 0;
61 while i < n {
62 let mut j = i;
63 while j + 1 < n && values[idx_scratch[j + 1]] == values[idx_scratch[i]] {
64 j += 1;
65 }
66 let avg_rank = ((i + 1) as f64 + (j + 1) as f64) / 2.0;
67 for k in i..=j {
68 ranks_out[idx_scratch[k]] = avg_rank;
69 }
70 i = j + 1;
71 }
72 Some(())
73}
74
75pub fn kendall(x: &[f64], y: &[f64]) -> Option<f64> {
78 let n = x.len();
79 if n != y.len() || n < 2 {
80 return None;
81 }
82 let mut concordant: i64 = 0;
83 let mut discordant: i64 = 0;
84 for i in 0..n {
85 for j in (i + 1)..n {
86 let p = (x[i] - x[j]) * (y[i] - y[j]);
87 if p > 0.0 {
88 concordant += 1;
89 } else if p < 0.0 {
90 discordant += 1;
91 }
92 }
93 }
94 let total = concordant + discordant;
95 if total == 0 {
96 return Some(0.0);
97 }
98 Some((concordant - discordant) as f64 / total as f64)
99}
100
101pub fn spearman(x: &[f64], y: &[f64]) -> Option<f64> {
106 let n = x.len();
107 if n != y.len() || n < 2 {
108 return None;
109 }
110 let mut idx = vec![0usize; n];
111 let mut rx = vec![0.0f64; n];
112 let mut ry = vec![0.0f64; n];
113 rank_into(x, &mut idx, &mut rx)?;
114 rank_into(y, &mut idx, &mut ry)?;
115 pearson(&rx, &ry)
116}
117
118pub fn correlation_p_value(r: f64, n: usize) -> Option<f64> {
122 if n < 3 {
123 return None;
124 }
125 let df = (n - 2) as f64;
126 let denom = 1.0 - r * r;
127 if denom <= 0.0 {
128 return Some(0.0); }
130 let t = r * (df / denom).sqrt();
131 Some(students_t::two_sided_p(t, df))
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 const EPS: f64 = 1e-9;
138
139 #[test]
140 fn pearson_perfect_and_anti() {
141 let x = [1.0, 2.0, 3.0, 4.0];
142 let up = [2.0, 4.0, 6.0, 8.0];
143 let down = [8.0, 6.0, 4.0, 2.0];
144 assert!((pearson(&x, &up).unwrap() - 1.0).abs() < EPS);
145 assert!((pearson(&x, &down).unwrap() + 1.0).abs() < EPS);
146 }
147
148 #[test]
149 fn pearson_guards_and_zero_variance() {
150 assert_eq!(pearson(&[1.0], &[1.0]), None); assert_eq!(pearson(&[1.0, 2.0], &[1.0]), None); assert_eq!(pearson(&[5.0, 5.0, 5.0], &[1.0, 2.0, 3.0]), Some(0.0)); }
154
155 #[test]
156 fn rank_handles_ties() {
157 let v = [10.0, 20.0, 20.0, 40.0];
158 let mut idx = [0usize; 4];
159 let mut ranks = [0.0; 4];
160 rank_into(&v, &mut idx, &mut ranks).unwrap();
161 assert!((ranks[0] - 1.0).abs() < EPS);
163 assert!((ranks[1] - 2.5).abs() < EPS);
164 assert!((ranks[2] - 2.5).abs() < EPS);
165 assert!((ranks[3] - 4.0).abs() < EPS);
166 assert!(rank_into(&v, &mut [0usize; 3], &mut ranks).is_none()); }
168
169 #[test]
170 fn spearman_via_rank_then_pearson_is_monotonic_1() {
171 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
173 let y = [1.0, 4.0, 9.0, 16.0, 25.0];
174 let mut ix = [0usize; 5];
175 let mut iy = [0usize; 5];
176 let mut rx = [0.0; 5];
177 let mut ry = [0.0; 5];
178 rank_into(&x, &mut ix, &mut rx).unwrap();
179 rank_into(&y, &mut iy, &mut ry).unwrap();
180 assert!((pearson(&rx, &ry).unwrap() - 1.0).abs() < EPS);
181 }
182
183 #[test]
184 fn kendall_signs() {
185 let x = [1.0, 2.0, 3.0];
186 assert!((kendall(&x, &[1.0, 2.0, 3.0]).unwrap() - 1.0).abs() < EPS);
187 assert!((kendall(&x, &[3.0, 2.0, 1.0]).unwrap() + 1.0).abs() < EPS);
188 assert_eq!(kendall(&[1.0], &[1.0]), None);
189 }
190
191 #[test]
192 fn spearman_is_one_for_monotone_nonlinear() {
193 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
194 let y = [1.0, 4.0, 9.0, 16.0, 25.0]; assert!((spearman(&x, &y).unwrap() - 1.0).abs() < EPS);
196 assert!(pearson(&x, &y).unwrap() < 1.0 - 1e-6);
198 assert_eq!(spearman(&[1.0], &[1.0]), None);
199 }
200
201 #[test]
202 fn correlation_p_value_significance() {
203 let r = 0.95;
205 let p = correlation_p_value(r, 30).unwrap();
206 assert!(p < 1e-6, "strong correlation must be significant: p={p}");
207 let p2 = correlation_p_value(0.1, 10).unwrap();
209 assert!(
210 p2 > 0.5,
211 "weak correlation should not be significant: p={p2}"
212 );
213 assert_eq!(correlation_p_value(0.5, 2), None); assert_eq!(correlation_p_value(1.0, 10), Some(0.0)); }
216}