use crate::percentiles::{mean, stddev};
pub fn ks_statistic(baseline: &[u64], candidate: &[u64]) -> Option<f64> {
if baseline.is_empty() || candidate.is_empty() {
return None;
}
let mut a = baseline.to_vec();
let mut b = candidate.to_vec();
a.sort_unstable();
b.sort_unstable();
let mut i = 0usize;
let mut j = 0usize;
let na = a.len() as f64;
let nb = b.len() as f64;
let mut max_d = 0.0f64;
while i < a.len() && j < b.len() {
if a[i] <= b[j] {
i += 1;
} else {
j += 1;
}
let cdf_a = i as f64 / na;
let cdf_b = j as f64 / nb;
let d = (cdf_a - cdf_b).abs();
if d > max_d {
max_d = d;
}
}
Some(max_d)
}
pub fn cohens_d(baseline: &[u64], candidate: &[u64]) -> Option<f64> {
if baseline.is_empty() || candidate.is_empty() {
return None;
}
let na = baseline.len();
let nb = candidate.len();
let ma = mean(baseline) as f64;
let mb = mean(candidate) as f64;
let sa = stddev(baseline) as f64;
let sb = stddev(candidate) as f64;
let pooled =
(((na - 1) as f64 * sa * sa + (nb - 1) as f64 * sb * sb) / ((na + nb - 2) as f64)).sqrt();
if pooled <= 0.0 {
return Some(0.0);
}
Some((mb - ma) / pooled)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ks_same_distribution_near_zero() {
let a: Vec<u64> = (0..1000).collect();
let b: Vec<u64> = (0..1000).collect();
assert!(ks_statistic(&a, &b).unwrap() < 0.01);
}
#[test]
fn ks_shifted_distribution_large() {
let a: Vec<u64> = (0..1000).collect();
let b: Vec<u64> = (500..1500).collect();
assert!(ks_statistic(&a, &b).unwrap() > 0.4);
}
#[test]
fn ks_empty_returns_none() {
assert!(ks_statistic(&[], &[1, 2]).is_none());
assert!(ks_statistic(&[1, 2], &[]).is_none());
}
#[test]
fn cohens_d_zero_for_identical() {
let a: Vec<u64> = (0..100).collect();
let b: Vec<u64> = (0..100).collect();
assert!(cohens_d(&a, &b).unwrap().abs() < 0.01);
}
#[test]
fn cohens_d_positive_when_candidate_slower() {
let a: Vec<u64> = (100..200).collect();
let b: Vec<u64> = (200..300).collect();
assert!(cohens_d(&a, &b).unwrap() > 0.5);
}
}