use crate::Observation;
use std::collections::VecDeque;
pub struct AnomalyDetector {
history: VecDeque<Vec<f64>>,
mean: Vec<f64>,
variance: Vec<f64>,
threshold: f64,
window_size: usize,
}
impl AnomalyDetector {
pub fn new(threshold: f64, window_size: usize) -> Self {
Self {
history: VecDeque::with_capacity(window_size),
mean: Vec::new(),
variance: Vec::new(),
threshold,
window_size,
}
}
pub fn update(&mut self, obs: &Observation) {
let features = self.extract_features(obs);
if self.history.len() >= self.window_size {
self.history.pop_front();
}
self.history.push_back(features.clone());
if self.history.len() >= 10 {
self.update_statistics();
}
}
pub fn is_anomaly(&self, obs: &Observation) -> bool {
if self.mean.is_empty() || self.history.len() < 10 {
return false;
}
let score = self.anomaly_score(obs);
score > self.threshold
}
pub fn anomaly_score(&self, obs: &Observation) -> f64 {
if self.mean.is_empty() {
return 0.0;
}
let features = self.extract_features(obs);
self.compute_zscore(&features)
}
fn compute_zscore(&self, features: &[f64]) -> f64 {
if self.mean.is_empty() || features.len() != self.mean.len() {
return 0.0;
}
let mut total_zscore = 0.0;
let mut count = 0;
for (i, &feature) in features.iter().enumerate().take(self.mean.len()) {
if self.variance[i] > 1e-10 {
let std_dev = self.variance[i].sqrt();
let zscore = ((feature - self.mean[i]) / std_dev).abs();
total_zscore += zscore;
count += 1;
}
}
if count > 0 {
total_zscore / count as f64
} else {
0.0
}
}
fn extract_features(&self, obs: &Observation) -> Vec<f64> {
let mut features = Vec::new();
if let Some(f) = obs.value.as_f64() {
features.push(f);
} else if let Some(i) = obs.value.as_i64() {
features.push(i as f64);
} else if let Some(b) = obs.value.as_bool() {
features.push(if b { 1.0 } else { 0.0 });
} else {
features.push(0.0);
}
features.push(obs.confidence.value() as f64);
features
}
fn update_statistics(&mut self) {
if self.history.is_empty() {
return;
}
let n = self.history.len();
let feature_dim = self.history[0].len();
self.mean = vec![0.0; feature_dim];
self.variance = vec![0.0; feature_dim];
for features in &self.history {
for (i, &value) in features.iter().enumerate() {
if i < feature_dim {
self.mean[i] += value;
}
}
}
for mean_val in &mut self.mean {
*mean_val /= n as f64;
}
for features in &self.history {
for (i, &value) in features.iter().enumerate() {
if i < feature_dim {
let diff = value - self.mean[i];
self.variance[i] += diff * diff;
}
}
}
for var_val in &mut self.variance {
*var_val /= n as f64;
*var_val = var_val.max(1e-10);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_anomaly_detector_creation() {
let detector = AnomalyDetector::new(2.0, 100);
assert_eq!(detector.threshold, 2.0);
assert_eq!(detector.window_size, 100);
}
#[test]
fn test_anomaly_detection_insufficient_data() {
let mut detector = AnomalyDetector::new(2.0, 100);
for i in 0..5 {
detector.update(&Observation::sensor("temp", 20.0 + i as f64));
}
let obs = Observation::sensor("temp", 100.0);
assert!(!detector.is_anomaly(&obs));
}
#[test]
fn test_anomaly_detection() {
let mut detector = AnomalyDetector::new(2.0, 100);
for i in 0..100 {
let value = 20.0 + (i % 5) as f64;
detector.update(&Observation::sensor("temp", value));
}
let normal = Observation::sensor("temp", 22.0);
assert!(!detector.is_anomaly(&normal));
let anomaly = Observation::sensor("temp", 100.0);
assert!(detector.is_anomaly(&anomaly));
}
#[test]
fn test_anomaly_score() {
let mut detector = AnomalyDetector::new(2.0, 100);
for i in 0..50 {
detector.update(&Observation::sensor("temp", 20.0 + (i % 3) as f64));
}
let normal = Observation::sensor("temp", 20.0);
let slightly_off = Observation::sensor("temp", 25.0);
let very_off = Observation::sensor("temp", 100.0);
let score_normal = detector.anomaly_score(&normal);
let score_slightly = detector.anomaly_score(&slightly_off);
let score_very = detector.anomaly_score(&very_off);
assert!(score_normal < score_slightly);
assert!(score_slightly < score_very);
}
#[test]
fn test_window_size_limit() {
let mut detector = AnomalyDetector::new(2.0, 5);
for i in 0..10 {
detector.update(&Observation::sensor("temp", i as f64));
}
assert_eq!(detector.history.len(), 5);
}
#[test]
fn test_statistics_update() {
let mut detector = AnomalyDetector::new(2.0, 100);
for _ in 0..20 {
detector.update(&Observation::sensor("temp", 10.0));
}
assert!(!detector.mean.is_empty());
assert!((detector.mean[0] - 10.0).abs() < 0.1);
assert!(detector.variance[0] < 0.1);
}
#[test]
fn test_different_value_types() {
let mut detector = AnomalyDetector::new(2.0, 100);
for i in 0..20 {
detector.update(&Observation::sensor("count", i));
}
detector.update(&Observation::sensor("flag", true));
detector.update(&Observation::sensor("flag", false));
assert!(detector.history.len() > 0);
}
#[test]
fn test_zscore_calculation() {
let mut detector = AnomalyDetector::new(2.0, 100);
for i in 0..100 {
let value = 50.0 + ((i % 20) as f64 - 10.0);
detector.update(&Observation::sensor("value", value));
}
let at_mean = Observation::sensor("value", 50.0);
let score_mean = detector.anomaly_score(&at_mean);
assert!(score_mean < 1.0);
let far_away = Observation::sensor("value", 80.0);
let score_far = detector.anomaly_score(&far_away);
assert!(score_far > 2.0);
}
}