1#[derive(Debug, Clone)]
6pub struct RunningStats {
7 count: u64,
8 mean: f64,
9 m2: f64,
10}
11
12impl RunningStats {
13 pub fn new() -> Self {
15 Self {
16 count: 0,
17 mean: 0.0,
18 m2: 0.0,
19 }
20 }
21
22 pub fn update(&mut self, value: f64) {
24 self.count += 1;
25 let delta = value - self.mean;
26 self.mean += delta / self.count as f64;
27 let delta2 = value - self.mean;
28 self.m2 += delta * delta2;
29 }
30
31 pub fn batch_update(&mut self, values: &[f64]) {
33 for &v in values {
34 self.update(v);
35 }
36 }
37
38 pub fn mean(&self) -> f64 {
40 self.mean
41 }
42
43 pub fn var(&self) -> f64 {
45 if self.count < 1 {
46 return 0.0;
47 }
48 self.m2 / self.count as f64
49 }
50
51 pub fn std(&self) -> f64 {
53 self.var().sqrt()
54 }
55
56 pub fn normalize(&self, value: f64) -> f64 {
59 let s = self.std();
60 if s < 1e-8 {
61 return 0.0;
62 }
63 (value - self.mean) / s
64 }
65
66 pub fn count(&self) -> u64 {
68 self.count
69 }
70
71 pub fn reset(&mut self) {
73 self.count = 0;
74 self.mean = 0.0;
75 self.m2 = 0.0;
76 }
77}
78
79impl Default for RunningStats {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85#[derive(Debug, Clone)]
91pub struct RunningStatsVec {
92 dim: usize,
93 count: u64,
94 mean: Vec<f64>,
95 m2: Vec<f64>,
96}
97
98impl RunningStatsVec {
99 pub fn new(dim: usize) -> Self {
101 Self {
102 dim,
103 count: 0,
104 mean: vec![0.0; dim],
105 m2: vec![0.0; dim],
106 }
107 }
108
109 #[inline]
115 pub fn update(&mut self, values: &[f64]) {
116 assert_eq!(
117 values.len(),
118 self.dim,
119 "expected {} dimensions, got {}",
120 self.dim,
121 values.len()
122 );
123 self.count += 1;
124 let n = self.count as f64;
125 for (i, &val) in values.iter().enumerate().take(self.dim) {
126 let delta = val - self.mean[i];
127 self.mean[i] += delta / n;
128 let delta2 = val - self.mean[i];
129 self.m2[i] += delta * delta2;
130 }
131 }
132
133 #[inline]
142 pub fn batch_update(&mut self, data: &[f64], batch_size: usize) {
143 assert_eq!(
144 data.len(),
145 batch_size * self.dim,
146 "expected {} elements (batch_size={} * dim={}), got {}",
147 batch_size * self.dim,
148 batch_size,
149 self.dim,
150 data.len()
151 );
152 for sample in data.chunks_exact(self.dim) {
153 self.update(sample);
154 }
155 }
156
157 #[inline]
159 pub fn mean(&self) -> Vec<f64> {
160 self.mean.clone()
161 }
162
163 #[inline]
165 pub fn mean_ref(&self) -> &[f64] {
166 &self.mean
167 }
168
169 #[inline]
171 pub fn var(&self) -> Vec<f64> {
172 if self.count < 1 {
173 return vec![0.0; self.dim];
174 }
175 let n = self.count as f64;
176 self.m2.iter().map(|&m| m / n).collect()
177 }
178
179 #[inline]
181 pub fn std(&self) -> Vec<f64> {
182 self.var().iter().map(|&v| v.sqrt()).collect()
183 }
184
185 #[inline]
191 pub fn normalize(&self, values: &[f64]) -> Vec<f64> {
192 assert_eq!(
193 values.len(),
194 self.dim,
195 "expected {} dimensions, got {}",
196 self.dim,
197 values.len()
198 );
199 let std = self.std();
200 values
201 .iter()
202 .zip(self.mean.iter())
203 .zip(std.iter())
204 .map(|((&v, &m), &s)| (v - m) / s.max(1e-8))
205 .collect()
206 }
207
208 #[inline]
214 pub fn normalize_batch(&self, data: &[f64], batch_size: usize) -> Vec<f64> {
215 assert_eq!(
216 data.len(),
217 batch_size * self.dim,
218 "expected {} elements (batch_size={} * dim={}), got {}",
219 batch_size * self.dim,
220 batch_size,
221 self.dim,
222 data.len()
223 );
224 let std = self.std();
225 let mut out = Vec::with_capacity(data.len());
226 for sample in data.chunks_exact(self.dim) {
227 for i in 0..self.dim {
228 out.push((sample[i] - self.mean[i]) / std[i].max(1e-8));
229 }
230 }
231 out
232 }
233
234 #[inline]
236 pub fn count(&self) -> u64 {
237 self.count
238 }
239
240 #[inline]
242 pub fn dim(&self) -> usize {
243 self.dim
244 }
245
246 pub fn reset(&mut self) {
248 self.count = 0;
249 self.mean.fill(0.0);
250 self.m2.fill(0.0);
251 }
252}
253
254#[derive(Debug, Clone)]
270pub struct ExponentialRunningStats {
271 alpha: f64,
272 mean: f64,
273 var: f64,
274 count: u64,
275 initialized: bool,
276}
277
278impl ExponentialRunningStats {
279 pub fn new(alpha: f64) -> Self {
288 assert!(
289 alpha > 0.0 && alpha < 1.0,
290 "alpha must be in (0, 1), got {alpha}"
291 );
292 Self {
293 alpha,
294 mean: 0.0,
295 var: 0.0,
296 count: 0,
297 initialized: false,
298 }
299 }
300
301 pub fn from_window(window: usize) -> Self {
303 assert!(window >= 1, "window must be >= 1, got {window}");
304 Self::new(2.0 / (window as f64 + 1.0))
305 }
306
307 pub fn from_halflife(halflife: f64) -> Self {
309 assert!(halflife > 0.0, "halflife must be > 0, got {halflife}");
310 Self::new(1.0 - (0.5_f64).powf(1.0 / halflife))
311 }
312
313 pub fn update(&mut self, value: f64) {
315 self.count += 1;
316 if !self.initialized {
317 self.mean = value;
318 self.var = 0.0;
319 self.initialized = true;
320 return;
321 }
322 let delta = value - self.mean;
323 self.mean += self.alpha * delta;
324 self.var = (1.0 - self.alpha) * (self.var + self.alpha * delta * delta);
326 }
327
328 pub fn batch_update(&mut self, values: &[f64]) {
330 for &v in values {
331 self.update(v);
332 }
333 }
334
335 pub fn mean(&self) -> f64 {
337 self.mean
338 }
339
340 pub fn var(&self) -> f64 {
342 self.var
343 }
344
345 pub fn std(&self) -> f64 {
347 self.var.sqrt()
348 }
349
350 pub fn normalize(&self, value: f64) -> f64 {
352 let s = self.std();
353 if s < 1e-8 {
354 return 0.0;
355 }
356 (value - self.mean) / s
357 }
358
359 pub fn count(&self) -> u64 {
361 self.count
362 }
363
364 pub fn alpha(&self) -> f64 {
366 self.alpha
367 }
368
369 pub fn reset(&mut self) {
371 self.mean = 0.0;
372 self.var = 0.0;
373 self.count = 0;
374 self.initialized = false;
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn running_stats_new_is_empty() {
384 let stats = RunningStats::new();
385 assert_eq!(stats.count(), 0);
386 }
387
388 #[test]
389 fn running_stats_single_sample() {
390 let mut stats = RunningStats::new();
391 stats.update(5.0);
392 assert!((stats.mean() - 5.0).abs() < 1e-10);
393 assert_eq!(stats.count(), 1);
394 let _ = stats.var();
395 let _ = stats.std();
396 }
397
398 #[test]
399 fn running_stats_welford_known_values() {
400 let mut stats = RunningStats::new();
401 for &x in &[2.0_f64, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
402 stats.update(x);
403 }
404 assert!(
405 (stats.mean() - 5.0).abs() < 1e-10,
406 "mean should be 5.0, got {}",
407 stats.mean()
408 );
409 assert!(
410 (stats.var() - 4.0).abs() < 1e-10,
411 "variance should be 4.0, got {}",
412 stats.var()
413 );
414 assert!(
415 (stats.std() - 2.0).abs() < 1e-10,
416 "std should be 2.0, got {}",
417 stats.std()
418 );
419 }
420
421 #[test]
422 fn running_stats_normalize_produces_z_score() {
423 let mut stats = RunningStats::new();
424 for &x in &[2.0_f64, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
425 stats.update(x);
426 }
427 let z = stats.normalize(5.0);
428 assert!(z.abs() < 1e-10, "normalize(mean) should be ~0, got {z}");
429 let z2 = stats.normalize(7.0);
430 assert!(
431 (z2 - 1.0).abs() < 1e-10,
432 "normalize(mean+std) should be ~1, got {z2}"
433 );
434 }
435
436 #[test]
437 fn running_stats_normalize_with_zero_std_does_not_panic() {
438 let mut stats = RunningStats::new();
439 stats.update(5.0);
440 stats.update(5.0);
441 stats.update(5.0);
442 let z = stats.normalize(5.0);
443 assert!(z.is_finite(), "normalize with zero std must be finite");
444 }
445
446 #[test]
447 fn running_stats_large_stream_numerically_stable() {
448 let mut stats = RunningStats::new();
449 let base = 1_000_000.0f64;
450 for i in 0..10_000 {
451 stats.update(base + (i as f64) * 0.001);
452 }
453 let expected_mean = base + 5.0 - 0.001 / 2.0;
454 assert!(
455 (stats.mean() - expected_mean).abs() < 0.01,
456 "mean imprecise for large offset: got {}, expected ~{expected_mean}",
457 stats.mean()
458 );
459 }
460
461 #[test]
462 fn running_stats_reset_clears_state() {
463 let mut stats = RunningStats::new();
464 for &x in &[1.0f64, 2.0, 3.0] {
465 stats.update(x);
466 }
467 stats.reset();
468 assert_eq!(stats.count(), 0);
469 }
470
471 #[test]
472 fn running_stats_nan_input_does_not_silently_corrupt() {
473 let mut stats = RunningStats::new();
474 stats.update(1.0);
475 stats.update(2.0);
476 let mean_before = stats.mean();
477 stats.update(f64::NAN);
478 let mean_after = stats.mean();
479 if mean_after.is_finite() {
480 assert!(
481 (mean_after - mean_before).abs() < 1e-10 || mean_after.is_nan(),
482 "NaN input corrupted finite mean: was {mean_before}, now {mean_after}"
483 );
484 }
485 }
486
487 #[test]
488 fn running_stats_batch_update() {
489 let mut stats = RunningStats::new();
490 stats.batch_update(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
491 assert!((stats.mean() - 5.0).abs() < 1e-10);
492 assert_eq!(stats.count(), 8);
493 }
494
495 #[test]
500 fn stats_vec_new_is_empty() {
501 let stats = RunningStatsVec::new(3);
502 assert_eq!(stats.count(), 0);
503 assert_eq!(stats.dim(), 3);
504 assert_eq!(stats.mean(), vec![0.0; 3]);
505 assert_eq!(stats.var(), vec![0.0; 3]);
506 }
507
508 #[test]
509 fn stats_vec_single_sample() {
510 let mut stats = RunningStatsVec::new(3);
511 stats.update(&[1.0, 2.0, 3.0]);
512 assert_eq!(stats.count(), 1);
513 assert_eq!(stats.mean(), vec![1.0, 2.0, 3.0]);
514 assert_eq!(stats.var(), vec![0.0, 0.0, 0.0]);
516 }
517
518 #[test]
519 fn stats_vec_known_values_per_dim() {
520 let mut stats = RunningStatsVec::new(2);
523 let samples: &[&[f64]] = &[
524 &[2.0, 1.0],
525 &[4.0, 1.0],
526 &[4.0, 1.0],
527 &[4.0, 1.0],
528 &[5.0, 1.0],
529 &[5.0, 1.0],
530 &[7.0, 1.0],
531 &[9.0, 1.0],
532 ];
533 for s in samples {
534 stats.update(s);
535 }
536 assert_eq!(stats.count(), 8);
537 let mean = stats.mean();
538 assert!((mean[0] - 5.0).abs() < 1e-10, "dim0 mean: {}", mean[0]);
539 assert!((mean[1] - 1.0).abs() < 1e-10, "dim1 mean: {}", mean[1]);
540 let var = stats.var();
541 assert!((var[0] - 4.0).abs() < 1e-10, "dim0 var: {}", var[0]);
542 assert!(var[1].abs() < 1e-10, "dim1 var: {}", var[1]);
543 let std = stats.std();
544 assert!((std[0] - 2.0).abs() < 1e-10, "dim0 std: {}", std[0]);
545 assert!(std[1].abs() < 1e-10, "dim1 std: {}", std[1]);
546 }
547
548 #[test]
549 fn stats_vec_batch_update_matches_sequential() {
550 let mut seq = RunningStatsVec::new(3);
551 let mut batch = RunningStatsVec::new(3);
552
553 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
554 for sample in data.chunks(3) {
555 seq.update(sample);
556 }
557 batch.batch_update(&data, 3);
558
559 assert_eq!(seq.count(), batch.count());
560 for i in 0..3 {
561 assert!(
562 (seq.mean()[i] - batch.mean()[i]).abs() < 1e-10,
563 "dim {i} mean mismatch"
564 );
565 assert!(
566 (seq.var()[i] - batch.var()[i]).abs() < 1e-10,
567 "dim {i} var mismatch"
568 );
569 }
570 }
571
572 #[test]
573 fn stats_vec_normalize_produces_z_scores() {
574 let mut stats = RunningStatsVec::new(2);
575 let dim0 = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
578 let dim1 = [10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0];
579 for i in 0..8 {
580 stats.update(&[dim0[i], dim1[i]]);
581 }
582
583 let z = stats.normalize(&[5.0, 45.0]);
585 assert!(z[0].abs() < 1e-10, "z[0] should be ~0, got {}", z[0]);
586 assert!(z[1].abs() < 1e-10, "z[1] should be ~0, got {}", z[1]);
587
588 let std = stats.std();
590 let z2 = stats.normalize(&[5.0 + std[0], 45.0 + std[1]]);
591 assert!(
592 (z2[0] - 1.0).abs() < 1e-10,
593 "z2[0] should be ~1, got {}",
594 z2[0]
595 );
596 assert!(
597 (z2[1] - 1.0).abs() < 1e-10,
598 "z2[1] should be ~1, got {}",
599 z2[1]
600 );
601 }
602
603 #[test]
604 fn stats_vec_normalize_with_zero_std_clamps() {
605 let mut stats = RunningStatsVec::new(2);
606 stats.update(&[5.0, 3.0]);
607 stats.update(&[5.0, 3.0]);
608 let z = stats.normalize(&[6.0, 4.0]);
610 assert!(z[0].is_finite(), "dim0 normalize must be finite");
611 assert!(z[1].is_finite(), "dim1 normalize must be finite");
612 assert!((z[0] - 1e8).abs() < 1.0, "dim0: {}", z[0]);
614 }
615
616 #[test]
617 fn stats_vec_normalize_batch() {
618 let mut stats = RunningStatsVec::new(2);
619 stats.update(&[0.0, 0.0]);
620 stats.update(&[10.0, 20.0]);
621 let data = [5.0, 10.0, 10.0, 20.0]; let out = stats.normalize_batch(&data, 2);
625 assert!(out[0].abs() < 1e-10, "sample0 dim0 should be 0");
626 assert!(out[1].abs() < 1e-10, "sample0 dim1 should be 0");
627 assert!((out[2] - 1.0).abs() < 1e-10, "sample1 dim0 should be 1");
628 assert!((out[3] - 1.0).abs() < 1e-10, "sample1 dim1 should be 1");
629 }
630
631 #[test]
632 fn stats_vec_reset_clears_state() {
633 let mut stats = RunningStatsVec::new(2);
634 stats.update(&[1.0, 2.0]);
635 stats.update(&[3.0, 4.0]);
636 stats.reset();
637 assert_eq!(stats.count(), 0);
638 assert_eq!(stats.dim(), 2);
639 assert_eq!(stats.mean(), vec![0.0, 0.0]);
640 }
641
642 #[test]
643 #[should_panic(expected = "expected 3 dimensions, got 2")]
644 fn stats_vec_update_wrong_dim_panics() {
645 let mut stats = RunningStatsVec::new(3);
646 stats.update(&[1.0, 2.0]);
647 }
648
649 #[test]
650 #[should_panic(expected = "expected 6 elements")]
651 fn stats_vec_batch_update_wrong_len_panics() {
652 let mut stats = RunningStatsVec::new(3);
653 stats.batch_update(&[1.0, 2.0, 3.0, 4.0], 2);
654 }
655
656 #[test]
657 #[should_panic(expected = "expected 2 dimensions, got 3")]
658 fn stats_vec_normalize_wrong_dim_panics() {
659 let stats = RunningStatsVec::new(2);
660 stats.normalize(&[1.0, 2.0, 3.0]);
661 }
662
663 #[test]
664 fn stats_vec_large_stream_numerically_stable() {
665 let mut stats = RunningStatsVec::new(2);
666 let base = 1_000_000.0f64;
667 for i in 0..10_000 {
668 let v = i as f64 * 0.001;
669 stats.update(&[base + v, -base - v]);
670 }
671 let expected_mean = base + 5.0 - 0.001 / 2.0;
672 let mean = stats.mean();
673 assert!(
674 (mean[0] - expected_mean).abs() < 0.01,
675 "dim0 mean imprecise: got {}, expected ~{expected_mean}",
676 mean[0]
677 );
678 assert!(
679 (mean[1] + expected_mean).abs() < 0.01,
680 "dim1 mean imprecise: got {}, expected ~{}",
681 mean[1],
682 -expected_mean
683 );
684 }
685
686 #[test]
687 fn stats_vec_hopper_like_multi_scale() {
688 let mut stats = RunningStatsVec::new(2);
690 for i in 0..1000 {
691 let t = i as f64 / 999.0;
692 let pos = -1.0 + 2.0 * t; let vel = -10.0 + 20.0 * t; stats.update(&[pos, vel]);
695 }
696 let std = stats.std();
697 assert!(
699 std[1] > std[0] * 5.0,
700 "velocity std ({}) should be much larger than position std ({})",
701 std[1],
702 std[0]
703 );
704 let z = stats.normalize(&[0.5, 5.0]);
706 assert!(
707 z[0].abs() < 5.0 && z[1].abs() < 5.0,
708 "normalized values should be moderate z-scores, got {:?}",
709 z
710 );
711 }
712
713 #[test]
718 fn ema_new_basic() {
719 let ema = ExponentialRunningStats::new(0.1);
720 assert_eq!(ema.count(), 0);
721 assert!((ema.alpha() - 0.1).abs() < 1e-10);
722 }
723
724 #[test]
725 fn ema_from_window() {
726 let ema = ExponentialRunningStats::from_window(19);
727 assert!((ema.alpha() - 0.1).abs() < 1e-10);
729 }
730
731 #[test]
732 fn ema_from_halflife() {
733 let ema = ExponentialRunningStats::from_halflife(10.0);
734 assert!(ema.alpha() > 0.0 && ema.alpha() < 1.0);
736 }
737
738 #[test]
739 fn ema_first_update_sets_mean() {
740 let mut ema = ExponentialRunningStats::new(0.1);
741 ema.update(5.0);
742 assert!((ema.mean() - 5.0).abs() < 1e-10);
743 assert_eq!(ema.count(), 1);
744 }
745
746 #[test]
747 fn ema_tracks_constant_signal() {
748 let mut ema = ExponentialRunningStats::new(0.1);
749 for _ in 0..100 {
750 ema.update(3.0);
751 }
752 assert!((ema.mean() - 3.0).abs() < 1e-8);
753 assert!(ema.var() < 1e-8);
754 }
755
756 #[test]
757 fn ema_adapts_to_level_shift() {
758 let mut ema = ExponentialRunningStats::new(0.1);
759 for _ in 0..50 {
761 ema.update(0.0);
762 }
763 assert!(ema.mean().abs() < 0.01);
764 for _ in 0..100 {
766 ema.update(10.0);
767 }
768 assert!((ema.mean() - 10.0).abs() < 0.1);
770 }
771
772 #[test]
773 fn ema_higher_alpha_adapts_faster() {
774 let mut fast = ExponentialRunningStats::new(0.5);
775 let mut slow = ExponentialRunningStats::new(0.01);
776 for _ in 0..20 {
777 fast.update(0.0);
778 slow.update(0.0);
779 }
780 for _ in 0..10 {
781 fast.update(10.0);
782 slow.update(10.0);
783 }
784 assert!(
786 (fast.mean() - 10.0).abs() < (slow.mean() - 10.0).abs(),
787 "fast={}, slow={}",
788 fast.mean(),
789 slow.mean()
790 );
791 }
792
793 #[test]
794 fn ema_normalize_zero_for_mean() {
795 let mut ema = ExponentialRunningStats::new(0.1);
796 for x in 0..100 {
797 ema.update(x as f64);
798 }
799 let z = ema.normalize(ema.mean());
801 assert!(z.abs() < 1e-8);
802 }
803
804 #[test]
805 fn ema_reset_clears_state() {
806 let mut ema = ExponentialRunningStats::new(0.1);
807 ema.update(5.0);
808 ema.update(10.0);
809 ema.reset();
810 assert_eq!(ema.count(), 0);
811 assert!((ema.mean()).abs() < 1e-10);
812 }
813
814 #[test]
815 #[should_panic(expected = "alpha must be in (0, 1)")]
816 fn ema_invalid_alpha_panics() {
817 ExponentialRunningStats::new(0.0);
818 }
819
820 #[test]
821 #[should_panic(expected = "alpha must be in (0, 1)")]
822 fn ema_alpha_one_panics() {
823 ExponentialRunningStats::new(1.0);
824 }
825
826 mod proptests {
827 use super::*;
828 use proptest::prelude::*;
829
830 proptest! {
831 #[test]
832 fn running_stats_mean_matches_batch_mean(
833 values in proptest::collection::vec(-1000.0f64..1000.0, 2..200)
834 ) {
835 let mut stats = RunningStats::new();
836 for &v in &values {
837 stats.update(v);
838 }
839 let batch_mean = values.iter().sum::<f64>() / values.len() as f64;
840 prop_assert!(
841 (stats.mean() - batch_mean).abs() < 1e-8,
842 "running mean {:.10} != batch mean {:.10}",
843 stats.mean(), batch_mean
844 );
845 }
846
847 #[test]
848 fn running_stats_variance_non_negative(
849 values in proptest::collection::vec(-1000.0f64..1000.0, 2..200)
850 ) {
851 let mut stats = RunningStats::new();
852 for &v in &values {
853 stats.update(v);
854 }
855 prop_assert!(stats.var() >= 0.0, "variance must be non-negative");
856 prop_assert!(stats.std() >= 0.0, "std must be non-negative");
857 }
858
859 #[test]
860 fn running_stats_std_equals_sqrt_var(
861 values in proptest::collection::vec(-100.0f64..100.0, 2..100)
862 ) {
863 let mut stats = RunningStats::new();
864 for &v in &values {
865 stats.update(v);
866 }
867 let computed_std = stats.var().sqrt();
868 prop_assert!(
869 (stats.std() - computed_std).abs() < 1e-10,
870 "std {} != sqrt(var) {}",
871 stats.std(), computed_std
872 );
873 }
874
875 #[test]
876 fn running_stats_count_matches_updates(
877 values in proptest::collection::vec(-100.0f64..100.0, 0..200)
878 ) {
879 let mut stats = RunningStats::new();
880 for &v in &values {
881 stats.update(v);
882 }
883 prop_assert_eq!(stats.count() as usize, values.len());
884 }
885
886 #[test]
887 fn stats_vec_per_dim_mean_matches_naive(
888 dim in 1usize..8,
889 n_samples in 2usize..50,
890 ) {
891 let mut data = Vec::with_capacity(n_samples * dim);
893 for s in 0..n_samples {
894 for d in 0..dim {
895 data.push((s as f64) * 0.1 + (d as f64) * 10.0);
896 }
897 }
898
899 let mut stats = RunningStatsVec::new(dim);
900 stats.batch_update(&data, n_samples);
901
902 for d in 0..dim {
904 let sum: f64 = (0..n_samples).map(|s| data[s * dim + d]).sum();
905 let naive_mean = sum / n_samples as f64;
906 prop_assert!(
907 (stats.mean()[d] - naive_mean).abs() < 1e-8,
908 "dim {d}: running mean {} != naive mean {}",
909 stats.mean()[d], naive_mean
910 );
911 }
912 }
913
914 #[test]
915 fn stats_vec_variance_non_negative(
916 dim in 1usize..6,
917 n_samples in 2usize..50,
918 ) {
919 let mut data = Vec::with_capacity(n_samples * dim);
920 for s in 0..n_samples {
921 for d in 0..dim {
922 data.push((s as f64) * 0.7 - (d as f64) * 3.0);
923 }
924 }
925
926 let mut stats = RunningStatsVec::new(dim);
927 stats.batch_update(&data, n_samples);
928
929 for d in 0..dim {
930 prop_assert!(
931 stats.var()[d] >= 0.0,
932 "dim {d} variance must be non-negative, got {}",
933 stats.var()[d]
934 );
935 }
936 }
937
938 #[test]
939 fn stats_vec_normalize_roundtrip_z_mean_zero(
940 dim in 1usize..6,
941 n_samples in 5usize..50,
942 ) {
943 let mut data = Vec::with_capacity(n_samples * dim);
944 for s in 0..n_samples {
945 for d in 0..dim {
946 data.push((s as f64) * 1.3 + (d as f64) * 7.0);
947 }
948 }
949
950 let mut stats = RunningStatsVec::new(dim);
951 stats.batch_update(&data, n_samples);
952
953 let z = stats.normalize(&stats.mean());
955 for (d, &val) in z.iter().enumerate().take(dim) {
956 prop_assert!(
957 val.abs() < 1e-8,
958 "normalize(mean)[{d}] should be ~0, got {}",
959 val
960 );
961 }
962 }
963 }
964 }
965}