use super::{StatsError, StatsResult, mean, validate_values};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MomentConvention {
Population,
UnbiasedSample,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct StandardizedMoments {
pub skewness: f64,
pub kurtosis: f64,
pub convention: MomentConvention,
pub samples: usize,
}
pub fn standardized_moments(
values: &[f64],
convention: MomentConvention,
) -> StatsResult<StandardizedMoments> {
validate_values("standardized_moments", values)?;
let minimum = if convention == MomentConvention::UnbiasedSample {
4
} else {
2
};
if values.len() < minimum {
return Err(StatsError::InsufficientInput {
metric: "standardized_moments",
minimum,
actual: values.len(),
});
}
let center = mean(values)?;
let n = values.len() as f64;
let m2 = values.iter().map(|x| (x - center).powi(2)).sum::<f64>() / n;
if m2 == 0.0 {
return Err(StatsError::InvalidControl {
field: "variance",
reason: "standardized moments require positive variance",
});
}
let m3 = values.iter().map(|x| (x - center).powi(3)).sum::<f64>() / n;
let m4 = values.iter().map(|x| (x - center).powi(4)).sum::<f64>() / n;
let (skewness, kurtosis) = match convention {
MomentConvention::Population => (m3 / m2.powf(1.5), m4 / (m2 * m2)),
MomentConvention::UnbiasedSample => {
let g1 = m3 / m2.powf(1.5);
let g2 = m4 / (m2 * m2) - 3.0;
(
(n * (n - 1.0)).sqrt() / (n - 2.0) * g1,
(n - 1.0) / ((n - 2.0) * (n - 3.0)) * ((n + 1.0) * g2 + 6.0),
)
}
};
Ok(StandardizedMoments {
skewness,
kurtosis,
convention,
samples: values.len(),
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum KsMethod {
ExactStatistic,
Asymptotic,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KsResult {
pub statistic: f64,
pub p_value: Option<f64>,
pub method: KsMethod,
pub effective_samples: f64,
}
pub fn kolmogorov_smirnov_one_sample(
values: &[f64],
cdf: impl Fn(f64) -> f64,
method: KsMethod,
) -> StatsResult<KsResult> {
validate_values("kolmogorov_smirnov_one_sample", values)?;
let mut sorted = values.to_vec();
sorted.sort_by(f64::total_cmp);
let n = sorted.len() as f64;
let mut d = 0.0_f64;
for (i, x) in sorted.into_iter().enumerate() {
let p = cdf(x);
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return Err(StatsError::ProbabilityOutOfRange {
metric: "kolmogorov_smirnov_one_sample",
index: Some(i),
value: p,
});
}
d = d
.max(((i + 1) as f64 / n - p).abs())
.max((p - i as f64 / n).abs());
}
Ok(ks_result(d, n, method))
}
pub fn kolmogorov_smirnov_two_sample(
left: &[f64],
right: &[f64],
method: KsMethod,
) -> StatsResult<KsResult> {
validate_values("kolmogorov_smirnov_two_sample:left", left)?;
validate_values("kolmogorov_smirnov_two_sample:right", right)?;
let mut a = left.to_vec();
let mut b = right.to_vec();
a.sort_by(f64::total_cmp);
b.sort_by(f64::total_cmp);
let (mut i, mut j, mut d) = (0, 0, 0.0_f64);
while i < a.len() || j < b.len() {
let x = match (a.get(i), b.get(j)) {
(Some(x), Some(y)) => {
if x.total_cmp(y).is_le() {
*x
} else {
*y
}
}
(Some(x), None) => *x,
(None, Some(y)) => *y,
(None, None) => break,
};
while i < a.len() && a[i] <= x {
i += 1
}
while j < b.len() && b[j] <= x {
j += 1
}
d = d.max((i as f64 / a.len() as f64 - j as f64 / b.len() as f64).abs());
}
let effective = (a.len() as f64 * b.len() as f64) / (a.len() + b.len()) as f64;
Ok(ks_result(d, effective, method))
}
fn ks_result(d: f64, effective: f64, method: KsMethod) -> KsResult {
let p_value = (method == KsMethod::Asymptotic).then(|| {
let lambda = (effective.sqrt() + 0.12 + 0.11 / effective.sqrt()) * d;
(1..=100)
.map(|k| {
let sign = if k % 2 == 1 { 1.0 } else { -1.0 };
2.0 * sign * (-2.0 * (k * k) as f64 * lambda * lambda).exp()
})
.sum::<f64>()
.clamp(0.0, 1.0)
});
KsResult {
statistic: d,
p_value,
method,
effective_samples: effective,
}
}