use scirs2_core::numeric::Float;
pub(crate) fn f_test_p_value<F>(f_statistic: F, df_model: usize, df_residuals: usize) -> F
where
F: Float,
{
if df_model == 0 || df_residuals == 0 {
return F::one();
}
if f_statistic.is_nan() {
return F::nan();
}
if f_statistic.is_infinite() {
return if f_statistic > F::zero() {
F::zero()
} else {
F::one()
};
}
if f_statistic <= F::zero() {
return F::one();
}
let dfn = F::from(df_model).expect("Failed to convert degrees of freedom to float");
let dfd = F::from(df_residuals).expect("Failed to convert degrees of freedom to float");
match crate::distributions::f::F::new(dfn, dfd, F::zero(), F::one()) {
Ok(dist) => {
let p = F::one() - dist.cdf(f_statistic);
if p < F::zero() {
F::zero()
} else if p > F::one() {
F::one()
} else {
p
}
}
Err(_) => F::one(),
}
}
pub(crate) fn t_test_p_value<F>(t_statistic: F, df: usize) -> F
where
F: Float + Send + Sync + 'static + std::fmt::Display,
{
if df == 0 {
return F::one();
}
if t_statistic.is_nan() {
return F::nan();
}
let t_abs = if t_statistic < F::zero() {
-t_statistic
} else {
t_statistic
};
if t_abs.is_infinite() {
return F::zero();
}
let dff = F::from(df).expect("Failed to convert degrees of freedom to float");
match crate::distributions::student_t::StudentT::new(dff, F::zero(), F::one()) {
Ok(dist) => {
let p = (F::one() - dist.cdf(t_abs))
* F::from(2.0).expect("Failed to convert constant to float");
if p < F::zero() {
F::zero()
} else if p > F::one() {
F::one()
} else {
p
}
}
Err(_) => F::one(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_f_test_p_value_matches_scipy_stats_f_sf() {
let cases: &[(f64, usize, usize, f64)] = &[
(1.0, 1, 10, 0.34089313230206),
(5.0, 2, 20, 0.01734152991583262),
(10.0, 3, 16, 0.0005942866595794999),
(2.5, 4, 30, 0.0634764397982508),
(0.5, 5, 25, 0.7732635745548724),
(4.35, 1, 18, 0.05151514478735162),
];
for &(f_stat, df1, df2, expected_p) in cases {
let p = f_test_p_value(f_stat, df1, df2);
assert_relative_eq!(p, expected_p, epsilon = 1e-6, max_relative = 1e-4);
}
}
#[test]
fn test_f_test_p_value_not_hardcoded_zero() {
let p = f_test_p_value(1.0_f64, 1, 10);
assert!(p > 0.3, "expected p ~= 0.34, got {p}");
}
#[test]
fn test_f_test_p_value_edge_cases() {
assert_eq!(f_test_p_value(5.0_f64, 0, 10), 1.0);
assert_eq!(f_test_p_value(5.0_f64, 3, 0), 1.0);
assert_eq!(f_test_p_value(f64::INFINITY, 2, 10), 0.0);
assert!(f_test_p_value(f64::NAN, 2, 10).is_nan());
assert_eq!(f_test_p_value(0.0_f64, 2, 10), 1.0);
assert_eq!(f_test_p_value(-1.0_f64, 2, 10), 1.0);
}
#[test]
fn test_f_test_p_value_monotonically_decreasing_in_f() {
let fs = [0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0, 200.0];
let mut prev = f_test_p_value(fs[0], 3, 20);
for &f in &fs[1..] {
let p = f_test_p_value(f, 3, 20);
assert!(p < prev, "p({f}) = {p} should be < p of smaller F = {prev}");
prev = p;
}
}
#[test]
fn test_t_test_p_value_matches_scipy_stats_t_sf() {
let cases: &[(f64, usize, f64)] = &[
(1.0, 10, 0.34089313230205986),
(2.11, 17, 0.04998212471210528),
(3.5, 5, 0.017284431785293375),
(0.5, 25, 0.6214477851902287),
(4.35, 18, 0.0003858868406823035),
(10.0, 3, 0.002128399058414221),
];
for &(t, df, expected_p) in cases {
let p = t_test_p_value(t, df);
assert_relative_eq!(p, expected_p, epsilon = 1e-6, max_relative = 1e-4);
let p_neg = t_test_p_value(-t, df);
assert_relative_eq!(p, p_neg, epsilon = 1e-12);
}
}
#[test]
fn test_old_formula_produced_out_of_range_values() {
fn old_formula(t: f64, df: f64) -> f64 {
let t_abs = t.abs();
2.0 * (1.0 - t_abs / (df + t_abs * t_abs).sqrt())
}
let cases: &[(f64, f64)] = &[(1.0, 10.0), (2.0, 17.0), (0.5, 30.0), (3.0, 100.0)];
for &(t, df) in cases {
let old_p = old_formula(t, df);
assert!(
old_p > 1.0,
"expected the old formula to exceed 1.0 (demonstrating it was invalid) for t={t}, df={df}, got {old_p}"
);
}
}
#[test]
fn test_t_test_p_value_always_in_valid_range() {
for &df in &[1usize, 2, 5, 10, 17, 30, 100] {
for &t in &[0.0, 0.1, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 50.0] {
let p = t_test_p_value(t, df);
assert!(
(0.0..=1.0).contains(&p),
"t_test_p_value({t}, {df}) = {p} is out of the valid [0, 1] range"
);
}
}
}
#[test]
fn test_t_test_p_value_edge_cases() {
assert_eq!(t_test_p_value(5.0_f64, 0), 1.0);
assert_eq!(t_test_p_value(f64::INFINITY, 10), 0.0);
assert_eq!(t_test_p_value(f64::NEG_INFINITY, 10), 0.0);
assert!(t_test_p_value(f64::NAN, 10).is_nan());
assert_relative_eq!(t_test_p_value(0.0_f64, 10), 1.0, epsilon = 1e-9);
}
#[test]
fn test_t_test_p_value_monotonically_decreasing_in_t() {
let ts = [0.0, 0.5, 1.0, 2.0, 5.0, 10.0, 50.0];
let mut prev = t_test_p_value(ts[0], 15);
for &t in &ts[1..] {
let p = t_test_p_value(t, 15);
assert!(p < prev, "p({t}) = {p} should be < p of smaller t = {prev}");
prev = p;
}
}
}