use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use statrs::distribution::{Binomial, DiscreteCDF};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Ci {
pub mean: f64,
pub lo: f64,
pub hi: f64,
}
pub fn bootstrap_ci(samples: &[f64], confidence: f64, n_boot: usize, seed: u64) -> Ci {
let n = samples.len();
if n == 0 {
return Ci { mean: 0.0, lo: 0.0, hi: 0.0 };
}
let mean = samples.iter().sum::<f64>() / n as f64;
if n == 1 || n_boot == 0 {
return Ci { mean, lo: mean, hi: mean };
}
let mut rng = StdRng::seed_from_u64(seed);
let mut means = Vec::with_capacity(n_boot);
for _ in 0..n_boot {
let mut acc = 0.0;
for _ in 0..n {
acc += samples[rng.gen_range(0..n)];
}
means.push(acc / n as f64);
}
means.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let alpha = (1.0 - confidence) / 2.0;
Ci {
mean,
lo: percentile(&means, alpha),
hi: percentile(&means, 1.0 - alpha),
}
}
fn percentile(sorted: &[f64], q: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
let q = q.clamp(0.0, 1.0);
let idx = q * (sorted.len() - 1) as f64;
let lo = idx.floor() as usize;
let hi = idx.ceil() as usize;
if lo == hi {
sorted[lo]
} else {
sorted[lo] + (idx - lo as f64) * (sorted[hi] - sorted[lo])
}
}
pub fn sign_test(a: &[f64], b: &[f64]) -> f64 {
let n = a.len().min(b.len());
let mut plus = 0u64;
let mut minus = 0u64;
for i in 0..n {
let d = a[i] - b[i];
if d > 0.0 {
plus += 1;
} else if d < 0.0 {
minus += 1;
}
}
let m = plus + minus;
if m == 0 {
return 1.0;
}
let k = plus.min(minus);
let binom = Binomial::new(0.5, m).expect("0.5 is a valid Binomial p");
(2.0 * binom.cdf(k)).min(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ci_of_constant_is_zero_width() {
let ci = bootstrap_ci(&[5.0; 16], 0.95, 1000, 42);
assert_eq!(ci.mean, 5.0);
assert_eq!(ci.lo, 5.0);
assert_eq!(ci.hi, 5.0);
}
#[test]
fn ci_brackets_the_mean_and_is_reproducible() {
let xs: Vec<f64> = (0..50).map(|i| (i as f64) * 0.1).collect();
let a = bootstrap_ci(&xs, 0.95, 2000, 7);
let b = bootstrap_ci(&xs, 0.95, 2000, 7);
assert_eq!(a, b, "seeded bootstrap is reproducible");
assert!(a.lo <= a.mean && a.mean <= a.hi, "mean inside the interval");
assert!(a.lo < a.hi, "non-degenerate interval has positive width");
}
#[test]
fn edge_cases() {
assert_eq!(bootstrap_ci(&[], 0.95, 100, 1).mean, 0.0);
let one = bootstrap_ci(&[3.0], 0.95, 100, 1);
assert_eq!((one.lo, one.mean, one.hi), (3.0, 3.0, 3.0));
}
#[test]
fn sign_test_extremes() {
let a = vec![1.0; 10];
let b = vec![0.0; 10];
let p = sign_test(&a, &b);
assert!(p < 0.01, "all-positive sign test should be significant, got {p}");
assert!((p - 2.0 * 0.5_f64.powi(10)).abs() < 1e-9);
assert_eq!(sign_test(&a, &a), 1.0);
let x = vec![1.0, -1.0, 1.0, -1.0, 1.0, -1.0];
let y = vec![0.0; 6];
assert!(sign_test(&x, &y) > 0.9);
}
}