use anyhow::{Context, Result};
use aprender::stats::DescriptiveStats;
use trueno::Vector;
#[derive(Debug, Clone)]
pub struct StatisticalTest {
pub statistic: f32,
pub pvalue: f32,
pub df: f32,
pub baseline_median: f32,
pub current_median: f32,
pub baseline_variance: f32,
pub current_variance: f32,
}
pub fn compare_distributions(baseline: &[f32], current: &[f32]) -> Result<StatisticalTest> {
if baseline.is_empty() || current.is_empty() {
anyhow::bail!("Cannot compare empty distributions");
}
if baseline.len() < 2 || current.len() < 2 {
anyhow::bail!("Need at least 2 samples per distribution for t-test");
}
let ttest_result = aprender::stats::hypothesis::ttest_ind(baseline, current, false)
.context("Failed to compute t-test")?;
let baseline_vec = Vector::from_slice(baseline);
let current_vec = Vector::from_slice(current);
let baseline_median = median(&baseline_vec)?;
let current_median = median(¤t_vec)?;
let baseline_variance =
baseline_vec.variance().context("Failed to compute baseline variance")?;
let current_variance = current_vec.variance().context("Failed to compute current variance")?;
Ok(StatisticalTest {
statistic: ttest_result.statistic,
pvalue: ttest_result.pvalue,
df: ttest_result.df,
baseline_median,
current_median,
baseline_variance,
current_variance,
})
}
pub fn median(vector: &Vector<f32>) -> Result<f32> {
let stats = DescriptiveStats::new(vector);
stats.quantile(0.5).map_err(|e| anyhow::anyhow!("Failed to compute median: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_median_odd_length() {
let vec = Vector::from_slice(&[1.0, 3.0, 5.0, 7.0, 9.0]);
assert_eq!(median(&vec).expect("test"), 5.0);
}
#[test]
fn test_median_even_length() {
let vec = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
assert_eq!(median(&vec).expect("test"), 2.5);
}
#[test]
fn test_variance_basic() {
let vec = Vector::from_slice(&[2.0, 4.0, 6.0, 8.0]);
let var = vec.variance().expect("test");
assert!((var - 5.0).abs() < 0.01);
}
#[test]
fn test_variance_constant() {
let vec = Vector::from_slice(&[5.0, 5.0, 5.0, 5.0]);
assert_eq!(vec.variance().expect("test"), 0.0);
}
#[test]
fn test_compare_distributions_significant_difference() {
let baseline = vec![10.0, 12.0, 11.0, 13.0, 10.0];
let current = vec![25.0, 27.0, 26.0, 28.0, 25.0];
let result = compare_distributions(&baseline, ¤t).expect("test");
assert!(result.pvalue < 0.05, "p-value {} should be < 0.05", result.pvalue);
assert!(result.current_median > result.baseline_median);
}
#[test]
fn test_compare_distributions_no_difference() {
let baseline = vec![10.0, 12.0, 11.0, 13.0, 10.0];
let current = vec![11.0, 13.0, 10.0, 12.0, 11.0];
let result = compare_distributions(&baseline, ¤t).expect("test");
assert!(result.pvalue >= 0.05, "p-value {} should be >= 0.05", result.pvalue);
}
#[test]
fn test_compare_distributions_empty_baseline() {
let baseline: Vec<f32> = vec![];
let current = vec![10.0, 12.0];
assert!(compare_distributions(&baseline, ¤t).is_err());
}
#[test]
fn test_compare_distributions_insufficient_samples() {
let baseline = vec![10.0]; let current = vec![12.0, 13.0];
assert!(compare_distributions(&baseline, ¤t).is_err());
}
}