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 !input.0.is_finite() || !input.1.is_finite() {
127 return None;
128 }
129 if self.window.len() == self.period {
130 self.window.pop_front();
131 }
132 self.window.push_back(input);
133 if self.window.len() < self.period {
134 return None;
135 }
136 rank_into(
138 self.window.iter().map(|p| p.0),
139 &mut self.rx,
140 &mut self.scratch,
141 );
142 rank_into(
143 self.window.iter().map(|p| p.1),
144 &mut self.ry,
145 &mut self.scratch,
146 );
147 let n = self.period as f64;
151 let mut sum_x = 0.0;
152 let mut sum_y = 0.0;
153 let mut sum_xx = 0.0;
154 let mut sum_yy = 0.0;
155 let mut sum_xy = 0.0;
156 for i in 0..self.period {
157 let x = self.rx[i];
158 let y = self.ry[i];
159 sum_x += x;
160 sum_y += y;
161 sum_xx += x * x;
162 sum_yy += y * y;
163 sum_xy += x * y;
164 }
165 let mean_x = sum_x / n;
166 let mean_y = sum_y / n;
167 let var_x = (sum_xx / n - mean_x * mean_x).max(0.0);
168 let var_y = (sum_yy / n - mean_y * mean_y).max(0.0);
169 let cov = sum_xy / n - mean_x * mean_y;
170 let denom = (var_x * var_y).sqrt();
171 if denom == 0.0 {
172 return Some(0.0);
173 }
174 Some((cov / denom).clamp(-1.0, 1.0))
175 }
176
177 fn reset(&mut self) {
178 self.window.clear();
179 self.scratch.clear();
180 self.rx.iter_mut().for_each(|r| *r = 0.0);
181 self.ry.iter_mut().for_each(|r| *r = 0.0);
182 }
183
184 fn warmup_period(&self) -> usize {
185 self.period
186 }
187
188 fn is_ready(&self) -> bool {
189 self.window.len() == self.period
190 }
191
192 fn name(&self) -> &'static str {
193 "SpearmanCorrelation"
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::traits::BatchExt;
201 use approx::assert_relative_eq;
202
203 #[test]
204 fn rejects_period_below_two() {
205 assert!(SpearmanCorrelation::new(0).is_err());
206 assert!(SpearmanCorrelation::new(1).is_err());
207 assert!(SpearmanCorrelation::new(2).is_ok());
208 }
209
210 #[test]
211 fn accessors_and_metadata() {
212 let s = SpearmanCorrelation::new(14).unwrap();
213 assert_eq!(s.period(), 14);
214 assert_eq!(s.warmup_period(), 14);
215 assert_eq!(s.name(), "SpearmanCorrelation");
216 }
217
218 #[test]
219 fn perfect_monotone_relationship_is_one() {
220 let pairs: Vec<(f64, f64)> = (1..=10)
223 .map(|i| (f64::from(i), (f64::from(i)).powi(3)))
224 .collect();
225 let last = SpearmanCorrelation::new(5)
226 .unwrap()
227 .batch(&pairs)
228 .into_iter()
229 .flatten()
230 .last()
231 .unwrap();
232 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
233 }
234
235 #[test]
236 fn perfect_inverse_is_minus_one() {
237 let pairs: Vec<(f64, f64)> = (1..=10)
238 .map(|i| (f64::from(i), 1.0 / (f64::from(i))))
239 .collect();
240 let last = SpearmanCorrelation::new(5)
241 .unwrap()
242 .batch(&pairs)
243 .into_iter()
244 .flatten()
245 .last()
246 .unwrap();
247 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
248 }
249
250 #[test]
251 fn constant_channel_yields_zero() {
252 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (f64::from(i), 7.0)).collect();
253 let last = SpearmanCorrelation::new(5)
254 .unwrap()
255 .batch(&pairs)
256 .into_iter()
257 .flatten()
258 .last()
259 .unwrap();
260 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
261 }
262
263 #[test]
264 fn output_in_minus_one_to_one_range() {
265 let pairs: Vec<(f64, f64)> = (0..60)
266 .map(|i| {
267 let t = f64::from(i);
268 (100.0 + t.sin() * 5.0, 50.0 + (t * 0.7).cos() * 3.0)
269 })
270 .collect();
271 let mut s = SpearmanCorrelation::new(20).unwrap();
272 for v in s.batch(&pairs).into_iter().flatten() {
273 assert!((-1.0..=1.0).contains(&v));
274 }
275 }
276
277 #[test]
278 fn handles_ties_via_mid_ranks() {
279 let pairs = [(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (3.0, 4.0)];
282 let last = SpearmanCorrelation::new(4)
283 .unwrap()
284 .batch(&pairs)
285 .into_iter()
286 .flatten()
287 .last()
288 .unwrap();
289 assert!(last > 0.0 && last < 1.0);
292 }
293
294 #[test]
295 fn reset_clears_state() {
296 let mut s = SpearmanCorrelation::new(5).unwrap();
297 s.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0), (5.0, 10.0)]);
298 assert!(s.is_ready());
299 s.reset();
300 assert!(!s.is_ready());
301 assert_eq!(s.update((1.0, 1.0)), None);
302 }
303
304 #[test]
305 fn batch_equals_streaming() {
306 let pairs: Vec<(f64, f64)> = (0..60)
307 .map(|i| {
308 let t = f64::from(i);
309 (t.sin() + (t * 0.1).cos(), (t * 0.3).cos())
310 })
311 .collect();
312 let batch = SpearmanCorrelation::new(14).unwrap().batch(&pairs);
313 let mut b = SpearmanCorrelation::new(14).unwrap();
314 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
315 assert_eq!(batch, streamed);
316 }
317
318 #[test]
319 fn non_finite_input_returns_none() {
320 let mut s = SpearmanCorrelation::new(2).unwrap();
321 assert_eq!(s.update((f64::NAN, 1.0)), None);
322 assert_eq!(s.update((1.0, f64::INFINITY)), None);
323 assert_eq!(s.update((1.0, 2.0)), None);
325 assert!(s.update((2.0, 5.0)).is_some());
326 }
327}