use super::anomaly_detection::{
AnomalyDetectionResult, AnomalySeverity, AnomalyType, StatisticalAnomalyDetector,
};
use super::optimizer::StreamingDataPoint;
use scirs2_core::numeric::Float;
use std::collections::{HashMap, VecDeque};
fn from_f64<A: Float>(value: f64) -> Result<A, String> {
A::from(value).ok_or_else(|| format!("{value} cannot be represented in the element type"))
}
fn severity_and_confidence<A: Float>(
score: A,
threshold: A,
) -> Result<(AnomalySeverity, A), String> {
if threshold <= A::zero() {
return Ok((AnomalySeverity::Low, A::zero()));
}
let ratio = score / threshold;
let floor = from_f64::<A>(MIN_REPORTED_CONFIDENCE)?;
let confidence = (ratio - A::one()).abs().min(A::one()).max(floor);
let critical_ratio = from_f64::<A>(2.0)?;
let high_ratio = from_f64::<A>(1.5)?;
let severity = if ratio >= critical_ratio {
AnomalySeverity::Critical
} else if ratio >= high_ratio {
AnomalySeverity::High
} else if ratio >= A::one() {
AnomalySeverity::Medium
} else {
AnomalySeverity::Low
};
Ok((severity, confidence))
}
const MIN_REPORTED_CONFIDENCE: f64 = 0.05;
pub struct ZScoreDetector<A: Float + Send + Sync> {
threshold: A,
running_mean: A,
running_variance: A,
sample_count: usize,
}
impl<A: Float + Default + Clone + Send + Sync + Send + Sync> ZScoreDetector<A> {
pub fn new(threshold: f64) -> Result<Self, String> {
Ok(Self {
threshold: from_f64(threshold)?,
running_mean: A::zero(),
running_variance: A::zero(),
sample_count: 0,
})
}
}
impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> StatisticalAnomalyDetector<A>
for ZScoreDetector<A>
{
fn detect_anomaly(
&mut self,
data_point: &StreamingDataPoint<A>,
) -> Result<AnomalyDetectionResult<A>, String> {
if self.sample_count < 10 {
return Ok(AnomalyDetectionResult {
is_anomaly: false,
anomaly_score: A::zero(),
confidence: A::zero(),
anomaly_type: None,
severity: AnomalySeverity::Low,
metadata: HashMap::new(),
});
}
let feature_sum = data_point.features.iter().cloned().sum::<A>();
let z_score = if self.running_variance > A::zero() {
let count = A::from(self.sample_count).unwrap_or(A::one());
let variance = self.running_variance / count;
(feature_sum - self.running_mean) / variance.sqrt()
} else {
A::zero()
};
let is_anomaly = z_score.abs() > self.threshold;
let anomaly_score = z_score.abs();
let (severity, confidence) = severity_and_confidence(anomaly_score, self.threshold)?;
let mut metadata = HashMap::new();
metadata.insert("z_score".to_string(), z_score);
metadata.insert("running_mean".to_string(), self.running_mean);
metadata.insert(
"sample_count".to_string(),
from_f64(self.sample_count as f64)?,
);
Ok(AnomalyDetectionResult {
is_anomaly,
anomaly_score,
confidence,
anomaly_type: is_anomaly.then_some(AnomalyType::StatisticalOutlier),
severity,
metadata,
})
}
fn update(&mut self, data_point: &StreamingDataPoint<A>) -> Result<(), String> {
let feature_sum = data_point.features.iter().cloned().sum::<A>();
self.sample_count += 1;
let count = from_f64::<A>(self.sample_count as f64)?;
let delta = feature_sum - self.running_mean;
self.running_mean = self.running_mean + delta / count;
let delta2 = feature_sum - self.running_mean;
self.running_variance = self.running_variance + delta * delta2;
Ok(())
}
fn reset(&mut self) {
self.running_mean = A::zero();
self.running_variance = A::zero();
self.sample_count = 0;
}
fn name(&self) -> String {
"zscore".to_string()
}
fn get_threshold(&self) -> A {
self.threshold
}
fn set_threshold(&mut self, threshold: A) {
self.threshold = threshold;
}
}
pub struct IQRDetector<A: Float + Send + Sync> {
threshold: A,
recent_values: VecDeque<A>,
window_size: usize,
}
impl<A: Float + Default + Clone + Send + Sync + Send + Sync> IQRDetector<A> {
pub fn new(threshold: f64) -> Result<Self, String> {
Ok(Self {
threshold: from_f64(threshold)?,
recent_values: VecDeque::with_capacity(100),
window_size: 100,
})
}
}
impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> StatisticalAnomalyDetector<A>
for IQRDetector<A>
{
fn detect_anomaly(
&mut self,
data_point: &StreamingDataPoint<A>,
) -> Result<AnomalyDetectionResult<A>, String> {
if self.recent_values.len() < 20 {
return Ok(AnomalyDetectionResult {
is_anomaly: false,
anomaly_score: A::zero(),
confidence: A::zero(),
anomaly_type: None,
severity: AnomalySeverity::Low,
metadata: HashMap::new(),
});
}
let mut sorted_values: Vec<A> = self.recent_values.iter().cloned().collect();
super::statistics::sort_ascending(&mut sorted_values);
sorted_values.retain(|value| value.is_finite());
if sorted_values.len() < 4 {
return Err(
"IQR detector: fewer than four finite observations in the window".to_string(),
);
}
let finite_len = sorted_values.len();
let q1_idx = finite_len / 4;
let q3_idx = (3 * finite_len / 4).min(finite_len - 1);
let q1 = sorted_values[q1_idx];
let q3 = sorted_values[q3_idx];
let iqr = q3 - q1;
let lower_bound = q1 - self.threshold * iqr;
let upper_bound = q3 + self.threshold * iqr;
let feature_sum = data_point.features.iter().cloned().sum::<A>();
let is_anomaly = feature_sum < lower_bound || feature_sum > upper_bound;
let distance_from_bounds = if feature_sum < lower_bound {
lower_bound - feature_sum
} else if feature_sum > upper_bound {
feature_sum - upper_bound
} else {
A::zero()
};
let epsilon = from_f64::<A>(1e-8)?;
let anomaly_score = distance_from_bounds / iqr.max(epsilon);
let (severity, confidence) = severity_and_confidence(
anomaly_score + if is_anomaly { A::one() } else { A::zero() },
A::one(),
)?;
let mut metadata = HashMap::new();
metadata.insert("q1".to_string(), q1);
metadata.insert("q3".to_string(), q3);
metadata.insert("iqr".to_string(), iqr);
metadata.insert("lower_bound".to_string(), lower_bound);
metadata.insert("upper_bound".to_string(), upper_bound);
Ok(AnomalyDetectionResult {
is_anomaly,
anomaly_score,
confidence,
anomaly_type: is_anomaly.then_some(AnomalyType::StatisticalOutlier),
severity,
metadata,
})
}
fn update(&mut self, data_point: &StreamingDataPoint<A>) -> Result<(), String> {
let feature_sum = data_point.features.iter().cloned().sum::<A>();
if self.recent_values.len() >= self.window_size {
self.recent_values.pop_front();
}
self.recent_values.push_back(feature_sum);
Ok(())
}
fn reset(&mut self) {
self.recent_values.clear();
}
fn name(&self) -> String {
"iqr".to_string()
}
fn get_threshold(&self) -> A {
self.threshold
}
fn set_threshold(&mut self, threshold: A) {
self.threshold = threshold;
}
}