pub const DEFAULT_ZSCORE_THRESHOLD: f64 = 3.0;
pub const DEFAULT_IQR_MULTIPLIER: f64 = 1.5;
pub const MIN_SAMPLES_FOR_DETECTION: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AnomalySeverity {
Info,
Warning,
Critical,
}
impl AnomalySeverity {
pub fn name(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Critical => "critical",
}
}
pub fn from_deviation(deviation: f64) -> Self {
if deviation >= 5.0 {
Self::Critical
} else if deviation >= 3.0 {
Self::Warning
} else {
Self::Info
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnomalyType {
Outlier,
Spike,
Drop,
ChangePoint,
Periodic,
Correlated,
}
impl AnomalyType {
pub fn name(&self) -> &'static str {
match self {
Self::Outlier => "outlier",
Self::Spike => "spike",
Self::Drop => "drop",
Self::ChangePoint => "change_point",
Self::Periodic => "periodic",
Self::Correlated => "correlated",
}
}
}
#[derive(Debug, Clone)]
pub struct Anomaly {
pub index: usize,
pub value: f64,
pub expected: f64,
pub deviation: f64,
pub anomaly_type: AnomalyType,
pub severity: AnomalySeverity,
pub description: Option<String>,
}
impl Anomaly {
pub fn new(
index: usize,
value: f64,
expected: f64,
deviation: f64,
anomaly_type: AnomalyType,
) -> Self {
Self {
index,
value,
expected,
deviation,
anomaly_type,
severity: AnomalySeverity::from_deviation(deviation.abs()),
description: None,
}
}
pub fn with_description(mut self, desc: &str) -> Self {
self.description = Some(desc.to_string());
self
}
pub fn is_critical(&self) -> bool {
self.severity == AnomalySeverity::Critical
}
pub fn to_json(&self) -> String {
format!(
r#"{{"index":{},"value":{},"expected":{},"deviation":{},"type":"{}","severity":"{}"}}"#,
self.index,
self.value,
self.expected,
self.deviation,
self.anomaly_type.name(),
self.severity.name()
)
}
}
#[derive(Debug, Clone)]
pub struct ChangePoint {
pub index: usize,
pub mean_before: f64,
pub mean_after: f64,
pub magnitude: f64,
pub direction: f64,
}
impl ChangePoint {
pub fn new(index: usize, mean_before: f64, mean_after: f64) -> Self {
let magnitude = (mean_after - mean_before).abs();
let direction = mean_after - mean_before;
Self {
index,
mean_before,
mean_after,
magnitude,
direction,
}
}
pub fn is_significant(&self) -> bool {
if self.mean_before.abs() < 1e-10 {
return self.magnitude > 1e-10;
}
(self.magnitude / self.mean_before.abs()) > 0.1
}
}
#[derive(Debug, Clone)]
pub struct AnomalyReport {
pub total_points: usize,
pub anomalies: Vec<Anomaly>,
pub change_points: Vec<ChangePoint>,
pub mean: f64,
pub std_dev: f64,
pub method: &'static str,
}
impl AnomalyReport {
pub fn count_by_severity(&self, severity: AnomalySeverity) -> usize {
self.anomalies
.iter()
.filter(|a| a.severity == severity)
.count()
}
pub fn critical_anomalies(&self) -> Vec<&Anomaly> {
self.anomalies.iter().filter(|a| a.is_critical()).collect()
}
pub fn has_critical(&self) -> bool {
self.anomalies.iter().any(|a| a.is_critical())
}
pub fn to_json(&self) -> String {
let anomalies_json: Vec<String> = self.anomalies.iter().map(|a| a.to_json()).collect();
format!(
r#"{{"total_points":{},"anomaly_count":{},"critical_count":{},"mean":{},"std_dev":{},"method":"{}","anomalies":[{}]}}"#,
self.total_points,
self.anomalies.len(),
self.count_by_severity(AnomalySeverity::Critical),
self.mean,
self.std_dev,
self.method,
anomalies_json.join(",")
)
}
}