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    /// Initializes `OnlineStats` from a sample.
71    #[must_use]
72    pub fn from_slice<T: ToPrimitive>(samples: &[T]) -> OnlineStats {
73        // safety: OnlineStats is only for numbers
74        samples
75            .iter()
76            .map(|n| unsafe { n.to_f64().unwrap_unchecked() })
77            .collect()
78    }
79
80    /// Return the current mean.
81    #[must_use]
82    pub const fn mean(&self) -> f64 {
83        if self.is_empty() { f64::NAN } else { self.mean }
84    }
85
86    /// Return the current standard deviation.
87    #[must_use]
88    pub fn stddev(&self) -> f64 {
89        self.variance().sqrt()
90    }
91
92    /// Return the current population variance (using N denominator, not N-1).
93    // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
94    #[must_use]
95    pub const fn variance(&self) -> f64 {
96        if self.is_empty() { f64::NAN } else { self.q / (self.size as f64) }
97    }
98
99    /// Return the current harmonic mean.
100    #[must_use]
101    pub fn harmonic_mean(&self) -> f64 {
102        if self.is_empty() || self.n_zero > 0 || self.n_negative > 0 {
103            f64::NAN
104        } else {
105            (self.size as f64) / self.harmonic_sum
106        }
107    }
108
109    /// Return the current geometric mean.
110    #[must_use]
111    pub fn geometric_mean(&self) -> f64 {
112        if self.is_empty()
113            || self.n_negative > 0
114            || self.geometric_sum.is_nan()
115            || self.geometric_sum == f64::INFINITY
116        {
117            f64::NAN
118        } else if self.n_zero > 0 || self.geometric_sum == f64::NEG_INFINITY {
119            // geometric_sum == -Inf means data values approach zero;
120            // geometric mean correctly approaches 0.0 in that limit
121            0.0
122        } else {
123            (self.geometric_sum / (self.size as f64)).exp()
124        }
125    }
126
127    /// Return the number of negative, zero and positive counts.
128    ///
129    /// Returns a tuple `(negative_count, zero_count, positive_count)` where:
130    /// - `negative_count`: number of values with negative sign bit (including -0.0)
131    /// - `zero_count`: number of values equal to +0.0
132    /// - `positive_count`: number of values greater than 0
133    ///
134    /// Note: -0.0 and +0.0 are distinguished by their sign bit and counted separately.
135    ///
136    /// # Example
137    ///
138    /// ```
139    /// use stats::OnlineStats;
140    ///
141    /// let mut stats = OnlineStats::new();
142    /// stats.extend(vec![-2, -1, 0, 0, 1, 2, 3]);
143    ///
144    /// let (neg, zero, pos) = stats.n_counts();
145    /// assert_eq!(neg, 2);   // -2, -1
146    /// assert_eq!(zero, 2);  // 0, 0
147    /// assert_eq!(pos, 3);   // 1, 2, 3
148    /// ```
149    #[must_use]
150    pub const fn n_counts(&self) -> (u64, u64, u64) {
151        (self.n_negative, self.n_zero, self.n_positive)
152    }
153
154    // TODO: Calculate kurtosis
155    // also see https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
156
157    /// Add a new sample.
158    ///
159    /// NaN values are silently skipped to prevent corrupting the statistics.
160    #[inline]
161    pub fn add<T: ToPrimitive>(&mut self, sample: &T) {
162        // safety: we only add samples for numbers, so safe to unwrap
163        let sample = unsafe { sample.to_f64().unwrap_unchecked() };
164
165        if sample.is_nan() {
166            return;
167        }
168
169        // Taken from: https://en.wikipedia.org/wiki/Standard_deviation#Rapid_calculation_methods
170        // See also: https://api.semanticscholar.org/CorpusID:120126049
171        self.size += 1;
172        let delta = sample - self.mean;
173
174        // FMA: equivalent to: self.mean += delta * (1.0 / (self.size as f64));
175        self.mean = delta.mul_add(1.0 / (self.size as f64), self.mean);
176
177        // FMA: equivalent to: self.q += delta * (sample - self.mean);
178        self.q = delta.mul_add(sample - self.mean, self.q);
179
180        // Handle positive numbers (most common case)
181        if sample > 0.0 {
182            if self.hg_sums {
183                // Fast path: compute harmonic & geometric sums directly
184                self.harmonic_sum += 1.0 / sample;
185                self.geometric_sum += sample.ln();
186            }
187            self.n_positive += 1;
188        } else {
189            // Handle special cases (zero and negative numbers)
190            if sample.is_sign_negative() {
191                self.n_negative += 1;
192            } else {
193                self.n_zero += 1;
194            }
195            self.hg_sums = false;
196        }
197    }
198
199    /// Add a new f64 sample.
200    /// Skipping the `ToPrimitive` conversion.
201    ///
202    /// NaN values are silently skipped to prevent corrupting the statistics.
203    #[inline]
204    pub fn add_f64(&mut self, sample: f64) {
205        if sample.is_nan() {
206            return;
207        }
208
209        self.size += 1;
210        let delta = sample - self.mean;
211
212        self.mean = delta.mul_add(1.0 / (self.size as f64), self.mean);
213        self.q = delta.mul_add(sample - self.mean, self.q);
214
215        // Handle positive numbers (most common case)
216        if sample > 0.0 {
217            if self.hg_sums {
218                self.harmonic_sum += 1.0 / sample;
219                self.geometric_sum += sample.ln();
220            }
221            self.n_positive += 1;
222        } else {
223            // Handle special cases (zero and negative numbers)
224            if sample.is_sign_negative() {
225                self.n_negative += 1;
226            } else {
227                self.n_zero += 1;
228            }
229            self.hg_sums = false;
230        }
231    }
232
233    /// Add a new NULL value to the population.
234    /// This increases the population size by `1` and treats null as `0.0`,
235    /// which increments `n_zero` and disables harmonic/geometric sum tracking.
236    #[inline]
237    pub fn add_null(&mut self) {
238        self.add_f64(0.0);
239    }
240
241    /// Returns the number of data points.
242    #[inline]
243    #[must_use]
244    pub const fn len(&self) -> usize {
245        self.size as usize
246    }
247
248    /// Returns if empty.
249    #[inline]
250    #[must_use]
251    pub const fn is_empty(&self) -> bool {
252        self.size == 0
253    }
254}
255
256impl Commute for OnlineStats {
257    #[inline]
258    fn merge(&mut self, v: OnlineStats) {
259        if v.is_empty() {
260            return;
261        }
262
263        // Taken from: https://en.wikipedia.org/wiki/Standard_deviation#Combining_standard_deviations
264        let (s1, s2) = (self.size as f64, v.size as f64);
265        let total = s1 + s2;
266        let delta = self.mean - v.mean;
267        let meandiffsq = delta * delta;
268
269        self.size += v.size;
270
271        // Incremental form avoids forming large intermediate products,
272        // preventing catastrophic cancellation when means are large and similar
273        self.mean = (v.mean - self.mean).mul_add(s2 / total, self.mean);
274
275        // self.q += v.q + meandiffsq * s1 * s2 / (s1 + s2);
276        // below is the fused multiply add version of the statement above
277        self.q += meandiffsq.mul_add(s1 * s2 / total, v.q);
278
279        self.hg_sums = self.hg_sums && v.hg_sums;
280        self.harmonic_sum += v.harmonic_sum;
281        self.geometric_sum += v.geometric_sum;
282
283        self.n_zero += v.n_zero;
284        self.n_negative += v.n_negative;
285        self.n_positive += v.n_positive;
286    }
287}
288
289impl Default for OnlineStats {
290    fn default() -> OnlineStats {
291        OnlineStats {
292            size: 0,
293            mean: 0.0,
294            q: 0.0,
295            harmonic_sum: 0.0,
296            geometric_sum: 0.0,
297            n_zero: 0,
298            n_negative: 0,
299            n_positive: 0,
300            hg_sums: true,
301        }
302    }
303}
304
305impl fmt::Debug for OnlineStats {
306    #[inline]
307    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
308        write!(f, "{:.10} +/- {:.10}", self.mean(), self.stddev())
309    }
310}
311
312impl<T: ToPrimitive> FromIterator<T> for OnlineStats {
313    #[inline]
314    fn from_iter<I: IntoIterator<Item = T>>(it: I) -> OnlineStats {
315        let mut v = OnlineStats::new();
316        v.extend(it);
317        v
318    }
319}
320
321impl<T: ToPrimitive> Extend<T> for OnlineStats {
322    #[inline]
323    fn extend<I: IntoIterator<Item = T>>(&mut self, it: I) {
324        for sample in it {
325            self.add(&sample);
326        }
327    }
328}
329
330#[cfg(test)]
331mod test {
332    use super::{OnlineStats, mean, stddev, variance};
333    use {crate::Commute, crate::merge_all};
334
335    #[test]
336    fn online() {
337        // TODO: Convert this to a quickcheck test.
338        let expected = OnlineStats::from_slice(&[1usize, 2, 3, 2, 4, 6]);
339
340        let var1 = OnlineStats::from_slice(&[1usize, 2, 3]);
341        let var2 = OnlineStats::from_slice(&[2usize, 4, 6]);
342        let mut got = var1;
343        got.merge(var2);
344        assert_eq!(expected.stddev(), got.stddev());
345        assert_eq!(expected.mean(), got.mean());
346        assert_eq!(expected.variance(), got.variance());
347    }
348
349    #[test]
350    fn online_empty() {
351        let expected = OnlineStats::new();
352        assert!(expected.is_empty());
353    }
354
355    #[test]
356    fn online_many() {
357        // TODO: Convert this to a quickcheck test.
358        let expected = OnlineStats::from_slice(&[1usize, 2, 3, 2, 4, 6, 3, 6, 9]);
359
360        let vars = vec![
361            OnlineStats::from_slice(&[1usize, 2, 3]),
362            OnlineStats::from_slice(&[2usize, 4, 6]),
363            OnlineStats::from_slice(&[3usize, 6, 9]),
364        ];
365        assert_eq!(
366            expected.stddev(),
367            merge_all(vars.clone().into_iter()).unwrap().stddev()
368        );
369        assert_eq!(
370            expected.mean(),
371            merge_all(vars.clone().into_iter()).unwrap().mean()
372        );
373        assert_eq!(
374            expected.variance(),
375            merge_all(vars.into_iter()).unwrap().variance()
376        );
377    }
378
379    #[test]
380    fn test_means() {
381        let mut stats = OnlineStats::new();
382        stats.extend(vec![2.0f64, 4.0, 8.0]);
383
384        // Arithmetic mean = (2 + 4 + 8) / 3 = 4.666...
385        assert!((stats.mean() - 4.666666666667).abs() < 1e-10);
386
387        // Harmonic mean = 3 / (1/2 + 1/4 + 1/8) = 3.428571429
388        assert_eq!("3.42857143", format!("{:.8}", stats.harmonic_mean()));
389
390        // Geometric mean = (2 * 4 * 8)^(1/3) = 4.0
391        assert!((stats.geometric_mean() - 4.0).abs() < 1e-10);
392    }
393
394    #[test]
395    fn test_means_with_negative() {
396        let mut stats = OnlineStats::new();
397        stats.extend(vec![-2.0f64, 2.0]);
398
399        // Arithmetic mean = (-2 + 2) / 2 = 0
400        assert!(stats.mean().abs() < 1e-10);
401
402        // Geometric mean is NaN for negative numbers
403        assert!(stats.geometric_mean().is_nan());
404
405        // Harmonic mean is undefined when values have different signs
406        assert!(stats.harmonic_mean().is_nan());
407    }
408
409    #[test]
410    fn test_means_with_zero() {
411        let mut stats = OnlineStats::new();
412        stats.extend(vec![0.0f64, 4.0, 8.0]);
413
414        // Arithmetic mean = (0 + 4 + 8) / 3 = 4
415        assert!((stats.mean() - 4.0).abs() < 1e-10);
416
417        // Geometric mean = (0 * 4 * 8)^(1/3) = 0
418        assert!(stats.geometric_mean().abs() < 1e-10);
419
420        // Harmonic mean is undefined when any value is 0
421        assert!(stats.harmonic_mean().is_nan());
422    }
423
424    #[test]
425    fn test_means_with_zero_and_negative_values() {
426        let mut stats = OnlineStats::new();
427        stats.extend(vec![-10i32, -5, 0, 5, 10]);
428
429        // Arithmetic mean = (-10 + -5 + 0 + 5 + 10) / 5 = 0
430        assert!(stats.mean().abs() < 1e-10);
431
432        // Geometric mean is NaN due to negative values
433        assert!(stats.geometric_mean().is_nan());
434
435        // Harmonic mean is NaN due to zero value
436        assert!(stats.harmonic_mean().is_nan());
437    }
438
439    #[test]
440    fn test_means_single_value() {
441        let mut stats = OnlineStats::new();
442        stats.extend(vec![5.0f64]);
443
444        // All means should equal the single value
445        assert!((stats.mean() - 5.0).abs() < 1e-10);
446        assert!((stats.geometric_mean() - 5.0).abs() < 1e-10);
447        assert!((stats.harmonic_mean() - 5.0).abs() < 1e-10);
448    }
449
450    #[test]
451    fn test_means_empty() {
452        let stats = OnlineStats::new();
453
454        // All means should be NaN for empty stats
455        assert!(stats.mean().is_nan());
456        assert!(stats.geometric_mean().is_nan());
457        assert!(stats.harmonic_mean().is_nan());
458    }
459
460    // Tests for wrapper functions: stddev(), variance(), mean()
461
462    #[test]
463    fn test_mean_wrapper_basic() {
464        // Test with f64 values
465        let result = mean(vec![1.0f64, 2.0, 3.0, 4.0, 5.0]);
466        assert!((result - 3.0).abs() < 1e-10);
467
468        // Test with i32 values
469        let result = mean(vec![1i32, 2, 3, 4, 5]);
470        assert!((result - 3.0).abs() < 1e-10);
471
472        // Test with u32 values
473        let result = mean(vec![10u32, 20, 30]);
474        assert!((result - 20.0).abs() < 1e-10);
475    }
476
477    #[test]
478    fn test_mean_wrapper_empty() {
479        let result = mean(Vec::<f64>::new());
480        assert!(result.is_nan());
481    }
482
483    #[test]
484    fn test_mean_wrapper_single_element() {
485        assert!((mean(vec![42.0f64]) - 42.0).abs() < 1e-10);
486        assert!((mean(vec![100i32]) - 100.0).abs() < 1e-10);
487        assert!((mean(vec![0u8]) - 0.0).abs() < 1e-10);
488    }
489
490    #[test]
491    fn test_mean_wrapper_negative_values() {
492        let result = mean(vec![-5.0f64, 5.0]);
493        assert!(result.abs() < 1e-10); // Mean should be 0
494
495        let result = mean(vec![-10i32, -20, -30]);
496        assert!((result - (-20.0)).abs() < 1e-10);
497    }
498
499    #[test]
500    fn test_mean_wrapper_various_numeric_types() {
501        // Test with different numeric types
502        assert!((mean(vec![1u8, 2, 3]) - 2.0).abs() < 1e-10);
503        assert!((mean(vec![1u16, 2, 3]) - 2.0).abs() < 1e-10);
504        assert!((mean(vec![1u64, 2, 3]) - 2.0).abs() < 1e-10);
505        assert!((mean(vec![1i8, 2, 3]) - 2.0).abs() < 1e-10);
506        assert!((mean(vec![1i16, 2, 3]) - 2.0).abs() < 1e-10);
507        assert!((mean(vec![1i64, 2, 3]) - 2.0).abs() < 1e-10);
508        assert!((mean(vec![1.0f32, 2.0, 3.0]) - 2.0).abs() < 1e-6);
509        assert!((mean(vec![1usize, 2, 3]) - 2.0).abs() < 1e-10);
510        assert!((mean(vec![1isize, 2, 3]) - 2.0).abs() < 1e-10);
511    }
512
513    #[test]
514    fn test_variance_wrapper_basic() {
515        // Variance of [1, 2, 3, 4, 5] = 2.0 (population variance)
516        let result = variance(vec![1.0f64, 2.0, 3.0, 4.0, 5.0]);
517        assert!((result - 2.0).abs() < 1e-10);
518
519        // Test with i32 values
520        let result = variance(vec![1i32, 2, 3, 4, 5]);
521        assert!((result - 2.0).abs() < 1e-10);
522    }
523
524    #[test]
525    fn test_variance_wrapper_empty() {
526        let result = variance(Vec::<f64>::new());
527        assert!(result.is_nan());
528    }
529
530    #[test]
531    fn test_variance_wrapper_single_element() {
532        // Variance of a single element is 0
533        assert!(variance(vec![42.0f64]).abs() < 1e-10);
534        assert!(variance(vec![100i32]).abs() < 1e-10);
535    }
536
537    #[test]
538    fn test_variance_wrapper_identical_values() {
539        // Variance of identical values is 0
540        let result = variance(vec![5.0f64, 5.0, 5.0, 5.0]);
541        assert!(result.abs() < 1e-10);
542    }
543
544    #[test]
545    fn test_variance_wrapper_various_numeric_types() {
546        // Test with different numeric types - variance of [1, 2, 3] = 2/3
547        let expected = 2.0 / 3.0;
548        assert!((variance(vec![1u8, 2, 3]) - expected).abs() < 1e-10);
549        assert!((variance(vec![1u16, 2, 3]) - expected).abs() < 1e-10);
550        assert!((variance(vec![1i32, 2, 3]) - expected).abs() < 1e-10);
551        assert!((variance(vec![1i64, 2, 3]) - expected).abs() < 1e-10);
552        assert!((variance(vec![1usize, 2, 3]) - expected).abs() < 1e-10);
553    }
554
555    #[test]
556    fn test_stddev_wrapper_basic() {
557        // Standard deviation of [1, 2, 3, 4, 5] = sqrt(2.0)
558        let result = stddev(vec![1.0f64, 2.0, 3.0, 4.0, 5.0]);
559        assert!((result - 2.0f64.sqrt()).abs() < 1e-10);
560
561        // Test with i32 values
562        let result = stddev(vec![1i32, 2, 3, 4, 5]);
563        assert!((result - 2.0f64.sqrt()).abs() < 1e-10);
564    }
565
566    #[test]
567    fn test_stddev_wrapper_empty() {
568        let result = stddev(Vec::<f64>::new());
569        assert!(result.is_nan());
570    }
571
572    #[test]
573    fn test_stddev_wrapper_single_element() {
574        // Standard deviation of a single element is 0
575        assert!(stddev(vec![42.0f64]).abs() < 1e-10);
576        assert!(stddev(vec![100i32]).abs() < 1e-10);
577    }
578
579    #[test]
580    fn test_stddev_wrapper_identical_values() {
581        // Standard deviation of identical values is 0
582        let result = stddev(vec![5.0f64, 5.0, 5.0, 5.0]);
583        assert!(result.abs() < 1e-10);
584    }
585
586    #[test]
587    fn test_stddev_wrapper_various_numeric_types() {
588        // Test with different numeric types - stddev of [1, 2, 3] = sqrt(2/3)
589        let expected = (2.0f64 / 3.0).sqrt();
590        assert!((stddev(vec![1u8, 2, 3]) - expected).abs() < 1e-10);
591        assert!((stddev(vec![1u16, 2, 3]) - expected).abs() < 1e-10);
592        assert!((stddev(vec![1i32, 2, 3]) - expected).abs() < 1e-10);
593        assert!((stddev(vec![1i64, 2, 3]) - expected).abs() < 1e-10);
594        assert!((stddev(vec![1usize, 2, 3]) - expected).abs() < 1e-10);
595    }
596
597    #[test]
598    fn test_wrapper_functions_consistency() {
599        // Verify that wrapper functions produce same results as OnlineStats methods
600        let data = vec![1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
601        let stats = OnlineStats::from_slice(&data);
602
603        assert!((mean(data.clone()) - stats.mean()).abs() < 1e-10);
604        assert!((variance(data.clone()) - stats.variance()).abs() < 1e-10);
605        assert!((stddev(data) - stats.stddev()).abs() < 1e-10);
606    }
607
608    #[test]
609    fn test_wrapper_functions_with_iterators() {
610        // Test that wrappers work with various iterator types
611        let arr = [1, 2, 3, 4, 5];
612
613        // Array iterator
614        assert!((mean(arr) - 3.0).abs() < 1e-10);
615
616        // Range iterator
617        assert!((mean(1..=5) - 3.0).abs() < 1e-10);
618
619        // Mapped iterator
620        let result = mean((1..=5).map(|x| x * 2));
621        assert!((result - 6.0).abs() < 1e-10);
622    }
623
624    // Tests for n_counts functionality
625
626    #[test]
627    fn test_n_counts_basic() {
628        let mut stats = OnlineStats::new();
629        stats.extend(vec![-5, -3, 0, 0, 2, 4, 6]);
630
631        let (neg, zero, pos) = stats.n_counts();
632        assert_eq!(neg, 2, "Should have 2 negative values");
633        assert_eq!(zero, 2, "Should have 2 zero values");
634        assert_eq!(pos, 3, "Should have 3 positive values");
635    }
636
637    #[test]
638    fn test_n_counts_all_positive() {
639        let mut stats = OnlineStats::new();
640        stats.extend(vec![1.0, 2.0, 3.0, 4.0]);
641
642        let (neg, zero, pos) = stats.n_counts();
643        assert_eq!(neg, 0);
644        assert_eq!(zero, 0);
645        assert_eq!(pos, 4);
646    }
647
648    #[test]
649    fn test_n_counts_all_negative() {
650        let mut stats = OnlineStats::new();
651        stats.extend(vec![-1.0, -2.0, -3.0]);
652
653        let (neg, zero, pos) = stats.n_counts();
654        assert_eq!(neg, 3);
655        assert_eq!(zero, 0);
656        assert_eq!(pos, 0);
657    }
658
659    #[test]
660    fn test_n_counts_all_zeros() {
661        let mut stats = OnlineStats::new();
662        stats.extend(vec![0.0, 0.0, 0.0]);
663
664        let (neg, zero, pos) = stats.n_counts();
665        assert_eq!(neg, 0);
666        assert_eq!(zero, 3);
667        assert_eq!(pos, 0);
668    }
669
670    #[test]
671    fn test_n_counts_with_merge() {
672        let mut stats1 = OnlineStats::new();
673        stats1.extend(vec![-2, 0, 3]);
674
675        let mut stats2 = OnlineStats::new();
676        stats2.extend(vec![-1, 5, 7]);
677
678        stats1.merge(stats2);
679
680        let (neg, zero, pos) = stats1.n_counts();
681        assert_eq!(neg, 2, "Should have 2 negative values after merge");
682        assert_eq!(zero, 1, "Should have 1 zero value after merge");
683        assert_eq!(pos, 3, "Should have 3 positive values after merge");
684    }
685
686    #[test]
687    fn test_n_counts_empty() {
688        let stats = OnlineStats::new();
689
690        let (neg, zero, pos) = stats.n_counts();
691        assert_eq!(neg, 0);
692        assert_eq!(zero, 0);
693        assert_eq!(pos, 0);
694    }
695
696    #[test]
697    fn test_n_counts_negative_zero() {
698        let mut stats = OnlineStats::new();
699        // -0.0 and +0.0 are distinguished by their sign bit
700        // -0.0 is counted as negative, +0.0 is counted as zero
701        stats.extend(vec![-0.0f64, 0.0]);
702
703        let (neg, zero, pos) = stats.n_counts();
704        assert_eq!(neg, 1, "-0.0 has negative sign bit");
705        assert_eq!(zero, 1, "+0.0 is zero");
706        assert_eq!(pos, 0);
707    }
708
709    #[test]
710    fn test_n_counts_floats_boundary() {
711        let mut stats = OnlineStats::new();
712        // Test with very small positive and negative numbers
713        stats.extend(vec![-0.0001f64, 0.0, 0.0001]);
714
715        let (neg, zero, pos) = stats.n_counts();
716        assert_eq!(neg, 1);
717        assert_eq!(zero, 1);
718        assert_eq!(pos, 1);
719    }
720
721    #[test]
722    fn test_nan_skipped() {
723        let mut stats = OnlineStats::new();
724        stats.add(&1.0f64);
725        stats.add(&f64::NAN);
726        stats.add(&3.0f64);
727        // NaN should be silently skipped - only 2 values counted
728        assert_eq!(stats.len(), 2);
729        assert!((stats.mean() - 2.0).abs() < 1e-10);
730    }
731
732    #[test]
733    fn test_nan_only() {
734        let mut stats = OnlineStats::new();
735        stats.add(&f64::NAN);
736        stats.add(&f64::NAN);
737        assert_eq!(stats.len(), 0);
738        assert!(stats.mean().is_nan());
739        assert!(stats.variance().is_nan());
740        assert!(stats.stddev().is_nan());
741    }
742
743    #[test]
744    fn test_infinity_add() {
745        let mut stats = OnlineStats::new();
746        stats.add(&f64::INFINITY);
747        assert_eq!(stats.len(), 1);
748        assert!(stats.mean().is_infinite());
749    }
750
751    #[test]
752    fn test_neg_infinity_add() {
753        let mut stats = OnlineStats::new();
754        stats.add(&f64::NEG_INFINITY);
755        assert_eq!(stats.len(), 1);
756        assert!(stats.mean().is_infinite());
757    }
758
759    #[test]
760    fn test_infinity_mixed() {
761        let mut stats = OnlineStats::new();
762        stats.add(&f64::INFINITY);
763        stats.add(&f64::NEG_INFINITY);
764        // Inf + (-Inf) produces NaN for the mean
765        assert!(stats.mean().is_nan());
766    }
767
768    #[test]
769    fn test_add_f64_nan_skipped() {
770        let mut stats = OnlineStats::new();
771        stats.add_f64(1.0);
772        stats.add_f64(f64::NAN);
773        stats.add_f64(3.0);
774        assert_eq!(stats.len(), 2);
775        assert!((stats.mean() - 2.0).abs() < 1e-10);
776    }
777
778    #[test]
779    fn test_geometric_mean_infinity() {
780        let mut stats = OnlineStats::new();
781        stats.add(&f64::INFINITY);
782        // ln(Inf) = Inf, so geometric_sum becomes Inf -> returns NaN
783        assert!(stats.geometric_mean().is_nan());
784    }
785
786    #[test]
787    fn test_harmonic_mean_infinity() {
788        let mut stats = OnlineStats::new();
789        stats.add(&f64::INFINITY);
790        stats.add(&1.0f64);
791        // 1/Inf = 0, so harmonic_sum = 0 + 1 = 1, result = 2/1 = 2
792        assert!((stats.harmonic_mean() - 2.0).abs() < 1e-10);
793    }
794}