use crate::error::{Result, StatError};
use crate::Alternative;
use statrs::distribution::{ContinuousCDF, StudentsT};
#[derive(Debug, Clone)]
pub struct YuenConfInt {
pub lower: f64,
pub upper: f64,
pub conf_level: f64,
}
#[derive(Debug, Clone)]
pub struct YuenResult {
pub statistic: f64,
pub df: f64,
pub p_value: f64,
pub diff: f64,
pub trimmed_mean_x: f64,
pub trimmed_mean_y: f64,
pub conf_int: Option<YuenConfInt>,
}
pub fn yuen_test(
x: &[f64],
y: &[f64],
trim: f64,
alternative: Alternative,
conf_level: Option<f64>,
) -> Result<YuenResult> {
if !(0.0..0.5).contains(&trim) {
return Err(StatError::InvalidParameter(format!(
"trim must be in [0, 0.5), got {}",
trim
)));
}
if x.is_empty() {
return Err(StatError::EmptyData);
}
if y.is_empty() {
return Err(StatError::EmptyData);
}
let nx = x.len();
let ny = y.len();
let gx = (trim * nx as f64).floor() as usize;
let gy = (trim * ny as f64).floor() as usize;
let hx = nx - 2 * gx;
let hy = ny - 2 * gy;
if hx < 2 {
return Err(StatError::InsufficientData { needed: 2, got: hx });
}
if hy < 2 {
return Err(StatError::InsufficientData { needed: 2, got: hy });
}
let trimmed_mean_x = trimmed_mean(x, gx);
let trimmed_mean_y = trimmed_mean(y, gy);
let winvar_x = winsorized_variance(x, gx);
let winvar_y = winsorized_variance(y, gy);
let hx_f = hx as f64;
let hy_f = hy as f64;
let dx = (nx - 1) as f64 * winvar_x / (hx_f * (hx_f - 1.0));
let dy = (ny - 1) as f64 * winvar_y / (hy_f * (hy_f - 1.0));
let diff = trimmed_mean_x - trimmed_mean_y;
let se = (dx + dy).sqrt();
let t_stat = diff / se;
let df = (dx + dy).powi(2) / (dx.powi(2) / (hx_f - 1.0) + dy.powi(2) / (hy_f - 1.0));
let t_dist = StudentsT::new(0.0, 1.0, df).unwrap();
let p_value = match alternative {
Alternative::TwoSided => 2.0 * t_dist.sf(t_stat.abs()),
Alternative::Less => t_dist.cdf(t_stat),
Alternative::Greater => t_dist.sf(t_stat),
};
let conf_int = if let Some(level) = conf_level {
if !(0.0 < level && level < 1.0) {
return Err(StatError::InvalidParameter(
"conf_level must be between 0 and 1".to_string(),
));
}
let alpha = 1.0 - level;
let t_crit = t_dist.inverse_cdf(1.0 - alpha / 2.0);
let margin = t_crit * se;
Some(YuenConfInt {
lower: diff - margin,
upper: diff + margin,
conf_level: level,
})
} else {
None
};
Ok(YuenResult {
statistic: t_stat,
df,
p_value,
diff,
trimmed_mean_x,
trimmed_mean_y,
conf_int,
})
}
fn trimmed_mean(data: &[f64], g: usize) -> f64 {
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = sorted.len();
let trimmed = &sorted[g..n - g];
trimmed.iter().sum::<f64>() / trimmed.len() as f64
}
fn winsorized_variance(data: &[f64], g: usize) -> f64 {
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = sorted.len();
let mut winsorized = sorted.clone();
let low_val = sorted[g];
let high_val = sorted[n - g - 1];
for item in winsorized.iter_mut().take(g) {
*item = low_val;
}
for item in winsorized.iter_mut().skip(n - g) {
*item = high_val;
}
let mean: f64 = winsorized.iter().sum::<f64>() / n as f64;
let sum_sq: f64 = winsorized.iter().map(|x| (x - mean).powi(2)).sum();
sum_sq / (n - 1) as f64
}