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#[must_use]
62pub fn rolling_mean(values: &[f64], window: usize) -> Vec<f64> {
63 let n = values.len();
64 if window == 0 || window > n {
65 return vec![f64::NAN; n];
66 }
67 let mut out = vec![f64::NAN; n];
68 let w = window as f64;
69 let mut sum: f64 = values[..window].iter().sum();
70 out[window - 1] = sum / w;
71 for i in window..n {
72 sum += values[i] - values[i - window];
73 if (i - window + 1).is_multiple_of(1024) {
76 sum = values[i + 1 - window..=i].iter().sum();
77 }
78 out[i] = sum / w;
79 }
80 out
81}
82
83pub fn rolling_std(values: &[f64], window: usize) -> Vec<f64> {
87 let n = values.len();
88 if window == 0 || window > n {
89 return vec![f64::NAN; n];
90 }
91 if window == 1 {
93 return vec![0.0; n];
94 }
95 let mut out = vec![f64::NAN; n];
96 let w = window as f64;
97
98 let mut mean = 0.0_f64;
100 let mut m2 = 0.0_f64;
101 for (count_0, &v) in values.iter().enumerate().take(window) {
102 let count = (count_0 + 1) as f64;
103 let delta = v - mean;
104 mean += delta / count;
105 let delta2 = v - mean;
106 m2 += delta * delta2;
107 }
108 out[window - 1] = (m2 / (w - 1.0)).sqrt();
109
110 for i in window..n {
113 let old = values[i - window];
114 let new = values[i];
115
116 let old_mean = mean;
118 mean += (new - old) / w;
119 m2 += (new - old) * (new - mean + old - old_mean);
120 if m2 < 0.0 {
122 m2 = 0.0;
123 }
124
125 if (i - window + 1).is_multiple_of(1024) {
127 let start = i + 1 - window;
128 let slice = &values[start..=i];
129 mean = slice.iter().sum::<f64>() / w;
130 m2 = slice.iter().map(|v| (v - mean).powi(2)).sum::<f64>();
131 }
132
133 out[i] = (m2 / (w - 1.0)).sqrt();
134 }
135 out
136}
137
138pub fn rolling_corr(a: &[f64], b: &[f64], window: usize) -> Vec<f64> {
143 let n = a.len().min(b.len());
144 if window < 2 || window > n {
145 return vec![f64::NAN; n];
146 }
147 let mut out = vec![f64::NAN; n];
148 let w = window as f64;
149
150 let mut sum_a = 0.0_f64;
152 let mut sum_b = 0.0_f64;
153 let mut sum_ab = 0.0_f64;
154 let mut sum_a2 = 0.0_f64;
155 let mut sum_b2 = 0.0_f64;
156 for j in 0..window {
157 sum_a += a[j];
158 sum_b += b[j];
159 sum_ab += a[j] * b[j];
160 sum_a2 += a[j] * a[j];
161 sum_b2 += b[j] * b[j];
162 }
163 let cov = sum_ab - sum_a * sum_b / w;
164 let va = (sum_a2 - sum_a * sum_a / w).max(0.0);
165 let vb = (sum_b2 - sum_b * sum_b / w).max(0.0);
166 let denom = (va * vb).sqrt();
167 out[window - 1] = if denom < f64::EPSILON {
168 f64::NAN
169 } else {
170 cov / denom
171 };
172
173 const RECOMPUTE_INTERVAL: usize = 1024;
176 for i in window..n {
177 if (i - window).is_multiple_of(RECOMPUTE_INTERVAL) && i > window {
178 sum_a = 0.0;
180 sum_b = 0.0;
181 sum_ab = 0.0;
182 sum_a2 = 0.0;
183 sum_b2 = 0.0;
184 for j in (i - window + 1)..=i {
185 sum_a += a[j];
186 sum_b += b[j];
187 sum_ab += a[j] * b[j];
188 sum_a2 += a[j] * a[j];
189 sum_b2 += b[j] * b[j];
190 }
191 } else {
192 let old_a = a[i - window];
193 let old_b = b[i - window];
194 let new_a = a[i];
195 let new_b = b[i];
196 sum_a += new_a - old_a;
197 sum_b += new_b - old_b;
198 sum_ab += new_a * new_b - old_a * old_b;
199 sum_a2 += new_a * new_a - old_a * old_a;
200 sum_b2 += new_b * new_b - old_b * old_b;
201 }
202
203 let cov = sum_ab - sum_a * sum_b / w;
204 let va = (sum_a2 - sum_a * sum_a / w).max(0.0);
205 let vb = (sum_b2 - sum_b * sum_b / w).max(0.0);
206 let denom = (va * vb).sqrt();
207 out[i] = if denom < f64::EPSILON {
208 f64::NAN
209 } else {
210 cov / denom
211 };
212 }
213 out
214}
215
216pub fn zscore(values: &[f64]) -> Vec<f64> {
222 let n = values.len();
223 if n < 2 {
224 return vec![f64::NAN; n];
225 }
226 let mean = values.iter().sum::<f64>() / n as f64;
227 let var = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
228 let std = var.sqrt();
229 if std < f64::EPSILON {
230 return vec![f64::NAN; n];
231 }
232 values.iter().map(|v| (v - mean) / std).collect()
233}
234
235pub fn ewm(values: &[f64], alpha: f64) -> Vec<f64> {
239 let n = values.len();
240 if n == 0 {
241 return vec![];
242 }
243 let alpha = alpha.clamp(f64::EPSILON, 1.0);
244 let mut out = Vec::with_capacity(n);
245 out.push(values[0]);
246 for i in 1..n {
247 let prev = out[i - 1];
248 out.push(alpha * values[i] + (1.0 - alpha) * prev);
249 }
250 out
251}
252
253#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn approx(a: f64, b: f64, eps: f64) -> bool {
261 if a.is_nan() && b.is_nan() {
262 return true;
263 }
264 (a - b).abs() < eps
265 }
266
267 #[test]
268 fn test_lag_basic() {
269 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
270 let l = lag(&v, 2);
271 assert!(l[0].is_nan());
272 assert!(l[1].is_nan());
273 assert_eq!(l[2], 1.0);
274 assert_eq!(l[3], 2.0);
275 assert_eq!(l[4], 3.0);
276 }
277
278 #[test]
279 fn test_lag_zero_offset() {
280 let v = vec![10.0, 20.0];
281 let l = lag(&v, 0);
282 assert_eq!(l, v);
283 }
284
285 #[test]
286 fn test_diff_order_1() {
287 let v = vec![1.0, 3.0, 6.0, 10.0];
288 let d = diff(&v, 1);
289 assert!(d[0].is_nan());
290 assert_eq!(d[1], 2.0);
291 assert_eq!(d[2], 3.0);
292 assert_eq!(d[3], 4.0);
293 }
294
295 #[test]
296 fn test_diff_order_2() {
297 let v = vec![1.0, 3.0, 6.0, 10.0, 15.0];
298 let d = diff(&v, 2);
299 assert!(d[0].is_nan());
300 assert!(d[1].is_nan());
301 assert_eq!(d[2], 1.0); assert_eq!(d[3], 1.0);
303 assert_eq!(d[4], 1.0);
304 }
305
306 #[test]
307 fn test_diff_order_0() {
308 let v = vec![1.0, 2.0, 3.0];
309 assert_eq!(diff(&v, 0), v);
310 }
311
312 #[test]
313 fn test_pct_change() {
314 let v = vec![100.0, 110.0, 99.0];
315 let p = pct_change(&v);
316 assert!(p[0].is_nan());
317 assert!(approx(p[1], 0.1, 1e-9));
318 assert!(approx(p[2], -0.1, 1e-9));
319 }
320
321 #[test]
322 fn test_pct_change_zero_denominator() {
323 let v = vec![0.0, 5.0];
324 let p = pct_change(&v);
325 assert!(p[1].is_nan());
326 }
327
328 #[test]
329 fn test_rolling_std() {
330 let v = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
331 let r = rolling_std(&v, 3);
332 assert!(r[0].is_nan());
333 assert!(r[1].is_nan());
334 let expected: f64 = ((2.0_f64 - 10.0 / 3.0).powi(2)
336 + (4.0_f64 - 10.0 / 3.0).powi(2)
337 + (4.0_f64 - 10.0 / 3.0).powi(2))
338 / 2.0;
339 assert!(approx(r[2], expected.sqrt(), 1e-9));
340 }
341
342 #[test]
343 fn test_rolling_std_window_one() {
344 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
346 let r = rolling_std(&v, 1);
347 assert_eq!(r.len(), v.len());
348 for val in &r {
349 assert_eq!(*val, 0.0, "rolling_std with window=1 must be 0");
350 }
351 }
352
353 #[test]
354 fn test_rolling_corr_perfect() {
355 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
356 let b = vec![2.0, 4.0, 6.0, 8.0, 10.0];
357 let r = rolling_corr(&a, &b, 3);
358 assert!(r[0].is_nan());
359 assert!(r[1].is_nan());
360 for &v in &r[2..] {
361 assert!(approx(v, 1.0, 1e-9));
362 }
363 }
364
365 #[test]
366 fn test_rolling_corr_negative() {
367 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
368 let b = vec![5.0, 4.0, 3.0, 2.0, 1.0];
369 let r = rolling_corr(&a, &b, 3);
370 for &v in &r[2..] {
371 assert!(approx(v, -1.0, 1e-9));
372 }
373 }
374
375 #[test]
376 fn test_zscore() {
377 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
378 let z = zscore(&v);
379 assert_eq!(z.len(), 5);
380 let std_dev = 2.5_f64.sqrt();
382 assert!(approx(z[0], -2.0 / std_dev, 1e-9));
383 assert!(approx(z[2], 0.0, 1e-9));
384 assert!(approx(z[4], 2.0 / std_dev, 1e-9));
385 }
386
387 #[test]
388 fn test_zscore_constant() {
389 let v = vec![5.0, 5.0, 5.0];
390 let z = zscore(&v);
391 assert!(z.iter().all(|v| v.is_nan()));
392 }
393
394 #[test]
395 fn test_ewm() {
396 let v = vec![1.0, 2.0, 3.0, 4.0, 5.0];
397 let e = ewm(&v, 0.5);
398 assert_eq!(e[0], 1.0);
399 assert!(approx(e[1], 1.5, 1e-9)); assert!(approx(e[2], 2.25, 1e-9)); }
402
403 #[test]
404 fn test_ewm_alpha_one() {
405 let v = vec![1.0, 10.0, 100.0];
406 let e = ewm(&v, 1.0);
407 assert_eq!(e, v);
409 }
410
411 #[test]
412 fn test_ewm_empty() {
413 let e = ewm(&[], 0.5);
414 assert!(e.is_empty());
415 }
416}