1pub fn mean(data: &[f64]) -> Option<f64> {
35 if data.is_empty() {
36 return None;
37 }
38 if !data.iter().all(|x| x.is_finite()) {
39 return None;
40 }
41 Some(kahan_sum(data) / data.len() as f64)
42}
43
44pub fn variance(data: &[f64]) -> Option<f64> {
69 if data.len() < 2 {
70 return None;
71 }
72 if !data.iter().all(|x| x.is_finite()) {
73 return None;
74 }
75 let mut acc = WelfordAccumulator::new();
76 for &x in data {
77 acc.update(x);
78 }
79 acc.sample_variance()
80}
81
82pub fn population_variance(data: &[f64]) -> Option<f64> {
96 if data.is_empty() {
97 return None;
98 }
99 if !data.iter().all(|x| x.is_finite()) {
100 return None;
101 }
102 let mut acc = WelfordAccumulator::new();
103 for &x in data {
104 acc.update(x);
105 }
106 acc.population_variance()
107}
108
109pub fn std_dev(data: &[f64]) -> Option<f64> {
124 variance(data).map(f64::sqrt)
125}
126
127pub fn population_std_dev(data: &[f64]) -> Option<f64> {
134 population_variance(data).map(f64::sqrt)
135}
136
137pub fn min(data: &[f64]) -> Option<f64> {
148 if data.is_empty() {
149 return None;
150 }
151 data.iter().copied().try_fold(f64::INFINITY, |acc, x| {
152 if x.is_nan() {
153 None
154 } else {
155 Some(acc.min(x))
156 }
157 })
158}
159
160pub fn max(data: &[f64]) -> Option<f64> {
171 if data.is_empty() {
172 return None;
173 }
174 data.iter().copied().try_fold(f64::NEG_INFINITY, |acc, x| {
175 if x.is_nan() {
176 None
177 } else {
178 Some(acc.max(x))
179 }
180 })
181}
182
183pub fn median(data: &[f64]) -> Option<f64> {
201 if data.is_empty() {
202 return None;
203 }
204 if data.iter().any(|x| x.is_nan()) {
205 return None;
206 }
207 let mut sorted = data.to_vec();
208 sorted.sort_unstable_by(|a, b| a.partial_cmp(b).expect("NaN filtered above"));
209 let n = sorted.len();
210 if n % 2 == 1 {
211 Some(sorted[n / 2])
212 } else {
213 Some((sorted[n / 2 - 1] + sorted[n / 2]) / 2.0)
214 }
215}
216
217pub fn quantile(data: &[f64], p: f64) -> Option<f64> {
247 if data.is_empty() || !(0.0..=1.0).contains(&p) {
248 return None;
249 }
250 if data.iter().any(|x| x.is_nan()) {
251 return None;
252 }
253 let mut sorted = data.to_vec();
254 sorted.sort_unstable_by(|a, b| a.partial_cmp(b).expect("NaN filtered above"));
255 quantile_sorted(&sorted, p)
256}
257
258pub fn quantile_sorted(sorted_data: &[f64], p: f64) -> Option<f64> {
267 let n = sorted_data.len();
268 if n == 0 || !(0.0..=1.0).contains(&p) {
269 return None;
270 }
271 if n == 1 {
272 return Some(sorted_data[0]);
273 }
274
275 let h = (n - 1) as f64 * p;
276 let j = h.floor() as usize;
277 let g = h - h.floor();
278
279 if j + 1 >= n {
280 Some(sorted_data[n - 1])
281 } else {
282 Some((1.0 - g) * sorted_data[j] + g * sorted_data[j + 1])
283 }
284}
285
286pub fn skewness(data: &[f64]) -> Option<f64> {
324 let n = data.len();
325 if n < 3 {
326 return None;
327 }
328 if !data.iter().all(|x| x.is_finite()) {
329 return None;
330 }
331 let nf = n as f64;
332 let m = kahan_sum(data) / nf;
333 let mut sum2 = 0.0;
334 let mut sum3 = 0.0;
335 for &x in data {
336 let d = x - m;
337 let d2 = d * d;
338 sum2 += d2;
339 sum3 += d2 * d;
340 }
341 let m2 = sum2 / nf;
342 if m2 == 0.0 {
343 return None;
344 }
345 let m3 = sum3 / nf;
346 let g1 = m3 / m2.powf(1.5);
347 let correction = (nf * (nf - 1.0)).sqrt() / (nf - 2.0);
348 Some(correction * g1)
349}
350
351pub fn kurtosis(data: &[f64]) -> Option<f64> {
385 let n = data.len();
386 if n < 4 {
387 return None;
388 }
389 if !data.iter().all(|x| x.is_finite()) {
390 return None;
391 }
392 let nf = n as f64;
393 let m = kahan_sum(data) / nf;
394 let mut sum2 = 0.0;
395 let mut sum4 = 0.0;
396 for &x in data {
397 let d = x - m;
398 let d2 = d * d;
399 sum2 += d2;
400 sum4 += d2 * d2;
401 }
402 let s2 = sum2 / (nf - 1.0);
404 if s2 == 0.0 {
405 return None;
406 }
407 let s4 = s2 * s2;
408 let sum_z4 = sum4 / s4;
410 let a = nf * (nf + 1.0) / ((nf - 1.0) * (nf - 2.0) * (nf - 3.0));
411 let b = 3.0 * (nf - 1.0) * (nf - 1.0) / ((nf - 2.0) * (nf - 3.0));
412 Some(a * sum_z4 - b)
413}
414
415pub fn skewness_moment(data: &[f64]) -> Option<f64> {
451 let (_, m2, m3, _) = central_moments(data, 3)?;
452 Some(m3 / m2.powf(1.5))
453}
454
455pub fn kurtosis_moment(data: &[f64]) -> Option<f64> {
490 let (_, m2, _, m4) = central_moments(data, 4)?;
491 Some(m4 / (m2 * m2) - 3.0)
492}
493
494fn central_moments(data: &[f64], order: usize) -> Option<(f64, f64, f64, f64)> {
499 let n = data.len();
500 if n < order {
501 return None;
502 }
503 if !data.iter().all(|x| x.is_finite()) {
504 return None;
505 }
506 let nf = n as f64;
507 let mean = kahan_sum(data) / nf;
508 let (mut s2, mut s3, mut s4) = (0.0, 0.0, 0.0);
509 for &x in data {
510 let d = x - mean;
511 let d2 = d * d;
512 s2 += d2;
513 s3 += d2 * d;
514 s4 += d2 * d2;
515 }
516 let m2 = s2 / nf;
517 if m2 == 0.0 {
518 return None;
519 }
520 Some((0.0, m2, s3 / nf, s4 / nf))
521}
522
523pub fn covariance(x: &[f64], y: &[f64]) -> Option<f64> {
547 let n = x.len();
548 if n != y.len() || n < 2 {
549 return None;
550 }
551 if !x.iter().chain(y.iter()).all(|v| v.is_finite()) {
552 return None;
553 }
554 let nf = n as f64;
555 let mean_x = kahan_sum(x) / nf;
556 let mean_y = kahan_sum(y) / nf;
557 let mut sum = 0.0;
558 for i in 0..n {
559 sum += (x[i] - mean_x) * (y[i] - mean_y);
560 }
561 Some(sum / (nf - 1.0))
562}
563
564pub fn kahan_sum(data: &[f64]) -> f64 {
584 let mut sum = 0.0_f64;
585 let mut c = 0.0_f64;
586 for &x in data {
587 let t = sum + x;
588 if sum.abs() >= x.abs() {
589 c += (sum - t) + x;
590 } else {
591 c += (x - t) + sum;
592 }
593 sum = t;
594 }
595 sum + c
596}
597
598#[derive(Debug, Clone)]
631pub struct WelfordAccumulator {
632 count: u64,
633 mean_acc: f64,
634 m2: f64,
635 m3: f64,
636 m4: f64,
637}
638
639impl WelfordAccumulator {
640 pub fn new() -> Self {
642 Self {
643 count: 0,
644 mean_acc: 0.0,
645 m2: 0.0,
646 m3: 0.0,
647 m4: 0.0,
648 }
649 }
650
651 pub fn update(&mut self, value: f64) {
660 let n1 = self.count;
661 self.count += 1;
662
663 if n1 == 0 {
664 self.mean_acc = value;
666 return;
667 }
668
669 let n = self.count as f64;
670 let delta = value - self.mean_acc;
671 let delta_n = delta / n;
672 let delta_n2 = delta_n * delta_n;
673 let term1 = delta * delta_n * n1 as f64;
674
675 self.m4 += term1 * delta_n2 * (n * n - 3.0 * n + 3.0) + 6.0 * delta_n2 * self.m2
677 - 4.0 * delta_n * self.m3;
678 self.m3 += term1 * delta_n * (n - 2.0) - 3.0 * delta_n * self.m2;
679 self.m2 += term1;
680 self.mean_acc += delta_n;
681 }
682
683 pub fn count(&self) -> u64 {
685 self.count
686 }
687
688 pub fn mean(&self) -> Option<f64> {
690 if self.count == 0 {
691 None
692 } else {
693 Some(self.mean_acc)
694 }
695 }
696
697 pub fn sample_variance(&self) -> Option<f64> {
700 if self.count < 2 {
701 None
702 } else {
703 Some(self.m2 / (self.count - 1) as f64)
704 }
705 }
706
707 pub fn population_variance(&self) -> Option<f64> {
710 if self.count == 0 {
711 None
712 } else {
713 Some(self.m2 / self.count as f64)
714 }
715 }
716
717 pub fn sample_std_dev(&self) -> Option<f64> {
720 self.sample_variance().map(f64::sqrt)
721 }
722
723 pub fn population_std_dev(&self) -> Option<f64> {
726 self.population_variance().map(f64::sqrt)
727 }
728
729 pub fn skewness(&self) -> Option<f64> {
734 if self.count < 3 {
735 return None;
736 }
737 let n = self.count as f64;
738 if self.m2 == 0.0 {
739 return None;
740 }
741 let g1 = n.sqrt() * self.m3 / self.m2.powf(1.5);
743 let correction = (n * (n - 1.0)).sqrt() / (n - 2.0);
745 Some(correction * g1)
746 }
747
748 pub fn kurtosis(&self) -> Option<f64> {
754 if self.count < 4 {
755 return None;
756 }
757 let n = self.count as f64;
758 if self.m2 == 0.0 {
759 return None;
760 }
761 let g2 = n * self.m4 / (self.m2 * self.m2) - 3.0;
763 let correction = (n - 1.0) / ((n - 2.0) * (n - 3.0));
765 Some(correction * ((n + 1.0) * g2 + 6.0))
766 }
767
768 pub fn merge(&mut self, other: &WelfordAccumulator) {
777 if other.count == 0 {
778 return;
779 }
780 if self.count == 0 {
781 *self = other.clone();
782 return;
783 }
784 let na = self.count as f64;
785 let nb = other.count as f64;
786 let total = self.count + other.count;
787 let n = total as f64;
788 let delta = other.mean_acc - self.mean_acc;
789 let delta2 = delta * delta;
790 let delta3 = delta2 * delta;
791 let delta4 = delta2 * delta2;
792
793 let new_mean = self.mean_acc + delta * (nb / n);
794
795 let new_m2 = self.m2 + other.m2 + delta2 * na * nb / n;
796
797 let new_m3 = self.m3
798 + other.m3
799 + delta3 * na * nb * (na - nb) / (n * n)
800 + 3.0 * delta * (na * other.m2 - nb * self.m2) / n;
801
802 let new_m4 = self.m4
803 + other.m4
804 + delta4 * na * nb * (na * na - na * nb + nb * nb) / (n * n * n)
805 + 6.0 * delta2 * (na * na * other.m2 + nb * nb * self.m2) / (n * n)
806 + 4.0 * delta * (na * other.m3 - nb * self.m3) / n;
807
808 self.count = total;
809 self.mean_acc = new_mean;
810 self.m2 = new_m2;
811 self.m3 = new_m3;
812 self.m4 = new_m4;
813 }
814}
815
816impl Default for WelfordAccumulator {
817 fn default() -> Self {
818 Self::new()
819 }
820}
821
822#[cfg(test)]
827mod tests {
828 use super::*;
829
830 #[test]
835 fn moment_and_adjusted_estimators_are_related_but_not_equal() {
836 let data = [
837 2.1, 3.4, 1.9, 5.6, 2.2, 3.1, 4.8, 2.9, 3.3, 7.2, 2.5, 3.0, 4.1, 2.7, 3.8, 2.4, 6.1,
838 3.6, 2.8, 3.2,
839 ];
840 let n = data.len() as f64;
841
842 let g1 = skewness_moment(&data).expect("20 finite points with spread");
843 let big_g1 = skewness(&data).expect("same");
844 let expected = g1 * (n * (n - 1.0)).sqrt() / (n - 2.0);
846 assert!((big_g1 - expected).abs() < 1e-12, "{big_g1} vs {expected}");
847 assert!(big_g1 > g1, "at n = 20 the adjustment is about 8 %");
848
849 let g2 = kurtosis_moment(&data).expect("20 finite points with spread");
850 let big_g2 = kurtosis(&data).expect("same");
851 let expected = (n - 1.0) * ((n + 1.0) * g2 + 6.0) / ((n - 2.0) * (n - 3.0));
853 assert!((big_g2 - expected).abs() < 1e-12, "{big_g2} vs {expected}");
854
855 assert!((g1 - 1.236_103_873_8).abs() < 1e-9, "g1 = {g1}");
857 assert!((g2 - 0.751_429_343_3).abs() < 1e-9, "g2 = {g2}");
858 }
859
860 #[test]
863 fn the_kurtosis_adjustment_can_change_the_sign_at_a_small_sample() {
864 let data: Vec<f64> = (0..20).map(|i| (i as f64) - 9.5).collect();
866 let g2 = kurtosis_moment(&data).expect("spread");
867 let big_g2 = kurtosis(&data).expect("spread");
868 assert!(g2 < 0.0, "a uniform ramp is platykurtic: {g2}");
869 assert!(g2 < big_g2, "the adjustment lifts it: {g2} -> {big_g2}");
870 }
871
872 #[test]
873 fn moment_estimators_refuse_what_the_adjusted_ones_refuse() {
874 assert_eq!(skewness_moment(&[1.0, 2.0]), None, "fewer than 3");
875 assert_eq!(kurtosis_moment(&[1.0, 2.0, 3.0]), None, "fewer than 4");
876 assert_eq!(skewness_moment(&[2.0; 5]), None, "no spread");
877 assert_eq!(kurtosis_moment(&[2.0; 5]), None, "no spread");
878 assert_eq!(skewness_moment(&[1.0, 2.0, f64::NAN]), None, "not finite");
879 assert_eq!(
880 kurtosis_moment(&[1.0, 2.0, 3.0, f64::INFINITY]),
881 None,
882 "not finite"
883 );
884 }
885
886 #[test]
887 fn test_mean_basic() {
888 assert_eq!(mean(&[1.0, 2.0, 3.0, 4.0, 5.0]), Some(3.0));
889 }
890
891 #[test]
892 fn test_mean_single() {
893 assert_eq!(mean(&[42.0]), Some(42.0));
894 }
895
896 #[test]
897 fn test_mean_empty() {
898 assert_eq!(mean(&[]), None);
899 }
900
901 #[test]
902 fn test_mean_nan() {
903 assert_eq!(mean(&[1.0, f64::NAN, 3.0]), None);
904 }
905
906 #[test]
907 fn test_mean_inf() {
908 assert_eq!(mean(&[1.0, f64::INFINITY, 3.0]), None);
909 }
910
911 #[test]
914 fn test_variance_basic() {
915 let v = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
916 let var = variance(&v).unwrap();
917 assert!((var - 4.571428571428571).abs() < 1e-10);
918 }
919
920 #[test]
921 fn test_variance_constant() {
922 let v = [5.0; 100];
923 assert!((variance(&v).unwrap()).abs() < 1e-15);
924 }
925
926 #[test]
927 fn test_variance_single() {
928 assert_eq!(variance(&[1.0]), None);
929 }
930
931 #[test]
932 fn test_variance_empty() {
933 assert_eq!(variance(&[]), None);
934 }
935
936 #[test]
937 fn test_population_variance() {
938 let v = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
939 let var = population_variance(&v).unwrap();
940 assert!((var - 4.0).abs() < 1e-10);
941 }
942
943 #[test]
946 fn test_std_dev() {
947 let v = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
948 let sd = std_dev(&v).unwrap();
949 let expected = 4.571428571428571_f64.sqrt();
950 assert!((sd - expected).abs() < 1e-10);
951 }
952
953 #[test]
956 fn test_min_max() {
957 let v = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0];
958 assert_eq!(min(&v), Some(1.0));
959 assert_eq!(max(&v), Some(9.0));
960 }
961
962 #[test]
963 fn test_min_max_empty() {
964 assert_eq!(min(&[]), None);
965 assert_eq!(max(&[]), None);
966 }
967
968 #[test]
969 fn test_min_max_nan() {
970 assert_eq!(min(&[1.0, f64::NAN]), None);
971 assert_eq!(max(&[1.0, f64::NAN]), None);
972 }
973
974 #[test]
977 fn test_median_odd() {
978 assert_eq!(median(&[3.0, 1.0, 2.0]), Some(2.0));
979 }
980
981 #[test]
982 fn test_median_even() {
983 assert_eq!(median(&[4.0, 1.0, 3.0, 2.0]), Some(2.5));
984 }
985
986 #[test]
987 fn test_median_single() {
988 assert_eq!(median(&[7.0]), Some(7.0));
989 }
990
991 #[test]
992 fn test_median_empty() {
993 assert_eq!(median(&[]), None);
994 }
995
996 #[test]
999 fn test_quantile_extremes() {
1000 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1001 assert_eq!(quantile(&data, 0.0), Some(1.0));
1002 assert_eq!(quantile(&data, 1.0), Some(5.0));
1003 }
1004
1005 #[test]
1006 fn test_quantile_median() {
1007 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1008 assert_eq!(quantile(&data, 0.5), Some(3.0));
1009 }
1010
1011 #[test]
1012 fn test_quantile_interpolation() {
1013 let data = [1.0, 2.0, 3.0, 4.0];
1014 let q = quantile(&data, 0.25).unwrap();
1017 assert!((q - 1.75).abs() < 1e-15);
1018 }
1019
1020 #[test]
1021 fn test_quantile_invalid_p() {
1022 assert_eq!(quantile(&[1.0, 2.0], -0.1), None);
1023 assert_eq!(quantile(&[1.0, 2.0], 1.1), None);
1024 }
1025
1026 #[test]
1027 fn test_quantile_empty() {
1028 assert_eq!(quantile(&[], 0.5), None);
1029 }
1030
1031 #[test]
1032 fn test_quantile_single() {
1033 assert_eq!(quantile(&[42.0], 0.0), Some(42.0));
1034 assert_eq!(quantile(&[42.0], 0.5), Some(42.0));
1035 assert_eq!(quantile(&[42.0], 1.0), Some(42.0));
1036 }
1037
1038 #[test]
1041 fn test_kahan_sum_basic() {
1042 let v = [1.0, 2.0, 3.0];
1043 assert!((kahan_sum(&v) - 6.0).abs() < 1e-15);
1044 }
1045
1046 #[test]
1047 fn test_kahan_sum_precision() {
1048 let v = [1e16, 1.0, -1e16];
1050 let result = kahan_sum(&v);
1051 assert!(
1052 (result - 1.0).abs() < 1e-10,
1053 "Kahan sum should preserve the 1.0: got {result}"
1054 );
1055 }
1056
1057 #[test]
1060 fn test_welford_empty() {
1061 let acc = WelfordAccumulator::new();
1062 assert_eq!(acc.count(), 0);
1063 assert_eq!(acc.mean(), None);
1064 assert_eq!(acc.sample_variance(), None);
1065 }
1066
1067 #[test]
1068 fn test_welford_single() {
1069 let mut acc = WelfordAccumulator::new();
1070 acc.update(5.0);
1071 assert_eq!(acc.mean(), Some(5.0));
1072 assert_eq!(acc.sample_variance(), None);
1073 assert_eq!(acc.population_variance(), Some(0.0));
1074 }
1075
1076 #[test]
1077 fn test_welford_matches_batch() {
1078 let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1079 let mut acc = WelfordAccumulator::new();
1080 for &x in &data {
1081 acc.update(x);
1082 }
1083 let batch_mean = mean(&data).unwrap();
1084 let batch_var = variance(&data).unwrap();
1085 assert!((acc.mean().unwrap() - batch_mean).abs() < 1e-14);
1086 assert!((acc.sample_variance().unwrap() - batch_var).abs() < 1e-10);
1087 }
1088
1089 #[test]
1090 fn test_welford_merge() {
1091 let data_a = [1.0, 2.0, 3.0, 4.0];
1092 let data_b = [5.0, 6.0, 7.0, 8.0];
1093 let data_all: Vec<f64> = data_a.iter().chain(data_b.iter()).copied().collect();
1094
1095 let mut acc_a = WelfordAccumulator::new();
1096 for &x in &data_a {
1097 acc_a.update(x);
1098 }
1099 let mut acc_b = WelfordAccumulator::new();
1100 for &x in &data_b {
1101 acc_b.update(x);
1102 }
1103 acc_a.merge(&acc_b);
1104
1105 let expected_mean = mean(&data_all).unwrap();
1106 let expected_var = variance(&data_all).unwrap();
1107
1108 assert!((acc_a.mean().unwrap() - expected_mean).abs() < 1e-14);
1109 assert!((acc_a.sample_variance().unwrap() - expected_var).abs() < 1e-10);
1110 }
1111
1112 #[test]
1115 fn test_skewness_symmetric() {
1116 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
1118 assert!(skewness(&data).unwrap().abs() < 1e-14);
1119 }
1120
1121 #[test]
1122 fn test_skewness_right_skewed() {
1123 let data = [1.0, 2.0, 3.0, 4.0, 50.0];
1125 let s = skewness(&data).unwrap();
1126 assert!(s > 0.0, "expected positive skewness, got {s}");
1127 }
1128
1129 #[test]
1130 fn test_skewness_left_skewed() {
1131 let data = [-50.0, 1.0, 2.0, 3.0, 4.0];
1133 let s = skewness(&data).unwrap();
1134 assert!(s < 0.0, "expected negative skewness, got {s}");
1135 }
1136
1137 #[test]
1138 fn test_skewness_edge_cases() {
1139 assert_eq!(skewness(&[]), None);
1140 assert_eq!(skewness(&[1.0]), None);
1141 assert_eq!(skewness(&[1.0, 2.0]), None);
1142 assert_eq!(skewness(&[5.0, 5.0, 5.0]), None);
1144 assert_eq!(skewness(&[1.0, f64::NAN, 3.0]), None);
1146 }
1147
1148 #[test]
1149 fn test_skewness_known_value() {
1150 let data = [1.0, 2.0, 3.0, 4.0, 8.0];
1160 let s = skewness(&data).unwrap();
1161 assert!(
1162 (s - 1.339).abs() < 0.01,
1163 "expected skewness ≈ 1.34, got {s}"
1164 );
1165 }
1166
1167 #[test]
1170 fn test_kurtosis_uniform_negative() {
1171 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1173 let k = kurtosis(&data).unwrap();
1174 assert!(k < 0.0, "uniform data should be platykurtic, got {k}");
1175 }
1176
1177 #[test]
1178 fn test_kurtosis_edge_cases() {
1179 assert_eq!(kurtosis(&[]), None);
1180 assert_eq!(kurtosis(&[1.0]), None);
1181 assert_eq!(kurtosis(&[1.0, 2.0]), None);
1182 assert_eq!(kurtosis(&[1.0, 2.0, 3.0]), None);
1183 assert_eq!(kurtosis(&[5.0, 5.0, 5.0, 5.0]), None);
1185 assert_eq!(kurtosis(&[1.0, f64::NAN, 3.0, 4.0]), None);
1187 }
1188
1189 #[test]
1190 fn test_kurtosis_heavy_tails() {
1191 let data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 100.0];
1193 let k = kurtosis(&data).unwrap();
1194 assert!(k > 0.0, "heavy-tailed data should be leptokurtic, got {k}");
1195 }
1196
1197 #[test]
1200 fn test_covariance_perfect_positive() {
1201 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1202 let y = [2.0, 4.0, 6.0, 8.0, 10.0];
1203 let cov = covariance(&x, &y).unwrap();
1204 assert!((cov - 5.0).abs() < 1e-14);
1205 }
1206
1207 #[test]
1208 fn test_covariance_perfect_negative() {
1209 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
1210 let y = [10.0, 8.0, 6.0, 4.0, 2.0];
1211 let cov = covariance(&x, &y).unwrap();
1212 assert!((cov - (-5.0)).abs() < 1e-14);
1213 }
1214
1215 #[test]
1216 fn test_covariance_zero() {
1217 let x = [1.0, 2.0, 3.0, 4.0];
1219 let y = [5.0, 5.0, 5.0, 5.0]; let cov = covariance(&x, &y).unwrap();
1221 assert!(cov.abs() < 1e-14);
1222 }
1223
1224 #[test]
1225 fn test_covariance_self_is_variance() {
1226 let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1227 let cov_xx = covariance(&data, &data).unwrap();
1228 let var = variance(&data).unwrap();
1229 assert!((cov_xx - var).abs() < 1e-10);
1230 }
1231
1232 #[test]
1233 fn test_covariance_edge_cases() {
1234 assert_eq!(covariance(&[], &[]), None);
1235 assert_eq!(covariance(&[1.0], &[2.0]), None);
1236 assert_eq!(covariance(&[1.0, 2.0], &[1.0]), None); assert_eq!(covariance(&[1.0, f64::NAN], &[1.0, 2.0]), None);
1238 }
1239
1240 #[test]
1243 fn test_welford_skewness_matches_batch() {
1244 let data = [1.0, 2.0, 3.0, 4.0, 50.0];
1245 let mut acc = WelfordAccumulator::new();
1246 for &x in &data {
1247 acc.update(x);
1248 }
1249 let batch = skewness(&data).unwrap();
1250 let stream = acc.skewness().unwrap();
1251 assert!(
1252 (batch - stream).abs() < 1e-10,
1253 "batch={batch}, stream={stream}"
1254 );
1255 }
1256
1257 #[test]
1258 fn test_welford_kurtosis_matches_batch() {
1259 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 100.0];
1260 let mut acc = WelfordAccumulator::new();
1261 for &x in &data {
1262 acc.update(x);
1263 }
1264 let batch = kurtosis(&data).unwrap();
1265 let stream = acc.kurtosis().unwrap();
1266 assert!(
1267 (batch - stream).abs() < 1e-8,
1268 "batch={batch}, stream={stream}"
1269 );
1270 }
1271
1272 #[test]
1273 fn test_welford_skewness_kurtosis_edge_cases() {
1274 let mut acc = WelfordAccumulator::new();
1275 assert_eq!(acc.skewness(), None);
1276 assert_eq!(acc.kurtosis(), None);
1277 acc.update(1.0);
1278 assert_eq!(acc.skewness(), None);
1279 assert_eq!(acc.kurtosis(), None);
1280 acc.update(2.0);
1281 assert_eq!(acc.skewness(), None);
1282 assert_eq!(acc.kurtosis(), None);
1283 acc.update(3.0);
1284 assert!(acc.skewness().is_some());
1286 assert_eq!(acc.kurtosis(), None);
1287 acc.update(4.0);
1288 assert!(acc.kurtosis().is_some());
1290 }
1291
1292 #[test]
1293 fn test_welford_merge_skewness_kurtosis() {
1294 let data_a = [1.0, 3.0, 5.0, 7.0, 9.0, 11.0];
1295 let data_b = [2.0, 50.0, 4.0, 6.0, 8.0, 100.0];
1296 let all: Vec<f64> = data_a.iter().chain(data_b.iter()).copied().collect();
1297
1298 let mut acc_a = WelfordAccumulator::new();
1299 for &x in &data_a {
1300 acc_a.update(x);
1301 }
1302 let mut acc_b = WelfordAccumulator::new();
1303 for &x in &data_b {
1304 acc_b.update(x);
1305 }
1306 acc_a.merge(&acc_b);
1307
1308 let expected_skew = skewness(&all).unwrap();
1309 let expected_kurt = kurtosis(&all).unwrap();
1310 let merged_skew = acc_a.skewness().unwrap();
1311 let merged_kurt = acc_a.kurtosis().unwrap();
1312
1313 assert!(
1314 (expected_skew - merged_skew).abs() < 1e-8,
1315 "skewness: expected={expected_skew}, merged={merged_skew}"
1316 );
1317 assert!(
1318 (expected_kurt - merged_kurt).abs() < 1e-6,
1319 "kurtosis: expected={expected_kurt}, merged={merged_kurt}"
1320 );
1321 }
1322
1323 #[test]
1326 fn test_variance_large_offset() {
1327 let data: Vec<f64> = (1..=5).map(|i| 1e9 + i as f64).collect();
1330 let var = variance(&data).unwrap();
1331 assert!(
1333 (var - 2.5).abs() < 1e-5,
1334 "Variance of offset data should be ~2.5, got {var}"
1335 );
1336 }
1337}
1338
1339#[cfg(test)]
1340mod proptests {
1341 use super::*;
1342 use proptest::prelude::*;
1343
1344 fn finite_vec(min_len: usize, max_len: usize) -> impl Strategy<Value = Vec<f64>> {
1346 proptest::collection::vec(
1347 prop::num::f64::NORMAL.prop_filter("finite", |x| x.is_finite() && x.abs() < 1e12),
1348 min_len..=max_len,
1349 )
1350 }
1351
1352 proptest! {
1353 #![proptest_config(ProptestConfig::with_cases(500))]
1354
1355 #[test]
1357 fn variance_non_negative(data in finite_vec(2, 100)) {
1358 let var = variance(&data).unwrap();
1359 prop_assert!(var >= 0.0, "variance must be >= 0, got {}", var);
1360 }
1361
1362 #[test]
1364 fn variance_of_constant_is_zero(
1365 value in prop::num::f64::NORMAL.prop_filter("finite", |x| x.is_finite()),
1366 n in 2_usize..50,
1367 ) {
1368 let data = vec![value; n];
1369 let var = variance(&data).unwrap();
1370 prop_assert!(var.abs() < 1e-10, "variance of constant should be ~0, got {}", var);
1371 }
1372
1373 #[test]
1375 fn std_dev_is_sqrt_of_variance(data in finite_vec(2, 100)) {
1376 let var = variance(&data).unwrap();
1377 let sd = std_dev(&data).unwrap();
1378 let diff = (sd * sd - var).abs();
1379 prop_assert!(diff < 1e-10 * var.max(1.0), "sd² should equal variance");
1380 }
1381
1382 #[test]
1384 fn mean_linearity(
1385 data in finite_vec(1, 100),
1386 a in -100.0_f64..100.0,
1387 b in -100.0_f64..100.0,
1388 ) {
1389 prop_assume!(a.is_finite() && b.is_finite());
1390 let m = mean(&data).unwrap();
1391 let transformed: Vec<f64> = data.iter().map(|&x| a * x + b).collect();
1392 if let Some(mt) = mean(&transformed) {
1393 let expected = a * m + b;
1394 let tol = 1e-8 * expected.abs().max(1.0);
1395 prop_assert!(
1396 (mt - expected).abs() < tol,
1397 "mean(a*x+b)={} != a*mean(x)+b={}",
1398 mt, expected
1399 );
1400 }
1401 }
1402
1403 #[test]
1405 fn quantile_extremes_are_min_max(data in finite_vec(1, 100)) {
1406 let q0 = quantile(&data, 0.0).unwrap();
1407 let q1 = quantile(&data, 1.0).unwrap();
1408 let mn = min(&data).unwrap();
1409 let mx = max(&data).unwrap();
1410 prop_assert!((q0 - mn).abs() < 1e-15, "quantile(0) should be min");
1411 prop_assert!((q1 - mx).abs() < 1e-15, "quantile(1) should be max");
1412 }
1413
1414 #[test]
1416 fn quantiles_monotonic(
1417 data in finite_vec(2, 100),
1418 p1 in 0.0_f64..=1.0,
1419 p2 in 0.0_f64..=1.0,
1420 ) {
1421 let (lo, hi) = if p1 <= p2 { (p1, p2) } else { (p2, p1) };
1422 let q_lo = quantile(&data, lo).unwrap();
1423 let q_hi = quantile(&data, hi).unwrap();
1424 prop_assert!(q_lo <= q_hi + 1e-15, "quantiles should be monotonic");
1425 }
1426
1427 #[test]
1429 fn median_equals_quantile_half(data in finite_vec(1, 100)) {
1430 let med = median(&data).unwrap();
1431 let q50 = quantile(&data, 0.5).unwrap();
1432 prop_assert!(
1433 (med - q50).abs() < 1e-14,
1434 "median={} != quantile(0.5)={}",
1435 med, q50
1436 );
1437 }
1438
1439 #[test]
1441 fn welford_merge_equals_sequential(
1442 data_a in finite_vec(1, 50),
1443 data_b in finite_vec(1, 50),
1444 ) {
1445 let mut sequential = WelfordAccumulator::new();
1446 for &x in data_a.iter().chain(data_b.iter()) {
1447 sequential.update(x);
1448 }
1449
1450 let mut acc_a = WelfordAccumulator::new();
1451 for &x in &data_a { acc_a.update(x); }
1452 let mut acc_b = WelfordAccumulator::new();
1453 for &x in &data_b { acc_b.update(x); }
1454 acc_a.merge(&acc_b);
1455
1456 let seq_mean = sequential.mean().unwrap();
1457 let mrg_mean = acc_a.mean().unwrap();
1458 prop_assert!(
1459 (seq_mean - mrg_mean).abs() < 1e-10 * seq_mean.abs().max(1.0),
1460 "merged mean should match sequential"
1461 );
1462
1463 if sequential.count() >= 2 {
1464 let seq_var = sequential.sample_variance().unwrap();
1465 let mrg_var = acc_a.sample_variance().unwrap();
1466 prop_assert!(
1467 (seq_var - mrg_var).abs() < 1e-8 * seq_var.max(1.0),
1468 "merged variance should match sequential"
1469 );
1470 }
1471 }
1472
1473 #[test]
1475 fn skewness_of_symmetric_is_zero(
1476 half in proptest::collection::vec(-1e6_f64..1e6, 2..=50),
1477 ) {
1478 prop_assume!(half.iter().all(|x| x.is_finite()));
1479 let mut data: Vec<f64> = half.clone();
1481 data.extend(half.iter().map(|x| -x));
1482 if let Some(s) = skewness(&data) {
1483 prop_assert!(
1484 s.abs() < 1e-8,
1485 "symmetric data should have ~0 skewness, got {}",
1486 s
1487 );
1488 }
1489 }
1490
1491 #[test]
1495 fn welford_skewness_matches_batch(
1496 data in proptest::collection::vec(-1e6_f64..1e6, 3..=100)
1497 ) {
1498 let mut acc = WelfordAccumulator::new();
1499 for &x in &data { acc.update(x); }
1500 match (skewness(&data), acc.skewness()) {
1501 (Some(batch), Some(stream)) if batch.is_finite() && stream.is_finite() => {
1502 let tol = 1e-8 * batch.abs().max(1.0);
1503 prop_assert!(
1504 (batch - stream).abs() < tol,
1505 "batch={} stream={}", batch, stream
1506 );
1507 }
1508 _ => {} }
1510 }
1511
1512 #[test]
1514 fn welford_kurtosis_matches_batch(
1515 data in proptest::collection::vec(-1e6_f64..1e6, 4..=100)
1516 ) {
1517 let mut acc = WelfordAccumulator::new();
1518 for &x in &data { acc.update(x); }
1519 match (kurtosis(&data), acc.kurtosis()) {
1520 (Some(batch), Some(stream)) if batch.is_finite() && stream.is_finite() => {
1521 let tol = 1e-6 * batch.abs().max(1.0);
1522 prop_assert!(
1523 (batch - stream).abs() < tol,
1524 "batch={} stream={}", batch, stream
1525 );
1526 }
1527 _ => {} }
1529 }
1530
1531 #[test]
1533 fn covariance_self_is_variance(data in finite_vec(2, 100)) {
1534 let cov = covariance(&data, &data).unwrap();
1535 let var = variance(&data).unwrap();
1536 let tol = 1e-10 * var.max(1.0);
1537 prop_assert!(
1538 (cov - var).abs() < tol,
1539 "Cov(x,x)={} != Var(x)={}", cov, var
1540 );
1541 }
1542
1543 #[test]
1545 fn covariance_symmetric(
1546 x in finite_vec(2, 50),
1547 y in finite_vec(2, 50),
1548 ) {
1549 let n = x.len().min(y.len());
1550 if n >= 2 {
1551 let x_slice = &x[..n];
1552 let y_slice = &y[..n];
1553 let cov_xy = covariance(x_slice, y_slice).unwrap();
1554 let cov_yx = covariance(y_slice, x_slice).unwrap();
1555 prop_assert!(
1556 (cov_xy - cov_yx).abs() < 1e-10 * cov_xy.abs().max(1.0),
1557 "Cov(x,y)={} != Cov(y,x)={}", cov_xy, cov_yx
1558 );
1559 }
1560 }
1561 }
1562}