use super::{len_f64, log_p_from_t, mean, p_from_t, variance};
use crate::error::{Error, Result};
use crate::tests_stat::{Alternative, TestResult};
pub fn t_test_1samp(sample: &[f64], popmean: f64, alternative: Alternative) -> Result<TestResult> {
let n = sample.len();
if n < 2 {
return Err(Error::InsufficientData);
}
let var = variance(sample);
if var <= 0.0 {
return Err(Error::DegenerateInput("zero-variance sample".to_owned()));
}
let df = len_f64(n - 1);
let se = (var / len_f64(n)).sqrt();
let t = (mean(sample) - popmean) / se;
Ok(result(t, df, alternative))
}
pub fn t_test_ind(a: &[f64], b: &[f64], alternative: Alternative) -> Result<TestResult> {
let (na, nb) = (a.len(), b.len());
if na < 2 || nb < 2 {
return Err(Error::InsufficientData);
}
let (va, vb) = (variance(a), variance(b));
let df_int = na + nb - 2;
let pooled_var = (len_f64(na - 1) * va + len_f64(nb - 1) * vb) / len_f64(df_int);
if pooled_var <= 0.0 {
return Err(Error::DegenerateInput("zero pooled variance".to_owned()));
}
let se = (pooled_var * (1.0 / len_f64(na) + 1.0 / len_f64(nb))).sqrt();
let t = (mean(a) - mean(b)) / se;
Ok(result(t, len_f64(df_int), alternative))
}
pub fn t_test_paired(a: &[f64], b: &[f64], alternative: Alternative) -> Result<TestResult> {
if a.len() != b.len() {
return Err(Error::InvalidInput(
"paired samples differ in length".to_owned(),
));
}
let diffs: Vec<f64> = a.iter().zip(b).map(|(&x, &y)| x - y).collect();
t_test_1samp(&diffs, 0.0, alternative)
}
pub fn t_test_welch(a: &[f64], b: &[f64], alternative: Alternative) -> Result<TestResult> {
let (na, nb) = (a.len(), b.len());
if na < 2 || nb < 2 {
return Err(Error::InsufficientData);
}
let (va, vb) = (variance(a), variance(b));
let (sa, sb) = (va / len_f64(na), vb / len_f64(nb));
let denom = sa + sb;
if denom <= 0.0 {
return Err(Error::DegenerateInput(
"zero variance in both samples".to_owned(),
));
}
let t = (mean(a) - mean(b)) / denom.sqrt();
let df = denom * denom / (sa * sa / len_f64(na - 1) + sb * sb / len_f64(nb - 1));
Ok(result(t, df, alternative))
}
fn result(t: f64, df: f64, alternative: Alternative) -> TestResult {
TestResult {
statistic: t,
p_value: p_from_t(t, df, alternative),
log_p_value: Some(log_p_from_t(t, df, alternative)),
df: Some(df),
effect_size: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t_at_sample_mean_is_zero() -> Result<()> {
let sample = [1.0, 2.0, 3.0, 4.0, 5.0];
let r = t_test_1samp(&sample, 3.0, Alternative::TwoSided)?;
assert!(r.statistic.abs() < 1e-12, "t was {}", r.statistic);
assert!((r.p_value - 1.0).abs() < 1e-9, "p was {}", r.p_value);
Ok(())
}
#[test]
fn log_p_at_sample_mean_is_zero() -> Result<()> {
let sample = [1.0, 2.0, 3.0, 4.0, 5.0];
let r = t_test_1samp(&sample, 3.0, Alternative::TwoSided)?;
let log_p = r
.log_p_value
.ok_or_else(|| Error::InvalidInput("no log p".to_owned()))?;
assert!(log_p.abs() < 1e-12, "log p was {log_p}");
Ok(())
}
#[test]
fn log_p_consistent_in_body_and_finite_in_deep_tail() -> Result<()> {
let sample = [4.0, 6.0, 5.0, 7.0, 3.0];
let r = t_test_1samp(&sample, 0.0, Alternative::TwoSided)?;
let log_p = r
.log_p_value
.ok_or_else(|| Error::InvalidInput("no log p".to_owned()))?;
assert!(
(log_p - r.p_value.ln()).abs() < 1e-9,
"log p {log_p} should equal ln(p) {}",
r.p_value.ln()
);
let tight = [100.0, 100.001, 99.999, 100.0005, 99.9995];
let r2 = t_test_1samp(&tight, 0.0, Alternative::TwoSided)?;
let log_p2 = r2
.log_p_value
.ok_or_else(|| Error::InvalidInput("no log p".to_owned()))?;
assert!(
log_p2.is_finite() && log_p2 < -30.0,
"deep-tail log p should be finite and far negative, was {log_p2}"
);
Ok(())
}
#[test]
fn paired_length_mismatch_is_invalid() {
assert!(matches!(
t_test_paired(&[1.0, 2.0], &[1.0], Alternative::TwoSided),
Err(Error::InvalidInput(_))
));
}
}