use crate::weightstats::Alternative;
use solow_distributions::special::{betainc, lgamma};
use solow_distributions::{norm_cdf, norm_sf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compare {
Ratio,
Diff,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoissonMethod {
Wald,
WaldCcv,
Score,
WaldLog,
ScoreLog,
Sqrt,
ExactCond,
CondMidp,
}
#[derive(Debug, Clone, Copy)]
pub struct PoissonResult {
pub statistic: f64,
pub pvalue: f64,
pub rate1: f64,
pub rate2: f64,
pub ratio: f64,
pub diff: f64,
}
fn ln_binom(n: f64, k: f64) -> f64 {
lgamma(n + 1.0) - lgamma(k + 1.0) - lgamma(n - k + 1.0)
}
fn binom_pmf(k: f64, n: f64, p: f64) -> f64 {
if k < 0.0 || k > n {
return 0.0;
}
if p <= 0.0 {
return if k == 0.0 { 1.0 } else { 0.0 };
}
if p >= 1.0 {
return if k == n { 1.0 } else { 0.0 };
}
(ln_binom(n, k) + k * p.ln() + (n - k) * (1.0 - p).ln()).exp()
}
fn binom_cdf(k: f64, n: f64, p: f64) -> f64 {
let k = k.floor();
if k < 0.0 {
return 0.0;
}
if k >= n {
return 1.0;
}
betainc(n - k, k + 1.0, 1.0 - p)
}
fn binom_sf(k: f64, n: f64, p: f64) -> f64 {
1.0 - binom_cdf(k, n, p)
}
fn binary_search_binom(a: &dyn Fn(f64) -> f64, d: f64, mut lo: f64, mut hi: f64) -> f64 {
while lo < hi {
let mid = lo + ((hi - lo) / 2.0).floor();
let midval = a(mid);
if midval < d {
lo = mid + 1.0;
} else if midval > d {
hi = mid - 1.0;
} else {
return mid;
}
}
if a(lo) <= d {
lo
} else {
lo - 1.0
}
}
fn binom_test_two_sided(k: f64, n: f64, p: f64) -> f64 {
let d = binom_pmf(k, n, p);
let rerr = 1.0 + 1e-7;
let pval = if k == p * n {
1.0
} else if k < p * n {
let neg_pmf = |x: f64| -binom_pmf(x, n, p);
let ix = binary_search_binom(&neg_pmf, -d * rerr, (p * n).ceil(), n);
let y = n - ix
+ if d * rerr == binom_pmf(ix, n, p) {
1.0
} else {
0.0
};
binom_cdf(k, n, p) + binom_sf(n - y, n, p)
} else {
let pmf = |x: f64| binom_pmf(x, n, p);
let ix = binary_search_binom(&pmf, d * rerr, 0.0, (p * n).floor());
let y = ix + 1.0;
binom_cdf(y - 1.0, n, p) + binom_sf(k - 1.0, n, p)
};
pval.min(1.0)
}
fn binom_test(count: f64, total: f64, prop: f64, alternative: Alternative) -> f64 {
match alternative {
Alternative::TwoSided => binom_test_two_sided(count, total, prop),
Alternative::Larger => binom_sf(count - 1.0, total, prop),
Alternative::Smaller => binom_cdf(count, total, prop),
}
}
fn z_pvalue(stat: f64, alternative: Alternative) -> f64 {
match alternative {
Alternative::TwoSided => norm_sf(stat.abs()) * 2.0,
Alternative::Larger => norm_sf(stat),
Alternative::Smaller => norm_cdf(stat),
}
}
#[allow(clippy::too_many_arguments)]
pub fn test_poisson_2indep(
count1: f64,
exposure1: f64,
count2: f64,
exposure2: f64,
value: Option<f64>,
method: PoissonMethod,
compare: Compare,
alternative: Alternative,
) -> PoissonResult {
let (y1, n1, y2, n2) = (count1, exposure1, count2, exposure2);
let d = n2 / n1;
let rate1 = y1 / n1;
let rate2 = y2 / n2;
let (stat, pvalue) = match compare {
Compare::Ratio => {
let r = value.unwrap_or(1.0);
let r_d = r / d; match method {
PoissonMethod::Score => {
let stat = (y1 - y2 * r_d) / ((y1 + y2) * r_d).sqrt();
(stat, z_pvalue(stat, alternative))
}
PoissonMethod::Wald => {
let stat = (y1 - y2 * r_d) / (y1 + y2 * r_d * r_d).sqrt();
(stat, z_pvalue(stat, alternative))
}
PoissonMethod::ScoreLog => {
let stat =
((y1 / y2).ln() - r_d.ln()) / ((2.0 + 1.0 / r_d + r_d) / (y1 + y2)).sqrt();
(stat, z_pvalue(stat, alternative))
}
PoissonMethod::WaldLog => {
let stat = ((y1 / y2).ln() - r_d.ln()) / (1.0 / y1 + 1.0 / y2).sqrt();
(stat, z_pvalue(stat, alternative))
}
PoissonMethod::Sqrt => {
let stat = 2.0 * ((y1 + 3.0 / 8.0).sqrt() - ((y2 + 3.0 / 8.0) * r_d).sqrt())
/ (1.0 + r_d).sqrt();
(stat, z_pvalue(stat, alternative))
}
PoissonMethod::ExactCond => {
let bp = r_d / (1.0 + r_d);
let y_total = y1 + y2;
(f64::NAN, binom_test(y1, y_total, bp, alternative))
}
PoissonMethod::CondMidp => {
let bp = r_d / (1.0 + r_d);
let y_total = y1 + y2;
let p =
binom_test(y1, y_total, bp, alternative) - 0.5 * binom_pmf(y1, y_total, bp);
(f64::NAN, p)
}
PoissonMethod::WaldCcv => {
panic!("waldccv is only defined for compare = diff");
}
}
}
Compare::Diff => {
let v = value.unwrap_or(0.0);
match method {
PoissonMethod::Wald => {
let stat = (rate1 - rate2 - v) / (rate1 / n1 + rate2 / n2).sqrt();
(stat, z_pvalue(stat, alternative))
}
PoissonMethod::WaldCcv => {
let stat = (rate1 - rate2 - v)
/ ((y1 + 0.5) / (n1 * n1) + (y2 + 0.5) / (n2 * n2)).sqrt();
(stat, z_pvalue(stat, alternative))
}
PoissonMethod::Score => {
let count_pooled = y1 + y2;
let rate_pooled = count_pooled / (n1 + n2);
let dt = rate_pooled - v;
let r2_cmle = 0.5 * (dt + (dt * dt + 4.0 * v * y2 / (n1 + n2)).sqrt());
let r1_cmle = r2_cmle + v;
let stat = (rate1 - rate2 - v) / (r1_cmle / n1 + r2_cmle / n2).sqrt();
(stat, z_pvalue(stat, alternative))
}
_ => panic!("method is not valid for compare = diff"),
}
}
};
PoissonResult {
statistic: stat,
pvalue,
rate1,
rate2,
ratio: rate1 / rate2,
diff: rate1 - rate2,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn score_ratio_runs() {
let r = test_poisson_2indep(
60.0,
51477.5,
30.0,
54308.7,
None,
PoissonMethod::Score,
Compare::Ratio,
Alternative::TwoSided,
);
assert!(r.statistic > 0.0);
assert!((0.0..=1.0).contains(&r.pvalue));
}
#[test]
fn binom_cdf_matches_pmf_sum() {
let (n, p) = (10.0, 0.3);
let direct: f64 = (0..=3).map(|k| binom_pmf(k as f64, n, p)).sum();
assert!((binom_cdf(3.0, n, p) - direct).abs() < 1e-12);
}
}