use crate::categorical::{validate_2x2_table, Alternative};
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct FisherResult {
pub p_value: f64,
pub odds_ratio: f64,
pub conf_int_lower: f64,
pub conf_int_upper: f64,
pub alternative: Alternative,
pub method: String,
}
pub fn fisher_exact(table: &[[usize; 2]; 2], alternative: Alternative) -> Result<FisherResult> {
validate_2x2_table(table)?;
let a = table[0][0];
let b = table[0][1];
let c = table[1][0];
let d = table[1][1];
let row1 = a + b;
let _row2 = c + d;
let col1 = a + c;
let col2 = b + d;
let n = a + b + c + d;
let odds_ratio = if b == 0 || c == 0 {
if a == 0 || d == 0 {
f64::NAN
} else {
f64::INFINITY
}
} else if a == 0 || d == 0 {
0.0
} else {
(a as f64 * d as f64) / (b as f64 * c as f64)
};
let observed_prob = hypergeometric_pmf(a, col1, n - col1, row1);
let p_value = match alternative {
Alternative::TwoSided => {
let min_a = row1.saturating_sub(col2);
let max_a = row1.min(col1);
let mut p = 0.0;
for k in min_a..=max_a {
let prob = hypergeometric_pmf(k, col1, n - col1, row1);
if prob <= observed_prob + 1e-10 {
p += prob;
}
}
p.min(1.0)
}
Alternative::Greater => {
let max_a = row1.min(col1);
let mut p = 0.0;
for k in a..=max_a {
p += hypergeometric_pmf(k, col1, n - col1, row1);
}
p.min(1.0)
}
Alternative::Less => {
let min_a = row1.saturating_sub(col2);
let mut p = 0.0;
for k in min_a..=a {
p += hypergeometric_pmf(k, col1, n - col1, row1);
}
p.min(1.0)
}
};
let (conf_int_lower, conf_int_upper) = odds_ratio_ci(a, b, c, d);
Ok(FisherResult {
p_value,
odds_ratio,
conf_int_lower,
conf_int_upper,
alternative,
method: "Fisher's Exact Test for Count Data".to_string(),
})
}
fn hypergeometric_pmf(k: usize, big_k: usize, big_n_minus_k: usize, n: usize) -> f64 {
let n_minus_k = if n >= k { n - k } else { return 0.0 };
if k > big_k || n_minus_k > big_n_minus_k {
return 0.0;
}
let big_n = big_k + big_n_minus_k;
let log_prob = log_binomial_coeff(big_k, k) + log_binomial_coeff(big_n_minus_k, n_minus_k)
- log_binomial_coeff(big_n, n);
log_prob.exp()
}
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() + 1.0 / (12.0 * n_f)
- 1.0 / (360.0 * n_f.powi(3))
}
fn odds_ratio_ci(a: usize, b: usize, c: usize, d: usize) -> (f64, f64) {
if a == 0 || b == 0 || c == 0 || d == 0 {
let a_adj = a as f64 + 0.5;
let b_adj = b as f64 + 0.5;
let c_adj = c as f64 + 0.5;
let d_adj = d as f64 + 0.5;
let log_or = (a_adj * d_adj).ln() - (b_adj * c_adj).ln();
let se_log_or = (1.0 / a_adj + 1.0 / b_adj + 1.0 / c_adj + 1.0 / d_adj).sqrt();
let z = 1.96; let lower = (log_or - z * se_log_or).exp();
let upper = (log_or + z * se_log_or).exp();
return (lower, upper);
}
let a_f = a as f64;
let b_f = b as f64;
let c_f = c as f64;
let d_f = d as f64;
let log_or = (a_f * d_f).ln() - (b_f * c_f).ln();
let se_log_or = (1.0 / a_f + 1.0 / b_f + 1.0 / c_f + 1.0 / d_f).sqrt();
let z = 1.96; let lower = (log_or - z * se_log_or).exp();
let upper = (log_or + z * se_log_or).exp();
(lower, upper)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fisher_exact_two_sided() {
let table = [[3, 1], [1, 3]];
let result = fisher_exact(&table, Alternative::TwoSided).unwrap();
assert!(result.p_value > 0.4 && result.p_value < 0.6);
assert!((result.odds_ratio - 9.0).abs() < 1e-10); }
#[test]
fn test_fisher_exact_one_sided_greater() {
let table = [[4, 0], [0, 4]];
let result = fisher_exact(&table, Alternative::Greater).unwrap();
assert!(result.p_value < 0.05);
}
#[test]
fn test_fisher_exact_one_sided_less() {
let table = [[0, 4], [4, 0]];
let result = fisher_exact(&table, Alternative::Less).unwrap();
assert!(result.p_value < 0.05);
}
#[test]
fn test_fisher_exact_no_association() {
let table = [[10, 10], [10, 10]];
let result = fisher_exact(&table, Alternative::TwoSided).unwrap();
assert!(result.p_value > 0.9);
assert!((result.odds_ratio - 1.0).abs() < 1e-10);
}
#[test]
fn test_hypergeometric_pmf() {
let prob = hypergeometric_pmf(2, 5, 5, 4);
assert!((prob - 0.476).abs() < 0.01);
}
#[test]
fn test_log_binomial_coeff() {
let log_c = log_binomial_coeff(10, 4);
assert!((log_c.exp() - 210.0).abs() < 0.001);
let log_c0 = log_binomial_coeff(5, 0);
assert!((log_c0.exp() - 1.0).abs() < 1e-10);
let log_c5 = log_binomial_coeff(5, 5);
assert!((log_c5.exp() - 1.0).abs() < 1e-10);
}
}