use crate::error::StatError;
use crate::dist::{Bound, DiscreteCdf, DiscreteMass, Distribution};
pub(crate) fn ln_choose(n: f64, k: f64) -> f64 {
crate::special::lgamma(n + 1.0)
- crate::special::lgamma(k + 1.0)
- crate::special::lgamma(n - k + 1.0)
}
pub(crate) fn discrete_bsearch_quantile<D: DiscreteCdf + ?Sized>(
d: &D,
p: f64,
lo: i64,
hi: i64,
) -> Result<i64, StatError> {
if !(0.0..=1.0).contains(&p) {
return Err(StatError::ProbabilityOutOfRange(p));
}
if p == 0.0 { return Ok(lo); }
let (mut a, mut b) = (lo, hi);
while a < b {
let mid = a + (b - a) / 2;
if d.cdf(mid) < p { a = mid + 1; } else { b = mid; }
}
Ok(a)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Bernoulli { p: f64 }
impl Bernoulli {
pub fn new(p: f64) -> Result<Self, StatError> {
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return Err(StatError::ProbabilityOutOfRange(p));
}
Ok(Bernoulli { p })
}
}
impl Distribution for Bernoulli {
fn support_min(&self) -> Bound { Bound::Finite(0.0) }
fn support_max(&self) -> Bound { Bound::Finite(1.0) }
fn mean(&self) -> Option<f64> { Some(self.p) }
fn variance(&self) -> Option<f64> { Some(self.p * (1.0 - self.p)) }
}
impl DiscreteMass for Bernoulli {
fn mass(&self, k: i64) -> f64 {
match k { 0 => 1.0 - self.p, 1 => self.p, _ => 0.0 }
}
fn log_mass(&self, k: i64) -> f64 {
match k {
0 => if self.p >= 1.0 { f64::NEG_INFINITY } else { (1.0 - self.p).ln() },
1 => if self.p <= 0.0 { f64::NEG_INFINITY } else { self.p.ln() },
_ => f64::NEG_INFINITY,
}
}
}
impl DiscreteCdf for Bernoulli {
fn cdf(&self, k: i64) -> f64 {
if k < 0 { 0.0 } else if k == 0 { 1.0 - self.p } else { 1.0 }
}
fn quantile(&self, p: f64) -> Result<i64, StatError> {
if !(0.0..=1.0).contains(&p) {
return Err(StatError::ProbabilityOutOfRange(p));
}
Ok(if p <= 1.0 - self.p { 0 } else { 1 })
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Binomial { n: i64, p: f64 }
impl Binomial {
pub fn new(n: i64, p: f64) -> Result<Self, StatError> {
if n < 1 {
return Err(StatError::DomainError("Binomial: n must be ≥ 1"));
}
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return Err(StatError::ProbabilityOutOfRange(p));
}
Ok(Binomial { n, p })
}
}
impl Distribution for Binomial {
fn support_min(&self) -> Bound { Bound::Finite(0.0) }
fn support_max(&self) -> Bound { Bound::Finite(self.n as f64) }
fn mean(&self) -> Option<f64> { Some(self.n as f64 * self.p) }
fn variance(&self) -> Option<f64> { Some(self.n as f64 * self.p * (1.0 - self.p)) }
}
impl DiscreteMass for Binomial {
fn mass(&self, k: i64) -> f64 {
if k < 0 || k > self.n { return 0.0; }
self.log_mass(k).exp()
}
fn log_mass(&self, k: i64) -> f64 {
if k < 0 || k > self.n { return f64::NEG_INFINITY; }
let (kf, nf) = (k as f64, self.n as f64);
let lp = if self.p <= 0.0 { if k == 0 { return 0.0; } f64::NEG_INFINITY } else { self.p.ln() };
let lq = if self.p >= 1.0 { if k == self.n { return 0.0; } f64::NEG_INFINITY } else { (1.0 - self.p).ln() };
ln_choose(nf, kf) + kf * lp + (nf - kf) * lq
}
}
impl DiscreteCdf for Binomial {
fn cdf(&self, k: i64) -> f64 {
if k < 0 { return 0.0; }
if k >= self.n { return 1.0; }
crate::special::betai((self.n - k) as f64, (k + 1) as f64, 1.0 - self.p)
}
fn quantile(&self, p: f64) -> Result<i64, StatError> {
discrete_bsearch_quantile(self, p, 0, self.n)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Poisson { lambda: f64 }
impl Poisson {
pub fn new(lambda: f64) -> Result<Self, StatError> {
if !lambda.is_finite() || lambda <= 0.0 {
return Err(StatError::DomainError("Poisson: lambda must be finite and > 0"));
}
Ok(Poisson { lambda })
}
fn search_hi(&self) -> i64 {
(self.lambda + 10.0 * self.lambda.sqrt() + 20.0).ceil() as i64
}
}
impl Distribution for Poisson {
fn support_min(&self) -> Bound { Bound::Finite(0.0) }
fn support_max(&self) -> Bound { Bound::PosInfinity }
fn mean(&self) -> Option<f64> { Some(self.lambda) }
fn variance(&self) -> Option<f64> { Some(self.lambda) }
fn skewness(&self) -> Option<f64> { Some(1.0 / self.lambda.sqrt()) }
fn kurtosis(&self) -> Option<f64> { Some(1.0 / self.lambda) }
}
impl DiscreteMass for Poisson {
fn mass(&self, k: i64) -> f64 {
if k < 0 { return 0.0; }
self.log_mass(k).exp()
}
fn log_mass(&self, k: i64) -> f64 {
if k < 0 { return f64::NEG_INFINITY; }
let kf = k as f64;
-self.lambda + kf * self.lambda.ln() - crate::special::lgamma(kf + 1.0)
}
}
impl DiscreteCdf for Poisson {
fn cdf(&self, k: i64) -> f64 {
if k < 0 { return 0.0; }
crate::special::gammq((k + 1) as f64, self.lambda)
}
fn quantile(&self, p: f64) -> Result<i64, StatError> {
discrete_bsearch_quantile(self, p, 0, self.search_hi())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Geometric { p: f64 }
impl Geometric {
pub fn new(p: f64) -> Result<Self, StatError> {
if !p.is_finite() || p <= 0.0 || p > 1.0 {
return Err(StatError::ProbabilityOutOfRange(p));
}
Ok(Geometric { p })
}
}
impl Distribution for Geometric {
fn support_min(&self) -> Bound { Bound::Finite(1.0) }
fn support_max(&self) -> Bound { Bound::PosInfinity }
fn mean(&self) -> Option<f64> { Some(1.0 / self.p) }
fn variance(&self) -> Option<f64> { Some((1.0 - self.p) / (self.p * self.p)) }
}
impl DiscreteMass for Geometric {
fn mass(&self, k: i64) -> f64 {
if k < 1 { return 0.0; }
self.log_mass(k).exp()
}
fn log_mass(&self, k: i64) -> f64 {
if k < 1 { return f64::NEG_INFINITY; }
if self.p >= 1.0 { return if k == 1 { 0.0 } else { f64::NEG_INFINITY }; }
(k as f64 - 1.0) * (1.0 - self.p).ln() + self.p.ln()
}
}
impl DiscreteCdf for Geometric {
fn cdf(&self, k: i64) -> f64 {
if k < 1 { return 0.0; }
-((k as f64) * (1.0 - self.p).ln()).exp_m1() }
fn quantile(&self, p: f64) -> Result<i64, StatError> {
if !(0.0..=1.0).contains(&p) {
return Err(StatError::ProbabilityOutOfRange(p));
}
if p == 0.0 { return Ok(1); }
if self.p >= 1.0 { return Ok(1); }
let k = ((1.0 - p).ln() / (1.0 - self.p).ln()).ceil();
Ok((k.max(1.0)) as i64)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NegBinomial { r: f64, p: f64 }
impl NegBinomial {
pub fn new(r: f64, p: f64) -> Result<Self, StatError> {
if !r.is_finite() || r <= 0.0 {
return Err(StatError::DomainError("NegBinomial: r must be finite and > 0"));
}
if !p.is_finite() || p <= 0.0 || p >= 1.0 {
return Err(StatError::ProbabilityOutOfRange(p));
}
Ok(NegBinomial { r, p })
}
fn search_hi(&self) -> i64 {
let mean = self.r * self.p / (1.0 - self.p);
let var = self.r * self.p / ((1.0 - self.p) * (1.0 - self.p));
(mean + 12.0 * var.sqrt() + 30.0).ceil() as i64
}
}
impl Distribution for NegBinomial {
fn support_min(&self) -> Bound { Bound::Finite(0.0) }
fn support_max(&self) -> Bound { Bound::PosInfinity }
fn mean(&self) -> Option<f64> { Some(self.r * self.p / (1.0 - self.p)) }
fn variance(&self) -> Option<f64> {
Some(self.r * self.p / ((1.0 - self.p) * (1.0 - self.p)))
}
}
impl DiscreteMass for NegBinomial {
fn mass(&self, k: i64) -> f64 {
if k < 0 { return 0.0; }
self.log_mass(k).exp()
}
fn log_mass(&self, k: i64) -> f64 {
if k < 0 { return f64::NEG_INFINITY; }
let kf = k as f64;
crate::special::lgamma(kf + self.r)
- crate::special::lgamma(self.r)
- crate::special::lgamma(kf + 1.0)
+ self.r * (1.0 - self.p).ln()
+ kf * self.p.ln()
}
}
impl DiscreteCdf for NegBinomial {
fn cdf(&self, k: i64) -> f64 {
if k < 0 { return 0.0; }
crate::special::betai(self.r, (k + 1) as f64, 1.0 - self.p)
}
fn quantile(&self, p: f64) -> Result<i64, StatError> {
discrete_bsearch_quantile(self, p, 0, self.search_hi())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Hypergeometric { big_n: i64, k: i64, n: i64 }
impl Hypergeometric {
pub fn new(big_n: i64, k: i64, n: i64) -> Result<Self, StatError> {
if big_n < 0 || k < 0 || n < 0 || k > big_n || n > big_n {
return Err(StatError::DomainError("Hypergeometric: require 0 ≤ K ≤ N and 0 ≤ n ≤ N"));
}
Ok(Hypergeometric { big_n, k, n })
}
fn k_lo(&self) -> i64 { (self.n + self.k - self.big_n).max(0) }
fn k_hi(&self) -> i64 { self.n.min(self.k) }
}
impl Distribution for Hypergeometric {
fn support_min(&self) -> Bound { Bound::Finite(self.k_lo() as f64) }
fn support_max(&self) -> Bound { Bound::Finite(self.k_hi() as f64) }
fn mean(&self) -> Option<f64> {
Some(self.n as f64 * self.k as f64 / self.big_n as f64)
}
fn variance(&self) -> Option<f64> {
let (nn, kk, n) = (self.big_n as f64, self.k as f64, self.n as f64);
if nn <= 1.0 { return Some(0.0); }
Some(n * (kk / nn) * ((nn - kk) / nn) * ((nn - n) / (nn - 1.0)))
}
}
impl DiscreteMass for Hypergeometric {
fn mass(&self, k: i64) -> f64 {
if k < self.k_lo() || k > self.k_hi() { return 0.0; }
self.log_mass(k).exp()
}
fn log_mass(&self, k: i64) -> f64 {
if k < self.k_lo() || k > self.k_hi() { return f64::NEG_INFINITY; }
let (nn, kk, n, kf) = (self.big_n as f64, self.k as f64, self.n as f64, k as f64);
ln_choose(kk, kf) + ln_choose(nn - kk, n - kf) - ln_choose(nn, n)
}
}
impl DiscreteCdf for Hypergeometric {
fn cdf(&self, k: i64) -> f64 {
if k < self.k_lo() { return 0.0; }
if k >= self.k_hi() { return 1.0; }
let mut acc = 0.0;
for j in self.k_lo()..=k {
acc += self.mass(j);
}
acc.min(1.0)
}
fn quantile(&self, p: f64) -> Result<i64, StatError> {
discrete_bsearch_quantile(self, p, self.k_lo(), self.k_hi())
}
}