use anofox_forecast::core::TimeSeries;
use anofox_forecast::models::arima::{AutoARIMA, AutoARIMAConfig, ARIMA, SARIMA};
use anofox_forecast::models::baseline::SeasonalWindowAverage;
use anofox_forecast::models::baseline::{
HistoricAverage, Naive, RandomWalkWithDrift, SeasonalNaive, WindowAverage,
};
use anofox_forecast::models::exponential::{
AutoETS, AutoETSConfig, ETSSpec, HoltWinters, SeasonalES, SeasonalType,
SimpleExponentialSmoothing, ETS,
};
use anofox_forecast::models::garch::GARCH;
use anofox_forecast::models::intermittent::{Croston, ADIDA, IMAPA, TSB};
use anofox_forecast::models::mfles::MFLES;
use anofox_forecast::models::mstl_forecaster::MSTLForecaster;
use anofox_forecast::models::tbats::{AutoTBATS, TBATS};
use anofox_forecast::models::theta::{
AutoTheta, DynamicOptimizedTheta, DynamicTheta, OptimizedTheta, Theta,
};
use anofox_forecast::models::Forecaster;
use chrono::{NaiveDateTime, TimeZone, Utc};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
const HORIZON: usize = 12;
const SEASONAL_PERIOD: usize = 12;
const CONFIDENCE_LEVELS: [f64; 3] = [0.80, 0.90, 0.95];
const DATA_DIR: &str = "validation/data";
const RESULTS_DIR: &str = "validation/results/rust";
const SERIES_TYPES: [&str; 25] = [
"stationary",
"trend",
"seasonal",
"trend_seasonal",
"seasonal_negative", "multiplicative_seasonal", "intermittent", "high_frequency", "structural_break", "long_memory", "noisy_seasonal", "exponential_trend", "damped_trend", "strong_seasonal", "quarterly_seasonal", "multiplicative_trend_seasonal", "heteroscedastic", "random_walk", "ar1", "outlier_series", "step_seasonal", "bimodal_seasonal", "asymmetric_seasonal", "seasonal_trend_break", "low_count", ];
#[allow(clippy::type_complexity)]
fn read_csv(
path: &Path,
) -> Result<(Vec<chrono::DateTime<Utc>>, Vec<f64>), Box<dyn std::error::Error>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut timestamps = Vec::new();
let mut values = Vec::new();
for (i, line) in reader.lines().enumerate() {
let line = line?;
if i == 0 {
continue;
}
let parts: Vec<&str> = line.split(',').collect();
if parts.len() >= 2 {
let ts_str = parts[0].trim();
let naive =
NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%d %H:%M:%S").or_else(|_| {
chrono::NaiveDate::parse_from_str(ts_str, "%Y-%m-%d")
.map(|d| d.and_hms_opt(0, 0, 0).unwrap())
})?;
let ts = Utc.from_utc_datetime(&naive);
timestamps.push(ts);
let value: f64 = parts[1].trim().parse()?;
values.push(value);
}
}
Ok((timestamps, values))
}
struct ForecastResult {
model_name: String,
series_type: String,
point_forecasts: Vec<f64>,
intervals: HashMap<String, (Vec<f64>, Vec<f64>)>, }
fn run_model<F: Forecaster>(
model: &mut F,
ts: &TimeSeries,
model_name: &str,
series_type: &str,
has_native_intervals: bool,
) -> Option<ForecastResult> {
if model.fit(ts).is_err() {
eprintln!(" Warning: {} failed to fit on {}", model_name, series_type);
return None;
}
let point_forecast = match model.predict(HORIZON) {
Ok(f) => f,
Err(e) => {
eprintln!(
" Warning: {} failed to predict on {}: {}",
model_name, series_type, e
);
return None;
}
};
let point_forecasts = point_forecast.primary().to_vec();
let mut intervals = HashMap::new();
if has_native_intervals {
for &level in &CONFIDENCE_LEVELS {
match model.predict_with_intervals(HORIZON, level) {
Ok(forecast_ci) => {
if let (Ok(lower), Ok(upper)) =
(forecast_ci.lower_series(0), forecast_ci.upper_series(0))
{
let level_key = format!("{:.0}", level * 100.0);
intervals.insert(level_key, (lower.to_vec(), upper.to_vec()));
}
}
Err(e) => {
eprintln!(
" Warning: {} CI level {} failed on {}: {}",
model_name, level, series_type, e
);
}
}
}
}
Some(ForecastResult {
model_name: model_name.to_string(),
series_type: series_type.to_string(),
point_forecasts,
intervals,
})
}
fn write_point_forecasts(results: &[ForecastResult], path: &Path) -> std::io::Result<()> {
let mut file = File::create(path)?;
writeln!(file, "series_type,model,step,forecast")?;
for result in results {
for (i, &forecast) in result.point_forecasts.iter().enumerate() {
writeln!(
file,
"{},{},{},{}",
result.series_type,
result.model_name,
i + 1,
forecast
)?;
}
}
Ok(())
}
fn write_confidence_intervals(results: &[ForecastResult], path: &Path) -> std::io::Result<()> {
let mut file = File::create(path)?;
writeln!(file, "series_type,model,step,level,lower,upper")?;
for result in results {
for (level, (lower, upper)) in &result.intervals {
for i in 0..lower.len() {
writeln!(
file,
"{},{},{},{},{},{}",
result.series_type,
result.model_name,
i + 1,
level,
lower[i],
upper[i]
)?;
}
}
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Rust Forecast Validation Export ===\n");
fs::create_dir_all(RESULTS_DIR)?;
let mut all_results: Vec<ForecastResult> = Vec::new();
for series_type in SERIES_TYPES {
println!("Processing {} series...", series_type);
let csv_path = Path::new(DATA_DIR).join(format!("{}.csv", series_type));
if !csv_path.exists() {
eprintln!(" Error: Data file not found: {:?}", csv_path);
eprintln!(" Run 'python generate_data.py' first to create the data files.");
continue;
}
let (timestamps, values) = read_csv(&csv_path)?;
let ts = TimeSeries::univariate(timestamps, values)?;
println!(" Loaded {} observations", ts.len());
{
let mut model = Naive::new();
if let Some(result) = run_model(&mut model, &ts, "Naive", series_type, true) {
all_results.push(result);
println!(" ✓ Naive");
}
}
{
let mut model = SeasonalNaive::new(SEASONAL_PERIOD);
if let Some(result) = run_model(&mut model, &ts, "SeasonalNaive", series_type, true) {
all_results.push(result);
println!(" ✓ SeasonalNaive");
}
}
{
let mut model = RandomWalkWithDrift::new();
if let Some(result) =
run_model(&mut model, &ts, "RandomWalkWithDrift", series_type, true)
{
all_results.push(result);
println!(" ✓ RandomWalkWithDrift");
}
}
{
let mut model = SimpleExponentialSmoothing::new(0.1);
if let Some(result) = run_model(&mut model, &ts, "SES", series_type, false) {
all_results.push(result);
println!(" ✓ SES (point only)");
}
}
{
let mut model = ETS::new(ETSSpec::aan(), SEASONAL_PERIOD);
if let Some(result) = run_model(&mut model, &ts, "Holt", series_type, true) {
all_results.push(result);
println!(" ✓ Holt");
}
}
{
let mut model = HoltWinters::auto(SEASONAL_PERIOD, SeasonalType::Additive);
if let Some(result) = run_model(&mut model, &ts, "HoltWinters", series_type, true) {
all_results.push(result);
println!(" ✓ HoltWinters");
}
}
{
let mut model = ARIMA::new(1, 1, 1);
if let Some(result) = run_model(&mut model, &ts, "ARIMA_1_1_1", series_type, true) {
all_results.push(result);
println!(" ✓ ARIMA(1,1,1)");
}
}
{
let config = AutoARIMAConfig::default()
.with_seasonal_period(SEASONAL_PERIOD)
.with_seasonal_orders(1, 1, 1);
let mut model = AutoARIMA::with_config(config);
if let Some(result) = run_model(&mut model, &ts, "AutoARIMA", series_type, true) {
all_results.push(result);
println!(" ✓ AutoARIMA (with SARIMA)");
}
}
{
let mut model = SARIMA::new(1, 1, 1, 1, 1, 1, SEASONAL_PERIOD);
if let Some(result) =
run_model(&mut model, &ts, "SARIMA_1_1_1_1_1_1_12", series_type, true)
{
all_results.push(result);
println!(" ✓ SARIMA(1,1,1)(1,1,1)[12]");
}
}
{
let config = AutoETSConfig::with_period(SEASONAL_PERIOD);
let mut model = AutoETS::with_config(config);
if let Some(result) = run_model(&mut model, &ts, "AutoETS", series_type, true) {
all_results.push(result);
println!(" ✓ AutoETS");
}
}
{
let mut model = Theta::seasonal(SEASONAL_PERIOD);
if let Some(result) = run_model(&mut model, &ts, "Theta", series_type, true) {
all_results.push(result);
println!(" ✓ Theta");
}
}
{
let mut model = Croston::new();
if let Some(result) = run_model(&mut model, &ts, "Croston", series_type, false) {
all_results.push(result);
println!(" ✓ Croston (point only)");
}
}
{
let mut model = Croston::new().sba();
if let Some(result) = run_model(&mut model, &ts, "CrostonSBA", series_type, false) {
all_results.push(result);
println!(" ✓ CrostonSBA (point only)");
}
}
{
let mut model = TSB::new();
if let Some(result) = run_model(&mut model, &ts, "TSB", series_type, false) {
all_results.push(result);
println!(" ✓ TSB (point only)");
}
}
{
let mut model = ADIDA::new();
if let Some(result) = run_model(&mut model, &ts, "ADIDA", series_type, false) {
all_results.push(result);
println!(" ✓ ADIDA (point only)");
}
}
{
let mut model = SeasonalWindowAverage::new(SEASONAL_PERIOD, 2);
if let Some(result) =
run_model(&mut model, &ts, "SeasonalWindowAverage", series_type, false)
{
all_results.push(result);
println!(" ✓ SeasonalWindowAverage (point only)");
}
}
{
let mut model = IMAPA::new();
if let Some(result) = run_model(&mut model, &ts, "IMAPA", series_type, false) {
all_results.push(result);
println!(" ✓ IMAPA (point only)");
}
}
{
let mut model = OptimizedTheta::seasonal(SEASONAL_PERIOD);
if let Some(result) = run_model(&mut model, &ts, "OptimizedTheta", series_type, true) {
all_results.push(result);
println!(" ✓ OptimizedTheta");
}
}
{
let mut model = DynamicTheta::seasonal(SEASONAL_PERIOD);
if let Some(result) = run_model(&mut model, &ts, "DynamicTheta", series_type, true) {
all_results.push(result);
println!(" ✓ DynamicTheta");
}
}
{
let mut model = DynamicOptimizedTheta::seasonal_optimized(SEASONAL_PERIOD);
if let Some(result) =
run_model(&mut model, &ts, "DynamicOptimizedTheta", series_type, true)
{
all_results.push(result);
println!(" ✓ DynamicOptimizedTheta");
}
}
{
let mut model = AutoTheta::seasonal(SEASONAL_PERIOD);
if let Some(result) = run_model(&mut model, &ts, "AutoTheta", series_type, true) {
all_results.push(result);
println!(" ✓ AutoTheta");
}
}
{
let mut model = MSTLForecaster::new(vec![SEASONAL_PERIOD]);
if let Some(result) = run_model(&mut model, &ts, "MSTLForecaster", series_type, true) {
all_results.push(result);
println!(" ✓ MSTLForecaster");
}
}
{
let mut model = MFLES::new(vec![SEASONAL_PERIOD]);
if let Some(result) = run_model(&mut model, &ts, "MFLES", series_type, true) {
all_results.push(result);
if series_type == "noisy_seasonal" {
let (trend, penalty, seasonality, is_mult) = model.debug_state();
eprintln!(" DEBUG MFLES noisy_seasonal:");
eprintln!(" is_multiplicative: {}", is_mult);
eprintln!(" trend: {:?}", trend);
eprintln!(" penalty: {:?}", penalty);
if let Some(s) = seasonality {
eprintln!(" seasonality (first 5): {:?}", &s[..5.min(s.len())]);
}
}
println!(" ✓ MFLES");
}
}
{
let mut model = SeasonalES::new(SEASONAL_PERIOD);
if let Some(result) = run_model(&mut model, &ts, "SeasonalES", series_type, true) {
all_results.push(result);
println!(" ✓ SeasonalES");
}
}
{
let mut model = TBATS::new(vec![SEASONAL_PERIOD]);
if let Some(result) = run_model(&mut model, &ts, "TBATS", series_type, true) {
all_results.push(result);
println!(" ✓ TBATS");
}
}
{
let mut model = AutoTBATS::new(vec![SEASONAL_PERIOD]);
if let Some(result) = run_model(&mut model, &ts, "AutoTBATS", series_type, true) {
all_results.push(result);
println!(" ✓ AutoTBATS");
}
}
{
let mut model = GARCH::new(1, 1);
if let Some(result) = run_model(&mut model, &ts, "GARCH", series_type, true) {
all_results.push(result);
println!(" ✓ GARCH");
}
}
{
let mut model = HistoricAverage::new();
if let Some(result) = run_model(&mut model, &ts, "HistoricAverage", series_type, false)
{
all_results.push(result);
println!(" ✓ HistoricAverage (point only)");
}
}
{
let mut model = WindowAverage::new(12); if let Some(result) = run_model(&mut model, &ts, "WindowAverage", series_type, false) {
all_results.push(result);
println!(" ✓ WindowAverage (point only)");
}
}
println!();
}
println!("Writing results...");
let point_path = Path::new(RESULTS_DIR).join("point_forecasts.csv");
write_point_forecasts(&all_results, &point_path)?;
println!(" ✓ Point forecasts: {:?}", point_path);
let ci_path = Path::new(RESULTS_DIR).join("confidence_intervals.csv");
write_confidence_intervals(&all_results, &ci_path)?;
println!(" ✓ Confidence intervals: {:?}", ci_path);
println!("\n=== Export Complete ===");
println!(
"Total forecasts exported: {} model/series combinations",
all_results.len()
);
Ok(())
}