1use std::fmt;
2
3use num_traits::ToPrimitive;
4use serde::{Deserialize, Serialize};
5
6use crate::Commute;
7
8#[inline]
10pub fn stddev<I, T>(x: I) -> f64
11where
12 I: IntoIterator<Item = T>,
13 T: ToPrimitive,
14{
15 x.into_iter().collect::<OnlineStats>().stddev()
16}
17
18#[inline]
20pub fn variance<I, T>(x: I) -> f64
21where
22 I: IntoIterator<Item = T>,
23 T: ToPrimitive,
24{
25 x.into_iter().collect::<OnlineStats>().variance()
26}
27
28#[inline]
30pub fn mean<I, T>(x: I) -> f64
31where
32 I: IntoIterator<Item = T>,
33 T: ToPrimitive,
34{
35 x.into_iter().collect::<OnlineStats>().mean()
36}
37
38#[allow(clippy::unsafe_derive_deserialize)]
43#[derive(Clone, Copy, Serialize, Deserialize, PartialEq)]
44pub struct OnlineStats {
45 size: u64, mean: f64, q: f64, hg_sums: bool, harmonic_sum: f64, geometric_sum: f64, n_positive: u64, n_zero: u64, n_negative: u64, }
60
61impl OnlineStats {
62 #[must_use]
66 pub fn new() -> OnlineStats {
67 Default::default()
68 }
69
70 #[must_use]
76 pub const fn without_hg_sums() -> OnlineStats {
77 OnlineStats {
78 size: 0,
79 mean: 0.0,
80 q: 0.0,
81 harmonic_sum: 0.0,
82 geometric_sum: 0.0,
83 n_zero: 0,
84 n_negative: 0,
85 n_positive: 0,
86 hg_sums: false,
87 }
88 }
89
90 #[inline]
93 const fn hg_opted_out(&self) -> bool {
94 !self.hg_sums && self.n_zero == 0 && self.n_negative == 0
95 }
96
97 #[must_use]
99 pub fn from_slice<T: ToPrimitive>(samples: &[T]) -> OnlineStats {
100 samples
102 .iter()
103 .map(|n| unsafe { n.to_f64().unwrap_unchecked() })
104 .collect()
105 }
106
107 #[must_use]
109 pub const fn mean(&self) -> f64 {
110 if self.is_empty() { f64::NAN } else { self.mean }
111 }
112
113 #[must_use]
115 pub fn stddev(&self) -> f64 {
116 self.variance().sqrt()
117 }
118
119 #[must_use]
122 pub const fn variance(&self) -> f64 {
123 if self.is_empty() {
124 f64::NAN
125 } else {
126 self.q / (self.size as f64)
127 }
128 }
129
130 #[must_use]
132 pub fn harmonic_mean(&self) -> f64 {
133 if self.is_empty() || self.n_zero > 0 || self.n_negative > 0 || self.hg_opted_out() {
134 f64::NAN
135 } else {
136 (self.size as f64) / self.harmonic_sum
137 }
138 }
139
140 #[must_use]
142 pub fn geometric_mean(&self) -> f64 {
143 if self.is_empty()
144 || self.n_negative > 0
145 || self.hg_opted_out()
146 || self.geometric_sum.is_nan()
147 || self.geometric_sum == f64::INFINITY
148 {
149 f64::NAN
150 } else if self.n_zero > 0 || self.geometric_sum == f64::NEG_INFINITY {
151 0.0
154 } else {
155 (self.geometric_sum / (self.size as f64)).exp()
156 }
157 }
158
159 #[must_use]
182 pub const fn n_counts(&self) -> (u64, u64, u64) {
183 (self.n_negative, self.n_zero, self.n_positive)
184 }
185
186 #[inline]
193 pub fn add<T: ToPrimitive>(&mut self, sample: &T) {
194 let sample = unsafe { sample.to_f64().unwrap_unchecked() };
196
197 if sample.is_nan() {
198 return;
199 }
200
201 self.size += 1;
204 let delta = sample - self.mean;
205
206 self.mean = delta.mul_add(1.0 / (self.size as f64), self.mean);
208
209 self.q = delta.mul_add(sample - self.mean, self.q);
211
212 if sample > 0.0 {
214 if self.hg_sums {
215 self.harmonic_sum += 1.0 / sample;
217 self.geometric_sum += sample.ln();
218 }
219 self.n_positive += 1;
220 } else {
221 if sample.is_sign_negative() {
223 self.n_negative += 1;
224 } else {
225 self.n_zero += 1;
226 }
227 self.hg_sums = false;
228 }
229 }
230
231 #[inline]
236 pub fn add_f64(&mut self, sample: f64) {
237 if sample.is_nan() {
238 return;
239 }
240
241 self.size += 1;
242 let delta = sample - self.mean;
243
244 self.mean = delta.mul_add(1.0 / (self.size as f64), self.mean);
245 self.q = delta.mul_add(sample - self.mean, self.q);
246
247 if sample > 0.0 {
249 if self.hg_sums {
250 self.harmonic_sum += 1.0 / sample;
251 self.geometric_sum += sample.ln();
252 }
253 self.n_positive += 1;
254 } else {
255 if sample.is_sign_negative() {
257 self.n_negative += 1;
258 } else {
259 self.n_zero += 1;
260 }
261 self.hg_sums = false;
262 }
263 }
264
265 #[inline]
269 pub fn add_null(&mut self) {
270 self.add_f64(0.0);
271 }
272
273 #[inline]
275 #[must_use]
276 pub const fn len(&self) -> usize {
277 self.size as usize
278 }
279
280 #[inline]
282 #[must_use]
283 pub const fn is_empty(&self) -> bool {
284 self.size == 0
285 }
286}
287
288impl Commute for OnlineStats {
289 #[inline]
290 fn merge(&mut self, v: OnlineStats) {
291 if v.is_empty() {
292 return;
293 }
294
295 let (s1, s2) = (self.size as f64, v.size as f64);
297 let total = s1 + s2;
298 let delta = self.mean - v.mean;
299 let meandiffsq = delta * delta;
300
301 self.size += v.size;
302
303 self.mean = (v.mean - self.mean).mul_add(s2 / total, self.mean);
306
307 self.q += meandiffsq.mul_add(s1 * s2 / total, v.q);
310
311 self.hg_sums = self.hg_sums && v.hg_sums;
312 self.harmonic_sum += v.harmonic_sum;
313 self.geometric_sum += v.geometric_sum;
314
315 self.n_zero += v.n_zero;
316 self.n_negative += v.n_negative;
317 self.n_positive += v.n_positive;
318 }
319}
320
321impl Default for OnlineStats {
322 fn default() -> OnlineStats {
323 OnlineStats {
324 size: 0,
325 mean: 0.0,
326 q: 0.0,
327 harmonic_sum: 0.0,
328 geometric_sum: 0.0,
329 n_zero: 0,
330 n_negative: 0,
331 n_positive: 0,
332 hg_sums: true,
333 }
334 }
335}
336
337impl fmt::Debug for OnlineStats {
338 #[inline]
339 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
340 write!(f, "{:.10} +/- {:.10}", self.mean(), self.stddev())
341 }
342}
343
344impl<T: ToPrimitive> FromIterator<T> for OnlineStats {
345 #[inline]
346 fn from_iter<I: IntoIterator<Item = T>>(it: I) -> OnlineStats {
347 let mut v = OnlineStats::new();
348 v.extend(it);
349 v
350 }
351}
352
353impl<T: ToPrimitive> Extend<T> for OnlineStats {
354 #[inline]
355 fn extend<I: IntoIterator<Item = T>>(&mut self, it: I) {
356 for sample in it {
357 self.add(&sample);
358 }
359 }
360}
361
362#[cfg(test)]
363mod test {
364 use super::{OnlineStats, mean, stddev, variance};
365 use {crate::Commute, crate::merge_all};
366
367 #[test]
368 fn online() {
369 let expected = OnlineStats::from_slice(&[1usize, 2, 3, 2, 4, 6]);
371
372 let var1 = OnlineStats::from_slice(&[1usize, 2, 3]);
373 let var2 = OnlineStats::from_slice(&[2usize, 4, 6]);
374 let mut got = var1;
375 got.merge(var2);
376 assert_eq!(expected.stddev(), got.stddev());
377 assert_eq!(expected.mean(), got.mean());
378 assert_eq!(expected.variance(), got.variance());
379 }
380
381 #[test]
382 fn online_empty() {
383 let expected = OnlineStats::new();
384 assert!(expected.is_empty());
385 }
386
387 #[test]
388 fn online_many() {
389 let expected = OnlineStats::from_slice(&[1usize, 2, 3, 2, 4, 6, 3, 6, 9]);
391
392 let vars = vec![
393 OnlineStats::from_slice(&[1usize, 2, 3]),
394 OnlineStats::from_slice(&[2usize, 4, 6]),
395 OnlineStats::from_slice(&[3usize, 6, 9]),
396 ];
397 assert_eq!(
398 expected.stddev(),
399 merge_all(vars.clone().into_iter()).unwrap().stddev()
400 );
401 assert_eq!(
402 expected.mean(),
403 merge_all(vars.clone().into_iter()).unwrap().mean()
404 );
405 assert_eq!(
406 expected.variance(),
407 merge_all(vars.into_iter()).unwrap().variance()
408 );
409 }
410
411 #[test]
412 fn test_means() {
413 let mut stats = OnlineStats::new();
414 stats.extend(vec![2.0f64, 4.0, 8.0]);
415
416 assert!((stats.mean() - 4.666666666667).abs() < 1e-10);
418
419 assert_eq!("3.42857143", format!("{:.8}", stats.harmonic_mean()));
421
422 assert!((stats.geometric_mean() - 4.0).abs() < 1e-10);
424 }
425
426 #[test]
427 fn test_means_with_negative() {
428 let mut stats = OnlineStats::new();
429 stats.extend(vec![-2.0f64, 2.0]);
430
431 assert!(stats.mean().abs() < 1e-10);
433
434 assert!(stats.geometric_mean().is_nan());
436
437 assert!(stats.harmonic_mean().is_nan());
439 }
440
441 #[test]
442 fn test_means_with_zero() {
443 let mut stats = OnlineStats::new();
444 stats.extend(vec![0.0f64, 4.0, 8.0]);
445
446 assert!((stats.mean() - 4.0).abs() < 1e-10);
448
449 assert!(stats.geometric_mean().abs() < 1e-10);
451
452 assert!(stats.harmonic_mean().is_nan());
454 }
455
456 #[test]
457 fn test_means_with_zero_and_negative_values() {
458 let mut stats = OnlineStats::new();
459 stats.extend(vec![-10i32, -5, 0, 5, 10]);
460
461 assert!(stats.mean().abs() < 1e-10);
463
464 assert!(stats.geometric_mean().is_nan());
466
467 assert!(stats.harmonic_mean().is_nan());
469 }
470
471 #[test]
472 fn test_means_single_value() {
473 let mut stats = OnlineStats::new();
474 stats.extend(vec![5.0f64]);
475
476 assert!((stats.mean() - 5.0).abs() < 1e-10);
478 assert!((stats.geometric_mean() - 5.0).abs() < 1e-10);
479 assert!((stats.harmonic_mean() - 5.0).abs() < 1e-10);
480 }
481
482 #[test]
483 fn test_means_empty() {
484 let stats = OnlineStats::new();
485
486 assert!(stats.mean().is_nan());
488 assert!(stats.geometric_mean().is_nan());
489 assert!(stats.harmonic_mean().is_nan());
490 }
491
492 #[test]
495 fn test_mean_wrapper_basic() {
496 let result = mean(vec![1.0f64, 2.0, 3.0, 4.0, 5.0]);
498 assert!((result - 3.0).abs() < 1e-10);
499
500 let result = mean(vec![1i32, 2, 3, 4, 5]);
502 assert!((result - 3.0).abs() < 1e-10);
503
504 let result = mean(vec![10u32, 20, 30]);
506 assert!((result - 20.0).abs() < 1e-10);
507 }
508
509 #[test]
510 fn test_mean_wrapper_empty() {
511 let result = mean(Vec::<f64>::new());
512 assert!(result.is_nan());
513 }
514
515 #[test]
516 fn test_mean_wrapper_single_element() {
517 assert!((mean(vec![42.0f64]) - 42.0).abs() < 1e-10);
518 assert!((mean(vec![100i32]) - 100.0).abs() < 1e-10);
519 assert!((mean(vec![0u8]) - 0.0).abs() < 1e-10);
520 }
521
522 #[test]
523 fn test_mean_wrapper_negative_values() {
524 let result = mean(vec![-5.0f64, 5.0]);
525 assert!(result.abs() < 1e-10); let result = mean(vec![-10i32, -20, -30]);
528 assert!((result - (-20.0)).abs() < 1e-10);
529 }
530
531 #[test]
532 fn test_mean_wrapper_various_numeric_types() {
533 assert!((mean(vec![1u8, 2, 3]) - 2.0).abs() < 1e-10);
535 assert!((mean(vec![1u16, 2, 3]) - 2.0).abs() < 1e-10);
536 assert!((mean(vec![1u64, 2, 3]) - 2.0).abs() < 1e-10);
537 assert!((mean(vec![1i8, 2, 3]) - 2.0).abs() < 1e-10);
538 assert!((mean(vec![1i16, 2, 3]) - 2.0).abs() < 1e-10);
539 assert!((mean(vec![1i64, 2, 3]) - 2.0).abs() < 1e-10);
540 assert!((mean(vec![1.0f32, 2.0, 3.0]) - 2.0).abs() < 1e-6);
541 assert!((mean(vec![1usize, 2, 3]) - 2.0).abs() < 1e-10);
542 assert!((mean(vec![1isize, 2, 3]) - 2.0).abs() < 1e-10);
543 }
544
545 #[test]
546 fn test_variance_wrapper_basic() {
547 let result = variance(vec![1.0f64, 2.0, 3.0, 4.0, 5.0]);
549 assert!((result - 2.0).abs() < 1e-10);
550
551 let result = variance(vec![1i32, 2, 3, 4, 5]);
553 assert!((result - 2.0).abs() < 1e-10);
554 }
555
556 #[test]
557 fn test_variance_wrapper_empty() {
558 let result = variance(Vec::<f64>::new());
559 assert!(result.is_nan());
560 }
561
562 #[test]
563 fn test_variance_wrapper_single_element() {
564 assert!(variance(vec![42.0f64]).abs() < 1e-10);
566 assert!(variance(vec![100i32]).abs() < 1e-10);
567 }
568
569 #[test]
570 fn test_variance_wrapper_identical_values() {
571 let result = variance(vec![5.0f64, 5.0, 5.0, 5.0]);
573 assert!(result.abs() < 1e-10);
574 }
575
576 #[test]
577 fn test_variance_wrapper_various_numeric_types() {
578 let expected = 2.0 / 3.0;
580 assert!((variance(vec![1u8, 2, 3]) - expected).abs() < 1e-10);
581 assert!((variance(vec![1u16, 2, 3]) - expected).abs() < 1e-10);
582 assert!((variance(vec![1i32, 2, 3]) - expected).abs() < 1e-10);
583 assert!((variance(vec![1i64, 2, 3]) - expected).abs() < 1e-10);
584 assert!((variance(vec![1usize, 2, 3]) - expected).abs() < 1e-10);
585 }
586
587 #[test]
588 fn test_stddev_wrapper_basic() {
589 let result = stddev(vec![1.0f64, 2.0, 3.0, 4.0, 5.0]);
591 assert!((result - 2.0f64.sqrt()).abs() < 1e-10);
592
593 let result = stddev(vec![1i32, 2, 3, 4, 5]);
595 assert!((result - 2.0f64.sqrt()).abs() < 1e-10);
596 }
597
598 #[test]
599 fn test_stddev_wrapper_empty() {
600 let result = stddev(Vec::<f64>::new());
601 assert!(result.is_nan());
602 }
603
604 #[test]
605 fn test_stddev_wrapper_single_element() {
606 assert!(stddev(vec![42.0f64]).abs() < 1e-10);
608 assert!(stddev(vec![100i32]).abs() < 1e-10);
609 }
610
611 #[test]
612 fn test_stddev_wrapper_identical_values() {
613 let result = stddev(vec![5.0f64, 5.0, 5.0, 5.0]);
615 assert!(result.abs() < 1e-10);
616 }
617
618 #[test]
619 fn test_stddev_wrapper_various_numeric_types() {
620 let expected = (2.0f64 / 3.0).sqrt();
622 assert!((stddev(vec![1u8, 2, 3]) - expected).abs() < 1e-10);
623 assert!((stddev(vec![1u16, 2, 3]) - expected).abs() < 1e-10);
624 assert!((stddev(vec![1i32, 2, 3]) - expected).abs() < 1e-10);
625 assert!((stddev(vec![1i64, 2, 3]) - expected).abs() < 1e-10);
626 assert!((stddev(vec![1usize, 2, 3]) - expected).abs() < 1e-10);
627 }
628
629 #[test]
630 fn test_wrapper_functions_consistency() {
631 let data = vec![1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
633 let stats = OnlineStats::from_slice(&data);
634
635 assert!((mean(data.clone()) - stats.mean()).abs() < 1e-10);
636 assert!((variance(data.clone()) - stats.variance()).abs() < 1e-10);
637 assert!((stddev(data) - stats.stddev()).abs() < 1e-10);
638 }
639
640 #[test]
641 fn test_wrapper_functions_with_iterators() {
642 let arr = [1, 2, 3, 4, 5];
644
645 assert!((mean(arr) - 3.0).abs() < 1e-10);
647
648 assert!((mean(1..=5) - 3.0).abs() < 1e-10);
650
651 let result = mean((1..=5).map(|x| x * 2));
653 assert!((result - 6.0).abs() < 1e-10);
654 }
655
656 #[test]
659 fn test_n_counts_basic() {
660 let mut stats = OnlineStats::new();
661 stats.extend(vec![-5, -3, 0, 0, 2, 4, 6]);
662
663 let (neg, zero, pos) = stats.n_counts();
664 assert_eq!(neg, 2, "Should have 2 negative values");
665 assert_eq!(zero, 2, "Should have 2 zero values");
666 assert_eq!(pos, 3, "Should have 3 positive values");
667 }
668
669 #[test]
670 fn test_n_counts_all_positive() {
671 let mut stats = OnlineStats::new();
672 stats.extend(vec![1.0, 2.0, 3.0, 4.0]);
673
674 let (neg, zero, pos) = stats.n_counts();
675 assert_eq!(neg, 0);
676 assert_eq!(zero, 0);
677 assert_eq!(pos, 4);
678 }
679
680 #[test]
681 fn test_n_counts_all_negative() {
682 let mut stats = OnlineStats::new();
683 stats.extend(vec![-1.0, -2.0, -3.0]);
684
685 let (neg, zero, pos) = stats.n_counts();
686 assert_eq!(neg, 3);
687 assert_eq!(zero, 0);
688 assert_eq!(pos, 0);
689 }
690
691 #[test]
692 fn test_n_counts_all_zeros() {
693 let mut stats = OnlineStats::new();
694 stats.extend(vec![0.0, 0.0, 0.0]);
695
696 let (neg, zero, pos) = stats.n_counts();
697 assert_eq!(neg, 0);
698 assert_eq!(zero, 3);
699 assert_eq!(pos, 0);
700 }
701
702 #[test]
703 fn test_n_counts_with_merge() {
704 let mut stats1 = OnlineStats::new();
705 stats1.extend(vec![-2, 0, 3]);
706
707 let mut stats2 = OnlineStats::new();
708 stats2.extend(vec![-1, 5, 7]);
709
710 stats1.merge(stats2);
711
712 let (neg, zero, pos) = stats1.n_counts();
713 assert_eq!(neg, 2, "Should have 2 negative values after merge");
714 assert_eq!(zero, 1, "Should have 1 zero value after merge");
715 assert_eq!(pos, 3, "Should have 3 positive values after merge");
716 }
717
718 #[test]
719 fn test_n_counts_empty() {
720 let stats = OnlineStats::new();
721
722 let (neg, zero, pos) = stats.n_counts();
723 assert_eq!(neg, 0);
724 assert_eq!(zero, 0);
725 assert_eq!(pos, 0);
726 }
727
728 #[test]
729 fn test_n_counts_negative_zero() {
730 let mut stats = OnlineStats::new();
731 stats.extend(vec![-0.0f64, 0.0]);
734
735 let (neg, zero, pos) = stats.n_counts();
736 assert_eq!(neg, 1, "-0.0 has negative sign bit");
737 assert_eq!(zero, 1, "+0.0 is zero");
738 assert_eq!(pos, 0);
739 }
740
741 #[test]
742 fn test_n_counts_floats_boundary() {
743 let mut stats = OnlineStats::new();
744 stats.extend(vec![-0.0001f64, 0.0, 0.0001]);
746
747 let (neg, zero, pos) = stats.n_counts();
748 assert_eq!(neg, 1);
749 assert_eq!(zero, 1);
750 assert_eq!(pos, 1);
751 }
752
753 #[test]
754 fn test_nan_skipped() {
755 let mut stats = OnlineStats::new();
756 stats.add(&1.0f64);
757 stats.add(&f64::NAN);
758 stats.add(&3.0f64);
759 assert_eq!(stats.len(), 2);
761 assert!((stats.mean() - 2.0).abs() < 1e-10);
762 }
763
764 #[test]
765 fn test_nan_only() {
766 let mut stats = OnlineStats::new();
767 stats.add(&f64::NAN);
768 stats.add(&f64::NAN);
769 assert_eq!(stats.len(), 0);
770 assert!(stats.mean().is_nan());
771 assert!(stats.variance().is_nan());
772 assert!(stats.stddev().is_nan());
773 }
774
775 #[test]
776 fn test_infinity_add() {
777 let mut stats = OnlineStats::new();
778 stats.add(&f64::INFINITY);
779 assert_eq!(stats.len(), 1);
780 assert!(stats.mean().is_infinite());
781 }
782
783 #[test]
784 fn test_neg_infinity_add() {
785 let mut stats = OnlineStats::new();
786 stats.add(&f64::NEG_INFINITY);
787 assert_eq!(stats.len(), 1);
788 assert!(stats.mean().is_infinite());
789 }
790
791 #[test]
792 fn test_infinity_mixed() {
793 let mut stats = OnlineStats::new();
794 stats.add(&f64::INFINITY);
795 stats.add(&f64::NEG_INFINITY);
796 assert!(stats.mean().is_nan());
798 }
799
800 #[test]
801 fn test_add_f64_nan_skipped() {
802 let mut stats = OnlineStats::new();
803 stats.add_f64(1.0);
804 stats.add_f64(f64::NAN);
805 stats.add_f64(3.0);
806 assert_eq!(stats.len(), 2);
807 assert!((stats.mean() - 2.0).abs() < 1e-10);
808 }
809
810 #[test]
811 fn test_geometric_mean_infinity() {
812 let mut stats = OnlineStats::new();
813 stats.add(&f64::INFINITY);
814 assert!(stats.geometric_mean().is_nan());
816 }
817
818 #[test]
819 fn test_harmonic_mean_infinity() {
820 let mut stats = OnlineStats::new();
821 stats.add(&f64::INFINITY);
822 stats.add(&1.0f64);
823 assert!((stats.harmonic_mean() - 2.0).abs() < 1e-10);
825 }
826
827 #[test]
828 fn test_without_hg_sums_matches_moments() {
829 let data = [3usize, 7, 12, 1, 40, 9, 5];
830 let mut full = OnlineStats::new();
831 let mut lean = OnlineStats::without_hg_sums();
832 for x in &data {
833 full.add(x);
834 lean.add(x);
835 }
836 assert_eq!(full.len(), lean.len());
837 assert_eq!(full.mean().to_bits(), lean.mean().to_bits());
838 assert_eq!(full.variance().to_bits(), lean.variance().to_bits());
839 assert_eq!(full.stddev().to_bits(), lean.stddev().to_bits());
840 assert_eq!(full.n_counts(), lean.n_counts());
841 assert!(lean.geometric_mean().is_nan());
842 assert!(lean.harmonic_mean().is_nan());
843 assert!(full.geometric_mean().is_finite());
844 assert!(full.harmonic_mean().is_finite());
845 }
846
847 #[test]
848 fn test_without_hg_sums_merge_poisons_hg() {
849 let mut full = OnlineStats::new();
850 full.add_f64(2.0);
851 full.add_f64(8.0);
852 let mut lean = OnlineStats::without_hg_sums();
853 lean.add_f64(4.0);
854 full.merge(lean);
855 assert!(full.geometric_mean().is_nan());
856 assert!(full.harmonic_mean().is_nan());
857 assert!((full.mean() - 14.0 / 3.0).abs() < 1e-12);
858 }
859
860 #[test]
861 fn test_without_hg_sums_zero_semantics_unchanged() {
862 let mut lean = OnlineStats::without_hg_sums();
863 lean.add_f64(0.0);
864 lean.add_f64(5.0);
865 assert_eq!(lean.geometric_mean(), 0.0);
866 assert!(lean.harmonic_mean().is_nan());
867 }
868}