sim_lib_numbers_stats/
distribution.rs1use super::{StatsError, StatsResult, mean, validate_values};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum MomentConvention {
8 Population,
10 UnbiasedSample,
12}
13#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct StandardizedMoments {
16 pub skewness: f64,
18 pub kurtosis: f64,
20 pub convention: MomentConvention,
22 pub samples: usize,
24}
25pub fn standardized_moments(
27 values: &[f64],
28 convention: MomentConvention,
29) -> StatsResult<StandardizedMoments> {
30 validate_values("standardized_moments", values)?;
31 let minimum = if convention == MomentConvention::UnbiasedSample {
32 4
33 } else {
34 2
35 };
36 if values.len() < minimum {
37 return Err(StatsError::InsufficientInput {
38 metric: "standardized_moments",
39 minimum,
40 actual: values.len(),
41 });
42 }
43 let center = mean(values)?;
44 let n = values.len() as f64;
45 let m2 = values.iter().map(|x| (x - center).powi(2)).sum::<f64>() / n;
46 if m2 == 0.0 {
47 return Err(StatsError::InvalidControl {
48 field: "variance",
49 reason: "standardized moments require positive variance",
50 });
51 }
52 let m3 = values.iter().map(|x| (x - center).powi(3)).sum::<f64>() / n;
53 let m4 = values.iter().map(|x| (x - center).powi(4)).sum::<f64>() / n;
54 let (skewness, kurtosis) = match convention {
55 MomentConvention::Population => (m3 / m2.powf(1.5), m4 / (m2 * m2)),
56 MomentConvention::UnbiasedSample => {
57 let g1 = m3 / m2.powf(1.5);
58 let g2 = m4 / (m2 * m2) - 3.0;
59 (
60 (n * (n - 1.0)).sqrt() / (n - 2.0) * g1,
61 (n - 1.0) / ((n - 2.0) * (n - 3.0)) * ((n + 1.0) * g2 + 6.0),
62 )
63 }
64 };
65 Ok(StandardizedMoments {
66 skewness,
67 kurtosis,
68 convention,
69 samples: values.len(),
70 })
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub enum KsMethod {
76 ExactStatistic,
78 Asymptotic,
80}
81#[derive(Clone, Copy, Debug, PartialEq)]
83pub struct KsResult {
84 pub statistic: f64,
86 pub p_value: Option<f64>,
88 pub method: KsMethod,
90 pub effective_samples: f64,
92}
93
94pub fn kolmogorov_smirnov_one_sample(
96 values: &[f64],
97 cdf: impl Fn(f64) -> f64,
98 method: KsMethod,
99) -> StatsResult<KsResult> {
100 validate_values("kolmogorov_smirnov_one_sample", values)?;
101 let mut sorted = values.to_vec();
102 sorted.sort_by(f64::total_cmp);
103 let n = sorted.len() as f64;
104 let mut d = 0.0_f64;
105 for (i, x) in sorted.into_iter().enumerate() {
106 let p = cdf(x);
107 if !p.is_finite() || !(0.0..=1.0).contains(&p) {
108 return Err(StatsError::ProbabilityOutOfRange {
109 metric: "kolmogorov_smirnov_one_sample",
110 index: Some(i),
111 value: p,
112 });
113 }
114 d = d
115 .max(((i + 1) as f64 / n - p).abs())
116 .max((p - i as f64 / n).abs());
117 }
118 Ok(ks_result(d, n, method))
119}
120pub fn kolmogorov_smirnov_two_sample(
122 left: &[f64],
123 right: &[f64],
124 method: KsMethod,
125) -> StatsResult<KsResult> {
126 validate_values("kolmogorov_smirnov_two_sample:left", left)?;
127 validate_values("kolmogorov_smirnov_two_sample:right", right)?;
128 let mut a = left.to_vec();
129 let mut b = right.to_vec();
130 a.sort_by(f64::total_cmp);
131 b.sort_by(f64::total_cmp);
132 let (mut i, mut j, mut d) = (0, 0, 0.0_f64);
133 while i < a.len() || j < b.len() {
134 let x = match (a.get(i), b.get(j)) {
135 (Some(x), Some(y)) => {
136 if x.total_cmp(y).is_le() {
137 *x
138 } else {
139 *y
140 }
141 }
142 (Some(x), None) => *x,
143 (None, Some(y)) => *y,
144 (None, None) => break,
145 };
146 while i < a.len() && a[i] <= x {
147 i += 1
148 }
149 while j < b.len() && b[j] <= x {
150 j += 1
151 }
152 d = d.max((i as f64 / a.len() as f64 - j as f64 / b.len() as f64).abs());
153 }
154 let effective = (a.len() as f64 * b.len() as f64) / (a.len() + b.len()) as f64;
155 Ok(ks_result(d, effective, method))
156}
157fn ks_result(d: f64, effective: f64, method: KsMethod) -> KsResult {
158 let p_value = (method == KsMethod::Asymptotic).then(|| {
159 let lambda = (effective.sqrt() + 0.12 + 0.11 / effective.sqrt()) * d;
160 (1..=100)
161 .map(|k| {
162 let sign = if k % 2 == 1 { 1.0 } else { -1.0 };
163 2.0 * sign * (-2.0 * (k * k) as f64 * lambda * lambda).exp()
164 })
165 .sum::<f64>()
166 .clamp(0.0, 1.0)
167 });
168 KsResult {
169 statistic: d,
170 p_value,
171 method,
172 effective_samples: effective,
173 }
174}