mod dist;
pub use dist::{BootDist, NullDist, Sidedness};
use crate::accum::Accumulator;
use crate::error::StatError;
use crate::rng::CommonStatsRng;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scheme {
Permutation,
Bootstrap,
}
pub fn gen_resample_indices(
scheme: Scheme,
n: usize,
draw_id: u64,
seed: u64,
out: &mut [u32],
) -> Result<(), StatError> {
if out.len() != n {
return Err(StatError::MismatchedLengths { a: n, b: out.len() });
}
if n == 0 {
return Ok(());
}
let mut rng = CommonStatsRng::new(seed, draw_id);
match scheme {
Scheme::Permutation => {
for (i, slot) in out.iter_mut().enumerate() {
*slot = i as u32;
}
for i in (1..n).rev() {
let j = rng.bounded((i + 1) as u32) as usize;
out.swap(i, j);
}
}
Scheme::Bootstrap => {
let n32 = n as u32;
for slot in out.iter_mut() {
*slot = rng.bounded(n32);
}
}
}
Ok(())
}
pub fn run_serial<D, F>(
scheme: Scheme,
base: &[&[f64]],
n: usize,
b: u64,
seed: u64,
statistic: F,
dist: &mut D,
) -> Result<(), StatError>
where
D: Accumulator<Item = f64>,
F: Fn(&[&[f64]], &[u32]) -> f64,
{
if b == 0 {
return Err(StatError::DomainError("run_serial: B must be >= 1"));
}
let mut idx = vec![0u32; n];
for draw_id in 0..b {
gen_resample_indices(scheme, n, draw_id, seed, &mut idx)?;
dist.update(statistic(base, &idx));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permutation_is_a_valid_permutation() {
let n = 50;
for draw_id in 0..20 {
let mut idx = vec![0u32; n];
gen_resample_indices(Scheme::Permutation, n, draw_id, 123, &mut idx).unwrap();
let mut sorted = idx.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..n as u32).collect::<Vec<_>>(), "draw {draw_id} not a permutation");
}
}
#[test]
fn bootstrap_indices_in_range_and_has_replacement() {
let n = 40;
let mut idx = vec![0u32; n];
gen_resample_indices(Scheme::Bootstrap, n, 0, 9, &mut idx).unwrap();
assert!(idx.iter().all(|&i| (i as usize) < n));
let distinct: std::collections::HashSet<u32> = idx.iter().copied().collect();
assert!(distinct.len() < n, "bootstrap of n from n should repeat some rows");
}
#[test]
fn indices_are_draw_addressable() {
let n = 30;
let mut a = vec![0u32; n];
let mut b = vec![0u32; n];
gen_resample_indices(Scheme::Permutation, n, 5, 77, &mut a).unwrap();
gen_resample_indices(Scheme::Permutation, n, 5, 77, &mut b).unwrap();
assert_eq!(a, b, "same (seed, draw_id) must reproduce the index set");
let mut c = vec![0u32; n];
gen_resample_indices(Scheme::Permutation, n, 6, 77, &mut c).unwrap();
assert_ne!(a, c, "different draw_id must differ");
}
#[test]
fn length_mismatch_errors() {
let mut idx = vec![0u32; 4];
assert_eq!(
gen_resample_indices(Scheme::Bootstrap, 5, 0, 1, &mut idx),
Err(StatError::MismatchedLengths { a: 5, b: 4 }),
);
}
#[test]
fn run_serial_b_zero_errors() {
let data = [1.0, 2.0, 3.0];
let base: &[&[f64]] = &[&data];
let mut dist = NullDist::new(2.0, Sidedness::Greater);
let stat = |b: &[&[f64]], idx: &[u32]| b[0][idx[0] as usize];
assert!(matches!(
run_serial(Scheme::Permutation, base, 3, 0, 1, stat, &mut dist),
Err(StatError::DomainError(_)),
));
}
#[test]
fn run_serial_permutation_mean_is_invariant() {
let data = [10.0, 20.0, 30.0, 40.0];
let base: &[&[f64]] = &[&data];
let stat = |b: &[&[f64]], idx: &[u32]| {
idx.iter().map(|&i| b[0][i as usize]).sum::<f64>() / idx.len() as f64
};
let observed = stat(base, &[0, 1, 2, 3]);
let mut dist = NullDist::new(observed, Sidedness::TwoSided);
run_serial(Scheme::Permutation, base, 4, 100, 3, stat, &mut dist).unwrap();
assert!((dist.finalize() - 1.0).abs() < 1e-15);
}
#[test]
fn run_serial_bootstrap_ci_brackets_mean() {
let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let base: &[&[f64]] = &[&data];
let mean = data.iter().sum::<f64>() / data.len() as f64; let stat = |b: &[&[f64]], idx: &[u32]| {
idx.iter().map(|&i| b[0][i as usize]).sum::<f64>() / idx.len() as f64
};
let mut dist = BootDist::new(0.95);
run_serial(Scheme::Bootstrap, base, data.len(), 2000, 1, stat, &mut dist).unwrap();
let ci = dist.finalize();
assert!(ci.lower < mean && mean < ci.upper, "CI {:?} must bracket {mean}", ci);
}
}