use serde::Serialize;
#[derive(Debug, Clone)]
pub enum PredictionMethod {
Linear,
ExponentialSmoothing { alpha: f64 },
MovingAverage { window: usize },
}
#[derive(Debug, Clone, Serialize)]
pub struct Prediction {
pub timestamp: u64,
pub value: f64,
pub confidence_lower: f64,
pub confidence_upper: f64,
}
pub fn predict(
history: &[(u64, f64)],
method: &PredictionMethod,
horizon: usize,
interval_micros: u64,
) -> Vec<Prediction> {
if history.is_empty() || horizon == 0 {
return vec![];
}
match method {
PredictionMethod::Linear => predict_linear(history, horizon, interval_micros),
PredictionMethod::ExponentialSmoothing { alpha } => {
predict_exp_smoothing(history, *alpha, horizon, interval_micros)
}
PredictionMethod::MovingAverage { window } => {
predict_moving_avg(history, *window, horizon, interval_micros)
}
}
}
fn predict_linear(history: &[(u64, f64)], horizon: usize, interval: u64) -> Vec<Prediction> {
let n = history.len() as f64;
if n < 2.0 {
return vec![];
}
let x_vals: Vec<f64> = (0..history.len()).map(|i| i as f64).collect();
let y_vals: Vec<f64> = history.iter().map(|(_, v)| *v).collect();
let x_mean = x_vals.iter().sum::<f64>() / n;
let y_mean = y_vals.iter().sum::<f64>() / n;
let numerator: f64 = x_vals
.iter()
.zip(y_vals.iter())
.map(|(x, y)| (x - x_mean) * (y - y_mean))
.sum();
let denominator: f64 = x_vals.iter().map(|x| (x - x_mean).powi(2)).sum();
let slope = if denominator > 0.0 {
numerator / denominator
} else {
0.0
};
let intercept = y_mean - slope * x_mean;
let residuals: f64 = x_vals
.iter()
.zip(y_vals.iter())
.map(|(x, y)| (y - (intercept + slope * x)).powi(2))
.sum();
let std_err = (residuals / (n - 2.0).max(1.0)).sqrt();
let last_ts = history.last().unwrap().0;
(1..=horizon)
.map(|h| {
let x = n + h as f64 - 1.0;
let predicted = intercept + slope * x;
let ci = 1.96 * std_err * (1.0 + 1.0 / n + (x - x_mean).powi(2) / denominator).sqrt();
Prediction {
timestamp: last_ts + h as u64 * interval,
value: predicted,
confidence_lower: predicted - ci,
confidence_upper: predicted + ci,
}
})
.collect()
}
fn predict_exp_smoothing(
history: &[(u64, f64)],
alpha: f64,
horizon: usize,
interval: u64,
) -> Vec<Prediction> {
let alpha = alpha.clamp(0.01, 0.99);
let mut level = history[0].1;
for (_, v) in history.iter().skip(1) {
level = alpha * v + (1.0 - alpha) * level;
}
let last_ts = history.last().unwrap().0;
let vals: Vec<f64> = history.iter().map(|(_, v)| *v).collect();
let std_dev = std_deviation(&vals);
(1..=horizon)
.map(|h| {
let ci = 1.96 * std_dev * (h as f64).sqrt() * alpha;
Prediction {
timestamp: last_ts + h as u64 * interval,
value: level,
confidence_lower: level - ci,
confidence_upper: level + ci,
}
})
.collect()
}
fn predict_moving_avg(
history: &[(u64, f64)],
window: usize,
horizon: usize,
interval: u64,
) -> Vec<Prediction> {
let vals: Vec<f64> = history.iter().map(|(_, v)| *v).collect();
let w = window.min(vals.len());
let tail = &vals[vals.len() - w..];
let avg = tail.iter().sum::<f64>() / w as f64;
let std_dev = std_deviation(tail);
let last_ts = history.last().unwrap().0;
(1..=horizon)
.map(|h| {
let ci = 1.96 * std_dev / (w as f64).sqrt();
Prediction {
timestamp: last_ts + h as u64 * interval,
value: avg,
confidence_lower: avg - ci,
confidence_upper: avg + ci,
}
})
.collect()
}
fn std_deviation(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let mean = values.iter().sum::<f64>() / values.len() as f64;
let var = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
var.sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_linear_prediction_trend() {
let history: Vec<(u64, f64)> = (0..5).map(|i| (i as u64 * 1000, i as f64 * 2.0)).collect();
let preds = predict(&history, &PredictionMethod::Linear, 3, 1000);
assert_eq!(preds.len(), 3);
assert!((preds[0].value - 10.0).abs() < 1.0);
assert!((preds[1].value - 12.0).abs() < 1.0);
}
#[test]
fn test_exp_smoothing_converges() {
let history: Vec<(u64, f64)> = (0..20).map(|i| (i as u64 * 1000, 10.0)).collect();
let preds = predict(
&history,
&PredictionMethod::ExponentialSmoothing { alpha: 0.3 },
5,
1000,
);
assert_eq!(preds.len(), 5);
for p in &preds {
assert!((p.value - 10.0).abs() < 0.01);
}
}
#[test]
fn test_moving_avg_prediction() {
let history: Vec<(u64, f64)> = vec![
(0, 10.0),
(1000, 12.0),
(2000, 11.0),
(3000, 13.0),
(4000, 12.0),
];
let preds = predict(
&history,
&PredictionMethod::MovingAverage { window: 3 },
2,
1000,
);
assert_eq!(preds.len(), 2);
assert!((preds[0].value - 12.0).abs() < 0.01);
}
#[test]
fn test_confidence_intervals_widen() {
let history: Vec<(u64, f64)> = (0..10)
.map(|i| (i as u64 * 1000, 10.0 + (i as f64 * 0.5)))
.collect();
let preds = predict(&history, &PredictionMethod::Linear, 5, 1000);
let width_1 = preds[0].confidence_upper - preds[0].confidence_lower;
let width_5 = preds[4].confidence_upper - preds[4].confidence_lower;
assert!(width_5 >= width_1);
}
#[test]
fn test_empty_history() {
assert!(predict(&[], &PredictionMethod::Linear, 5, 1000).is_empty());
}
}