const LN_2PI: f64 = 1.837_877_066_409_345_5;
const LN_SQRT_2PI: f64 = 0.918_938_533_204_672_8;
fn stirlerr(n: f64) -> f64 {
const S0: f64 = 1.0 / 12.0;
const S1: f64 = 1.0 / 360.0;
const S2: f64 = 1.0 / 1260.0;
const S3: f64 = 1.0 / 1680.0;
const S4: f64 = 1.0 / 1188.0;
if n <= 15.0 {
return libm::lgamma(n + 1.0) - (n + 0.5) * n.ln() + n - LN_SQRT_2PI;
}
let nn = n * n;
if n > 500.0 {
return (S0 - S1 / nn) / n;
}
if n > 80.0 {
return (S0 - (S1 - S2 / nn) / nn) / n;
}
if n > 35.0 {
return (S0 - (S1 - (S2 - S3 / nn) / nn) / nn) / n;
}
(S0 - (S1 - (S2 - (S3 - S4 / nn) / nn) / nn) / nn) / n
}
fn bd0(x: f64, np: f64) -> f64 {
if (x - np).abs() < 0.1 * (x + np) {
let mut v = (x - np) / (x + np);
let mut s = (x - np) * v;
if s.abs() < f64::MIN_POSITIVE {
return s;
}
let mut ej = 2.0 * x * v;
v *= v;
for j in 1..1000 {
ej *= v;
let s1 = s + ej / (2 * j + 1) as f64;
if s1 == s {
return s1;
}
s = s1;
}
}
x * (x / np).ln() + np - x
}
fn dbinom_raw(x: f64, n: f64, p: f64, q: f64) -> f64 {
if p == 0.0 {
return if x == 0.0 { 1.0 } else { 0.0 };
}
if q == 0.0 {
return if x == n { 1.0 } else { 0.0 };
}
if x == 0.0 {
if n == 0.0 {
return 1.0;
}
return if p > q {
q.powf(n)
} else {
(n * (-p).ln_1p()).exp()
};
}
if x == n {
return if p > q {
(n * (-q).ln_1p()).exp()
} else {
p.powf(n)
};
}
if x < 0.0 || x > n {
return 0.0;
}
let lc = stirlerr(n) - stirlerr(x) - stirlerr(n - x) - bd0(x, n * p) - bd0(n - x, n * q);
let lf = LN_2PI + x.ln() + (-x / n).ln_1p();
(lc - 0.5 * lf).exp()
}
pub fn dbinom(x: u64, n: u64, p: f64) -> f64 {
dbinom_raw(x as f64, n as f64, p, 1.0 - p)
}
pub fn pbinom_upper(k: u64, n: u64, p: f64) -> f64 {
if k == 0 {
return 1.0;
}
if k > n || p <= 0.0 {
return 0.0;
}
if p >= 1.0 {
return 1.0;
}
let q = 1.0 - p;
let (nf, ratio) = (n as f64, p / q);
if k as f64 > nf * p {
let mut term = dbinom_raw(k as f64, nf, p, q);
let mut sum = term;
let mut i = k as f64;
while i < nf && term > f64::EPSILON * 1e-3 * sum {
term *= (nf - i) / (i + 1.0) * ratio;
sum += term;
i += 1.0;
}
sum.min(1.0)
} else {
let mut term = dbinom_raw((k - 1) as f64, nf, p, q);
let mut sum = term;
let mut i = (k - 1) as f64;
while i > 0.0 && term > f64::EPSILON * 1e-3 * sum {
term *= i / (nf - i + 1.0) / ratio;
sum += term;
i -= 1.0;
}
(1.0 - sum).max(0.0)
}
}
pub fn dhyper(x: u64, r: u64, b: u64, n: u64) -> f64 {
if n < x || r < x || n - x > b {
return 0.0;
}
if n == 0 {
return if x == 0 { 1.0 } else { 0.0 };
}
let (x, r, b, n) = (x as f64, r as f64, b as f64, n as f64);
let p = n / (r + b);
let q = (r + b - n) / (r + b);
let p1 = dbinom_raw(x, r, p, q);
let p2 = dbinom_raw(n - x, b, p, q);
let p3 = dbinom_raw(n, r + b, p, q);
p1 * p2 / p3
}
fn pdhyper(mut x: f64, nr: f64, nb: f64, n: f64) -> f64 {
let mut sum = 0.0;
let mut term = 1.0;
while x > 0.0 && term >= f64::EPSILON * sum {
term *= x * (nb - n + x) / (n + 1.0 - x) / (nr + 1.0 - x);
sum += term;
x -= 1.0;
}
1.0 + sum
}
pub fn phyper(x: i64, nr: u64, nb: u64, n: u64, lower: bool) -> f64 {
let (mut x, mut nr, mut nb, n) = (x as f64, nr as f64, nb as f64, n as f64);
let mut lower = lower;
if n > nr + nb {
return f64::NAN;
}
if x * (nr + nb) > n * nr {
std::mem::swap(&mut nr, &mut nb);
x = n - x - 1.0;
lower = !lower;
}
let (zero, one) = if lower { (0.0, 1.0) } else { (1.0, 0.0) };
if x < 0.0 || x < n - nb {
return zero;
}
if x >= nr || x >= n {
return one;
}
let d = dhyper(x as u64, nr as u64, nb as u64, n as u64);
if d == 0.0 {
return zero;
}
let pd = pdhyper(x, nr, nb, n);
if lower { d * pd } else { 0.5 - d * pd + 0.5 }
}
pub fn fisher_greater(a: u64, b: u64, c: u64, d: u64) -> f64 {
phyper(a as i64 - 1, a + c, b + d, a + b, false)
}
pub fn ln_fisher_greater(a: u64, b: u64, c: u64, d: u64) -> f64 {
let p = fisher_greater(a, b, c, d);
if p > 1e-300 {
return p.ln();
}
let (r, n, k) = (a + c, a + b + c + d, a + b);
let lchoose = |n: u64, k: u64| {
libm::lgamma(n as f64 + 1.0)
- libm::lgamma(k as f64 + 1.0)
- libm::lgamma((n - k) as f64 + 1.0)
};
let denom = lchoose(n, k);
let mut log_sum = f64::NEG_INFINITY;
for x in a..=r.min(k) {
let term = lchoose(r, x) + lchoose(n - r, k - x) - denom;
if term < log_sum - 40.0 {
break;
}
log_sum = if log_sum > term {
log_sum + (term - log_sum).exp().ln_1p()
} else {
term + (log_sum - term).exp().ln_1p()
};
}
log_sum.min(0.0)
}
pub fn ln_p_adjust_bh(ln_p: &[f64], m: f64) -> Vec<f64> {
let n = ln_p.len();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| ln_p[a].total_cmp(&ln_p[b]));
let mut q = vec![0.0; n];
let mut prev = 0.0f64;
for i in (0..n).rev() {
let qv = (ln_p[order[i]] + (m / (i + 1) as f64).ln()).min(prev);
prev = qv;
q[order[i]] = qv;
}
q
}
pub fn p_adjust_bh(p: &[f64]) -> Vec<f64> {
let n = p.len();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| p[b].total_cmp(&p[a]));
let mut out = vec![0.0; n];
let mut running = f64::INFINITY;
for (rank, &i) in order.iter().enumerate() {
let k = (n - rank) as f64;
running = running.min(n as f64 / k * p[i]);
out[i] = running.min(1.0);
}
out
}
pub fn p_adjust_bonferroni(p: &[f64]) -> Vec<f64> {
let n = p.len() as f64;
p.iter().map(|&x| (x * n).min(1.0)).collect()
}
pub fn median(values: &[f64]) -> Option<f64> {
if values.is_empty() {
return None;
}
let mut v = values.to_vec();
v.sort_by(f64::total_cmp);
let m = v.len() / 2;
Some(if v.len() % 2 == 1 {
v[m]
} else {
(v[m - 1] + v[m]) / 2.0
})
}
#[cfg(test)]
mod tests {
#[test]
fn bh_matches_r() {
let got = super::p_adjust_bh(&[0.01, 0.04, 0.03, 0.005, 0.5]);
let want = [0.025, 0.05, 0.05, 0.025, 0.5];
for (g, w) in got.iter().zip(want) {
assert!((g - w).abs() < 1e-15, "{got:?}");
}
}
}