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