use comnum_core::{Error, Result};
use comnum_math::{beta_inv, ln_binomial_pmf, qfuncinv};
pub fn z_score(confidence: f64) -> f64 {
qfuncinv((1.0 - confidence.clamp(0.0, 1.0 - 1e-16)) / 2.0)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ErrorRate {
pub errors: u64,
pub total: u64,
}
impl ErrorRate {
pub fn add(&mut self, errors: u64, total: u64) {
self.errors += errors;
self.total += total;
}
pub fn merge(&mut self, o: &ErrorRate) {
self.add(o.errors, o.total);
}
pub fn rate(&self) -> f64 {
if self.total == 0 { f64::NAN } else { self.errors as f64 / self.total as f64 }
}
pub fn wilson(&self, confidence: f64) -> (f64, f64) {
if self.total == 0 {
return (0.0, 1.0);
}
let n = self.total as f64;
let p = self.rate();
let z = z_score(confidence);
let z2 = z * z;
let centre = (p + z2 / (2.0 * n)) / (1.0 + z2 / n);
let half = z * (p * (1.0 - p) / n + z2 / (4.0 * n * n)).sqrt() / (1.0 + z2 / n);
let lower = if self.errors == 0 { 0.0 } else { (centre - half).max(0.0) };
(lower, (centre + half).min(1.0))
}
pub fn interval(&self, method: BinomialInterval, confidence: f64) -> (f64, f64) {
if self.total == 0 {
return (0.0, 1.0);
}
let (k, n) = (self.errors as f64, self.total as f64);
let alpha = 1.0 - confidence;
let z = z_score(confidence);
match method {
BinomialInterval::Wald => {
let p = k / n;
let half = z * (p * (1.0 - p) / n).sqrt();
(p - half, p + half)
}
BinomialInterval::Wilson => self.wilson(confidence),
BinomialInterval::AgrestiCoull => {
let nt = n + z * z;
let pt = (k + z * z / 2.0) / nt;
let half = z * (pt * (1.0 - pt) / nt).sqrt();
(pt - half, pt + half)
}
BinomialInterval::Jeffreys => {
let lo = if self.errors == 0 { 0.0 } else { beta_inv(alpha / 2.0, k + 0.5, n - k + 0.5) };
let hi =
if self.errors == self.total { 1.0 } else { beta_inv(1.0 - alpha / 2.0, k + 0.5, n - k + 0.5) };
(lo, hi)
}
BinomialInterval::ClopperPearson => {
let lo = if self.errors == 0 { 0.0 } else { beta_inv(alpha / 2.0, k, n - k + 1.0) };
let hi = if self.errors == self.total { 1.0 } else { beta_inv(1.0 - alpha / 2.0, k + 1.0, n - k) };
(lo, hi)
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BinomialInterval {
Wald,
Wilson,
AgrestiCoull,
Jeffreys,
ClopperPearson,
}
impl BinomialInterval {
pub const ALL: [BinomialInterval; 5] = [
BinomialInterval::Wald,
BinomialInterval::Wilson,
BinomialInterval::AgrestiCoull,
BinomialInterval::Jeffreys,
BinomialInterval::ClopperPearson,
];
pub fn name(self) -> &'static str {
match self {
BinomialInterval::Wald => "wald",
BinomialInterval::Wilson => "wilson",
BinomialInterval::AgrestiCoull => "agresti_coull",
BinomialInterval::Jeffreys => "jeffreys",
BinomialInterval::ClopperPearson => "clopper_pearson",
}
}
}
impl std::str::FromStr for BinomialInterval {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let key = s.to_ascii_lowercase().replace('-', "_");
BinomialInterval::ALL.into_iter().find(|m| m.name() == key).ok_or_else(|| {
let names: Vec<&str> = BinomialInterval::ALL.iter().map(|m| m.name()).collect();
Error::Config(format!("unknown binomial interval {s:?}: one of {}", names.join(", ")))
})
}
}
fn intervals_and_probabilities(method: BinomialInterval, n: u64, p: f64, confidence: f64) -> Vec<((f64, f64), f64)> {
let ln_pmf = ln_binomial_pmf(n as usize, p);
(0..=n)
.map(|k| (ErrorRate { errors: k, total: n }.interval(method, confidence), ln_pmf[k as usize].exp()))
.collect()
}
pub fn coverage(method: BinomialInterval, n: u64, p: f64, confidence: f64) -> f64 {
intervals_and_probabilities(method, n, p, confidence)
.into_iter()
.filter(|((lo, hi), _)| *lo <= p && p <= *hi)
.map(|(_, w)| w)
.sum()
}
pub fn expected_length(method: BinomialInterval, n: u64, p: f64, confidence: f64) -> f64 {
intervals_and_probabilities(method, n, p, confidence).into_iter().map(|((lo, hi), w)| (hi - lo) * w).sum()
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[allow(clippy::struct_field_names, reason = "the running mean of Welford's algorithm")]
pub struct Mean {
n: u64,
mean: f64,
m2: f64,
}
impl Mean {
pub fn add(&mut self, x: f64) {
self.n += 1;
let d = x - self.mean;
self.mean += d / self.n as f64;
self.m2 += d * (x - self.mean);
}
pub fn merge(&mut self, o: &Mean) {
if o.n == 0 {
return;
}
if self.n == 0 {
*self = *o;
return;
}
let n = self.n + o.n;
let d = o.mean - self.mean;
self.mean += d * o.n as f64 / n as f64;
self.m2 += o.m2 + d * d * self.n as f64 * o.n as f64 / n as f64;
self.n = n;
}
pub fn count(&self) -> u64 {
self.n
}
pub fn mean(&self) -> f64 {
if self.n == 0 { f64::NAN } else { self.mean }
}
pub fn variance(&self) -> f64 {
if self.n < 2 { f64::NAN } else { self.m2 / (self.n - 1) as f64 }
}
pub fn std(&self) -> f64 {
self.variance().sqrt()
}
pub fn sem(&self) -> f64 {
self.std() / (self.n as f64).sqrt()
}
pub fn interval(&self, confidence: f64) -> (f64, f64) {
if self.n < 2 {
return (f64::NEG_INFINITY, f64::INFINITY);
}
let h = z_score(confidence) * self.sem();
(self.mean - h, self.mean + h)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Estimate {
Rate(ErrorRate),
Mean(Mean),
}
impl Estimate {
pub fn value(&self) -> f64 {
match self {
Estimate::Rate(r) => r.rate(),
Estimate::Mean(m) => m.mean(),
}
}
pub fn interval(&self, confidence: f64) -> (f64, f64) {
match self {
Estimate::Rate(r) => r.wilson(confidence),
Estimate::Mean(m) => m.interval(confidence),
}
}
pub fn relative_precision(&self, confidence: f64) -> f64 {
let (lo, hi) = self.interval(confidence);
let v = self.value().abs();
if v == 0.0 || !v.is_finite() { f64::INFINITY } else { 0.5 * (hi - lo) / v }
}
pub fn count(&self) -> u64 {
match self {
Estimate::Rate(r) => r.errors,
Estimate::Mean(m) => m.count(),
}
}
pub fn total(&self) -> u64 {
match self {
Estimate::Rate(r) => r.total,
Estimate::Mean(m) => m.count(),
}
}
pub fn merge(&mut self, o: &Estimate) -> bool {
match (self, o) {
(Estimate::Rate(a), Estimate::Rate(b)) => {
a.merge(b);
true
}
(Estimate::Mean(a), Estimate::Mean(b)) => {
a.merge(b);
true
}
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_intervals_nest_as_brown_cai_dasgupta_state() {
for n in 1..=60 {
for k in 0..=n {
let r = ErrorRate { errors: k, total: n };
let w = r.interval(BinomialInterval::Wilson, 0.95);
assert_eq!(w, r.wilson(0.95));
let ac = r.interval(BinomialInterval::AgrestiCoull, 0.95);
assert!(ac.0 <= w.0 + 1e-15 && w.1 <= ac.1 + 1e-15, "n {n} k {k}: {ac:?} {w:?}");
let j = r.interval(BinomialInterval::Jeffreys, 0.95);
let cp = r.interval(BinomialInterval::ClopperPearson, 0.95);
assert!(cp.0 <= j.0 && j.1 <= cp.1, "n {n} k {k}: {cp:?} {j:?}");
}
}
}
#[test]
fn clopper_pearson_covers_at_least_its_level() {
for n in [5, 17, 50] {
for i in 1..200 {
let p = f64::from(i) / 200.0;
let c = coverage(BinomialInterval::ClopperPearson, n, p, 0.95);
assert!(c >= 0.95 - 1e-12, "n {n} p {p}: {c}");
}
}
}
#[test]
fn the_names_parse_back_and_unknown_ones_are_errors() {
for m in BinomialInterval::ALL {
assert_eq!(m.name().parse::<BinomialInterval>().unwrap(), m);
}
assert_eq!("Clopper-Pearson".parse::<BinomialInterval>().unwrap(), BinomialInterval::ClopperPearson);
assert!("exact".parse::<BinomialInterval>().is_err());
assert_eq!(ErrorRate::default().interval(BinomialInterval::Wald, 0.95), (0.0, 1.0));
}
#[test]
fn wilson_with_no_error_and_merges() {
let r = ErrorRate { errors: 0, total: 1000 };
let (lo, hi) = r.wilson(0.95);
assert_eq!(lo, 0.0);
assert!((hi - 3.83e-3).abs() < 1e-4, "{hi}");
let mut a = Mean::default();
let mut b = Mean::default();
let mut all = Mean::default();
for i in 0..100 {
let v = (i as f64 * 0.37).sin();
if i < 30 {
a.add(v);
} else {
b.add(v);
}
all.add(v);
}
a.merge(&b);
assert!((a.mean() - all.mean()).abs() < 1e-14 && (a.variance() - all.variance()).abs() < 1e-14);
}
}