use super::{
Anomaly, AnomalyReport, AnomalyType, ChangePoint, DEFAULT_IQR_MULTIPLIER,
DEFAULT_ZSCORE_THRESHOLD, MIN_SAMPLES_FOR_DETECTION,
};
#[derive(Debug)]
pub struct AnomalyDetector {
data: Vec<f64>,
zscore_threshold: f64,
iqr_multiplier: f64,
window_size: usize,
anomalies: Vec<Anomaly>,
change_points: Vec<ChangePoint>,
}
impl Default for AnomalyDetector {
fn default() -> Self {
Self {
data: Vec::new(),
zscore_threshold: DEFAULT_ZSCORE_THRESHOLD,
iqr_multiplier: DEFAULT_IQR_MULTIPLIER,
window_size: 50,
anomalies: Vec::new(),
change_points: Vec::new(),
}
}
}
impl AnomalyDetector {
pub fn new() -> Self {
Self::default()
}
pub fn with_zscore_threshold(mut self, threshold: f64) -> Self {
self.zscore_threshold = threshold.max(1.0);
self
}
pub fn with_iqr_multiplier(mut self, multiplier: f64) -> Self {
self.iqr_multiplier = multiplier.max(0.5);
self
}
pub fn with_window_size(mut self, size: usize) -> Self {
self.window_size = size.max(10);
self
}
pub fn add(&mut self, value: f64) {
self.data.push(value);
}
pub fn add_all(&mut self, values: &[f64]) {
self.data.extend_from_slice(values);
}
pub fn data_count(&self) -> usize {
self.data.len()
}
pub fn has_sufficient_data(&self) -> bool {
self.data.len() >= MIN_SAMPLES_FOR_DETECTION
}
fn mean(data: &[f64]) -> f64 {
if data.is_empty() {
return 0.0;
}
data.iter().sum::<f64>() / data.len() as f64
}
fn std_dev(data: &[f64], mean: f64) -> f64 {
if data.len() < 2 {
return 0.0;
}
let variance =
data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (data.len() - 1) as f64;
variance.sqrt()
}
fn percentile(sorted_data: &[f64], p: f64) -> f64 {
if sorted_data.is_empty() {
return 0.0;
}
let idx = (p / 100.0 * (sorted_data.len() - 1) as f64).round() as usize;
sorted_data[idx.min(sorted_data.len() - 1)]
}
pub fn detect_zscore_outliers(&mut self) -> Vec<Anomaly> {
if !self.has_sufficient_data() {
return Vec::new();
}
let mean = Self::mean(&self.data);
let std_dev = Self::std_dev(&self.data, mean);
if std_dev < 1e-10 {
return Vec::new(); }
let mut outliers = Vec::new();
for (i, &value) in self.data.iter().enumerate() {
let z_score = (value - mean) / std_dev;
if z_score.abs() > self.zscore_threshold {
let anomaly_type = if z_score > 0.0 {
AnomalyType::Spike
} else {
AnomalyType::Drop
};
outliers.push(Anomaly::new(i, value, mean, z_score, anomaly_type));
}
}
self.anomalies.extend(outliers.clone());
outliers
}
pub fn detect_iqr_outliers(&mut self) -> Vec<Anomaly> {
if !self.has_sufficient_data() {
return Vec::new();
}
let mut sorted = self.data.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let q1 = Self::percentile(&sorted, 25.0);
let q3 = Self::percentile(&sorted, 75.0);
let iqr = q3 - q1;
if iqr < 1e-10 {
return Vec::new(); }
let lower_bound = q1 - self.iqr_multiplier * iqr;
let upper_bound = q3 + self.iqr_multiplier * iqr;
let median = Self::percentile(&sorted, 50.0);
let mut outliers = Vec::new();
for (i, &value) in self.data.iter().enumerate() {
if value < lower_bound || value > upper_bound {
let deviation = if value < lower_bound {
(lower_bound - value) / iqr
} else {
(value - upper_bound) / iqr
};
let anomaly_type = if value > median {
AnomalyType::Spike
} else {
AnomalyType::Drop
};
outliers.push(Anomaly::new(i, value, median, deviation, anomaly_type));
}
}
self.anomalies.extend(outliers.clone());
outliers
}
pub fn detect_change_points(&mut self) -> Vec<ChangePoint> {
if self.data.len() < 20 {
return Vec::new();
}
let overall_mean = Self::mean(&self.data);
let overall_std = Self::std_dev(&self.data, overall_mean);
if overall_std < 1e-10 {
return Vec::new();
}
let mut change_points = Vec::new();
let min_segment = 10;
for i in min_segment..(self.data.len() - min_segment) {
let before = &self.data[..i];
let after = &self.data[i..];
let mean_before = Self::mean(before);
let mean_after = Self::mean(after);
let change = ChangePoint::new(i, mean_before, mean_after);
if change.magnitude > overall_std {
change_points.push(change);
}
}
if change_points.len() > 1 {
change_points.sort_by(|a, b| {
b.magnitude
.partial_cmp(&a.magnitude)
.unwrap_or(std::cmp::Ordering::Equal)
});
change_points.truncate(3);
}
self.change_points.extend(change_points.clone());
change_points
}
pub fn classify_anomaly(&self, anomaly: &Anomaly) -> AnomalyType {
for cp in &self.change_points {
if (anomaly.index as i64 - cp.index as i64).abs() < 5 {
return AnomalyType::ChangePoint;
}
}
let nearby_count = self
.anomalies
.iter()
.filter(|a| {
(a.index as i64 - anomaly.index as i64).abs() < 3 && a.index != anomaly.index
})
.count();
if nearby_count >= 2 {
return AnomalyType::Correlated;
}
anomaly.anomaly_type
}
pub fn analyze(&mut self) -> AnomalyReport {
self.anomalies.clear();
self.change_points.clear();
let zscore_outliers = self.detect_zscore_outliers();
let _iqr_outliers = self.detect_iqr_outliers();
let change_points = self.detect_change_points();
let mut seen = std::collections::HashSet::new();
self.anomalies.retain(|a| seen.insert(a.index));
self.anomalies.sort_by(|a, b| b.severity.cmp(&a.severity));
let mean = Self::mean(&self.data);
let std_dev = Self::std_dev(&self.data, mean);
AnomalyReport {
total_points: self.data.len(),
anomalies: self.anomalies.clone(),
change_points,
mean,
std_dev,
method: if zscore_outliers.is_empty() {
"iqr"
} else {
"zscore"
},
}
}
pub fn detect_realtime(&mut self, new_value: f64) -> Option<Anomaly> {
self.data.push(new_value);
if self.data.len() < self.window_size {
return None;
}
let start = self.data.len().saturating_sub(self.window_size);
let window = &self.data[start..self.data.len() - 1];
let mean = Self::mean(window);
let std_dev = Self::std_dev(window, mean);
if std_dev < 1e-10 {
return None;
}
let z_score = (new_value - mean) / std_dev;
if z_score.abs() > self.zscore_threshold {
let anomaly_type = if z_score > 0.0 {
AnomalyType::Spike
} else {
AnomalyType::Drop
};
let anomaly = Anomaly::new(self.data.len() - 1, new_value, mean, z_score, anomaly_type);
self.anomalies.push(anomaly.clone());
return Some(anomaly);
}
None
}
pub fn get_anomalies(&self) -> &[Anomaly] {
&self.anomalies
}
pub fn get_change_points(&self) -> &[ChangePoint] {
&self.change_points
}
pub fn clear(&mut self) {
self.data.clear();
self.anomalies.clear();
self.change_points.clear();
}
}