use crate::alert_storage::StoredAlert;
use chrono::{DateTime, Utc, Duration, Timelike, Datelike};
use serde::Serialize;
use ndarray::{Array1, ArrayView1};
#[derive(Debug, Serialize)]
pub struct TimeSeriesAnalysis {
pub trend: TrendAnalysis,
pub seasonality: SeasonalityAnalysis,
pub forecasts: Vec<AlertForecast>,
}
#[derive(Debug, Serialize)]
pub struct TrendAnalysis {
pub direction: TrendDirection,
pub slope: f64,
pub confidence: f64,
}
#[derive(Debug, Serialize)]
pub enum TrendDirection {
Increasing,
Decreasing,
Stable,
}
#[derive(Debug, Serialize)]
pub struct SeasonalityAnalysis {
pub has_daily_pattern: bool,
pub has_weekly_pattern: bool,
pub daily_peak_hours: Vec<u32>,
pub weekly_peak_days: Vec<u32>,
pub confidence: f64,
}
#[derive(Debug, Serialize)]
pub struct AlertForecast {
pub timestamp: DateTime<Utc>,
pub expected_value: f64,
pub confidence_interval: (f64, f64),
pub probability: f64,
}
pub struct TimeSeriesAnalyzer {
config: TimeSeriesConfig,
}
#[derive(Clone)]
pub struct TimeSeriesConfig {
pub min_data_points: usize,
pub forecast_horizon: Duration,
pub seasonality_threshold: f64,
pub trend_threshold: f64,
}
impl TimeSeriesAnalyzer {
pub fn new(config: TimeSeriesConfig) -> Self {
Self { config }
}
pub fn analyze(&self, alerts: &[StoredAlert]) -> Option<TimeSeriesAnalysis> {
if alerts.len() < self.config.min_data_points {
return None;
}
let trend = self.analyze_trend(alerts);
let seasonality = self.analyze_seasonality(alerts);
let forecasts = self.generate_forecasts(alerts, &trend, &seasonality);
Some(TimeSeriesAnalysis {
trend,
seasonality,
forecasts,
})
}
fn analyze_trend(&self, alerts: &[StoredAlert]) -> TrendAnalysis {
let timestamps: Vec<i64> = alerts.iter()
.map(|a| a.created_at.timestamp())
.collect();
let values: Vec<f64> = alerts.iter()
.map(|a| a.current_value)
.collect();
let x = Array1::from_vec(timestamps);
let y = Array1::from_vec(values);
let x_mean = x.mean().unwrap() as f64;
let y_mean = y.mean().unwrap();
let numerator: f64 = x.iter()
.zip(y.iter())
.map(|(&x_i, &y_i)| {
let x_f64 = x_i as f64;
(x_f64 - x_mean) * (y_i - y_mean)
})
.sum();
let denominator: f64 = x.iter()
.map(|&x_i| {
let x_f64 = x_i as f64;
(x_f64 - x_mean).powi(2)
})
.sum();
let slope = numerator / denominator;
let x_view = x.view();
let y_view = y.view();
let confidence = self.calculate_trend_confidence(&x_view, &y_view, slope, x_mean, y_mean);
TrendAnalysis {
direction: if slope.abs() < self.config.trend_threshold {
TrendDirection::Stable
} else if slope > 0.0 {
TrendDirection::Increasing
} else {
TrendDirection::Decreasing
},
slope,
confidence,
}
}
fn analyze_seasonality(&self, alerts: &[StoredAlert]) -> SeasonalityAnalysis {
let mut hourly_counts = vec![0; 24];
let mut daily_counts = vec![0; 7];
for alert in alerts {
let hour = alert.created_at.hour() as usize;
let day = alert.created_at.weekday().num_days_from_monday() as usize;
hourly_counts[hour] += 1;
daily_counts[day] += 1;
}
let daily_peaks = self.find_peaks(&hourly_counts, 3);
let weekly_peaks = self.find_peaks(&daily_counts, 2);
let hourly_variance = self.calculate_variance(&hourly_counts);
let daily_variance = self.calculate_variance(&daily_counts);
SeasonalityAnalysis {
has_daily_pattern: hourly_variance > self.config.seasonality_threshold,
has_weekly_pattern: daily_variance > self.config.seasonality_threshold,
daily_peak_hours: daily_peaks.into_iter().map(|i| i as u32).collect(),
weekly_peak_days: weekly_peaks.into_iter().map(|i| i as u32).collect(),
confidence: (hourly_variance + daily_variance) / 2.0,
}
}
fn generate_forecasts(
&self,
alerts: &[StoredAlert],
trend: &TrendAnalysis,
seasonality: &SeasonalityAnalysis,
) -> Vec<AlertForecast> {
let mut forecasts = Vec::new();
let last_time = alerts.last().unwrap().created_at;
for i in 1..=24 { let forecast_time = last_time + Duration::hours(i);
let base_value = self.calculate_base_forecast(alerts, &forecast_time);
let trend_effect = trend.slope * (i as f64);
let seasonal_effect = if seasonality.has_daily_pattern {
self.calculate_seasonal_effect(forecast_time.hour(), &seasonality.daily_peak_hours)
} else {
0.0
};
let expected_value = base_value + trend_effect + seasonal_effect;
let uncertainty = self.calculate_uncertainty(i as f64);
forecasts.push(AlertForecast {
timestamp: forecast_time,
expected_value,
confidence_interval: (
expected_value - uncertainty,
expected_value + uncertainty
),
probability: self.calculate_alert_probability(
expected_value,
uncertainty,
alerts
),
});
}
forecasts
}
fn calculate_base_forecast(&self, alerts: &[StoredAlert], forecast_time: &DateTime<Utc>) -> f64 {
let hour = forecast_time.hour();
let recent_alerts: Vec<_> = alerts.iter()
.filter(|a| a.created_at.hour() == hour)
.filter(|a| a.created_at + Duration::days(1) > *forecast_time)
.collect();
if recent_alerts.is_empty() {
alerts.last().unwrap().current_value
} else {
recent_alerts.iter()
.map(|a| a.current_value)
.sum::<f64>() / recent_alerts.len() as f64
}
}
fn calculate_seasonal_effect(&self, hour: u32, peak_hours: &[u32]) -> f64 {
if peak_hours.contains(&hour) {
10.0 } else {
0.0
}
}
fn calculate_uncertainty(&self, hours_ahead: f64) -> f64 {
5.0 + (hours_ahead / 24.0) * 10.0
}
fn calculate_alert_probability(&self, value: f64, uncertainty: f64, history: &[StoredAlert]) -> f64 {
let threshold = history.iter()
.map(|a| a.threshold)
.sum::<f64>() / history.len() as f64;
if value > threshold {
0.8 - (uncertainty / value)
} else {
0.2 * (value / threshold)
}
}
fn find_peaks(&self, values: &[i32], count: usize) -> Vec<usize> {
let mut peaks: Vec<(usize, i32)> = values.iter()
.enumerate()
.map(|(i, &v)| (i, v))
.collect();
peaks.sort_by_key(|&(_, v)| std::cmp::Reverse(v));
peaks.iter()
.take(count)
.map(|&(i, _)| i)
.collect()
}
fn calculate_variance(&self, values: &[i32]) -> f64 {
let mean = values.iter().sum::<i32>() as f64 / values.len() as f64;
let variance = values.iter()
.map(|&x| {
let diff = x as f64 - mean;
diff * diff
})
.sum::<f64>() / values.len() as f64;
variance.sqrt() / mean }
fn calculate_trend_confidence(
&self,
x: &ArrayView1<i64>,
y: &ArrayView1<f64>,
slope: f64,
x_mean: f64,
y_mean: f64,
) -> f64 {
let y_pred: Vec<f64> = x.iter()
.map(|&x_i| slope * ((x_i as f64) - x_mean) + y_mean)
.collect();
let ss_res: f64 = y.iter()
.zip(y_pred.iter())
.map(|(&y_i, &f_i)| (y_i - f_i).powi(2))
.sum();
let ss_tot: f64 = y.iter()
.map(|&y_i| (y_i - y_mean).powi(2))
.sum();
1.0 - (ss_res / ss_tot)
}
}