chronix_analytics/preprocess/
features.rs1#[inline]
11pub fn lag(values: &[f64], offset: usize) -> Vec<f64> {
12 let n = values.len();
13 let mut out = vec![f64::NAN; n];
14 if offset < n {
15 out[offset..n].copy_from_slice(&values[..(n - offset)]);
16 }
17 out
18}
19
20pub fn diff(values: &[f64], order: usize) -> Vec<f64> {
25 if order == 0 {
26 return values.to_vec();
27 }
28 let mut prev = values.to_vec();
29 for _ in 0..order {
30 let mut cur = vec![f64::NAN; prev.len()];
31 for i in 1..prev.len() {
32 cur[i] = prev[i] - prev[i - 1];
33 }
34 prev = cur;
35 }
36 prev
37}
38
39pub fn pct_change(values: &[f64]) -> Vec<f64> {
42 let n = values.len();
43 let mut out = vec![f64::NAN; n];
44 for i in 1..n {
45 let prev = values[i - 1];
46 out[i] = if prev.abs() < f64::EPSILON {
47 f64::NAN
48 } else {
49 (values[i] - prev) / prev
50 };
51 }
52 out
53}
54
55#[derive(Debug, Default, Clone, Copy)]
72struct Moments {
73 n: usize,
74 mean: f64,
75 m2: f64,
76}
77
78impl Moments {
79 fn add(&mut self, x: f64) {
80 if !x.is_finite() {
81 return;
82 }
83 self.n += 1;
84 let d = x - self.mean;
85 self.mean += d / self.n as f64;
86 self.m2 += d * (x - self.mean);
87 }
88
89 fn remove(&mut self, x: f64) {
90 if !x.is_finite() {
91 return;
92 }
93 if self.n <= 1 {
94 *self = Self::default();
95 return;
96 }
97 let prev_mean = self.mean;
98 self.n -= 1;
99 self.mean -= (x - prev_mean) / self.n as f64;
100 self.m2 -= (x - prev_mean) * (x - self.mean);
101 if self.m2 < 0.0 {
103 self.m2 = 0.0;
104 }
105 }
106
107 fn from_slice(xs: &[f64]) -> Self {
108 let mut m = Self::default();
109 for &x in xs {
110 m.add(x);
111 }
112 m
113 }
114
115 fn sample_std(&self) -> f64 {
117 if self.n < 2 {
118 return f64::NAN;
119 }
120 (self.m2 / (self.n - 1) as f64).sqrt()
121 }
122}
123
124const RECOMPUTE_INTERVAL: usize = 1024;
129
130#[must_use]
137pub fn rolling_mean(values: &[f64], window: usize) -> Vec<f64> {
138 let n = values.len();
139 if window == 0 || window > n {
140 return vec![f64::NAN; n];
141 }
142 let mut out = vec![f64::NAN; n];
143 let mut m = Moments::from_slice(&values[..window]);
144 out[window - 1] = if m.n == 0 { f64::NAN } else { m.mean };
145 for i in window..n {
146 m.remove(values[i - window]);
147 m.add(values[i]);
148 if (i - window + 1).is_multiple_of(RECOMPUTE_INTERVAL) {
149 m = Moments::from_slice(&values[i + 1 - window..=i]);
150 }
151 out[i] = if m.n == 0 { f64::NAN } else { m.mean };
152 }
153 out
154}
155
156#[must_use]
165pub fn rolling_std(values: &[f64], window: usize) -> Vec<f64> {
166 let n = values.len();
167 if window == 0 || window > n {
168 return vec![f64::NAN; n];
169 }
170 let mut out = vec![f64::NAN; n];
171 let mut m = Moments::from_slice(&values[..window]);
172 out[window - 1] = m.sample_std();
173 for i in window..n {
174 m.remove(values[i - window]);
175 m.add(values[i]);
176 if (i - window + 1).is_multiple_of(RECOMPUTE_INTERVAL) {
177 m = Moments::from_slice(&values[i + 1 - window..=i]);
178 }
179 out[i] = m.sample_std();
180 }
181 out
182}
183
184#[derive(Debug, Default, Clone, Copy)]
188struct CoMoments {
189 n: usize,
190 mx: f64,
191 my: f64,
192 m2x: f64,
193 m2y: f64,
194 cxy: f64,
195}
196
197impl CoMoments {
198 fn add(&mut self, x: f64, y: f64) {
199 if !x.is_finite() || !y.is_finite() {
200 return;
201 }
202 self.n += 1;
203 let nf = self.n as f64;
204 let dx = x - self.mx;
205 let dy = y - self.my;
206 self.mx += dx / nf;
207 self.my += dy / nf;
208 self.m2x += dx * (x - self.mx);
209 self.m2y += dy * (y - self.my);
210 self.cxy += dx * (y - self.my);
211 }
212
213 fn remove(&mut self, x: f64, y: f64) {
214 if !x.is_finite() || !y.is_finite() {
215 return;
216 }
217 if self.n <= 1 {
218 *self = Self::default();
219 return;
220 }
221 let (px, py) = (self.mx, self.my);
222 self.n -= 1;
223 let nf = self.n as f64;
224 self.mx -= (x - px) / nf;
225 self.my -= (y - py) / nf;
226 self.m2x -= (x - px) * (x - self.mx);
227 self.m2y -= (y - py) * (y - self.my);
228 self.cxy -= (x - px) * (y - self.my);
229 if self.m2x < 0.0 {
230 self.m2x = 0.0;
231 }
232 if self.m2y < 0.0 {
233 self.m2y = 0.0;
234 }
235 }
236
237 fn from_slices(xs: &[f64], ys: &[f64]) -> Self {
238 let mut c = Self::default();
239 for (&x, &y) in xs.iter().zip(ys) {
240 c.add(x, y);
241 }
242 c
243 }
244
245 fn correlation(&self) -> f64 {
249 if self.n < 2 {
250 return f64::NAN;
251 }
252 let denom = (self.m2x * self.m2y).sqrt();
253 if denom < f64::EPSILON {
254 return f64::NAN;
255 }
256 self.cxy / denom
257 }
258}
259
260#[must_use]
265pub fn rolling_corr(a: &[f64], b: &[f64], window: usize) -> Vec<f64> {
266 let n = a.len().min(b.len());
267 if window < 2 || window > n {
268 return vec![f64::NAN; n];
269 }
270 let mut out = vec![f64::NAN; n];
271 let mut c = CoMoments::from_slices(&a[..window], &b[..window]);
272 out[window - 1] = c.correlation();
273 for i in window..n {
274 c.remove(a[i - window], b[i - window]);
275 c.add(a[i], b[i]);
276 if (i - window + 1).is_multiple_of(RECOMPUTE_INTERVAL) {
277 c = CoMoments::from_slices(&a[i + 1 - window..=i], &b[i + 1 - window..=i]);
278 }
279 out[i] = c.correlation();
280 }
281 out
282}
283
284#[must_use]
295pub fn zscore(values: &[f64]) -> Vec<f64> {
296 let n = values.len();
297 let m = Moments::from_slice(values);
298 let std = m.sample_std();
299 if !std.is_finite() || std < f64::EPSILON {
300 return vec![f64::NAN; n];
301 }
302 values
303 .iter()
304 .map(|v| {
305 if v.is_finite() {
306 (v - m.mean) / std
307 } else {
308 f64::NAN
309 }
310 })
311 .collect()
312}
313
314#[must_use]
328pub fn ewm(values: &[f64], alpha: f64) -> Vec<f64> {
329 let n = values.len();
330 let alpha = alpha.clamp(f64::EPSILON, 1.0);
331 let mut out = vec![f64::NAN; n];
332 let mut state: Option<f64> = None;
333 for i in 0..n {
334 let v = values[i];
335 if v.is_finite() {
336 state = Some(match state {
337 None => v,
338 Some(prev) => alpha * v + (1.0 - alpha) * prev,
339 });
340 }
341 if let Some(cur) = state {
344 out[i] = cur;
345 }
346 }
347 out
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 fn approx(a: f64, b: f64, eps: f64) -> bool {
355 if a.is_nan() && b.is_nan() {
356 return true;
357 }
358 (a - b).abs() < eps
359 }
360
361 #[test]
362 fn test_lag_basic() {
363 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
364 let l = lag(&v, 2);
365 assert!(l[0].is_nan());
366 assert!(l[1].is_nan());
367 assert_eq!(l[2], 1.0);
368 assert_eq!(l[3], 2.0);
369 assert_eq!(l[4], 3.0);
370 }
371
372 #[test]
373 fn test_lag_zero_offset() {
374 let v = vec![10.0, 20.0];
375 let l = lag(&v, 0);
376 assert_eq!(l, v);
377 }
378
379 #[test]
380 fn test_diff_order_1() {
381 let v = vec![1.0, 3.0, 6.0, 10.0];
382 let d = diff(&v, 1);
383 assert!(d[0].is_nan());
384 assert_eq!(d[1], 2.0);
385 assert_eq!(d[2], 3.0);
386 assert_eq!(d[3], 4.0);
387 }
388
389 #[test]
390 fn test_diff_order_2() {
391 let v = vec![1.0, 3.0, 6.0, 10.0, 15.0];
392 let d = diff(&v, 2);
393 assert!(d[0].is_nan());
394 assert!(d[1].is_nan());
395 assert_eq!(d[2], 1.0); assert_eq!(d[3], 1.0);
397 assert_eq!(d[4], 1.0);
398 }
399
400 #[test]
401 fn test_diff_order_0() {
402 let v = vec![1.0, 2.0, 3.0];
403 assert_eq!(diff(&v, 0), v);
404 }
405
406 #[test]
407 fn test_pct_change() {
408 let v = vec![100.0, 110.0, 99.0];
409 let p = pct_change(&v);
410 assert!(p[0].is_nan());
411 assert!(approx(p[1], 0.1, 1e-9));
412 assert!(approx(p[2], -0.1, 1e-9));
413 }
414
415 #[test]
416 fn test_pct_change_zero_denominator() {
417 let v = vec![0.0, 5.0];
418 let p = pct_change(&v);
419 assert!(p[1].is_nan());
420 }
421
422 #[test]
423 fn test_rolling_std() {
424 let v = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
425 let r = rolling_std(&v, 3);
426 assert!(r[0].is_nan());
427 assert!(r[1].is_nan());
428 let expected: f64 = ((2.0_f64 - 10.0 / 3.0).powi(2)
430 + (4.0_f64 - 10.0 / 3.0).powi(2)
431 + (4.0_f64 - 10.0 / 3.0).powi(2))
432 / 2.0;
433 assert!(approx(r[2], expected.sqrt(), 1e-9));
434 }
435
436 #[test]
437 fn test_rolling_std_window_one() {
438 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
445 let r = rolling_std(&v, 1);
446 assert_eq!(r.len(), v.len());
447 for val in &r {
448 assert!(
449 val.is_nan(),
450 "rolling_std with window=1 is undefined, got {val}"
451 );
452 }
453 }
454
455 #[test]
456 fn test_rolling_corr_perfect() {
457 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
458 let b = vec![2.0, 4.0, 6.0, 8.0, 10.0];
459 let r = rolling_corr(&a, &b, 3);
460 assert!(r[0].is_nan());
461 assert!(r[1].is_nan());
462 for &v in &r[2..] {
463 assert!(approx(v, 1.0, 1e-9));
464 }
465 }
466
467 #[test]
468 fn test_rolling_corr_negative() {
469 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
470 let b = vec![5.0, 4.0, 3.0, 2.0, 1.0];
471 let r = rolling_corr(&a, &b, 3);
472 for &v in &r[2..] {
473 assert!(approx(v, -1.0, 1e-9));
474 }
475 }
476
477 #[test]
478 fn test_zscore() {
479 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
480 let z = zscore(&v);
481 assert_eq!(z.len(), 5);
482 let std_dev = 2.5_f64.sqrt();
484 assert!(approx(z[0], -2.0 / std_dev, 1e-9));
485 assert!(approx(z[2], 0.0, 1e-9));
486 assert!(approx(z[4], 2.0 / std_dev, 1e-9));
487 }
488
489 #[test]
490 fn test_zscore_constant() {
491 let v = vec![5.0, 5.0, 5.0];
492 let z = zscore(&v);
493 assert!(z.iter().all(|v| v.is_nan()));
494 }
495
496 #[test]
497 fn test_ewm() {
498 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
499 let e = ewm(&v, 0.5);
500 assert_eq!(e[0], 1.0);
501 assert!(approx(e[1], 1.5, 1e-9)); assert!(approx(e[2], 2.25, 1e-9)); }
504
505 #[test]
506 fn test_ewm_alpha_one() {
507 let v = vec![1.0, 10.0, 100.0];
508 let e = ewm(&v, 1.0);
509 assert_eq!(e, v);
511 }
512
513 #[test]
514 fn test_ewm_empty() {
515 let e = ewm(&[], 0.5);
516 assert!(e.is_empty());
517 }
518}