use crate::statistics::MomentsF64;
#[derive(Debug, Clone)]
pub struct DistributionShiftF64 {
fast: MomentsF64,
slow: MomentsF64,
fast_window: u64,
fast_count: u64,
count: u64,
}
#[derive(Debug, Clone)]
pub struct DistributionShiftF64Builder {
fast_window: Option<u64>,
}
impl DistributionShiftF64 {
#[inline]
#[must_use]
pub fn builder() -> DistributionShiftF64Builder {
DistributionShiftF64Builder { fast_window: None }
}
#[inline]
pub fn update(&mut self, value: f64) -> Result<(), crate::DataError> {
check_finite!(value);
self.count += 1;
self.fast_count += 1;
self.fast.update(value)?;
self.slow.update(value)?;
if self.fast_count >= self.fast_window {
self.fast = MomentsF64::new();
self.fast_count = 0;
}
Ok(())
}
#[inline]
#[must_use]
pub fn kurtosis_shift(&self) -> Option<f64> {
let fast_k = self.fast.excess_kurtosis()?;
let slow_k = self.slow.excess_kurtosis()?;
Some(fast_k - slow_k)
}
#[inline]
#[must_use]
pub fn skewness_shift(&self) -> Option<f64> {
let fast_s = self.fast.skewness()?;
let slow_s = self.slow.skewness()?;
Some(fast_s - slow_s)
}
#[inline]
#[must_use]
pub fn is_shifted(&self, threshold: f64) -> bool {
if let Some(ks) = self.kurtosis_shift()
&& ks.abs() > threshold
{
return true;
}
if let Some(ss) = self.skewness_shift()
&& ss.abs() > threshold
{
return true;
}
false
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.slow.count() >= 4 && self.fast.count() >= 4
}
pub fn reset(&mut self) {
self.fast = MomentsF64::new();
self.slow = MomentsF64::new();
self.fast_count = 0;
self.count = 0;
}
}
impl DistributionShiftF64Builder {
#[inline]
#[must_use]
pub fn fast_window(mut self, window: u64) -> Self {
self.fast_window = Some(window);
self
}
pub fn build(self) -> DistributionShiftF64 {
let fast_window = self.fast_window.unwrap_or(50);
assert!(fast_window > 0, "fast_window must be > 0");
DistributionShiftF64 {
fast: MomentsF64::new(),
slow: MomentsF64::new(),
fast_window,
fast_count: 0,
count: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_distribution_no_shift() {
let mut det = DistributionShiftF64::builder().fast_window(100).build();
for i in 0..500 {
det.update((i % 100) as f64).unwrap();
}
if let Some(ks) = det.kurtosis_shift() {
assert!(
ks.abs() < 2.0,
"kurtosis shift should be small for stable data, got {ks}"
);
}
}
#[test]
fn fat_tail_shift_detected() {
let mut det = DistributionShiftF64::builder().fast_window(50).build();
for i in 0..200 {
det.update((i as f64) * 0.01).unwrap();
}
for i in 0..50 {
let val = if i % 5 == 0 { 1000.0 } else { 1.0 };
det.update(val).unwrap();
}
assert!(det.count() > 200);
if let Some(ks) = det.kurtosis_shift() {
assert!(
ks > 1.0,
"expected significant positive kurtosis shift from outliers, got {ks}"
);
}
if let Some(ss) = det.skewness_shift() {
assert!(
ss.abs() > 0.1,
"expected measurable skewness shift from outliers, got {ss}"
);
}
}
#[test]
fn is_shifted_threshold() {
let mut det = DistributionShiftF64::builder().fast_window(100).build();
for i in 0..500 {
det.update((i % 50) as f64).unwrap();
}
assert!(!det.is_shifted(100.0));
}
#[test]
fn not_primed_before_4() {
let mut det = DistributionShiftF64::builder().fast_window(100).build();
det.update(1.0).unwrap();
det.update(2.0).unwrap();
det.update(3.0).unwrap();
assert!(!det.is_primed());
}
#[test]
fn primed_after_4() {
let mut det = DistributionShiftF64::builder().fast_window(100).build();
for i in 0..5 {
det.update(i as f64).unwrap();
}
assert!(det.is_primed());
}
#[test]
fn reset_clears_state() {
let mut det = DistributionShiftF64::builder().fast_window(50).build();
for i in 0..100 {
det.update(i as f64).unwrap();
}
det.reset();
assert_eq!(det.count(), 0);
assert!(!det.is_primed());
}
#[test]
fn nan_rejected() {
let mut det = DistributionShiftF64::builder().fast_window(50).build();
assert!(det.update(f64::NAN).is_err());
}
#[test]
fn inf_rejected() {
let mut det = DistributionShiftF64::builder().fast_window(50).build();
assert!(det.update(f64::INFINITY).is_err());
}
}