use super::ChangePointDetector;
pub struct Adwin {
pub delta: f64,
pub min_side: usize,
}
impl Default for Adwin {
fn default() -> Self {
Self {
delta: 0.002,
min_side: 5,
}
}
}
impl ChangePointDetector for Adwin {
fn name(&self) -> &'static str {
"adwin"
}
fn detect(&self, series: &[(f64, f64)]) -> Vec<f64> {
let mut cps = Vec::new();
if series.len() < 2 * self.min_side {
return cps;
}
let mut start: usize = 0;
let mut end: usize = 0;
while end < series.len() {
end += 1;
let n = end - start;
if n < 2 * self.min_side {
continue;
}
let (mut best_cut, mut best_diff) = (None, 0.0f64);
for k in (start + self.min_side)..=(end - self.min_side) {
let n0 = (k - start) as f64;
let n1 = (end - k) as f64;
let mean0 = series[start..k].iter().map(|(_, v)| v).sum::<f64>() / n0;
let mean1 = series[k..end].iter().map(|(_, v)| v).sum::<f64>() / n1;
let diff = (mean0 - mean1).abs();
if diff > best_diff {
best_diff = diff;
best_cut = Some(k);
}
}
let Some(k) = best_cut else { continue };
let n0 = (k - start) as f64;
let n1 = (end - k) as f64;
let eps_cut = self.epsilon_cut(series, start, end, n0, n1);
if best_diff > eps_cut {
cps.push(series[k].0);
start = k;
}
}
cps
}
}
impl Adwin {
fn epsilon_cut(
&self,
series: &[(f64, f64)],
start: usize,
end: usize,
n0: f64,
n1: f64,
) -> f64 {
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for &(_, v) in &series[start..end] {
if v < lo {
lo = v;
}
if v > hi {
hi = v;
}
}
let r = (hi - lo).max(f64::EPSILON);
let m = 2.0 * n0 * n1 / (n0 + n1);
let n = (end - start) as f64;
let d_prime = self.delta / n.max(1.0);
((r * r) / (2.0 * m) * (2.0 / d_prime).ln()).sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flags_large_mean_shift() {
let mut series = Vec::new();
for i in 0..50 {
series.push((i as f64, 0.0));
}
for i in 50..100 {
series.push((i as f64, 2.0));
}
let cps = Adwin::default().detect(&series);
assert!(!cps.is_empty(), "ADWIN should flag an obvious shift");
}
#[test]
fn stays_silent_on_flat_series() {
let series: Vec<(f64, f64)> = (0..200).map(|i| (i as f64, 0.0)).collect();
let cps = Adwin::default().detect(&series);
assert!(
cps.is_empty(),
"ADWIN should not invent changes on flat data"
);
}
}