use super::kolmogorov_sf;
use crate::distributions::Cdf;
use crate::distributions::NormalDistribution;
use crate::error::{Error, Result};
use crate::tests_stat::exact::ks as exact;
use crate::tests_stat::parametric::len_f64;
use crate::tests_stat::{Alternative, Mode, TestResult};
const EXACT_THRESHOLD: usize = 100;
pub fn ks_one_sample(sample: &[f64], mean: f64, std_dev: f64) -> Result<TestResult> {
if sample.is_empty() {
return Err(Error::EmptyInput);
}
if std_dev <= 0.0 {
return Err(Error::InvalidInput("std_dev must be positive".to_owned()));
}
let dist = NormalDistribution {
mean,
standard_deviation: std_dev,
..Default::default()
};
let mut sorted = sample.to_vec();
sorted.sort_by(f64::total_cmp);
let n = len_f64(sorted.len());
let mut d = 0.0_f64;
for (i, &x) in sorted.iter().enumerate() {
let cdf = dist.cdf(x);
let upper = len_f64(i + 1) / n - cdf;
let lower = cdf - len_f64(i) / n;
d = d.max(upper).max(lower);
}
let p_value = kolmogorov_sf(n.sqrt() * d);
Ok(TestResult {
statistic: d,
p_value,
log_p_value: None,
df: None,
effect_size: None,
})
}
pub fn ks_two_sample(first: &[f64], second: &[f64]) -> Result<TestResult> {
if first.is_empty() || second.is_empty() {
return Err(Error::EmptyInput);
}
let mut sorted_a = first.to_vec();
let mut sorted_b = second.to_vec();
sorted_a.sort_by(f64::total_cmp);
sorted_b.sort_by(f64::total_cmp);
let na = len_f64(sorted_a.len());
let nb = len_f64(sorted_b.len());
let mut idx_a = 0;
let mut idx_b = 0;
let mut cdf_a = 0.0;
let mut cdf_b = 0.0;
let mut d = 0.0_f64;
while idx_a < sorted_a.len() && idx_b < sorted_b.len() {
let va = sorted_a.get(idx_a).copied().unwrap_or(f64::NAN);
let vb = sorted_b.get(idx_b).copied().unwrap_or(f64::NAN);
if va <= vb {
idx_a += 1;
cdf_a = len_f64(idx_a) / na;
}
if vb <= va {
idx_b += 1;
cdf_b = len_f64(idx_b) / nb;
}
d = d.max((cdf_a - cdf_b).abs());
}
let en = (na * nb / (na + nb)).sqrt();
let p_value = kolmogorov_sf(en * d);
Ok(TestResult {
statistic: d,
p_value,
log_p_value: None,
df: None,
effect_size: None,
})
}
pub fn ks_one_sample_mode(
sample: &[f64],
mean: f64,
std_dev: f64,
alternative: Alternative,
mode: Mode,
) -> Result<TestResult> {
if sample.is_empty() {
return Err(Error::EmptyInput);
}
if std_dev <= 0.0 {
return Err(Error::InvalidInput("std_dev must be positive".to_owned()));
}
let dist = NormalDistribution {
mean,
standard_deviation: std_dev,
..Default::default()
};
let mut sorted = sample.to_vec();
sorted.sort_by(f64::total_cmp);
let count = sorted.len();
let n = len_f64(count);
let mut d_plus = 0.0_f64;
let mut d_minus = 0.0_f64;
for (i, &x) in sorted.iter().enumerate() {
let cdf = dist.cdf(x);
d_plus = d_plus.max(len_f64(i + 1) / n - cdf);
d_minus = d_minus.max(cdf - len_f64(i) / n);
}
let d = match alternative {
Alternative::TwoSided => d_plus.max(d_minus),
Alternative::Greater => d_plus,
Alternative::Less => d_minus,
};
let resolved = mode.resolve(count, EXACT_THRESHOLD);
let p_value = match (resolved, alternative) {
(Mode::Exact, _) => exact::one_sample_p(d, count, alternative),
(_, Alternative::TwoSided) => kolmogorov_sf(n.sqrt() * d),
(_, Alternative::Less | Alternative::Greater) => {
return Err(Error::InvalidInput(
"one-sided KS supports only exact mode here".to_owned(),
));
}
};
Ok(TestResult {
statistic: d,
p_value,
log_p_value: None,
df: None,
effect_size: None,
})
}
pub fn ks_two_sample_mode(first: &[f64], second: &[f64], mode: Mode) -> Result<TestResult> {
let base = ks_two_sample(first, second)?;
let n1 = first.len();
let n2 = second.len();
let resolved = mode.resolve(n1.min(n2), EXACT_THRESHOLD);
if matches!(resolved, Mode::Asymptotic) {
return Ok(base);
}
let p_value = exact::two_sample_p(base.statistic, n1, n2);
Ok(TestResult { p_value, ..base })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_one_sample_is_error() {
assert!(matches!(
ks_one_sample(&[], 0.0, 1.0),
Err(Error::EmptyInput)
));
}
#[test]
fn identical_two_samples_have_zero_d() -> Result<()> {
let a = [1.0, 2.0, 3.0, 4.0];
let r = ks_two_sample(&a, &a)?;
assert!(r.statistic.abs() < 1e-12, "D was {}", r.statistic);
Ok(())
}
}