///|
/// Exponentially weighted moving-average baseline for online drift comparison.
pub struct EwmaConfig {
alpha : Double
z_threshold : Double
warmup : Int
}
///|
pub fn EwmaConfig::new(
alpha : Double,
z_threshold : Double,
warmup : Int,
) -> Result[EwmaConfig, DetectError] {
if !finite(alpha) || alpha <= 0.0 || alpha >= 1.0 {
return Err(InvalidHazard)
}
if !finite(z_threshold) || z_threshold <= 0.0 || warmup < 2 {
return Err(InvalidRunLengthLimit)
}
Ok({ alpha, z_threshold, warmup, })
}
///|
pub struct EwmaUpdate {
step : Int
value : Double
mean : Double
variance : Double
z : Double
drift : Bool
}
///|
pub struct EwmaDetector {
config : EwmaConfig
mut step : Int
mut mean : Double
mut variance : Double
mut initialized : Bool
}
///|
pub fn EwmaDetector::new(config : EwmaConfig) -> EwmaDetector {
{ config, step: 0, mean: 0.0, variance: 0.0, initialized: false, }
}
///|
/// Update the EWMA mean and variance before comparing the new point to its prior state.
pub fn EwmaDetector::update(
self : EwmaDetector,
value : Double,
) -> Result[EwmaUpdate, DetectError] {
if !finite(value) {
return Err(NonFiniteObservation)
}
if !self.initialized {
self.initialized = true
self.step = 1
self.mean = value
return Ok({
step: 1,
value,
mean: value,
variance: 0.0,
z: 0.0,
drift: false,
})
}
let prior_mean = self.mean
let delta = value - prior_mean
let alpha = self.config.alpha
self.mean = prior_mean + alpha * delta
self.variance = (1.0 - alpha) * (self.variance + alpha * delta * delta)
self.step = self.step + 1
let scale = if self.variance <= 0.0 {
0.0
} else {
@math.exp(@math.ln(self.variance) * 0.5)
}
let z = if scale == 0.0 { 0.0 } else { delta / scale }
Ok({
step: self.step,
value,
mean: self.mean,
variance: self.variance,
z,
drift: self.step >= self.config.warmup &&
(z > self.config.z_threshold || z < -self.config.z_threshold),
})
}
///|
pub fn EwmaDetector::replay(
self : EwmaDetector,
values : Array[Double],
) -> Result[Array[EwmaUpdate], DetectError] {
let result : Array[EwmaUpdate] = []
for value in values {
match self.update(value) {
Ok(update) => result.push(update)
Err(error) => return Err(error)
}
}
Ok(result)
}
///|
/// CUSUM is a second deterministic baseline for a known reference mean.
pub struct CusumConfig {
reference_mean : Double
slack : Double
threshold : Double
}
///|
pub fn CusumConfig::new(
reference_mean : Double,
slack : Double,
threshold : Double,
) -> Result[CusumConfig, DetectError] {
if !finite(reference_mean) ||
!finite(slack) ||
!finite(threshold) ||
slack < 0.0 ||
threshold <= 0.0 {
return Err(InvalidVariance)
}
Ok({ reference_mean, slack, threshold, })
}
///|
pub struct CusumDetector {
config : CusumConfig
mut positive : Double
mut negative : Double
mut step : Int
}
///|
pub struct CusumUpdate {
step : Int
positive : Double
negative : Double
drift : Bool
}
///|
pub fn CusumDetector::new(config : CusumConfig) -> CusumDetector {
{ config, positive: 0.0, negative: 0.0, step: 0, }
}
///|
pub fn CusumDetector::update(
self : CusumDetector,
value : Double,
) -> Result[CusumUpdate, DetectError] {
if !finite(value) {
return Err(NonFiniteObservation)
}
let residual = value - self.config.reference_mean
let positive = self.positive + residual - self.config.slack
let negative = self.negative - residual - self.config.slack
self.positive = if positive > 0.0 { positive } else { 0.0 }
self.negative = if negative > 0.0 { negative } else { 0.0 }
self.step = self.step + 1
Ok({
step: self.step,
positive: self.positive,
negative: self.negative,
drift: self.positive >= self.config.threshold ||
self.negative >= self.config.threshold,
})
}