use crate::error::{Result, StatError};
use crate::nonparametric::ranks::rank_with_ties;
use crate::parametric::Alternative;
use statrs::distribution::{ContinuousCDF, Normal};
fn tie_correction(tie_sizes: &[usize]) -> f64 {
tie_sizes
.iter()
.map(|&t| {
let t = t as f64;
t * t * t - t
})
.sum()
}
fn compute_p_value(z: f64, alternative: &Alternative) -> f64 {
let normal = Normal::new(0.0, 1.0).unwrap();
match alternative {
Alternative::TwoSided => 2.0 * normal.sf(z.abs()),
Alternative::Less => normal.cdf(z),
Alternative::Greater => normal.sf(z),
}
}
#[derive(Debug, Clone)]
pub struct ConfidenceInterval {
pub lower: f64,
pub upper: f64,
pub conf_level: f64,
}
#[derive(Debug, Clone)]
pub struct MannWhitneyResult {
pub statistic: f64,
pub p_value: f64,
pub estimate: Option<f64>,
pub conf_int: Option<ConfidenceInterval>,
pub null_value: f64,
}
#[derive(Debug, Clone)]
pub struct WilcoxonResult {
pub statistic: f64,
pub p_value: f64,
pub estimate: Option<f64>,
pub conf_int: Option<ConfidenceInterval>,
pub null_value: f64,
}
pub fn mann_whitney_u(
x: &[f64],
y: &[f64],
alternative: Alternative,
continuity_correction: bool,
exact: bool,
conf_level: Option<f64>,
mu: Option<f64>,
) -> Result<MannWhitneyResult> {
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 n = nx + ny;
let mu_shift = mu.unwrap_or(0.0);
let y_shifted: Vec<f64> = y.iter().map(|yi| yi + mu_shift).collect();
let y_use = if mu.is_some() { &y_shifted } else { y };
let mut combined: Vec<f64> = Vec::with_capacity(n);
combined.extend_from_slice(x);
combined.extend_from_slice(y_use);
let (ranks, tie_sizes) = rank_with_ties(&combined)?;
let r1: f64 = ranks[..nx].iter().sum();
let u1 = r1 - (nx * (nx + 1)) as f64 / 2.0;
let nx_f = nx as f64;
let ny_f = ny as f64;
let n_f = n as f64;
let mu = nx_f * ny_f / 2.0;
let tc = tie_correction(&tie_sizes);
let sigma_sq = (nx_f * ny_f / 12.0) * ((n_f + 1.0) - tc / (n_f * (n_f - 1.0)));
let sigma = sigma_sq.sqrt();
let has_ties = tie_sizes.iter().any(|&t| t > 1);
let p_value = if exact && !has_ties {
mann_whitney_exact_p(nx, ny, u1 as usize, &alternative)
} else {
let correction = if continuity_correction { 0.5 } else { 0.0 };
let z = match alternative {
Alternative::TwoSided => {
if u1 > mu {
(u1 - mu - correction) / sigma
} else {
(u1 - mu + correction) / sigma
}
}
Alternative::Less => (u1 - mu + correction) / sigma,
Alternative::Greater => (u1 - mu - correction) / sigma,
};
compute_p_value(z, &alternative)
};
let (estimate, 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 (est, ci) = mann_whitney_estimate_ci(x, y, level, exact && !has_ties)?;
(Some(est), Some(ci))
} else {
(None, None)
};
Ok(MannWhitneyResult {
statistic: u1,
p_value,
estimate,
conf_int,
null_value: mu_shift,
})
}
pub fn wilcoxon_signed_rank(
x: &[f64],
y: &[f64],
alternative: Alternative,
continuity_correction: bool,
exact: bool,
conf_level: Option<f64>,
mu: Option<f64>,
) -> Result<WilcoxonResult> {
let n = x.len();
if n == 0 {
return Err(StatError::EmptyData);
}
if n != y.len() {
return Err(StatError::InvalidParameter(format!(
"Wilcoxon signed-rank test requires equal length samples, got {} and {}",
n,
y.len()
)));
}
let mu_shift = mu.unwrap_or(0.0);
let diffs: Vec<f64> = x
.iter()
.zip(y.iter())
.map(|(xi, yi)| xi - yi - mu_shift)
.filter(|&d| d != 0.0)
.collect();
let n_nonzero = diffs.len();
if n_nonzero == 0 {
return Ok(WilcoxonResult {
statistic: 0.0,
p_value: 1.0,
estimate: None,
conf_int: None,
null_value: mu_shift,
});
}
let abs_diffs: Vec<f64> = diffs.iter().map(|d| d.abs()).collect();
let (ranks, tie_sizes) = rank_with_ties(&abs_diffs)?;
let v: f64 = diffs
.iter()
.zip(ranks.iter())
.filter(|(&d, _)| d > 0.0)
.map(|(_, &r)| r)
.sum();
let n_f = n_nonzero as f64;
let mu = n_f * (n_f + 1.0) / 4.0;
let tc = tie_correction(&tie_sizes);
let sigma_sq = n_f * (n_f + 1.0) * (2.0 * n_f + 1.0) / 24.0 - tc / 48.0;
let sigma = sigma_sq.sqrt();
let has_ties = tie_sizes.iter().any(|&t| t > 1);
let p_value = if exact && !has_ties {
wilcoxon_exact_p(n_nonzero, v as usize, &alternative)
} else {
let correction = if continuity_correction { 0.5 } else { 0.0 };
let z = match alternative {
Alternative::TwoSided => {
if v > mu {
(v - mu - correction) / sigma
} else {
(v - mu + correction) / sigma
}
}
Alternative::Less => (v - mu + correction) / sigma,
Alternative::Greater => (v - mu - correction) / sigma,
};
compute_p_value(z, &alternative)
};
let (estimate, 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 (est, ci) = wilcoxon_estimate_ci(&diffs, level, exact && !has_ties)?;
(Some(est), Some(ci))
} else {
(None, None)
};
Ok(WilcoxonResult {
statistic: v,
p_value,
estimate,
conf_int,
null_value: mu_shift,
})
}
fn mann_whitney_exact_p(n1: usize, n2: usize, u: usize, alternative: &Alternative) -> f64 {
let total = mann_whitney_count_all(n1, n2);
match alternative {
Alternative::TwoSided => {
let p_lower = mann_whitney_count_le(n1, n2, u) as f64 / total as f64;
let u_upper = n1 * n2 - u;
let p_upper = mann_whitney_count_ge(n1, n2, u_upper) as f64 / total as f64;
2.0 * p_lower.min(p_upper).min(0.5)
}
Alternative::Less => {
mann_whitney_count_le(n1, n2, u) as f64 / total as f64
}
Alternative::Greater => {
mann_whitney_count_ge(n1, n2, u) as f64 / total as f64
}
}
}
fn mann_whitney_count_all(n1: usize, n2: usize) -> u64 {
binomial(n1 + n2, n1)
}
fn mann_whitney_count_le(n1: usize, n2: usize, u: usize) -> u64 {
mann_whitney_count_le_recursive(n1, n2, u)
}
fn mann_whitney_count_le_recursive(n1: usize, n2: usize, max_u: usize) -> u64 {
use std::collections::HashMap;
fn count(
n1: usize,
n2: usize,
u: usize,
memo: &mut HashMap<(usize, usize, usize), u64>,
) -> u64 {
if n1 == 0 {
return 1; }
if n2 == 0 {
return 1; }
if let Some(&result) = memo.get(&(n1, n2, u)) {
return result;
}
let mut result = count(n1, n2 - 1, u, memo); if u >= n2 {
result += count(n1 - 1, n2, u - n2, memo); }
memo.insert((n1, n2, u), result);
result
}
let mut memo = HashMap::new();
count(n1, n2, max_u, &mut memo)
}
fn mann_whitney_count_ge(n1: usize, n2: usize, u: usize) -> u64 {
let total = mann_whitney_count_all(n1, n2);
let less = if u > 0 {
mann_whitney_count_le_recursive(n1, n2, u - 1)
} else {
0
};
total - less
}
fn wilcoxon_exact_p(n: usize, v: usize, alternative: &Alternative) -> f64 {
let total = 1u64 << n; let max_v = n * (n + 1) / 2;
match alternative {
Alternative::TwoSided => {
let v_symmetric = max_v - v;
let p_lower = wilcoxon_count_le(n, v) as f64 / total as f64;
let p_upper = wilcoxon_count_le(n, v_symmetric) as f64 / total as f64;
2.0 * p_lower.min(p_upper).min(0.5)
}
Alternative::Less => {
wilcoxon_count_le(n, v) as f64 / total as f64
}
Alternative::Greater => {
wilcoxon_count_ge(n, v) as f64 / total as f64
}
}
}
fn wilcoxon_count_le(n: usize, v: usize) -> u64 {
let max_v = n * (n + 1) / 2;
let v = v.min(max_v);
let mut dp = vec![0u64; v + 1];
dp[0] = 1;
for rank in 1..=n {
for j in (rank..=v).rev() {
dp[j] += dp[j - rank];
}
}
dp.iter().sum()
}
fn wilcoxon_count_ge(n: usize, v: usize) -> u64 {
let total = 1u64 << n;
let less = if v > 0 {
wilcoxon_count_le(n, v - 1)
} else {
0
};
total - less
}
fn binomial(n: usize, k: usize) -> u64 {
if k > n {
return 0;
}
if k == 0 || k == n {
return 1;
}
let k = k.min(n - k); let mut result = 1u64;
for i in 0..k {
result = result * (n - i) as u64 / (i + 1) as u64;
}
result
}
fn mann_whitney_estimate_ci(
x: &[f64],
y: &[f64],
conf_level: f64,
exact: bool,
) -> Result<(f64, ConfidenceInterval)> {
let nx = x.len();
let ny = y.len();
let n_pairs = nx * ny;
let mut diffs: Vec<f64> = Vec::with_capacity(n_pairs);
for xi in x {
for yi in y {
diffs.push(xi - yi);
}
}
diffs.sort_by(|a, b| a.partial_cmp(b).unwrap());
let estimate = if n_pairs % 2 == 0 {
(diffs[n_pairs / 2 - 1] + diffs[n_pairs / 2]) / 2.0
} else {
diffs[n_pairs / 2]
};
let alpha = 1.0 - conf_level;
let (k_lower, k_upper) = if exact && nx <= 20 && ny <= 20 {
mann_whitney_ci_exact_k(nx, ny, alpha)
} else {
mann_whitney_ci_approx_k(nx, ny, alpha)
};
let lower = if k_lower > 0 {
diffs[k_lower - 1]
} else {
diffs[0]
};
let upper = if k_upper <= n_pairs {
diffs[k_upper - 1]
} else {
diffs[n_pairs - 1]
};
Ok((
estimate,
ConfidenceInterval {
lower,
upper,
conf_level,
},
))
}
fn mann_whitney_ci_exact_k(n1: usize, n2: usize, alpha: f64) -> (usize, usize) {
let total = mann_whitney_count_all(n1, n2) as f64;
let n_pairs = n1 * n2;
let mut wci_lo = 0;
for k in 0..n_pairs {
let p = mann_whitney_count_le_recursive(n1, n2, k) as f64 / total;
if p >= alpha / 2.0 {
wci_lo = k;
break;
}
}
let wci_hi = n_pairs - wci_lo;
let k_lower = if wci_lo > 0 { wci_lo } else { 1 };
let k_upper = wci_hi + 1;
(k_lower, k_upper)
}
fn mann_whitney_ci_approx_k(n1: usize, n2: usize, alpha: f64) -> (usize, usize) {
let n1_f = n1 as f64;
let n2_f = n2 as f64;
let n_pairs = n1 * n2;
let mu = n1_f * n2_f / 2.0;
let sigma = (n1_f * n2_f * (n1_f + n2_f + 1.0) / 12.0).sqrt();
let normal = Normal::new(0.0, 1.0).unwrap();
let z = normal.inverse_cdf(1.0 - alpha / 2.0);
let k_lower = ((mu - z * sigma).floor() as usize).max(1);
let k_upper = ((mu + z * sigma).ceil() as usize + 1).min(n_pairs);
(k_lower, k_upper)
}
fn wilcoxon_estimate_ci(
diffs: &[f64],
conf_level: f64,
exact: bool,
) -> Result<(f64, ConfidenceInterval)> {
let n = diffs.len();
let n_walsh = n * (n + 1) / 2;
let mut walsh: Vec<f64> = Vec::with_capacity(n_walsh);
for i in 0..n {
for j in i..n {
walsh.push((diffs[i] + diffs[j]) / 2.0);
}
}
walsh.sort_by(|a, b| a.partial_cmp(b).unwrap());
let estimate = if n_walsh % 2 == 0 {
(walsh[n_walsh / 2 - 1] + walsh[n_walsh / 2]) / 2.0
} else {
walsh[n_walsh / 2]
};
let alpha = 1.0 - conf_level;
let (k_lower, k_upper) = if exact && n <= 20 {
wilcoxon_ci_exact_k(n, alpha)
} else {
wilcoxon_ci_approx_k(n, alpha)
};
let lower = if k_lower > 0 {
walsh[k_lower - 1]
} else {
walsh[0]
};
let upper = if k_upper <= n_walsh {
walsh[k_upper - 1]
} else {
walsh[n_walsh - 1]
};
Ok((
estimate,
ConfidenceInterval {
lower,
upper,
conf_level,
},
))
}
fn wilcoxon_ci_exact_k(n: usize, alpha: f64) -> (usize, usize) {
let total = (1u64 << n) as f64;
let n_walsh = n * (n + 1) / 2;
let mut k_lo = 0;
for k in 0..=n_walsh {
let p = wilcoxon_count_le(n, k) as f64 / total;
if p >= alpha / 2.0 {
k_lo = k;
break;
}
}
let k_hi = n_walsh - k_lo;
let k_lower = if k_lo > 0 { k_lo } else { 1 };
let k_upper = k_hi + 1;
(k_lower, k_upper)
}
fn wilcoxon_ci_approx_k(n: usize, alpha: f64) -> (usize, usize) {
let n_f = n as f64;
let n_walsh = n * (n + 1) / 2;
let mu = n_f * (n_f + 1.0) / 4.0;
let sigma = (n_f * (n_f + 1.0) * (2.0 * n_f + 1.0) / 24.0).sqrt();
let normal = Normal::new(0.0, 1.0).unwrap();
let z = normal.inverse_cdf(1.0 - alpha / 2.0);
let k_lower = ((mu - z * sigma).floor() as usize).max(1);
let k_upper = ((mu + z * sigma).ceil() as usize + 1).min(n_walsh);
(k_lower, k_upper)
}