pub mod anova;
pub mod t_test;
pub mod variance_tests;
pub use anova::one_way_anova;
pub use t_test::{t_test_1samp, t_test_ind, t_test_paired, t_test_welch};
pub use variance_tests::{bartlett, levene};
use crate::distributions::{Cdf, LogCdf};
use crate::distributions::{FDistribution, TDistribution};
use crate::tests_stat::Alternative;
pub(crate) fn mean(xs: &[f64]) -> f64 {
if xs.is_empty() {
return 0.0;
}
xs.iter().sum::<f64>() / len_f64(xs.len())
}
pub(crate) fn variance(xs: &[f64]) -> f64 {
let n = xs.len();
if n < 2 {
return 0.0;
}
let m = mean(xs);
let ss: f64 = xs.iter().map(|&x| (x - m) * (x - m)).sum();
ss / len_f64(n - 1)
}
pub(crate) fn len_f64(n: usize) -> f64 {
let wide = u64::try_from(n).unwrap_or(u64::MAX);
let hi = u32::try_from(wide >> 32).unwrap_or(0);
let lo = u32::try_from(wide & 0xFFFF_FFFF).unwrap_or(0);
f64::from(hi).mul_add(4_294_967_296.0, f64::from(lo))
}
pub(crate) fn floor_to_i64(x: f64) -> i64 {
if x <= 0.0 {
return 0;
}
let cap: i64 = 1_000_000_000;
let (mut lo, mut hi) = (0i64, cap);
while lo < hi {
let mid = lo + (hi - lo + 1) / 2;
if i64_to_f64(mid) <= x {
lo = mid;
} else {
hi = mid - 1;
}
}
lo
}
fn i64_to_f64(n: i64) -> f64 {
let wide = u64::try_from(n).unwrap_or(0);
let hi = u32::try_from(wide >> 32).unwrap_or(0);
let lo = u32::try_from(wide & 0xFFFF_FFFF).unwrap_or(0);
f64::from(hi).mul_add(4_294_967_296.0, f64::from(lo))
}
pub(crate) fn p_from_t(t: f64, df: f64, alternative: Alternative) -> f64 {
let cdf = |x: f64| t_cdf_fractional(x, df);
let p = match alternative {
Alternative::Less => cdf(t),
Alternative::Greater => 1.0 - cdf(t),
Alternative::TwoSided => 2.0 * (1.0 - cdf(t.abs())),
};
p.clamp(0.0, 1.0)
}
pub(crate) fn log_p_from_t(t: f64, df: f64, alternative: Alternative) -> f64 {
let lo_df = df.floor();
let lo = log_p_from_t_integer(t, floor_to_i64(lo_df), alternative);
if (df - lo_df).abs() < 1e-12 {
return lo.min(0.0);
}
let hi = log_p_from_t_integer(t, floor_to_i64(lo_df) + 1, alternative);
let frac = df - lo_df;
frac.mul_add(hi - lo, lo).min(0.0)
}
fn log_p_from_t_integer(t: f64, df: i64, alternative: Alternative) -> f64 {
let dist = TDistribution {
degrees_of_freedom: df.max(1),
..Default::default()
};
match alternative {
Alternative::Less => dist.logcdf(t),
Alternative::Greater => dist.logsf(t),
Alternative::TwoSided => std::f64::consts::LN_2 + dist.logsf(t.abs()),
}
}
fn t_cdf_fractional(x: f64, df: f64) -> f64 {
let lo_df = df.floor();
let lo = t_cdf_integer(x, floor_to_i64(lo_df));
if (df - lo_df).abs() < 1e-12 {
return lo;
}
let hi = t_cdf_integer(x, floor_to_i64(lo_df) + 1);
let frac = df - lo_df;
frac.mul_add(hi - lo, lo)
}
fn t_cdf_integer(x: f64, df: i64) -> f64 {
let dist = TDistribution {
degrees_of_freedom: df.max(1),
..Default::default()
};
dist.cdf(x)
}
pub(crate) fn f_upper_tail(f: f64, df_num: i64, df_den: i64) -> f64 {
let dist = FDistribution {
numerator_df: df_num,
denominator_df: df_den,
..Default::default()
};
(1.0 - dist.cdf(f)).clamp(0.0, 1.0)
}
pub(crate) fn f_upper_log_tail(f: f64, df_num: i64, df_den: i64) -> f64 {
if f <= 0.0 {
return 0.0;
}
let dist = FDistribution {
numerator_df: df_num,
denominator_df: df_den,
..Default::default()
};
dist.logsf(f).min(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn floor_to_i64_floors_correctly() {
assert_eq!(floor_to_i64(0.0), 0);
assert_eq!(floor_to_i64(8.999), 8);
assert_eq!(floor_to_i64(17.964), 17);
assert_eq!(floor_to_i64(-3.0), 0);
}
#[test]
fn variance_is_bessel_corrected() {
let v = variance(&[2.0, 4.0, 6.0]);
assert!((v - 4.0).abs() < 1e-12, "variance was {v}");
}
}