1pub const DEFAULT_ZSCORE_THRESHOLD: f64 = 3.0;
5
6pub const DEFAULT_IQR_MULTIPLIER: f64 = 1.5;
8
9pub const MIN_SAMPLES_FOR_DETECTION: usize = 10;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub enum AnomalySeverity {
15 Info,
17 Warning,
19 Critical,
21}
22
23impl AnomalySeverity {
24 pub fn name(&self) -> &'static str {
26 match self {
27 Self::Info => "info",
28 Self::Warning => "warning",
29 Self::Critical => "critical",
30 }
31 }
32
33 pub fn from_deviation(deviation: f64) -> Self {
35 if deviation >= 5.0 {
36 Self::Critical
37 } else if deviation >= 3.0 {
38 Self::Warning
39 } else {
40 Self::Info
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum AnomalyType {
48 Outlier,
50 Spike,
52 Drop,
54 ChangePoint,
56 Periodic,
58 Correlated,
60}
61
62impl AnomalyType {
63 pub fn name(&self) -> &'static str {
65 match self {
66 Self::Outlier => "outlier",
67 Self::Spike => "spike",
68 Self::Drop => "drop",
69 Self::ChangePoint => "change_point",
70 Self::Periodic => "periodic",
71 Self::Correlated => "correlated",
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct Anomaly {
79 pub index: usize,
81 pub value: f64,
83 pub expected: f64,
85 pub deviation: f64,
87 pub anomaly_type: AnomalyType,
89 pub severity: AnomalySeverity,
91 pub description: Option<String>,
93}
94
95impl Anomaly {
96 pub fn new(
98 index: usize,
99 value: f64,
100 expected: f64,
101 deviation: f64,
102 anomaly_type: AnomalyType,
103 ) -> Self {
104 Self {
105 index,
106 value,
107 expected,
108 deviation,
109 anomaly_type,
110 severity: AnomalySeverity::from_deviation(deviation.abs()),
111 description: None,
112 }
113 }
114
115 pub fn with_description(mut self, desc: &str) -> Self {
117 self.description = Some(desc.to_string());
118 self
119 }
120
121 pub fn is_critical(&self) -> bool {
123 self.severity == AnomalySeverity::Critical
124 }
125
126 pub fn to_json(&self) -> String {
128 format!(
129 r#"{{"index":{},"value":{},"expected":{},"deviation":{},"type":"{}","severity":"{}"}}"#,
130 self.index,
131 self.value,
132 self.expected,
133 self.deviation,
134 self.anomaly_type.name(),
135 self.severity.name()
136 )
137 }
138}
139
140#[derive(Debug, Clone)]
142pub struct ChangePoint {
143 pub index: usize,
145 pub mean_before: f64,
147 pub mean_after: f64,
149 pub magnitude: f64,
151 pub direction: f64,
153}
154
155impl ChangePoint {
156 pub fn new(index: usize, mean_before: f64, mean_after: f64) -> Self {
158 let magnitude = (mean_after - mean_before).abs();
159 let direction = mean_after - mean_before;
160 Self {
161 index,
162 mean_before,
163 mean_after,
164 magnitude,
165 direction,
166 }
167 }
168
169 pub fn is_significant(&self) -> bool {
171 if self.mean_before.abs() < 1e-10 {
172 return self.magnitude > 1e-10;
173 }
174 (self.magnitude / self.mean_before.abs()) > 0.1
175 }
176}
177
178#[derive(Debug, Clone)]
180pub struct AnomalyReport {
181 pub total_points: usize,
183 pub anomalies: Vec<Anomaly>,
185 pub change_points: Vec<ChangePoint>,
187 pub mean: f64,
189 pub std_dev: f64,
191 pub method: &'static str,
193}
194
195impl AnomalyReport {
196 pub fn count_by_severity(&self, severity: AnomalySeverity) -> usize {
198 self.anomalies
199 .iter()
200 .filter(|a| a.severity == severity)
201 .count()
202 }
203
204 pub fn critical_anomalies(&self) -> Vec<&Anomaly> {
206 self.anomalies.iter().filter(|a| a.is_critical()).collect()
207 }
208
209 pub fn has_critical(&self) -> bool {
211 self.anomalies.iter().any(|a| a.is_critical())
212 }
213
214 pub fn to_json(&self) -> String {
216 let anomalies_json: Vec<String> = self.anomalies.iter().map(|a| a.to_json()).collect();
217 format!(
218 r#"{{"total_points":{},"anomaly_count":{},"critical_count":{},"mean":{},"std_dev":{},"method":"{}","anomalies":[{}]}}"#,
219 self.total_points,
220 self.anomalies.len(),
221 self.count_by_severity(AnomalySeverity::Critical),
222 self.mean,
223 self.std_dev,
224 self.method,
225 anomalies_json.join(",")
226 )
227 }
228}