use std::collections::VecDeque;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DriftStatus {
#[default]
Stable,
Warning,
Drift,
}
#[derive(Debug, Clone)]
pub struct DriftStats {
pub n_samples: u64,
pub error_rate: f64,
pub min_error_rate: f64,
pub std_dev: f64,
pub status: DriftStatus,
}
pub trait DriftDetector: Send + Sync {
fn add_element(&mut self, error: bool);
fn detected_change(&self) -> DriftStatus;
fn reset(&mut self);
fn stats(&self) -> DriftStats;
}
#[derive(Debug, Clone)]
pub struct DDM {
min_samples: u64,
warning_level: f64,
drift_level: f64,
n: u64,
p: f64,
s: f64,
p_min: f64,
s_min: f64,
status: DriftStatus,
}
impl Default for DDM {
fn default() -> Self {
Self::new()
}
}
impl DDM {
#[must_use]
pub fn new() -> Self {
Self {
min_samples: 30,
warning_level: 2.0,
drift_level: 3.0,
n: 0,
p: 1.0,
s: 0.0,
p_min: f64::MAX,
s_min: f64::MAX,
status: DriftStatus::Stable,
}
}
#[must_use]
pub fn with_thresholds(min_samples: u64, warning_level: f64, drift_level: f64) -> Self {
Self {
min_samples,
warning_level,
drift_level,
..Self::new()
}
}
}
impl DriftDetector for DDM {
fn add_element(&mut self, error: bool) {
self.n += 1;
let error_val = if error { 1.0 } else { 0.0 };
self.p += (error_val - self.p) / self.n as f64;
self.s = (self.p * (1.0 - self.p) / self.n as f64).sqrt();
if self.n >= self.min_samples {
if self.p + self.s < self.p_min + self.s_min {
self.p_min = self.p;
self.s_min = self.s;
}
if self.p + self.s > self.p_min + self.drift_level * self.s_min {
self.status = DriftStatus::Drift;
} else if self.p + self.s > self.p_min + self.warning_level * self.s_min {
self.status = DriftStatus::Warning;
} else {
self.status = DriftStatus::Stable;
}
}
}
fn detected_change(&self) -> DriftStatus {
self.status
}
fn reset(&mut self) {
self.n = 0;
self.p = 1.0;
self.s = 0.0;
self.p_min = f64::MAX;
self.s_min = f64::MAX;
self.status = DriftStatus::Stable;
}
fn stats(&self) -> DriftStats {
DriftStats {
n_samples: self.n,
error_rate: self.p,
min_error_rate: if self.p_min == f64::MAX {
0.0
} else {
self.p_min
},
std_dev: self.s,
status: self.status,
}
}
}
#[derive(Debug, Clone)]
pub struct PageHinkley {
delta: f64,
lambda: f64,
sum: f64,
mean: f64,
n: u64,
min_sum: f64,
status: DriftStatus,
}
impl Default for PageHinkley {
fn default() -> Self {
Self::new()
}
}
impl PageHinkley {
#[must_use]
pub fn new() -> Self {
Self {
delta: 0.005,
lambda: 50.0,
sum: 0.0,
mean: 0.0,
n: 0,
min_sum: f64::MAX,
status: DriftStatus::Stable,
}
}
#[must_use]
pub fn with_thresholds(delta: f64, lambda: f64) -> Self {
Self {
delta,
lambda,
..Self::new()
}
}
}
impl DriftDetector for PageHinkley {
fn add_element(&mut self, error: bool) {
self.n += 1;
let x = if error { 1.0 } else { 0.0 };
self.mean += (x - self.mean) / self.n as f64;
self.sum += x - self.mean - self.delta;
if self.sum < self.min_sum {
self.min_sum = self.sum;
}
if self.sum - self.min_sum > self.lambda {
self.status = DriftStatus::Drift;
} else if self.sum - self.min_sum > self.lambda * 0.5 {
self.status = DriftStatus::Warning;
} else {
self.status = DriftStatus::Stable;
}
}
fn detected_change(&self) -> DriftStatus {
self.status
}
fn reset(&mut self) {
self.sum = 0.0;
self.mean = 0.0;
self.n = 0;
self.min_sum = f64::MAX;
self.status = DriftStatus::Stable;
}
fn stats(&self) -> DriftStats {
DriftStats {
n_samples: self.n,
error_rate: self.mean,
min_error_rate: 0.0,
std_dev: (self.sum - self.min_sum).abs(),
status: self.status,
}
}
}
#[derive(Debug, Clone)]
struct Bucket {
total: f64,
count: usize,
}
#[derive(Debug, Clone)]
pub struct ADWIN {
delta: f64,
max_buckets: usize,
bucket_rows: Vec<VecDeque<Bucket>>,
total: f64,
count: usize,
width: usize,
status: DriftStatus,
last_bucket_row: usize,
}
impl Default for ADWIN {
fn default() -> Self {
Self::new()
}
}
include!("drift_detector_factory.rs");
include!("drift_tests.rs");