Lk / arima_model.py
Kashaf1's picture
Upload moirai_forecast_app contents
ef20ebe
Raw History Blame Contribute Delete
4.1 kB
"""
AutoARIMA forecaster, via Nixtla's statsforecast (actively maintained,
confirmed compatible with this project's pandas 3.x / numpy 2.x — the
alternative, pmdarima, was not tried after statsforecast installed and
ran cleanly on the first attempt; no need to chase a second option once
one is verified working).
Unlike Moirai-MoE and Chronos-2 (zero-shot pretrained foundation
models with a real load() step and a downloaded checkpoint), AutoARIMA
is a classical statistical method: it FITS a fresh ARIMA(p,d,q) model
to each context window at predict()-time. That is the normal, correct
way this kind of model works, not a shortcut or a simplification of
"real" loading — there is no pretrained checkpoint for it to load.
API verified by REAL execution in this sandbox (network access to
PyPI, no real market data needed — synthetic data exercises the same
code path): `pip install statsforecast`, fit on a
(unique_id, ds, y)-columned frame, `sf.predict(h=..., level=[50, 80])`
returns point forecasts plus 50%/80% prediction intervals, which map
exactly onto this project's five quantile levels (0.1/0.25/0.5/0.75/0.9)
since a symmetric interval at level L brackets the (100-L)/2 and
100-(100-L)/2 percentiles: level=80 -> P10/P90, level=50 -> P25/P75.
See tests/test_arima_model.py, which runs this for real (not stubbed)
since statsforecast has no heavy/network dependency to work around.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from forecast_types import ModelInferenceError, ModelInputError, ModelLoadError, QuantileForecast
MODEL_NAME = "AutoARIMA (statsforecast)"
class AutoArimaForecaster:
"""No real load() step (see module docstring) — kept for interface
parity with MoiraiMoEForecaster/Chronos2Forecaster so app.py can
treat all three uniformly."""
def __init__(self, season_length: int = 1, min_context: int = 10):
self.season_length = season_length
self.min_context = min_context
self._loaded = True
def load(self):
pass # nothing to download or initialize; always ready
def predict(self, context_df: pd.DataFrame, target_col: str, past_covariate_cols,
prediction_length: int, freq: str) -> QuantileForecast:
closes = context_df[target_col]
if closes.isna().any() or not np.isfinite(closes.to_numpy()).all():
raise ModelInputError("context contains NaN/inf values in the target column")
if len(closes) < self.min_context:
raise ModelInputError(
f"context too short for AutoARIMA: {len(closes)} row(s), need at least {self.min_context}"
)
try:
from statsforecast import StatsForecast
from statsforecast.models import AutoARIMA
except ImportError as e:
raise ModelLoadError(f"statsforecast not installed: {e}") from e
fit_df = pd.DataFrame({"unique_id": "series", "ds": context_df.index, "y": closes.to_numpy()})
try:
sf = StatsForecast(models=[AutoARIMA(season_length=self.season_length)], freq=freq, n_jobs=1)
sf.fit(fit_df)
fc = sf.predict(h=prediction_length, level=[50, 80])
except Exception as e:
raise ModelInferenceError(f"AutoARIMA fit/predict failed: {e}") from e
# Column names are statsforecast's own convention: "<Model>",
# "<Model>-lo-<level>", "<Model>-hi-<level>" -- verified against
# real output in this sandbox, not assumed from documentation.
quantiles = np.stack([
fc["AutoARIMA-lo-80"].to_numpy(),
fc["AutoARIMA-lo-50"].to_numpy(),
fc["AutoARIMA"].to_numpy(),
fc["AutoARIMA-hi-50"].to_numpy(),
fc["AutoARIMA-hi-80"].to_numpy(),
], axis=1) # shape (prediction_length, 5)
return QuantileForecast(
horizon=prediction_length, quantile_levels=[0.1, 0.25, 0.5, 0.75, 0.9],
quantiles=quantiles, samples=None,
context_length_used=len(context_df), prediction_length=prediction_length,
)