use crate::categorical::validate_2x2_table;
use crate::error::Result;
use statrs::distribution::{ChiSquared, ContinuousCDF};
#[derive(Debug, Clone)]
pub struct McNemarkResult {
pub statistic: f64,
pub df: f64,
pub p_value: f64,
pub corrected: bool,
pub method: String,
}
pub fn mcnemar_test(table: &[[usize; 2]; 2], correction: bool) -> Result<McNemarkResult> {
validate_2x2_table(table)?;
let b = table[0][1] as f64;
let c = table[1][0] as f64;
let bc_sum = b + c;
let statistic = if bc_sum == 0.0 {
0.0
} else if correction {
let diff = (b - c).abs() - 1.0;
if diff <= 0.0 {
0.0
} else {
diff * diff / bc_sum
}
} else {
(b - c).powi(2) / bc_sum
};
let df = 1.0;
let p_value = if statistic > 0.0 {
let chi_dist = ChiSquared::new(df).unwrap();
chi_dist.sf(statistic)
} else {
1.0
};
let method = if correction {
"McNemar's Chi-squared test with continuity correction"
} else {
"McNemar's Chi-squared test"
};
Ok(McNemarkResult {
statistic,
df,
p_value,
corrected: correction,
method: method.to_string(),
})
}
#[derive(Debug, Clone)]
pub struct McNemarkExactResult {
pub p_value: f64,
pub b: usize,
pub c: usize,
pub method: String,
}
pub fn mcnemar_exact(table: &[[usize; 2]; 2]) -> Result<McNemarkExactResult> {
validate_2x2_table(table)?;
let b = table[0][1];
let c = table[1][0];
let n = b + c;
let p_value = if n == 0 {
1.0
} else {
let k = b.min(c);
let mut p = 0.0;
for i in 0..=k {
p += binomial_pmf(n, i, 0.5);
}
(2.0 * p).min(1.0)
};
Ok(McNemarkExactResult {
p_value,
b,
c,
method: "McNemar's Chi-squared test (exact)".to_string(),
})
}
fn binomial_pmf(n: usize, k: usize, p: f64) -> f64 {
if k > n {
return 0.0;
}
log_binomial_coeff(n, k).exp() * p.powi(k as i32) * (1.0 - p).powi((n - k) as i32)
}
fn log_binomial_coeff(n: usize, k: usize) -> f64 {
if k > n {
return f64::NEG_INFINITY;
}
if k == 0 || k == n {
return 0.0;
}
log_factorial(n) - log_factorial(k) - log_factorial(n - k)
}
fn log_factorial(n: usize) -> f64 {
if n <= 1 {
return 0.0;
}
if n <= 20 {
let mut result = 0.0;
for i in 2..=n {
result += (i as f64).ln();
}
return result;
}
let n_f = n as f64;
n_f * n_f.ln() - n_f + 0.5 * (2.0 * std::f64::consts::PI * n_f).ln()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mcnemar_basic() {
let table = [[59, 6], [16, 80]];
let result = mcnemar_test(&table, false).unwrap();
assert!((result.statistic - 4.545454545).abs() < 0.001);
assert!((result.df - 1.0).abs() < 1e-10);
}
#[test]
fn test_mcnemar_with_correction() {
let table = [[59, 6], [16, 80]];
let without = mcnemar_test(&table, false).unwrap();
let with_correction = mcnemar_test(&table, true).unwrap();
assert!(with_correction.statistic < without.statistic);
}
#[test]
fn test_mcnemar_symmetric() {
let table = [[50, 10], [10, 30]];
let result = mcnemar_test(&table, false).unwrap();
assert!((result.statistic - 0.0).abs() < 1e-10);
assert!((result.p_value - 1.0).abs() < 1e-10);
}
#[test]
fn test_mcnemar_exact() {
let table = [[10, 3], [7, 80]];
let result = mcnemar_exact(&table).unwrap();
assert!(result.p_value > 0.0 && result.p_value < 1.0);
}
#[test]
fn test_mcnemar_exact_equal() {
let table = [[50, 5], [5, 40]];
let result = mcnemar_exact(&table).unwrap();
assert!((result.p_value - 1.0).abs() < 1e-10);
}
}