use crate::TimeSeries;
pub fn mae(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.is_empty() {
return 0.0;
}
let n = actual_data.len() as f64;
let sum_abs_errors: f64 = actual_data
.iter()
.zip(predicted_data.iter())
.map(|(&a, &p)| ((a - p) as f64).abs())
.sum();
sum_abs_errors / n
}
pub fn mse(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.is_empty() {
return 0.0;
}
let n = actual_data.len() as f64;
let sum_sq_errors: f64 = actual_data
.iter()
.zip(predicted_data.iter())
.map(|(&a, &p)| {
let err = (a - p) as f64;
err * err
})
.sum();
sum_sq_errors / n
}
pub fn rmse(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
mse(actual, predicted).sqrt()
}
pub fn mape(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.is_empty() {
return 0.0;
}
if actual_data.iter().any(|&a| a.abs() < 1e-10) {
return 0.0;
}
let n = actual_data.len() as f64;
let sum_pct_errors: f64 = actual_data
.iter()
.zip(predicted_data.iter())
.map(|(&a, &p)| {
let a_f64 = a as f64;
let p_f64 = p as f64;
((a_f64 - p_f64).abs() / a_f64.abs()) * 100.0
})
.sum();
sum_pct_errors / n
}
pub fn smape(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.is_empty() {
return 0.0;
}
let n = actual_data.len() as f64;
let sum_smape: f64 = actual_data
.iter()
.zip(predicted_data.iter())
.map(|(&a, &p)| {
let a_f64 = a as f64;
let p_f64 = p as f64;
let numerator = (a_f64 - p_f64).abs();
let denominator = a_f64.abs() + p_f64.abs();
if denominator < 1e-10 {
0.0 } else {
(2.0 * numerator / denominator) * 100.0
}
})
.sum();
sum_smape / n
}
pub fn r2(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.is_empty() {
return 0.0;
}
let n = actual_data.len() as f64;
let mean_actual: f64 = actual_data.iter().map(|&a| a as f64).sum::<f64>() / n;
let ss_res: f64 = actual_data
.iter()
.zip(predicted_data.iter())
.map(|(&a, &p)| {
let err = (a as f64) - (p as f64);
err * err
})
.sum();
let ss_tot: f64 = actual_data
.iter()
.map(|&a| {
let diff = (a as f64) - mean_actual;
diff * diff
})
.sum();
if ss_tot < 1e-10 {
return 0.0;
}
1.0 - (ss_res / ss_tot)
}
pub fn mase(actual: &TimeSeries, predicted: &TimeSeries, seasonal_period: usize) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.len() <= seasonal_period {
return 0.0;
}
let n = actual_data.len();
let forecast_mae = mae(actual, predicted);
let naive_errors_sum: f64 = (seasonal_period..n)
.map(|i| {
let actual_val = actual_data[i] as f64;
let naive_pred = actual_data[i - seasonal_period] as f64;
(actual_val - naive_pred).abs()
})
.sum();
let naive_mae = naive_errors_sum / ((n - seasonal_period) as f64);
if naive_mae < 1e-10 {
return 0.0; }
forecast_mae / naive_mae
}
pub fn theil_u(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.is_empty() {
return 0.0;
}
let n = actual_data.len() as f64;
let mse_val = mse(actual, predicted);
let rmse_val = mse_val.sqrt();
let rms_actual = (actual_data
.iter()
.map(|&a| {
let val = a as f64;
val * val
})
.sum::<f64>()
/ n)
.sqrt();
let rms_predicted = (predicted_data
.iter()
.map(|&p| {
let val = p as f64;
val * val
})
.sum::<f64>()
/ n)
.sqrt();
let denominator = rms_actual + rms_predicted;
if denominator < 1e-10 {
return 0.0; }
rmse_val / denominator
}
pub fn directional_accuracy(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.len() < 2 {
return 0.0;
}
let n = actual_data.len();
let mut correct_directions = 0;
let mut total_comparisons = 0;
for i in 1..n {
let actual_change = (actual_data[i] - actual_data[i - 1]) as f64;
let predicted_change = (predicted_data[i] - predicted_data[i - 1]) as f64;
if actual_change.abs() > 1e-10 || predicted_change.abs() > 1e-10 {
total_comparisons += 1;
if actual_change * predicted_change > 0.0 {
correct_directions += 1;
} else if actual_change.abs() < 1e-10 && predicted_change.abs() < 1e-10 {
correct_directions += 1;
}
}
}
if total_comparisons == 0 {
return 0.0;
}
(correct_directions as f64 / total_comparisons as f64) * 100.0
}
pub fn max_error(actual: &TimeSeries, predicted: &TimeSeries) -> f64 {
let actual_data = actual.values.to_vec().unwrap_or_default();
let predicted_data = predicted.values.to_vec().unwrap_or_default();
if actual_data.len() != predicted_data.len() || actual_data.is_empty() {
return 0.0;
}
actual_data
.iter()
.zip(predicted_data.iter())
.map(|(&a, &p)| ((a - p) as f64).abs())
.fold(0.0f64, f64::max)
}
#[derive(Debug, Clone)]
pub struct ForecastMetrics {
pub mae: f64,
pub mse: f64,
pub rmse: f64,
pub mape: f64,
pub smape: f64,
pub r2: f64,
pub mase: f64,
pub theil_u: f64,
pub directional_accuracy: f64,
pub max_error: f64,
}
pub fn evaluate_forecast(
actual: &TimeSeries,
predicted: &TimeSeries,
seasonal_period: Option<usize>,
) -> ForecastMetrics {
let seasonal_period = seasonal_period.unwrap_or(1);
ForecastMetrics {
mae: mae(actual, predicted),
mse: mse(actual, predicted),
rmse: rmse(actual, predicted),
mape: mape(actual, predicted),
smape: smape(actual, predicted),
r2: r2(actual, predicted),
mase: mase(actual, predicted, seasonal_period),
theil_u: theil_u(actual, predicted),
directional_accuracy: directional_accuracy(actual, predicted),
max_error: max_error(actual, predicted),
}
}
#[cfg(test)]
mod tests {
use super::*;
use torsh_tensor::Tensor;
fn create_test_series(data: Vec<f32>) -> TimeSeries {
let len = data.len();
let tensor = Tensor::from_vec(data, &[len]).expect("Tensor should succeed");
TimeSeries::new(tensor)
}
#[test]
fn test_mae() {
let actual = create_test_series(vec![1.0, 2.0, 3.0]);
let predicted = create_test_series(vec![1.1, 2.1, 2.9]);
let error = mae(&actual, &predicted);
assert!((error - 0.1).abs() < 1e-6);
}
#[test]
fn test_mse() {
let actual = create_test_series(vec![1.0, 2.0, 3.0]);
let predicted = create_test_series(vec![1.1, 2.1, 2.9]);
let error = mse(&actual, &predicted);
assert!((error - 0.01).abs() < 1e-6);
}
#[test]
fn test_rmse() {
let actual = create_test_series(vec![1.0, 2.0, 3.0]);
let predicted = create_test_series(vec![1.1, 2.1, 2.9]);
let error = rmse(&actual, &predicted);
assert!((error - 0.1).abs() < 1e-6);
}
#[test]
fn test_r2() {
let actual = create_test_series(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let predicted = create_test_series(vec![1.1, 2.0, 2.9, 4.1, 4.9]);
let r2_val = r2(&actual, &predicted);
assert!(r2_val > 0.95);
}
#[test]
fn test_max_error() {
let actual = create_test_series(vec![1.0, 2.0, 3.0, 4.0]);
let predicted = create_test_series(vec![1.5, 2.1, 2.9, 3.8]);
let max_err = max_error(&actual, &predicted);
assert!((max_err - 0.5).abs() < 1e-6);
}
#[test]
fn test_directional_accuracy() {
let actual = create_test_series(vec![1.0, 2.0, 3.0, 4.0]);
let predicted = create_test_series(vec![1.0, 2.1, 3.2, 4.3]);
let da = directional_accuracy(&actual, &predicted);
assert_eq!(da, 100.0);
}
#[test]
fn test_smape() {
let actual = create_test_series(vec![100.0, 200.0, 300.0]);
let predicted = create_test_series(vec![110.0, 190.0, 310.0]);
let smape_val = smape(&actual, &predicted);
assert!(smape_val > 0.0 && smape_val < 10.0);
}
#[test]
fn test_evaluate_forecast() {
let actual = create_test_series(vec![1.0, 2.0, 3.0, 4.0]);
let predicted = create_test_series(vec![1.1, 2.1, 2.9, 3.8]);
let metrics = evaluate_forecast(&actual, &predicted, Some(2));
assert!(metrics.mae > 0.0);
assert!(metrics.mse > 0.0);
assert!(metrics.rmse > 0.0);
assert!(metrics.r2 > 0.9); assert!(metrics.max_error > 0.0);
}
}