Skip to main content

stats/
online.rs

1use std::fmt;
2
3use num_traits::ToPrimitive;
4use serde::{Deserialize, Serialize};
5
6use crate::Commute;
7
8/// Compute the standard deviation of a stream in constant space.
9#[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/// Compute the variance of a stream in constant space.
19#[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/// Compute the mean of a stream in constant space.
29#[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/// Online state for computing mean, variance and standard deviation.
39///
40/// Optimized memory layout for better cache performance:
41/// - Grouped related fields together in hot, warm and cold paths.
42#[allow(clippy::unsafe_derive_deserialize)]
43#[derive(Clone, Copy, Serialize, Deserialize, PartialEq)]
44pub struct OnlineStats {
45    // Hot path - always accessed together (24 bytes)
46    size: u64, // 8 bytes - always accessed
47    mean: f64, // 8 bytes - always accessed
48    q: f64,    // 8 bytes - always accessed
49
50    // Warm path - fast path for positive numbers (25 bytes)
51    hg_sums: bool,      // 1 byte - checked before sums
52    harmonic_sum: f64,  // 8 bytes - warm path
53    geometric_sum: f64, // 8 bytes - warm path
54    n_positive: u64,    // 8 bytes - warm path
55
56    // Cold path - slow path for zeros/negatives (16 bytes)
57    n_zero: u64,     // 8 bytes - cold path
58    n_negative: u64, // 8 bytes - cold path
59}
60
61impl OnlineStats {
62    /// Create initial state.
63    ///
64    /// Population size, variance and mean are set to `0`.
65    #[must_use]
66    pub fn new() -> OnlineStats {
67        Default::default()
68    }
69
70    /// Create initial state that skips harmonic/geometric sum tracking.
71    ///
72    /// Use when only mean/variance/stddev are needed: `add` then avoids a
73    /// `ln()` and a division per positive sample. `geometric_mean()` and
74    /// For all-positive aggregate data, both return `NaN`; zero/negative inputs retain existing semantics.
75    #[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    /// True when `hg_sums` was disabled by construction/merge rather than by a
91    /// zero or negative sample, so the sums are incomplete, not merely unused.
92    #[inline]
93    const fn hg_opted_out(&self) -> bool {
94        !self.hg_sums && self.n_zero == 0 && self.n_negative == 0
95    }
96
97    /// Initializes `OnlineStats` from a sample.
98    #[must_use]
99    pub fn from_slice<T: ToPrimitive>(samples: &[T]) -> OnlineStats {
100        // safety: OnlineStats is only for numbers
101        samples
102            .iter()
103            .map(|n| unsafe { n.to_f64().unwrap_unchecked() })
104            .collect()
105    }
106
107    /// Return the current mean.
108    #[must_use]
109    pub const fn mean(&self) -> f64 {
110        if self.is_empty() { f64::NAN } else { self.mean }
111    }
112
113    /// Return the current standard deviation.
114    #[must_use]
115    pub fn stddev(&self) -> f64 {
116        self.variance().sqrt()
117    }
118
119    /// Return the current population variance (using N denominator, not N-1).
120    // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
121    #[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    /// Return the current harmonic mean.
131    #[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    /// Return the current geometric mean.
141    #[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            // geometric_sum == -Inf means data values approach zero;
152            // geometric mean correctly approaches 0.0 in that limit
153            0.0
154        } else {
155            (self.geometric_sum / (self.size as f64)).exp()
156        }
157    }
158
159    /// Return the number of negative, zero and positive counts.
160    ///
161    /// Returns a tuple `(negative_count, zero_count, positive_count)` where:
162    /// - `negative_count`: number of values with negative sign bit (including -0.0)
163    /// - `zero_count`: number of values equal to +0.0
164    /// - `positive_count`: number of values greater than 0
165    ///
166    /// Note: -0.0 and +0.0 are distinguished by their sign bit and counted separately.
167    ///
168    /// # Example
169    ///
170    /// ```
171    /// use stats::OnlineStats;
172    ///
173    /// let mut stats = OnlineStats::new();
174    /// stats.extend(vec![-2, -1, 0, 0, 1, 2, 3]);
175    ///
176    /// let (neg, zero, pos) = stats.n_counts();
177    /// assert_eq!(neg, 2);   // -2, -1
178    /// assert_eq!(zero, 2);  // 0, 0
179    /// assert_eq!(pos, 3);   // 1, 2, 3
180    /// ```
181    #[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    // TODO: Calculate kurtosis
187    // also see https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
188
189    /// Add a new sample.
190    ///
191    /// NaN values are silently skipped to prevent corrupting the statistics.
192    #[inline]
193    pub fn add<T: ToPrimitive>(&mut self, sample: &T) {
194        // safety: we only add samples for numbers, so safe to unwrap
195        let sample = unsafe { sample.to_f64().unwrap_unchecked() };
196
197        if sample.is_nan() {
198            return;
199        }
200
201        // Taken from: https://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods
202        // See also: https://api.semanticscholar.org/CorpusID:120126049
203        self.size += 1;
204        let delta = sample - self.mean;
205
206        // FMA: equivalent to: self.mean += delta * (1.0 / (self.size as f64));
207        self.mean = delta.mul_add(1.0 / (self.size as f64), self.mean);
208
209        // FMA: equivalent to: self.q += delta * (sample - self.mean);
210        self.q = delta.mul_add(sample - self.mean, self.q);
211
212        // Handle positive numbers (most common case)
213        if sample > 0.0 {
214            if self.hg_sums {
215                // Fast path: compute harmonic & geometric sums directly
216                self.harmonic_sum += 1.0 / sample;
217                self.geometric_sum += sample.ln();
218            }
219            self.n_positive += 1;
220        } else {
221            // Handle special cases (zero and negative numbers)
222            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    /// Add a new f64 sample.
232    /// Skipping the `ToPrimitive` conversion.
233    ///
234    /// NaN values are silently skipped to prevent corrupting the statistics.
235    #[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        // Handle positive numbers (most common case)
248        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            // Handle special cases (zero and negative numbers)
256            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    /// Add a new NULL value to the population.
266    /// This increases the population size by `1` and treats null as `0.0`,
267    /// which increments `n_zero` and disables harmonic/geometric sum tracking.
268    #[inline]
269    pub fn add_null(&mut self) {
270        self.add_f64(0.0);
271    }
272
273    /// Returns the number of data points.
274    #[inline]
275    #[must_use]
276    pub const fn len(&self) -> usize {
277        self.size as usize
278    }
279
280    /// Returns if empty.
281    #[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        // Taken from: https://en.wikipedia.org/wiki/Standard_deviation#Combining_standard_deviations
296        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        // Incremental form avoids forming large intermediate products,
304        // preventing catastrophic cancellation when means are large and similar
305        self.mean = (v.mean - self.mean).mul_add(s2 / total, self.mean);
306
307        // self.q += v.q + meandiffsq * s1 * s2 / (s1 + s2);
308        // below is the fused multiply add version of the statement above
309        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        // TODO: Convert this to a quickcheck test.
370        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        // TODO: Convert this to a quickcheck test.
390        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        // Arithmetic mean = (2 + 4 + 8) / 3 = 4.666...
417        assert!((stats.mean() - 4.666666666667).abs() < 1e-10);
418
419        // Harmonic mean = 3 / (1/2 + 1/4 + 1/8) = 3.428571429
420        assert_eq!("3.42857143", format!("{:.8}", stats.harmonic_mean()));
421
422        // Geometric mean = (2 * 4 * 8)^(1/3) = 4.0
423        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        // Arithmetic mean = (-2 + 2) / 2 = 0
432        assert!(stats.mean().abs() < 1e-10);
433
434        // Geometric mean is NaN for negative numbers
435        assert!(stats.geometric_mean().is_nan());
436
437        // Harmonic mean is undefined when values have different signs
438        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        // Arithmetic mean = (0 + 4 + 8) / 3 = 4
447        assert!((stats.mean() - 4.0).abs() < 1e-10);
448
449        // Geometric mean = (0 * 4 * 8)^(1/3) = 0
450        assert!(stats.geometric_mean().abs() < 1e-10);
451
452        // Harmonic mean is undefined when any value is 0
453        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        // Arithmetic mean = (-10 + -5 + 0 + 5 + 10) / 5 = 0
462        assert!(stats.mean().abs() < 1e-10);
463
464        // Geometric mean is NaN due to negative values
465        assert!(stats.geometric_mean().is_nan());
466
467        // Harmonic mean is NaN due to zero value
468        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        // All means should equal the single value
477        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        // All means should be NaN for empty stats
487        assert!(stats.mean().is_nan());
488        assert!(stats.geometric_mean().is_nan());
489        assert!(stats.harmonic_mean().is_nan());
490    }
491
492    // Tests for wrapper functions: stddev(), variance(), mean()
493
494    #[test]
495    fn test_mean_wrapper_basic() {
496        // Test with f64 values
497        let result = mean(vec![1.0f64, 2.0, 3.0, 4.0, 5.0]);
498        assert!((result - 3.0).abs() < 1e-10);
499
500        // Test with i32 values
501        let result = mean(vec![1i32, 2, 3, 4, 5]);
502        assert!((result - 3.0).abs() < 1e-10);
503
504        // Test with u32 values
505        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); // Mean should be 0
526
527        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        // Test with different numeric types
534        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        // Variance of [1, 2, 3, 4, 5] = 2.0 (population variance)
548        let result = variance(vec![1.0f64, 2.0, 3.0, 4.0, 5.0]);
549        assert!((result - 2.0).abs() < 1e-10);
550
551        // Test with i32 values
552        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        // Variance of a single element is 0
565        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        // Variance of identical values is 0
572        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        // Test with different numeric types - variance of [1, 2, 3] = 2/3
579        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        // Standard deviation of [1, 2, 3, 4, 5] = sqrt(2.0)
590        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        // Test with i32 values
594        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        // Standard deviation of a single element is 0
607        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        // Standard deviation of identical values is 0
614        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        // Test with different numeric types - stddev of [1, 2, 3] = sqrt(2/3)
621        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        // Verify that wrapper functions produce same results as OnlineStats methods
632        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        // Test that wrappers work with various iterator types
643        let arr = [1, 2, 3, 4, 5];
644
645        // Array iterator
646        assert!((mean(arr) - 3.0).abs() < 1e-10);
647
648        // Range iterator
649        assert!((mean(1..=5) - 3.0).abs() < 1e-10);
650
651        // Mapped iterator
652        let result = mean((1..=5).map(|x| x * 2));
653        assert!((result - 6.0).abs() < 1e-10);
654    }
655
656    // Tests for n_counts functionality
657
658    #[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        // -0.0 and +0.0 are distinguished by their sign bit
732        // -0.0 is counted as negative, +0.0 is counted as zero
733        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        // Test with very small positive and negative numbers
745        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        // NaN should be silently skipped - only 2 values counted
760        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        // Inf + (-Inf) produces NaN for the mean
797        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        // ln(Inf) = Inf, so geometric_sum becomes Inf -> returns NaN
815        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        // 1/Inf = 0, so harmonic_sum = 0 + 1 = 1, result = 2/1 = 2
824        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}