///|
/// Page-Hinkley change detector for gradual concept drift.
pub struct PageHinkleyDetector {
threshold : Double
delta : Double
mut mean : Double
mut cumulative : Double
mut minimum : Double
mut count : Double
}
///|
pub fn PageHinkleyDetector::new(
threshold? : Double = 50.0,
delta? : Double = 0.005,
) -> PageHinkleyDetector {
{
threshold: if threshold <= 0.0 {
50.0
} else {
threshold
},
delta: if delta < 0.0 {
0.0
} else {
delta
},
mean: 0.0,
cumulative: 0.0,
minimum: 0.0,
count: 0.0,
}
}
///|
pub fn PageHinkleyDetector::update(
self : PageHinkleyDetector,
value : Double,
) -> Bool {
self.count += 1.0
self.mean += (value - self.mean) / self.count
self.cumulative += value - self.mean - self.delta
if self.cumulative < self.minimum {
self.minimum = self.cumulative
}
self.cumulative - self.minimum > self.threshold
}
///|
pub fn PageHinkleyDetector::mean(self : PageHinkleyDetector) -> Double {
self.mean
}
///|
pub fn PageHinkleyDetector::count(self : PageHinkleyDetector) -> Double {
self.count
}
///|
pub fn PageHinkleyDetector::reset(self : PageHinkleyDetector) -> Unit {
self.mean = 0.0
self.cumulative = 0.0
self.minimum = 0.0
self.count = 0.0
}
///|
/// Two-sided CUSUM detector for abrupt shifts around a target level.
pub struct CumulativeSumDetector {
target : Double
allowance : Double
threshold : Double
mut positive : Double
mut negative : Double
}
///|
pub fn CumulativeSumDetector::new(
target? : Double = 0.0,
allowance? : Double = 0.5,
threshold? : Double = 5.0,
) -> CumulativeSumDetector {
{
target,
allowance: if allowance < 0.0 {
0.0
} else {
allowance
},
threshold: if threshold <= 0.0 {
5.0
} else {
threshold
},
positive: 0.0,
negative: 0.0,
}
}
///|
pub fn CumulativeSumDetector::update(
self : CumulativeSumDetector,
value : Double,
) -> Bool {
let difference = value - self.target
let positive_candidate = self.positive + difference - self.allowance
let negative_candidate = self.negative - difference - self.allowance
self.positive = if positive_candidate > 0.0 {
positive_candidate
} else {
0.0
}
self.negative = if negative_candidate > 0.0 {
negative_candidate
} else {
0.0
}
self.positive > self.threshold || self.negative > self.threshold
}
///|
pub fn CumulativeSumDetector::positive(self : CumulativeSumDetector) -> Double {
self.positive
}
///|
pub fn CumulativeSumDetector::negative(self : CumulativeSumDetector) -> Double {
self.negative
}
///|
pub fn CumulativeSumDetector::reset(self : CumulativeSumDetector) -> Unit {
self.positive = 0.0
self.negative = 0.0
}
///|
/// EWMA z-score detector. It reports an anomaly before updating its baseline,
/// which prevents a single spike from hiding itself.
pub struct EwmaAnomalyDetector {
baseline : ExponentialMovingVariance
threshold : Double
mut anomalies : Int
}
///|
pub fn EwmaAnomalyDetector::new(
alpha? : Double = 0.05,
threshold? : Double = 3.0,
) -> EwmaAnomalyDetector {
{
baseline: ExponentialMovingVariance::new(alpha~),
threshold: if threshold <= 0.0 {
3.0
} else {
threshold
},
anomalies: 0,
}
}
///|
pub fn EwmaAnomalyDetector::update(
self : EwmaAnomalyDetector,
value : Double,
) -> Bool {
let anomaly = self.baseline.initialized() &&
self.baseline.z_score(value).abs() >= self.threshold
if anomaly {
self.anomalies += 1
}
self.baseline.update(value)
anomaly
}
///|
pub fn EwmaAnomalyDetector::score(
self : EwmaAnomalyDetector,
value : Double,
) -> Double {
self.baseline.z_score(value).abs()
}
///|
pub fn EwmaAnomalyDetector::anomalies(self : EwmaAnomalyDetector) -> Int {
self.anomalies
}
///|
pub fn EwmaAnomalyDetector::reset(self : EwmaAnomalyDetector) -> Unit {
self.baseline.reset()
self.anomalies = 0
}
///|
/// Diagonal multivariate z-score detector for low-memory edge deployments.
pub struct MultivariateAnomalyDetector {
moments : VectorMoments
threshold : Double
mut anomalies : Int
}
///|
pub fn MultivariateAnomalyDetector::new(
dimension : Int,
threshold? : Double = 3.0,
) -> MultivariateAnomalyDetector {
{
moments: VectorMoments::new(dimension),
threshold: if threshold <= 0.0 {
3.0
} else {
threshold
},
anomalies: 0,
}
}
///|
pub fn MultivariateAnomalyDetector::score(
self : MultivariateAnomalyDetector,
values : Array[Double],
) -> Double {
let standardized = self.moments.standardize(values)
squared_norm(standardized).sqrt()
}
///|
pub fn MultivariateAnomalyDetector::update(
self : MultivariateAnomalyDetector,
values : Array[Double],
) -> Bool {
let score = self.score(values)
let anomaly = self.moments.dimension() > 0 && score >= self.threshold
if anomaly {
self.anomalies += 1
}
self.moments.update(values)
anomaly
}
///|
pub fn MultivariateAnomalyDetector::anomalies(
self : MultivariateAnomalyDetector,
) -> Int {
self.anomalies
}
///|
pub fn MultivariateAnomalyDetector::mean(
self : MultivariateAnomalyDetector,
) -> Array[Double] {
self.moments.mean()
}
///|
pub fn MultivariateAnomalyDetector::reset(
self : MultivariateAnomalyDetector,
) -> Unit {
self.moments.reset()
self.anomalies = 0
}
///|
pub struct DriftMonitor {
feature_detectors : Array[PageHinkleyDetector]
target_detector : PageHinkleyDetector
mut feature_drift_events : Int
mut target_drift_events : Int
}
///|
pub fn DriftMonitor::new(
dimension : Int,
threshold? : Double = 50.0,
delta? : Double = 0.005,
) -> DriftMonitor {
{
feature_detectors: Array::makei(if dimension < 0 { 0 } else { dimension }, _ => {
PageHinkleyDetector::new(threshold~, delta~)
}),
target_detector: PageHinkleyDetector::new(threshold~, delta~),
feature_drift_events: 0,
target_drift_events: 0,
}
}
///|
pub fn DriftMonitor::update(
self : DriftMonitor,
features : Array[Double],
target? : Double,
) -> Bool {
let feature_drift = Ref(false)
let limit = if features.length() < self.feature_detectors.length() {
features.length()
} else {
self.feature_detectors.length()
}
for i in 0.. self.target_detector.update(value)
None => false
}
if target_drift {
self.target_drift_events += 1
}
feature_drift.val || target_drift
}
///|
pub fn DriftMonitor::feature_events(self : DriftMonitor) -> Int {
self.feature_drift_events
}
///|
pub fn DriftMonitor::target_events(self : DriftMonitor) -> Int {
self.target_drift_events
}
///|
pub fn DriftMonitor::reset(self : DriftMonitor) -> Unit {
for detector in self.feature_detectors {
detector.reset()
}
self.target_detector.reset()
self.feature_drift_events = 0
self.target_drift_events = 0
}