use crate::accum::{quantile_sorted, Accumulator, Mergeable};
use crate::htest::Ci;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sidedness {
Less,
Greater,
TwoSided,
}
#[derive(Debug, Clone)]
pub struct NullDist {
observed: f64, side: Sidedness,
count: u64, b: u64, }
impl NullDist {
pub fn new(observed: f64, side: Sidedness) -> Self {
Self { observed, side, count: 0, b: 0 }
}
}
impl Mergeable for NullDist {
fn merge(&mut self, o: &Self) {
if self.observed.is_nan() {
self.observed = o.observed;
self.side = o.side;
}
self.count += o.count;
self.b += o.b;
}
}
impl Accumulator for NullDist {
type Item = f64;
type Output = f64;
fn empty() -> Self {
Self { observed: f64::NAN, side: Sidedness::TwoSided, count: 0, b: 0 }
}
fn update(&mut self, x: f64) {
self.b += 1;
let extreme = match self.side {
Sidedness::Less => x <= self.observed,
Sidedness::Greater => x >= self.observed,
Sidedness::TwoSided => x.abs() >= self.observed.abs(),
};
if extreme {
self.count += 1;
}
}
fn finalize(&self) -> f64 {
(self.count as f64 + 1.0) / (self.b as f64 + 1.0)
}
}
#[derive(Debug, Clone)]
pub struct BootDist {
stats: Vec<f64>,
level: f64, }
impl BootDist {
pub fn new(level: f64) -> Self {
Self { stats: Vec::new(), level }
}
}
impl Mergeable for BootDist {
fn merge(&mut self, o: &Self) {
if self.level.is_nan() {
self.level = o.level;
}
self.stats.extend_from_slice(&o.stats);
}
}
impl Accumulator for BootDist {
type Item = f64;
type Output = Ci;
fn empty() -> Self {
Self { stats: Vec::new(), level: f64::NAN }
}
fn update(&mut self, x: f64) {
self.stats.push(x);
}
fn finalize(&self) -> Ci {
let mut sorted = self.stats.clone();
sorted.sort_by(|a, b| a.total_cmp(b));
let alpha = 1.0 - self.level;
Ci {
lower: quantile_sorted(&sorted, alpha / 2.0),
upper: quantile_sorted(&sorted, 1.0 - alpha / 2.0),
level: self.level,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn feed(side: Sidedness, observed: f64, xs: &[f64]) -> f64 {
let mut d = NullDist::new(observed, side);
for &x in xs {
d.update(x);
}
d.finalize()
}
#[test]
fn nulldist_add_one_convention() {
let xs = [-2.0, -1.0, 0.0, 1.0, 2.0, 3.0]; assert!((feed(Sidedness::Greater, 2.0, &xs) - 3.0 / 7.0).abs() < 1e-15);
assert!((feed(Sidedness::Less, 2.0, &xs) - 6.0 / 7.0).abs() < 1e-15);
assert!((feed(Sidedness::TwoSided, 2.0, &xs) - 4.0 / 7.0).abs() < 1e-15);
}
#[test]
fn nulldist_never_returns_zero() {
let p = feed(Sidedness::Greater, 100.0, &[1.0, 2.0, 3.0]);
assert!((p - 1.0 / 4.0).abs() < 1e-15);
}
#[test]
fn nulldist_matches_full_enumeration() {
let diffs = [-2.0, -1.0, -1.0, 1.0, 1.0, 2.0];
assert!((feed(Sidedness::TwoSided, -2.0, &diffs) - 3.0 / 7.0).abs() < 1e-15);
}
#[test]
fn nulldist_merge_equals_single() {
let xs = [-2.0, -1.0, 0.0, 1.0, 2.0, 3.0];
let whole = feed(Sidedness::Greater, 1.5, &xs);
let mut a = NullDist::new(1.5, Sidedness::Greater);
for &x in &xs[..3] {
a.update(x);
}
let mut b = NullDist::new(1.5, Sidedness::Greater);
for &x in &xs[3..] {
b.update(x);
}
a.merge(&b);
assert!((a.finalize() - whole).abs() < 1e-15);
}
#[test]
fn nulldist_empty_merge_is_identity() {
let mut acc = NullDist::empty();
let mut configured = NullDist::new(1.0, Sidedness::Greater);
for x in [0.0, 1.0, 2.0] {
configured.update(x);
}
acc.merge(&configured);
assert!((acc.finalize() - configured.finalize()).abs() < 1e-15);
}
#[test]
fn bootdist_matches_numpy_percentile_clean() {
let mut d = BootDist::new(0.95);
for i in 0..=100 {
d.update(i as f64);
}
let ci = d.finalize();
assert!((ci.lower - 2.5).abs() < 1e-12, "lower {}", ci.lower);
assert!((ci.upper - 97.5).abs() < 1e-12, "upper {}", ci.upper);
assert_eq!(ci.level, 0.95);
}
#[test]
fn bootdist_linear_interpolation_small() {
let mut d = BootDist::new(0.90);
for &x in &[8.0, 1.0, 16.0, 2.0, 4.0] {
d.update(x);
}
let ci = d.finalize();
assert!((ci.lower - 1.2).abs() < 1e-12, "lower {}", ci.lower);
assert!((ci.upper - 14.4).abs() < 1e-12, "upper {}", ci.upper);
}
#[test]
fn bootdist_merge_concatenates() {
let mut whole = BootDist::new(0.95);
for i in 0..=100 {
whole.update(i as f64);
}
let mut a = BootDist::new(0.95);
for i in 0..=50 {
a.update(i as f64);
}
let mut b = BootDist::new(0.95);
for i in 51..=100 {
b.update(i as f64);
}
a.merge(&b);
let (ca, cw) = (a.finalize(), whole.finalize());
assert!((ca.lower - cw.lower).abs() < 1e-12);
assert!((ca.upper - cw.upper).abs() < 1e-12);
}
}