use crate::{DotF64, SumF64};
use num_bigint::{BigInt, BigUint, Sign};
use num_traits::{Signed, Zero};
const LIMBS: usize = 34;
const STATE_BITS: usize = LIMBS * 64;
const DOT_BYTES: usize = SumF64::BYTES + 1;
pub(crate) const UNIT_LOG2: usize = 1074;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum StatsError {
Empty,
NonFinite,
ExactnessLost,
Degenerate,
}
impl core::fmt::Display for StatsError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
StatsError::Empty => write!(f, "not enough samples"),
StatsError::NonFinite => write!(f, "non-finite input or overflow"),
StatsError::ExactnessLost => {
write!(f, "two-product underflow: exact rounding not certified")
}
StatsError::Degenerate => write!(f, "statistic undefined for this data"),
}
}
}
impl std::error::Error for StatsError {}
fn sum_flags(bytes: &[u8; SumF64::BYTES]) -> u8 {
bytes[LIMBS * 8]
}
pub(crate) fn sum_int(s: &SumF64) -> Result<BigInt, StatsError> {
let bytes = s.to_bytes();
if sum_flags(&bytes) != 0 {
return Err(StatsError::NonFinite);
}
Ok(twos_complement(&bytes[..LIMBS * 8]))
}
pub(crate) fn dot_int(d: &DotF64) -> Result<BigInt, StatsError> {
if !d.is_exact() {
return Err(StatsError::ExactnessLost);
}
let bytes = d.to_bytes();
let mut inner = [0u8; SumF64::BYTES];
inner.copy_from_slice(&bytes[..SumF64::BYTES]);
if sum_flags(&inner) != 0 {
return Err(StatsError::NonFinite);
}
Ok(twos_complement(&inner[..LIMBS * 8]))
}
fn twos_complement(le: &[u8]) -> BigInt {
let mag = BigUint::from_bytes_le(le);
let half = BigUint::from(1u8) << (STATE_BITS - 1);
if mag >= half {
BigInt::from_biguint(Sign::Minus, (BigUint::from(1u8) << STATE_BITS) - mag)
} else {
BigInt::from_biguint(Sign::Plus, mag)
}
}
pub(crate) fn round_rational(p: &BigInt, q: &BigInt) -> f64 {
debug_assert!(q.is_positive());
if p.is_zero() {
return 0.0; }
let neg = p.is_negative() != q.is_negative();
let p = p.magnitude().clone();
let q = q.magnitude().clone();
let mut e = p.bits() as i64 - q.bits() as i64;
let ge = if e >= 0 {
p >= (&q << e as usize)
} else {
(&p << (-e) as usize) >= q
};
if !ge {
e -= 1;
}
let g = core::cmp::max(e - 52, -(UNIT_LOG2 as i64));
let (num, den) = if g >= 0 {
(p, q << g as usize)
} else {
(p << (-g) as usize, q)
};
let mut m = &num / &den;
let r = &num - &m * &den;
let two_r = &r << 1usize;
let odd = (&m & BigUint::from(1u8)) == BigUint::from(1u8);
if two_r > den || (two_r == den && odd) {
m += 1u8;
}
let mut g = g;
if m.bits() > 53 {
m >>= 1usize;
g += 1;
}
let m64 = m.iter_u64_digits().next().unwrap_or(0);
if m64 == 0 {
return if neg { -0.0 } else { 0.0 };
}
let bits = if m64 >= (1u64 << 52) {
let e_unb = g + 52;
if e_unb > 1023 {
f64::INFINITY.to_bits()
} else {
(((e_unb + 1023) as u64) << 52) | (m64 & ((1u64 << 52) - 1))
}
} else {
m64
};
f64::from_bits(bits | ((neg as u64) << 63))
}
pub(crate) fn unit_pow(k: usize) -> BigInt {
BigInt::from(1u8) << (UNIT_LOG2 * k)
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct MomentsF64 {
sum: SumF64,
sumsq: DotF64,
}
impl MomentsF64 {
pub const BYTES: usize = SumF64::BYTES + DOT_BYTES;
pub fn new() -> Self {
Self {
sum: SumF64::new(),
sumsq: DotF64::new(),
}
}
pub fn add(&mut self, x: f64) {
self.sum.add(x);
self.sumsq.push(x, x);
}
pub fn merge(&mut self, o: &MomentsF64) {
self.sum.merge(&o.sum);
self.sumsq.merge(&o.sumsq);
}
pub const fn count(&self) -> u64 {
self.sum.count()
}
pub fn try_mean(&self) -> Result<f64, StatsError> {
let n = self.count();
if n == 0 {
return Err(StatsError::Empty);
}
let s = sum_int(&self.sum)?;
Ok(round_rational(&s, &(BigInt::from(n) * unit_pow(1))))
}
fn a2(&self) -> Result<(BigInt, u64), StatsError> {
let n = self.count();
if n == 0 {
return Err(StatsError::Empty);
}
let s = sum_int(&self.sum)?;
let q = dot_int(&self.sumsq)?;
let a2 = ((BigInt::from(n) * q) << UNIT_LOG2) - (&s * &s);
Ok((a2, n))
}
pub fn try_variance(&self) -> Result<f64, StatsError> {
let (a2, n) = self.a2()?;
Ok(round_rational(
&a2,
&(BigInt::from(n) * BigInt::from(n) * unit_pow(2)),
))
}
pub fn try_sample_variance(&self) -> Result<f64, StatsError> {
let (a2, n) = self.a2()?;
if n < 2 {
return Err(StatsError::Empty);
}
Ok(round_rational(
&a2,
&(BigInt::from(n) * BigInt::from(n - 1) * unit_pow(2)),
))
}
pub fn try_stddev(&self) -> Result<f64, StatsError> {
Ok(self.try_variance()?.sqrt())
}
pub fn mean(&self) -> f64 {
self.try_mean().unwrap_or(f64::NAN)
}
pub fn variance(&self) -> f64 {
self.try_variance().unwrap_or(f64::NAN)
}
pub fn stddev(&self) -> f64 {
self.try_stddev().unwrap_or(f64::NAN)
}
pub fn to_bytes(&self) -> [u8; Self::BYTES] {
let mut out = [0u8; Self::BYTES];
out[..SumF64::BYTES].copy_from_slice(&self.sum.to_bytes());
out[SumF64::BYTES..].copy_from_slice(&self.sumsq.to_bytes());
out
}
pub fn from_bytes(b: &[u8; Self::BYTES]) -> Option<Self> {
let mut sb = [0u8; SumF64::BYTES];
sb.copy_from_slice(&b[..SumF64::BYTES]);
let mut db = [0u8; DOT_BYTES];
db.copy_from_slice(&b[SumF64::BYTES..]);
Some(Self {
sum: SumF64::from_bytes(&sb)?,
sumsq: DotF64::from_bytes(&db)?,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct Moments4F64 {
sum: SumF64,
m2: DotF64,
m3: DotF64,
m4: DotF64,
split_underflow: bool,
}
impl Moments4F64 {
pub const BYTES: usize = SumF64::BYTES + 3 * DOT_BYTES + 1;
pub fn new() -> Self {
Self {
sum: SumF64::new(),
m2: DotF64::new(),
m3: DotF64::new(),
m4: DotF64::new(),
split_underflow: false,
}
}
pub fn add(&mut self, x: f64) {
self.sum.add(x);
self.m2.push(x, x);
let h = x * x;
let l = x.mul_add(x, -h);
if x != 0.0 && h.is_finite() && h.abs() < f64::MIN_POSITIVE {
self.split_underflow = true;
}
self.m3.push(h, x);
self.m3.push(l, x);
self.m4.push(h, h);
self.m4.push(h, l);
self.m4.push(l, h);
self.m4.push(l, l);
}
pub fn merge(&mut self, o: &Moments4F64) {
self.sum.merge(&o.sum);
self.m2.merge(&o.m2);
self.m3.merge(&o.m3);
self.m4.merge(&o.m4);
self.split_underflow |= o.split_underflow;
}
pub const fn count(&self) -> u64 {
self.sum.count()
}
fn a234(&self) -> Result<(BigInt, BigInt, BigInt, u64), StatsError> {
let n = self.count();
if n == 0 {
return Err(StatsError::Empty);
}
if self.split_underflow {
return Err(StatsError::ExactnessLost);
}
let s = sum_int(&self.sum)?;
let q2 = dot_int(&self.m2)?;
let q3 = dot_int(&self.m3)?;
let q4 = dot_int(&self.m4)?;
let n_b = BigInt::from(n);
let a2 = ((&n_b * &q2) << UNIT_LOG2) - (&s * &s);
let a3 = ((&n_b * &n_b * &q3) << (2 * UNIT_LOG2))
- ((BigInt::from(3u8) * &n_b * &q2 * &s) << UNIT_LOG2)
+ BigInt::from(2u8) * &s * &s * &s;
let a4 = ((&n_b * &n_b * &n_b * &q4) << (3 * UNIT_LOG2))
- ((BigInt::from(4u8) * &n_b * &n_b * &q3 * &s) << (2 * UNIT_LOG2))
+ ((BigInt::from(6u8) * &n_b * &q2 * &s * &s) << UNIT_LOG2)
- BigInt::from(3u8) * &s * &s * &s * &s;
Ok((a2, a3, a4, n))
}
pub fn try_mean(&self) -> Result<f64, StatsError> {
let n = self.count();
if n == 0 {
return Err(StatsError::Empty);
}
let s = sum_int(&self.sum)?;
Ok(round_rational(&s, &(BigInt::from(n) * unit_pow(1))))
}
pub fn try_variance(&self) -> Result<f64, StatsError> {
let (a2, _, _, n) = self.a234()?;
Ok(round_rational(
&a2,
&(BigInt::from(n) * BigInt::from(n) * unit_pow(2)),
))
}
pub fn try_kurtosis(&self) -> Result<f64, StatsError> {
let (a2, _, a4, _) = self.a234()?;
if a2.is_zero() {
return Err(StatsError::Degenerate);
}
Ok(round_rational(&a4, &(&a2 * &a2)))
}
pub fn try_excess_kurtosis(&self) -> Result<f64, StatsError> {
let (a2, _, a4, _) = self.a234()?;
if a2.is_zero() {
return Err(StatsError::Degenerate);
}
let a2sq = &a2 * &a2;
Ok(round_rational(&(a4 - BigInt::from(3u8) * &a2sq), &a2sq))
}
pub fn try_skewness_squared(&self) -> Result<f64, StatsError> {
let (a2, a3, _, _) = self.a234()?;
if a2.is_zero() {
return Err(StatsError::Degenerate);
}
let v = round_rational(&(&a3 * &a3), &(&a2 * &a2 * &a2));
Ok(if a3.is_negative() { -v } else { v })
}
pub fn try_skewness(&self) -> Result<f64, StatsError> {
let s2 = self.try_skewness_squared()?;
Ok(if s2 < 0.0 { -(-s2).sqrt() } else { s2.sqrt() })
}
pub fn kurtosis(&self) -> f64 {
self.try_kurtosis().unwrap_or(f64::NAN)
}
pub fn skewness(&self) -> f64 {
self.try_skewness().unwrap_or(f64::NAN)
}
pub fn to_bytes(&self) -> [u8; Self::BYTES] {
let mut out = [0u8; Self::BYTES];
let mut at = 0;
out[at..at + SumF64::BYTES].copy_from_slice(&self.sum.to_bytes());
at += SumF64::BYTES;
for d in [&self.m2, &self.m3, &self.m4] {
out[at..at + DOT_BYTES].copy_from_slice(&d.to_bytes());
at += DOT_BYTES;
}
out[at] = self.split_underflow as u8;
out
}
pub fn from_bytes(b: &[u8; Self::BYTES]) -> Option<Self> {
let mut sb = [0u8; SumF64::BYTES];
sb.copy_from_slice(&b[..SumF64::BYTES]);
let sum = SumF64::from_bytes(&sb)?;
let mut dots = [DotF64::new(), DotF64::new(), DotF64::new()];
let mut at = SumF64::BYTES;
for d in dots.iter_mut() {
let mut db = [0u8; DOT_BYTES];
db.copy_from_slice(&b[at..at + DOT_BYTES]);
*d = DotF64::from_bytes(&db)?;
at += DOT_BYTES;
}
let split_underflow = match b[at] {
0 => false,
1 => true,
_ => return None,
};
let [m2, m3, m4] = dots;
Some(Self {
sum,
m2,
m3,
m4,
split_underflow,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct CovF64 {
sx: SumF64,
sy: SumF64,
sxx: DotF64,
syy: DotF64,
sxy: DotF64,
}
impl CovF64 {
pub const BYTES: usize = 2 * SumF64::BYTES + 3 * DOT_BYTES;
pub fn new() -> Self {
Self {
sx: SumF64::new(),
sy: SumF64::new(),
sxx: DotF64::new(),
syy: DotF64::new(),
sxy: DotF64::new(),
}
}
pub fn add(&mut self, x: f64, y: f64) {
self.sx.add(x);
self.sy.add(y);
self.sxx.push(x, x);
self.syy.push(y, y);
self.sxy.push(x, y);
}
pub fn merge(&mut self, o: &CovF64) {
self.sx.merge(&o.sx);
self.sy.merge(&o.sy);
self.sxx.merge(&o.sxx);
self.syy.merge(&o.syy);
self.sxy.merge(&o.sxy);
}
pub const fn count(&self) -> u64 {
self.sx.count()
}
fn b_terms(&self) -> Result<(BigInt, BigInt, BigInt, u64), StatsError> {
let n = self.count();
if n == 0 {
return Err(StatsError::Empty);
}
let sx = sum_int(&self.sx)?;
let sy = sum_int(&self.sy)?;
let qxx = dot_int(&self.sxx)?;
let qyy = dot_int(&self.syy)?;
let qxy = dot_int(&self.sxy)?;
let n_b = BigInt::from(n);
let bxy = ((&n_b * qxy) << UNIT_LOG2) - (&sx * &sy);
let bxx = ((&n_b * qxx) << UNIT_LOG2) - (&sx * &sx);
let byy = ((&n_b * qyy) << UNIT_LOG2) - (&sy * &sy);
Ok((bxy, bxx, byy, n))
}
pub fn try_covariance(&self) -> Result<f64, StatsError> {
let (bxy, _, _, n) = self.b_terms()?;
Ok(round_rational(
&bxy,
&(BigInt::from(n) * BigInt::from(n) * unit_pow(2)),
))
}
pub fn try_slope(&self) -> Result<f64, StatsError> {
let (bxy, bxx, _, _) = self.b_terms()?;
if bxx.is_zero() {
return Err(StatsError::Degenerate);
}
Ok(round_rational(&bxy, &bxx))
}
pub fn try_intercept(&self) -> Result<f64, StatsError> {
let n = self.count();
let (bxy, bxx, _, _) = self.b_terms()?;
if bxx.is_zero() {
return Err(StatsError::Degenerate);
}
let sx = sum_int(&self.sx)?;
let sy = sum_int(&self.sy)?;
let num = sy * &bxx - bxy * sx;
let mut den = (BigInt::from(n) * bxx) << UNIT_LOG2;
let num = if den.is_negative() {
den = -den;
-num
} else {
num
};
Ok(round_rational(&num, &den))
}
pub fn try_r_squared(&self) -> Result<f64, StatsError> {
let (bxy, bxx, byy, _) = self.b_terms()?;
let den = &bxx * &byy;
if den.is_zero() {
return Err(StatsError::Degenerate);
}
Ok(round_rational(&(&bxy * &bxy), &den))
}
pub fn try_correlation(&self) -> Result<f64, StatsError> {
let (bxy, ..) = self.b_terms()?;
let r2 = self.try_r_squared()?;
let r = r2.sqrt();
Ok(if bxy.is_negative() { -r } else { r })
}
pub fn slope(&self) -> f64 {
self.try_slope().unwrap_or(f64::NAN)
}
pub fn intercept(&self) -> f64 {
self.try_intercept().unwrap_or(f64::NAN)
}
pub fn covariance(&self) -> f64 {
self.try_covariance().unwrap_or(f64::NAN)
}
pub fn correlation(&self) -> f64 {
self.try_correlation().unwrap_or(f64::NAN)
}
pub fn to_bytes(&self) -> [u8; Self::BYTES] {
let mut out = [0u8; Self::BYTES];
let mut at = 0;
for s in [&self.sx, &self.sy] {
out[at..at + SumF64::BYTES].copy_from_slice(&s.to_bytes());
at += SumF64::BYTES;
}
for d in [&self.sxx, &self.syy, &self.sxy] {
out[at..at + DOT_BYTES].copy_from_slice(&d.to_bytes());
at += DOT_BYTES;
}
out
}
pub fn from_bytes(b: &[u8; Self::BYTES]) -> Option<Self> {
let mut at = 0;
let mut sums = [SumF64::new(), SumF64::new()];
for s in sums.iter_mut() {
let mut sb = [0u8; SumF64::BYTES];
sb.copy_from_slice(&b[at..at + SumF64::BYTES]);
*s = SumF64::from_bytes(&sb)?;
at += SumF64::BYTES;
}
let mut dots = [DotF64::new(), DotF64::new(), DotF64::new()];
for d in dots.iter_mut() {
let mut db = [0u8; DOT_BYTES];
db.copy_from_slice(&b[at..at + DOT_BYTES]);
*d = DotF64::from_bytes(&db)?;
at += DOT_BYTES;
}
let [sx, sy] = sums;
let [sxx, syy, sxy] = dots;
Some(Self {
sx,
sy,
sxx,
syy,
sxy,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct WeightedMomentsF64 {
sw: SumF64,
swx: DotF64,
swx2: DotF64,
split_underflow: bool,
}
impl WeightedMomentsF64 {
pub const BYTES: usize = SumF64::BYTES + 2 * DOT_BYTES + 1;
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, x: f64, w: f64) {
self.sw.add(w);
self.swx.push(w, x);
let h = x * x;
let l = x.mul_add(x, -h);
if x != 0.0 && h.is_finite() && h.abs() < f64::MIN_POSITIVE {
self.split_underflow = true;
}
self.swx2.push(w, h);
self.swx2.push(w, l);
}
pub fn merge(&mut self, o: &WeightedMomentsF64) {
self.sw.merge(&o.sw);
self.swx.merge(&o.swx);
self.swx2.merge(&o.swx2);
self.split_underflow |= o.split_underflow;
}
pub const fn count(&self) -> u64 {
self.sw.count()
}
fn ints(&self) -> Result<(BigInt, BigInt, BigInt), StatsError> {
if self.count() == 0 {
return Err(StatsError::Empty);
}
if self.split_underflow {
return Err(StatsError::ExactnessLost);
}
Ok((
sum_int(&self.sw)?,
dot_int(&self.swx)?,
dot_int(&self.swx2)?,
))
}
pub fn try_mean(&self) -> Result<f64, StatsError> {
let (sw, swx, _) = self.ints()?;
if !sw.is_positive() {
return Err(StatsError::Degenerate);
}
Ok(round_rational(&swx, &sw))
}
pub fn try_variance(&self) -> Result<f64, StatsError> {
let (sw, swx, swx2) = self.ints()?;
if !sw.is_positive() {
return Err(StatsError::Degenerate);
}
let num = (&sw * swx2) - (&swx * &swx);
let den = &sw * &sw;
Ok(round_rational(&num, &den))
}
pub fn mean(&self) -> f64 {
self.try_mean().unwrap_or(f64::NAN)
}
pub fn variance(&self) -> f64 {
self.try_variance().unwrap_or(f64::NAN)
}
pub fn to_bytes(&self) -> [u8; Self::BYTES] {
let mut out = [0u8; Self::BYTES];
out[..SumF64::BYTES].copy_from_slice(&self.sw.to_bytes());
let mut at = SumF64::BYTES;
for d in [&self.swx, &self.swx2] {
out[at..at + DOT_BYTES].copy_from_slice(&d.to_bytes());
at += DOT_BYTES;
}
out[at] = self.split_underflow as u8;
out
}
pub fn from_bytes(b: &[u8; Self::BYTES]) -> Option<Self> {
let mut sb = [0u8; SumF64::BYTES];
sb.copy_from_slice(&b[..SumF64::BYTES]);
let sw = SumF64::from_bytes(&sb)?;
let mut at = SumF64::BYTES;
let mut dots = [DotF64::new(), DotF64::new()];
for d in dots.iter_mut() {
let mut db = [0u8; DOT_BYTES];
db.copy_from_slice(&b[at..at + DOT_BYTES]);
*d = DotF64::from_bytes(&db)?;
at += DOT_BYTES;
}
let split_underflow = match b[at] {
0 => false,
1 => true,
_ => return None,
};
let [swx, swx2] = dots;
Some(Self {
sw,
swx,
swx2,
split_underflow,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct PnMomentsF64 {
adds: MomentsF64,
removes: MomentsF64,
}
impl PnMomentsF64 {
pub const BYTES: usize = 2 * MomentsF64::BYTES;
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, x: f64) {
self.adds.add(x);
}
pub fn remove(&mut self, x: f64) {
self.removes.add(x);
}
pub fn merge(&mut self, o: &PnMomentsF64) {
self.adds.merge(&o.adds);
self.removes.merge(&o.removes);
}
pub fn live_count(&self) -> Option<u64> {
self.adds.count().checked_sub(self.removes.count())
}
pub const fn count(&self) -> u64 {
self.adds.count().saturating_add(self.removes.count())
}
fn net(&self) -> Result<(BigInt, BigInt, u64), StatsError> {
let n = self.live_count().ok_or(StatsError::Degenerate)?;
if n == 0 {
return Err(StatsError::Empty);
}
let s = sum_int(&self.adds.sum)? - sum_int(&self.removes.sum)?;
let q = dot_int(&self.adds.sumsq)? - dot_int(&self.removes.sumsq)?;
Ok((s, q, n))
}
pub fn try_mean(&self) -> Result<f64, StatsError> {
let (s, _, n) = self.net()?;
Ok(round_rational(&s, &(BigInt::from(n) * unit_pow(1))))
}
pub fn try_variance(&self) -> Result<f64, StatsError> {
let (s, q, n) = self.net()?;
let a2 = ((BigInt::from(n) * q) << UNIT_LOG2) - (&s * &s);
Ok(round_rational(
&a2,
&(BigInt::from(n) * BigInt::from(n) * unit_pow(2)),
))
}
pub fn mean(&self) -> f64 {
self.try_mean().unwrap_or(f64::NAN)
}
pub fn variance(&self) -> f64 {
self.try_variance().unwrap_or(f64::NAN)
}
pub fn to_bytes(&self) -> [u8; Self::BYTES] {
let mut out = [0u8; Self::BYTES];
out[..MomentsF64::BYTES].copy_from_slice(&self.adds.to_bytes());
out[MomentsF64::BYTES..].copy_from_slice(&self.removes.to_bytes());
out
}
pub fn from_bytes(b: &[u8; Self::BYTES]) -> Option<Self> {
let mut ab = [0u8; MomentsF64::BYTES];
ab.copy_from_slice(&b[..MomentsF64::BYTES]);
let mut rb = [0u8; MomentsF64::BYTES];
rb.copy_from_slice(&b[MomentsF64::BYTES..]);
Some(Self {
adds: MomentsF64::from_bytes(&ab)?,
removes: MomentsF64::from_bytes(&rb)?,
})
}
}
macro_rules! impl_mergeable_stats {
($($t:ty),+) => {$(
impl crate::Mergeable for $t {
fn merge(&mut self, other: &Self) {
<$t>::merge(self, other);
}
fn count(&self) -> u64 {
<$t>::count(self)
}
fn encode(&self) -> Vec<u8> {
self.to_bytes().to_vec()
}
fn decode(bytes: &[u8]) -> Option<Self> {
let arr: &[u8; <$t>::BYTES] = bytes.try_into().ok()?;
<$t>::from_bytes(arr)
}
}
)+};
}
impl_mergeable_stats!(
MomentsF64,
Moments4F64,
CovF64,
WeightedMomentsF64,
PnMomentsF64
);