use crate::binning::Histogram;
use crate::error::{DriftError, Result};
use statrs::distribution::{ChiSquared, ContinuousCDF};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ChiSquareResult {
pub statistic: f64,
pub p_value: f64,
pub degrees_of_freedom: usize,
}
pub fn chi_square_test(reference: &Histogram, live: &Histogram) -> Result<ChiSquareResult> {
if reference.len() != live.len() {
return Err(DriftError::BinCountMismatch {
reference: reference.len(),
live: live.len(),
});
}
let ref_counts = reference.counts();
let live_counts = live.counts();
let ref_total = reference.total();
let live_total = live.total();
if ref_total <= 0.0 || live_total <= 0.0 {
return Err(DriftError::EmptyInput(
"chi-square test needs a non-empty reference and live sample".into(),
));
}
let grand = ref_total + live_total;
let mut statistic = 0.0;
let mut effective_categories = 0usize;
for (&o_ref, &o_live) in ref_counts.iter().zip(live_counts) {
let col_total = o_ref + o_live;
if col_total <= 0.0 {
continue;
}
effective_categories += 1;
let e_ref = ref_total * col_total / grand;
let e_live = live_total * col_total / grand;
statistic += (o_ref - e_ref).powi(2) / e_ref;
statistic += (o_live - e_live).powi(2) / e_live;
}
if effective_categories < 2 {
return Err(DriftError::SampleTooSmall {
kind: "chi-square test",
minimum: 2,
actual: effective_categories,
});
}
let df = effective_categories - 1;
let dist = ChiSquared::new(df as f64)
.map_err(|e| DriftError::InvalidConfig(format!("chi-square distribution: {e}")))?;
let p_value = 1.0 - dist.cdf(statistic);
Ok(ChiSquareResult {
statistic,
p_value,
degrees_of_freedom: df,
})
}