1use crate::alert_storage::StoredAlert;
2use chrono::{DateTime, Utc, Duration, Timelike, Datelike};
3use serde::Serialize;
4use ndarray::{Array1, ArrayView1};
5
6#[derive(Debug, Serialize)]
7pub struct TimeSeriesAnalysis {
8 pub trend: TrendAnalysis,
9 pub seasonality: SeasonalityAnalysis,
10 pub forecasts: Vec<AlertForecast>,
11}
12
13#[derive(Debug, Serialize)]
14pub struct TrendAnalysis {
15 pub direction: TrendDirection,
16 pub slope: f64,
17 pub confidence: f64,
18}
19
20#[derive(Debug, Serialize)]
21pub enum TrendDirection {
22 Increasing,
23 Decreasing,
24 Stable,
25}
26
27#[derive(Debug, Serialize)]
28pub struct SeasonalityAnalysis {
29 pub has_daily_pattern: bool,
30 pub has_weekly_pattern: bool,
31 pub daily_peak_hours: Vec<u32>,
32 pub weekly_peak_days: Vec<u32>,
33 pub confidence: f64,
34}
35
36#[derive(Debug, Serialize)]
37pub struct AlertForecast {
38 pub timestamp: DateTime<Utc>,
39 pub expected_value: f64,
40 pub confidence_interval: (f64, f64),
41 pub probability: f64,
42}
43
44pub struct TimeSeriesAnalyzer {
45 config: TimeSeriesConfig,
46}
47
48#[derive(Clone)]
49pub struct TimeSeriesConfig {
50 pub min_data_points: usize,
51 pub forecast_horizon: Duration,
52 pub seasonality_threshold: f64,
53 pub trend_threshold: f64,
54}
55
56impl TimeSeriesAnalyzer {
57 pub fn new(config: TimeSeriesConfig) -> Self {
58 Self { config }
59 }
60
61 pub fn analyze(&self, alerts: &[StoredAlert]) -> Option<TimeSeriesAnalysis> {
62 if alerts.len() < self.config.min_data_points {
63 return None;
64 }
65
66 let trend = self.analyze_trend(alerts);
67 let seasonality = self.analyze_seasonality(alerts);
68 let forecasts = self.generate_forecasts(alerts, &trend, &seasonality);
69
70 Some(TimeSeriesAnalysis {
71 trend,
72 seasonality,
73 forecasts,
74 })
75 }
76
77 fn analyze_trend(&self, alerts: &[StoredAlert]) -> TrendAnalysis {
78 let timestamps: Vec<i64> = alerts.iter()
79 .map(|a| a.created_at.timestamp())
80 .collect();
81 let values: Vec<f64> = alerts.iter()
82 .map(|a| a.current_value)
83 .collect();
84
85 let x = Array1::from_vec(timestamps);
86 let y = Array1::from_vec(values);
87
88 let x_mean = x.mean().unwrap() as f64;
89 let y_mean = y.mean().unwrap();
90
91 let numerator: f64 = x.iter()
92 .zip(y.iter())
93 .map(|(&x_i, &y_i)| {
94 let x_f64 = x_i as f64;
95 (x_f64 - x_mean) * (y_i - y_mean)
96 })
97 .sum();
98
99 let denominator: f64 = x.iter()
100 .map(|&x_i| {
101 let x_f64 = x_i as f64;
102 (x_f64 - x_mean).powi(2)
103 })
104 .sum();
105
106 let slope = numerator / denominator;
107 let x_view = x.view();
108 let y_view = y.view();
109 let confidence = self.calculate_trend_confidence(&x_view, &y_view, slope, x_mean, y_mean);
110
111 TrendAnalysis {
112 direction: if slope.abs() < self.config.trend_threshold {
113 TrendDirection::Stable
114 } else if slope > 0.0 {
115 TrendDirection::Increasing
116 } else {
117 TrendDirection::Decreasing
118 },
119 slope,
120 confidence,
121 }
122 }
123
124 fn analyze_seasonality(&self, alerts: &[StoredAlert]) -> SeasonalityAnalysis {
125 let mut hourly_counts = vec![0; 24];
126 let mut daily_counts = vec![0; 7];
127
128 for alert in alerts {
129 let hour = alert.created_at.hour() as usize;
130 let day = alert.created_at.weekday().num_days_from_monday() as usize;
131
132 hourly_counts[hour] += 1;
133 daily_counts[day] += 1;
134 }
135
136 let daily_peaks = self.find_peaks(&hourly_counts, 3);
138 let weekly_peaks = self.find_peaks(&daily_counts, 2);
139
140 let hourly_variance = self.calculate_variance(&hourly_counts);
142 let daily_variance = self.calculate_variance(&daily_counts);
143
144 SeasonalityAnalysis {
145 has_daily_pattern: hourly_variance > self.config.seasonality_threshold,
146 has_weekly_pattern: daily_variance > self.config.seasonality_threshold,
147 daily_peak_hours: daily_peaks.into_iter().map(|i| i as u32).collect(),
148 weekly_peak_days: weekly_peaks.into_iter().map(|i| i as u32).collect(),
149 confidence: (hourly_variance + daily_variance) / 2.0,
150 }
151 }
152
153 fn generate_forecasts(
154 &self,
155 alerts: &[StoredAlert],
156 trend: &TrendAnalysis,
157 seasonality: &SeasonalityAnalysis,
158 ) -> Vec<AlertForecast> {
159 let mut forecasts = Vec::new();
160 let last_time = alerts.last().unwrap().created_at;
161
162 for i in 1..=24 { let forecast_time = last_time + Duration::hours(i);
165 let base_value = self.calculate_base_forecast(alerts, &forecast_time);
166
167 let trend_effect = trend.slope * (i as f64);
169
170 let seasonal_effect = if seasonality.has_daily_pattern {
172 self.calculate_seasonal_effect(forecast_time.hour(), &seasonality.daily_peak_hours)
173 } else {
174 0.0
175 };
176
177 let expected_value = base_value + trend_effect + seasonal_effect;
178 let uncertainty = self.calculate_uncertainty(i as f64);
179
180 forecasts.push(AlertForecast {
181 timestamp: forecast_time,
182 expected_value,
183 confidence_interval: (
184 expected_value - uncertainty,
185 expected_value + uncertainty
186 ),
187 probability: self.calculate_alert_probability(
188 expected_value,
189 uncertainty,
190 alerts
191 ),
192 });
193 }
194
195 forecasts
196 }
197
198 fn calculate_base_forecast(&self, alerts: &[StoredAlert], forecast_time: &DateTime<Utc>) -> f64 {
199 let hour = forecast_time.hour();
201 let recent_alerts: Vec<_> = alerts.iter()
202 .filter(|a| a.created_at.hour() == hour)
203 .filter(|a| a.created_at + Duration::days(1) > *forecast_time)
204 .collect();
205
206 if recent_alerts.is_empty() {
207 alerts.last().unwrap().current_value
208 } else {
209 recent_alerts.iter()
210 .map(|a| a.current_value)
211 .sum::<f64>() / recent_alerts.len() as f64
212 }
213 }
214
215 fn calculate_seasonal_effect(&self, hour: u32, peak_hours: &[u32]) -> f64 {
216 if peak_hours.contains(&hour) {
217 10.0 } else {
219 0.0
220 }
221 }
222
223 fn calculate_uncertainty(&self, hours_ahead: f64) -> f64 {
224 5.0 + (hours_ahead / 24.0) * 10.0
226 }
227
228 fn calculate_alert_probability(&self, value: f64, uncertainty: f64, history: &[StoredAlert]) -> f64 {
229 let threshold = history.iter()
231 .map(|a| a.threshold)
232 .sum::<f64>() / history.len() as f64;
233
234 if value > threshold {
235 0.8 - (uncertainty / value)
236 } else {
237 0.2 * (value / threshold)
238 }
239 }
240
241 fn find_peaks(&self, values: &[i32], count: usize) -> Vec<usize> {
242 let mut peaks: Vec<(usize, i32)> = values.iter()
243 .enumerate()
244 .map(|(i, &v)| (i, v))
245 .collect();
246
247 peaks.sort_by_key(|&(_, v)| std::cmp::Reverse(v));
248 peaks.iter()
249 .take(count)
250 .map(|&(i, _)| i)
251 .collect()
252 }
253
254 fn calculate_variance(&self, values: &[i32]) -> f64 {
255 let mean = values.iter().sum::<i32>() as f64 / values.len() as f64;
256 let variance = values.iter()
257 .map(|&x| {
258 let diff = x as f64 - mean;
259 diff * diff
260 })
261 .sum::<f64>() / values.len() as f64;
262
263 variance.sqrt() / mean }
265
266 fn calculate_trend_confidence(
267 &self,
268 x: &ArrayView1<i64>,
269 y: &ArrayView1<f64>,
270 slope: f64,
271 x_mean: f64,
272 y_mean: f64,
273 ) -> f64 {
274 let y_pred: Vec<f64> = x.iter()
275 .map(|&x_i| slope * ((x_i as f64) - x_mean) + y_mean)
276 .collect();
277
278 let ss_res: f64 = y.iter()
279 .zip(y_pred.iter())
280 .map(|(&y_i, &f_i)| (y_i - f_i).powi(2))
281 .sum();
282
283 let ss_tot: f64 = y.iter()
284 .map(|&y_i| (y_i - y_mean).powi(2))
285 .sum();
286
287 1.0 - (ss_res / ss_tot)
288 }
289}