wickra_core/indicators/
spearman_correlation.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
55pub struct SpearmanCorrelation {
56 period: usize,
57 window: VecDeque<(f64, f64)>,
58 scratch: Vec<(f64, usize)>,
60 rx: Vec<f64>,
62 ry: Vec<f64>,
63}
64
65impl SpearmanCorrelation {
66 pub fn new(period: usize) -> Result<Self> {
71 if period < 2 {
72 return Err(Error::InvalidPeriod {
73 message: "spearman correlation needs period >= 2",
74 });
75 }
76 Ok(Self {
77 period,
78 window: VecDeque::with_capacity(period),
79 scratch: Vec::with_capacity(period),
80 rx: vec![0.0; period],
81 ry: vec![0.0; period],
82 })
83 }
84
85 pub const fn period(&self) -> usize {
87 self.period
88 }
89}
90
91fn rank_into(
95 values: impl Iterator<Item = f64>,
96 ranks_out: &mut [f64],
97 scratch: &mut Vec<(f64, usize)>,
98) {
99 scratch.clear();
100 for (i, v) in values.enumerate() {
101 scratch.push((v, i));
102 }
103 scratch.sort_by(|a, b| a.0.total_cmp(&b.0));
104 let n = scratch.len();
105 let mut i = 0;
106 while i < n {
107 let mut j = i + 1;
108 while j < n && scratch[j].0 == scratch[i].0 {
109 j += 1;
110 }
111 let mid = (i as f64 + 1.0 + j as f64) / 2.0;
114 for k in i..j {
115 ranks_out[scratch[k].1] = mid;
116 }
117 i = j;
118 }
119}
120
121impl Indicator for SpearmanCorrelation {
122 type Input = (f64, f64);
123 type Output = f64;
124
125 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
126 if self.window.len() == self.period {
127 self.window.pop_front();
128 }
129 self.window.push_back(input);
130 if self.window.len() < self.period {
131 return None;
132 }
133 rank_into(
135 self.window.iter().map(|p| p.0),
136 &mut self.rx,
137 &mut self.scratch,
138 );
139 rank_into(
140 self.window.iter().map(|p| p.1),
141 &mut self.ry,
142 &mut self.scratch,
143 );
144 let n = self.period as f64;
148 let mut sum_x = 0.0;
149 let mut sum_y = 0.0;
150 let mut sum_xx = 0.0;
151 let mut sum_yy = 0.0;
152 let mut sum_xy = 0.0;
153 for i in 0..self.period {
154 let x = self.rx[i];
155 let y = self.ry[i];
156 sum_x += x;
157 sum_y += y;
158 sum_xx += x * x;
159 sum_yy += y * y;
160 sum_xy += x * y;
161 }
162 let mean_x = sum_x / n;
163 let mean_y = sum_y / n;
164 let var_x = (sum_xx / n - mean_x * mean_x).max(0.0);
165 let var_y = (sum_yy / n - mean_y * mean_y).max(0.0);
166 let cov = sum_xy / n - mean_x * mean_y;
167 let denom = (var_x * var_y).sqrt();
168 if denom == 0.0 {
169 return Some(0.0);
170 }
171 Some((cov / denom).clamp(-1.0, 1.0))
172 }
173
174 fn reset(&mut self) {
175 self.window.clear();
176 self.scratch.clear();
177 self.rx.iter_mut().for_each(|r| *r = 0.0);
178 self.ry.iter_mut().for_each(|r| *r = 0.0);
179 }
180
181 fn warmup_period(&self) -> usize {
182 self.period
183 }
184
185 fn is_ready(&self) -> bool {
186 self.window.len() == self.period
187 }
188
189 fn name(&self) -> &'static str {
190 "SpearmanCorrelation"
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::traits::BatchExt;
198 use approx::assert_relative_eq;
199
200 #[test]
201 fn rejects_period_below_two() {
202 assert!(SpearmanCorrelation::new(0).is_err());
203 assert!(SpearmanCorrelation::new(1).is_err());
204 assert!(SpearmanCorrelation::new(2).is_ok());
205 }
206
207 #[test]
208 fn accessors_and_metadata() {
209 let s = SpearmanCorrelation::new(14).unwrap();
210 assert_eq!(s.period(), 14);
211 assert_eq!(s.warmup_period(), 14);
212 assert_eq!(s.name(), "SpearmanCorrelation");
213 }
214
215 #[test]
216 fn perfect_monotone_relationship_is_one() {
217 let pairs: Vec<(f64, f64)> = (1..=10)
220 .map(|i| (f64::from(i), (f64::from(i)).powi(3)))
221 .collect();
222 let last = SpearmanCorrelation::new(5)
223 .unwrap()
224 .batch(&pairs)
225 .into_iter()
226 .flatten()
227 .last()
228 .unwrap();
229 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
230 }
231
232 #[test]
233 fn perfect_inverse_is_minus_one() {
234 let pairs: Vec<(f64, f64)> = (1..=10)
235 .map(|i| (f64::from(i), 1.0 / (f64::from(i))))
236 .collect();
237 let last = SpearmanCorrelation::new(5)
238 .unwrap()
239 .batch(&pairs)
240 .into_iter()
241 .flatten()
242 .last()
243 .unwrap();
244 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
245 }
246
247 #[test]
248 fn constant_channel_yields_zero() {
249 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (f64::from(i), 7.0)).collect();
250 let last = SpearmanCorrelation::new(5)
251 .unwrap()
252 .batch(&pairs)
253 .into_iter()
254 .flatten()
255 .last()
256 .unwrap();
257 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
258 }
259
260 #[test]
261 fn output_in_minus_one_to_one_range() {
262 let pairs: Vec<(f64, f64)> = (0..60)
263 .map(|i| {
264 let t = f64::from(i);
265 (100.0 + t.sin() * 5.0, 50.0 + (t * 0.7).cos() * 3.0)
266 })
267 .collect();
268 let mut s = SpearmanCorrelation::new(20).unwrap();
269 for v in s.batch(&pairs).into_iter().flatten() {
270 assert!((-1.0..=1.0).contains(&v));
271 }
272 }
273
274 #[test]
275 fn handles_ties_via_mid_ranks() {
276 let pairs = [(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (3.0, 4.0)];
279 let last = SpearmanCorrelation::new(4)
280 .unwrap()
281 .batch(&pairs)
282 .into_iter()
283 .flatten()
284 .last()
285 .unwrap();
286 assert!(last > 0.0 && last < 1.0);
289 }
290
291 #[test]
292 fn reset_clears_state() {
293 let mut s = SpearmanCorrelation::new(5).unwrap();
294 s.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0), (5.0, 10.0)]);
295 assert!(s.is_ready());
296 s.reset();
297 assert!(!s.is_ready());
298 assert_eq!(s.update((1.0, 1.0)), None);
299 }
300
301 #[test]
302 fn batch_equals_streaming() {
303 let pairs: Vec<(f64, f64)> = (0..60)
304 .map(|i| {
305 let t = f64::from(i);
306 (t.sin() + (t * 0.1).cos(), (t * 0.3).cos())
307 })
308 .collect();
309 let batch = SpearmanCorrelation::new(14).unwrap().batch(&pairs);
310 let mut b = SpearmanCorrelation::new(14).unwrap();
311 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
312 assert_eq!(batch, streamed);
313 }
314}