use crate::error::{Result, StatError};
use crate::nonparametric::ranks::rank;
use crate::parametric::Alternative;
use statrs::distribution::{ContinuousCDF, StudentsT};
#[derive(Debug, Clone)]
pub struct BrunnerMunzelConfInt {
pub lower: f64,
pub upper: f64,
pub conf_level: f64,
}
#[derive(Debug, Clone)]
pub struct BrunnerMunzelResult {
pub statistic: f64,
pub df: f64,
pub p_value: f64,
pub estimate: f64,
pub conf_int: Option<BrunnerMunzelConfInt>,
}
fn validate_bm_inputs(n1: usize, n2: usize) -> Result<()> {
if n1 == 0 || n2 == 0 {
return Err(StatError::EmptyData);
}
if n1 < 2 {
return Err(StatError::InsufficientData { needed: 2, got: n1 });
}
if n2 < 2 {
return Err(StatError::InsufficientData { needed: 2, got: n2 });
}
Ok(())
}
fn compute_bm_variance(
combined_ranks: &[f64],
within_ranks: &[f64],
mean_rank: f64,
n: f64,
) -> f64 {
combined_ranks
.iter()
.zip(within_ranks.iter())
.map(|(ri, wi)| {
let diff = ri - wi - mean_rank + (n + 1.0) / 2.0;
diff * diff
})
.sum::<f64>()
/ (n - 1.0)
}
fn compute_bm_pvalue(statistic: f64, df: f64, alternative: Alternative) -> f64 {
let t_dist = StudentsT::new(0.0, 1.0, df).unwrap();
match alternative {
Alternative::TwoSided => 2.0 * t_dist.sf(statistic.abs()).min(t_dist.cdf(statistic.abs())),
Alternative::Greater => t_dist.cdf(statistic),
Alternative::Less => t_dist.sf(statistic),
}
}
pub fn brunner_munzel(
x: &[f64],
y: &[f64],
alternative: Alternative,
alpha: Option<f64>,
) -> Result<BrunnerMunzelResult> {
let n1 = x.len();
let n2 = y.len();
validate_bm_inputs(n1, n2)?;
let n1_f = n1 as f64;
let n2_f = n2 as f64;
let r1 = rank(x)?;
let r2 = rank(y)?;
let combined: Vec<f64> = x.iter().chain(y.iter()).cloned().collect();
let r = rank(&combined)?;
let r_x = &r[0..n1];
let r_y = &r[n1..];
let m1: f64 = r_x.iter().sum::<f64>() / n1_f;
let m2: f64 = r_y.iter().sum::<f64>() / n2_f;
let pst = (m2 - (n2_f + 1.0) / 2.0) / n1_f;
let v1 = compute_bm_variance(r_x, &r1, m1, n1_f);
let v2 = compute_bm_variance(r_y, &r2, m2, n2_f);
let n_total = n1_f + n2_f;
let var_sum = n1_f * v1 + n2_f * v2;
if var_sum < 1e-14 {
return Err(StatError::InvalidParameter(
"Variance is effectively zero".to_string(),
));
}
let statistic = n1_f * n2_f * (m2 - m1) / n_total / var_sum.sqrt();
let df =
var_sum.powi(2) / ((n1_f * v1).powi(2) / (n1_f - 1.0) + (n2_f * v2).powi(2) / (n2_f - 1.0));
let p_value = compute_bm_pvalue(statistic, df, alternative);
let conf_int = if let Some(a) = alpha {
if !(0.0 < a && a < 1.0) {
return Err(StatError::InvalidParameter(
"alpha must be between 0 and 1".to_string(),
));
}
let t_dist = StudentsT::new(0.0, 1.0, df).unwrap();
let t_crit = t_dist.inverse_cdf(1.0 - a / 2.0);
let se = (v1 / (n1_f * n2_f * n2_f) + v2 / (n2_f * n1_f * n1_f)).sqrt();
let lower = pst - t_crit * se;
let upper = pst + t_crit * se;
Some(BrunnerMunzelConfInt {
lower,
upper,
conf_level: 1.0 - a,
})
} else {
None
};
Ok(BrunnerMunzelResult {
statistic,
df,
p_value,
estimate: pst,
conf_int,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_brunner_munzel_basic() {
let x = vec![
1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 4.0, 1.0, 1.0,
];
let y = vec![3.0, 3.0, 4.0, 3.0, 1.0, 2.0, 3.0, 1.0, 1.0, 5.0, 4.0];
let result = brunner_munzel(&x, &y, Alternative::TwoSided, None).unwrap();
assert_relative_eq!(result.statistic, 3.1375, epsilon = 0.001);
assert_relative_eq!(result.df, 17.683, epsilon = 0.01);
assert_relative_eq!(result.p_value, 0.005786, epsilon = 0.0001);
assert_relative_eq!(result.estimate, 0.788961, epsilon = 0.0001);
}
#[test]
fn test_brunner_munzel_equal_samples() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = brunner_munzel(&x, &y, Alternative::TwoSided, None).unwrap();
assert!(result.statistic.abs() < 0.5);
assert_relative_eq!(result.estimate, 0.5, epsilon = 0.1);
}
#[test]
fn test_brunner_munzel_empty() {
let x: Vec<f64> = vec![];
let y = vec![1.0, 2.0, 3.0];
assert!(brunner_munzel(&x, &y, Alternative::TwoSided, None).is_err());
}
#[test]
fn test_brunner_munzel_insufficient() {
let x = vec![1.0];
let y = vec![1.0, 2.0, 3.0];
assert!(brunner_munzel(&x, &y, Alternative::TwoSided, None).is_err());
}
}