///|
/// A slowly adapting baseline for streams whose normal level changes over time.
pub struct AdaptiveBaseline {
moments : OnlineMoments
mut value : Double
learning_rate : Double
mut initialized : Bool
}
///|
pub fn AdaptiveBaseline::new(
learning_rate? : Double = 0.05,
) -> AdaptiveBaseline {
{
moments: OnlineMoments::new(),
value: 0.0,
learning_rate: clamp_probability(learning_rate),
initialized: false,
}
}
///|
pub fn AdaptiveBaseline::update(
self : AdaptiveBaseline,
observation : Double,
) -> Double {
if !is_finite(observation) {
return self.value
}
if !self.initialized {
self.value = observation
self.initialized = true
} else {
self.value = self.value + self.learning_rate * (observation - self.value)
}
self.moments.push(observation)
self.value
}
///|
pub fn AdaptiveBaseline::value(self : AdaptiveBaseline) -> Double {
self.value
}
///|
pub fn AdaptiveBaseline::sample_count(self : AdaptiveBaseline) -> Int {
self.moments.count()
}
///|
pub fn AdaptiveBaseline::summary(self : AdaptiveBaseline) -> StatsSummary {
self.moments.summary()
}
///|
/// EWMA residual detector. It is useful for low-latency monitoring of a service metric.
pub struct EwmaDetector {
mut baseline : Double
mut variance : Double
alpha : Double
threshold : Double
warmup : Int
mut count : Int
mut index : Int
mut initialized : Bool
}
///|
pub fn EwmaDetector::new(
alpha? : Double = 0.2,
threshold? : Double = 3.0,
warmup? : Int = 8,
initial_mean? : Double = 0.0,
initial_variance? : Double = 1.0,
) -> EwmaDetector {
{
baseline: initial_mean,
variance: if initial_variance > 0.0 {
initial_variance
} else {
1.0
},
alpha: clamp_probability(alpha),
threshold: if threshold > 0.0 {
threshold
} else {
1.0
},
warmup: if warmup < 0 {
0
} else {
warmup
},
count: 0,
index: 0,
initialized: false,
}
}
///|
pub fn EwmaDetector::reset(self : EwmaDetector) -> Unit {
self.count = 0
self.index = 0
self.initialized = false
}
///|
pub fn EwmaDetector::baseline(self : EwmaDetector) -> Double {
self.baseline
}
///|
pub fn EwmaDetector::scale(self : EwmaDetector) -> Double {
self.variance.sqrt()
}
///|
fn direction_for_delta(
delta : Double,
variance_change? : Bool = false,
) -> ChangeDirection {
if variance_change {
if delta >= 0.0 {
VarianceIncrease
} else {
VarianceDecrease
}
} else if delta > 0.0 {
Increase
} else if delta < 0.0 {
Decrease
} else {
Unknown
}
}
///|
pub fn EwmaDetector::update(
self : EwmaDetector,
value : Double,
) -> DetectionResult {
self.index += 1
if !is_finite(value) {
return DetectionResult::quiet(index=self.index)
}
self.count += 1
if !self.initialized {
self.baseline = value
self.initialized = true
return DetectionResult::quiet(index=self.index)
}
let old_baseline = self.baseline
let residual = value - old_baseline
let alpha = self.alpha
self.baseline = old_baseline + alpha * residual
self.variance = (1.0 - alpha) * (self.variance + alpha * residual * residual)
let scale = if self.variance.sqrt() < 1.0e-12 {
1.0e-12
} else {
self.variance.sqrt()
}
let score = absolute(residual) / (self.threshold * scale)
let confidence = clamp_probability(
1.0 - @math.exp(-absolute(residual) / scale),
)
let changed = self.count > self.warmup && score >= 1.0
DetectionResult::new(
changed,
score,
confidence,
direction_for_delta(residual),
self.index,
evidence=residual,
)
}
///|
/// Detects variance changes by comparing short and long rolling windows.
pub struct VarianceShiftDetector {
short_window : DoubleWindow
long_window : DoubleWindow
threshold : Double
warmup : Int
mut index : Int
}
///|
pub fn VarianceShiftDetector::new(
short_window? : Int = 8,
long_window? : Int = 32,
threshold? : Double = 1.8,
) -> VarianceShiftDetector {
let safe_short = if short_window < 2 { 2 } else { short_window }
let safe_long = if long_window <= safe_short {
safe_short * 2
} else {
long_window
}
{
short_window: DoubleWindow::new(safe_short),
long_window: DoubleWindow::new(safe_long),
threshold: if threshold < 1.0 {
1.0
} else {
threshold
},
warmup: safe_long,
index: 0,
}
}
///|
pub fn VarianceShiftDetector::reset(self : VarianceShiftDetector) -> Unit {
self.short_window.clear()
self.long_window.clear()
self.index = 0
}
///|
pub fn VarianceShiftDetector::update(
self : VarianceShiftDetector,
value : Double,
) -> DetectionResult {
self.index += 1
if !is_finite(value) {
return DetectionResult::quiet(index=self.index)
}
ignore(self.short_window.push(value))
ignore(self.long_window.push(value))
if self.long_window.length() < self.warmup || self.short_window.length() < 2 {
return DetectionResult::quiet(index=self.index)
}
let short_variance = self.short_window.variance()
let long_variance = self.long_window.variance()
if long_variance <= 1.0e-12 {
return DetectionResult::quiet(index=self.index)
}
let ratio = short_variance / long_variance
let increase = ratio >= self.threshold
let decrease = ratio <= 1.0 / self.threshold
let distance = if increase {
ratio / self.threshold
} else if decrease {
1.0 / ratio / self.threshold
} else {
0.0
}
let score = if distance > 0.0 { distance } else { 0.0 }
DetectionResult::new(
increase || decrease,
score,
clamp_probability(score / 2.0),
if increase {
VarianceIncrease
} else {
VarianceDecrease
},
self.index,
evidence=ratio,
)
}
///|
/// A robust detector based on the rolling median and MAD.
pub struct RobustZDetector {
window : DoubleWindow
threshold : Double
warmup : Int
mut index : Int
}
///|
pub fn RobustZDetector::new(
window_size? : Int = 25,
threshold? : Double = 3.5,
) -> RobustZDetector {
let size = if window_size < 3 { 3 } else { window_size }
{
window: DoubleWindow::new(size),
threshold: if threshold <= 0.0 {
3.5
} else {
threshold
},
warmup: if size / 2 < 3 {
3
} else {
size / 2
},
index: 0,
}
}
///|
pub fn RobustZDetector::reset(self : RobustZDetector) -> Unit {
self.window.clear()
self.index = 0
}
///|
pub fn RobustZDetector::update(
self : RobustZDetector,
value : Double,
) -> DetectionResult {
self.index += 1
if !is_finite(value) {
return DetectionResult::quiet(index=self.index)
}
ignore(self.window.push(value))
if self.window.length() < self.warmup {
return DetectionResult::quiet(index=self.index)
}
let values = self.window.to_array()
let center = median(values)
let mad = median_absolute_deviation(values)
let scale = if mad < 1.0e-12 { 1.0e-12 } else { 1.4826 * mad }
let z = absolute(value - center) / scale
let score = z / self.threshold
DetectionResult::new(
z >= self.threshold,
score,
clamp_probability(1.0 - @math.exp(-z / 2.0)),
direction_for_delta(value - center),
self.index,
evidence=z,
)
}
///|
/// Detects a sustained slope rather than a one-point spike.
pub struct TrendShiftDetector {
window : DoubleWindow
slope_threshold : Double
persistence : Int
mut consecutive : Int
mut index : Int
}
///|
pub fn TrendShiftDetector::new(
window_size? : Int = 16,
slope_threshold? : Double = 0.1,
persistence? : Int = 3,
) -> TrendShiftDetector {
{
window: DoubleWindow::new(if window_size < 3 { 3 } else { window_size }),
slope_threshold: if slope_threshold <= 0.0 {
0.1
} else {
slope_threshold
},
persistence: if persistence < 1 {
1
} else {
persistence
},
consecutive: 0,
index: 0,
}
}
///|
pub fn TrendShiftDetector::reset(self : TrendShiftDetector) -> Unit {
self.window.clear()
self.consecutive = 0
self.index = 0
}
///|
pub fn TrendShiftDetector::update(
self : TrendShiftDetector,
value : Double,
) -> DetectionResult {
self.index += 1
if !is_finite(value) {
return DetectionResult::quiet(index=self.index)
}
ignore(self.window.push(value))
if self.window.length() < self.window.capacity() {
return DetectionResult::quiet(index=self.index)
}
let slope = self.window.slope()
if absolute(slope) >= self.slope_threshold {
self.consecutive += 1
} else {
self.consecutive = 0
}
let score = absolute(slope) / self.slope_threshold
DetectionResult::new(
self.consecutive >= self.persistence,
score,
clamp_probability(score / 2.0),
direction_for_delta(slope),
self.index,
evidence=slope,
)
}
///|
/// A lightweight detector for isolated spikes using a rolling interquartile fence.
pub struct IqrSpikeDetector {
window : DoubleWindow
multiplier : Double
warmup : Int
mut index : Int
}
///|
pub fn IqrSpikeDetector::new(
window_size? : Int = 25,
multiplier? : Double = 1.5,
) -> IqrSpikeDetector {
let size = if window_size < 4 { 4 } else { window_size }
{
window: DoubleWindow::new(size),
multiplier: if multiplier <= 0.0 {
1.5
} else {
multiplier
},
warmup: size / 2,
index: 0,
}
}
///|
pub fn IqrSpikeDetector::update(
self : IqrSpikeDetector,
value : Double,
) -> DetectionResult {
self.index += 1
if !is_finite(value) {
return DetectionResult::quiet(index=self.index)
}
ignore(self.window.push(value))
if self.window.length() < self.warmup {
return DetectionResult::quiet(index=self.index)
}
let values = self.window.to_array()
let q1 = quantile(values, 0.25)
let q3 = quantile(values, 0.75)
let spread = q3 - q1
let distance = if value < q1 {
q1 - value
} else if value > q3 {
value - q3
} else {
0.0
}
let fence_distance = self.multiplier * spread
let score = if fence_distance <= 1.0e-12 {
if distance > 0.0 {
1.0
} else {
0.0
}
} else {
distance / fence_distance
}
DetectionResult::new(
score >= 1.0,
score,
clamp_probability(score / 2.0),
direction_for_delta(value - median(values)),
self.index,
evidence=distance,
)
}