Skip to main content

forecast-arena 🥊

tests PyPI docs License: MIT

Modelos de series de tiempo que se pelean entre ellos. El de menor error gana el derecho a pronosticar.

📖 Documentación completa: https://Zurishadday.github.io/forecast-arena

Inspirado en Hyndman & Athanasopoulos, Forecasting: Principles and Practice (fpp3).

Instalación

pip install forecast-arena              # núcleo
pip install "forecast-arena[xgb]"       # + XGBoost (recomendado)
pip install "forecast-arena[plots]"     # + matplotlib para result.plot()

Uso en 5 líneas

import pandas as pd
from forecast_arena import Arena

y = pd.read_csv("ventas.csv", index_col=0, parse_dates=True).squeeze()

arena = Arena(season_length=12)
result = arena.compete(y, test_size=6, h=12, n_windows=3)

print(result.leaderboard)   # MAE, RMSE, MAPE, sMAPE, MASE por modelo
print(result.champion)      # p. ej. 'ETS'
print(result.forecast)      # pronóstico de 12 pasos del campeón

Los luchadores

Nombre Modelo fpp3
naive Repite el último valor cap. 5.2
snaive Naive estacional cap. 5.2
drift Extrapolación lineal primera→última obs. cap. 5.2
sarima auto_arima (pmdarima) autoconfigurado cap. 9
ets Suavizamiento exponencial cap. 8
xgb STL + tendencia por regresión + boosting recursivo cap. 3.6
theta Método Theta (ganador de M3)
stlf STL + ETS cap. 3.6
harmonic Fourier + ARIMA en errores (multi-estacional) cap. 12.1
croston Croston/SBA para demanda intermitente (no default)
Ensemble Media de todos los anteriores (compite también)

Los benchmarks naive siempre están en la pelea — fpp3 insiste: si tu modelo sofisticado no vence al naive (MASE < 1), no sirve.

Cómo pelean

  1. Perfil de la serie (SeriesProfile): fuerzas de tendencia y estacionalidad vía STL (fpp3 cap. 4.3), con guarda anti-estacionalidad-espuria (la ACF debe confirmar el lag estacional). El perfil autoconfigura SARIMA, ETS y el híbrido.
  2. Backtesting rolling-origin (fpp3 cap. 5.10): n_windows ventanas de test_size pasos que avanzan hacia el presente. Un modelo que falla en una ventana no detiene la pelea.
  3. Leaderboard en escala original (aunque uses Box-Cox), ordenado por la métrica que elijas (metric="MAE" default; también "MASE", "RMSE"...).
  4. El campeón reentrena con toda la serie y pronostica h pasos.

Opciones

Arena(
    models=["snaive", "ets", "sarima"],  # elige a los luchadores
    season_length=12,       # 12 mensual, 7 diario, 4 trimestral...
    metric="MASE",          # métrica que corona al campeón
    handle_outliers=True,   # winsoriza si generalized ESD detecta anomalías
    box_cox=True,           # transforma (métricas siguen en escala original)
    include_ensemble=True,
)

También puedes meter tu propio luchador: cualquier subclase de BaseForecaster con _fit(y) y _predict(h):

from forecast_arena import BaseForecaster

class MiModelo(BaseForecaster):
    name = "MiModelo"
    def _fit(self, y): ...
    def _predict(self, h): ...

Arena(models=["snaive", MiModelo()]).compete(y)

Resultado en AirPassengers (144 meses, 3 ventanas, Box-Cox por Guerrero)

            MAE   RMSE  MAPE%  sMAPE%  MASE
ETS       17.67  20.76   4.28    4.20  0.59
XGB       18.92  22.79   4.82    4.65  0.63
Sarima    24.13  27.24   5.90    5.89  0.82
Ensemble  31.98  40.03   6.90    7.20  1.08
SNaive    35.92  38.99   8.06    8.52  1.21
Drift     60.97  83.26  12.82   14.13  2.05
Naive     73.22  97.53  15.28   17.41  2.47

Detección de quiebres estructurales (v0.7)

Si tu serie "cambió de vida" — subida de precios, competidor nuevo, pandemia — los modelos entrenan sobre un pasado que ya no describe el presente. La Arena ahora lo detecta automáticamente con PELT (Killick et al. 2012, el algoritmo estándar de changepoint detection), implementado internamente:

result = Arena().compete(y, test_size=12, h=12)   # detección activa por default
result.changepoints
#  pos      fecha   tipo  antes  despues  cambio
#   70 2021-04-01  nivel   98.9    145.2    46.2

Distingue saltos de nivel (la serie brincó a otra media) de cambios de tendencia (la pendiente cambió), los marca en result.plot(), y ofrece tres reacciones vía changepoints=:

  • "report" (default): detecta y reporta; tú decides.
  • "trim": entrena solo con el régimen actual (post-quiebre), con guarda de historia mínima.
  • "dummy": convierte cada quiebre en variable de intervención (escalón 0→1, fpp3 cap. 10.4) que entra como exógena — el modelo usa TODA la historia pero sabiendo que hubo un antes y un después. Suele ser la mejor opción. Componible con tus propias exógenas.
  • None: apagado. Sensibilidad ajustable con detect_changepoints(y, penalty_scale=).

Calibración medida: 0 falsos positivos en 60 series limpias (ruido, tendencia y estacionalidad puras), 30/30 saltos de nivel localizados con error 0, 30/30 cambios de tendencia con error medio de 2 observaciones.

Auditoría de intervalos, jerárquico y más (v0.6)

Auditoría de intervalos (fpp3 cap. 5.9) — las bandas ya no son una promesa sin verificar: en cada ventana del backtesting se mide la cobertura empírica (¿la banda del 95% cubre de verdad el 95%?) y el Winkler score (premia bandas angostas que sí cumplen):

result.interval_report
#         coverage_80  winkler_80  coverage_95  winkler_95
# ETS           0.750      10.725        0.917      13.045
# XGB           0.667      18.535        0.833      28.052

Pronóstico jerárquico (fpp3 cap. 11) — cientos de series que suman (SKU → categoría → total) con reconciliación para que las cifras cuadren exactamente en todos los niveles:

from forecast_arena import forecast_hierarchy
tree = {"total": ["norte", "centro"], "norte": ["mty", "chih"], "centro": ["cdmx", "gdl"]}
hr = forecast_hierarchy(df_hojas, tree, test_size=6, h=12, method="ols")
hr.reconciled   # las sumas cuadran exacto
hr.summary      # campeón y métricas por nodo

Métodos: ols (default, combina información de todos los niveles), bottom_up, top_down. También API batch sin jerarquía: arena.compete_many({nombre: serie}) + Arena.summarize(resultados).

Efectos de calendario (fpp3 cap. 10.2) — calendar_features ahora incluye trading_days (días hábiles por periodo) y is_easter (Semana Santa, la fiesta móvil que salta entre marzo y abril y contamina cualquier estacionalidad mensual fija).

Refinamientos — la incertidumbre de las exógenas en cascada ahora SÍ se propaga a los intervalos (método delta: se perturba cada exógena ±σ de su holdout y se mide el efecto); K="auto" en Harmonic elige armónicos por AICc; bias_adjust=True entrega la media (en vez de la mediana) al destransformar Box-Cox (cap. 5.6) — útil cuando los pronósticos deben sumar.

Producciónresult.save(path) / forecast_arena.load(path) (el campeón cargado pronostica sin reentrenar), random_state= reproducible de punta a punta, progreso por ventana con verbose=True, y suite de tortura con series patológicas (constantes, m=365, escalas de 1e12, 3000+ puntos) con guardas estilo fable (ETS/SARIMA degradan a no estacionales si m>24).

Luchadores nuevos y calidad de vida (v0.5)

Tres luchadores más en el ring:

Nombre Modelo Cuándo brilla
theta Método Theta (ganador de la competencia M3) Casi siempre; simple y difícil de vencer. En el lineup default.
stlf STL + ETS (fpp3 cap. 3.6) Estacionalidad estable con tendencia caprichosa. En el lineup default.
croston Croston / SBA Demanda intermitente (muchos ceros, SKUs de baja rotación). Especialista: pídelo explícitamente.
Arena(models=["croston", "naive"]).compete(demanda_intermitente, test_size=8, h=4)

Calidad de vida:

  • result.plot(last=60) — histórico + pronóstico + bandas 80/95% (requiere matplotlib: pip install "forecast-arena[plots]").
  • Ensemble ponderadoensemble_weights="inverse_error": pesos proporcionales al inverso de la desviación de los residuales honestos de cada modelo, en lugar de media simple.
  • Manejo de huecos — fechas faltantes y NaN internos se detectan (reconstruyendo la malla temporal aunque el hueco rompa la inferencia de frecuencia), se imputan por interpolación lineal y se avisa; con impute=None la Arena exige serie completa. NaN en los extremos solo se recortan.
  • Paralelizaciónn_jobs=-1 entrena a los luchadores en paralelo por ventana (joblib); resultados idénticos al secuencial.

Intervalos, diagnóstico y estacionalidad múltiple (v0.4)

Las tres piezas metodológicas grandes de fpp3, explicadas a fondo en docs/METODOLOGIA.md:

result = Arena().compete(y, test_size=12, h=6)

result.forecast_intervals
#             forecast  lower_80  upper_80  lower_95  upper_95
# 1961-01-01     444.1     414.4     461.5     389.6     473.7
# ...

result.residual_diagnostics["verdict"]
# "Residuales consistentes con ruido blanco (Ljung-Box p=0.886) y sin
#  sesgo. El modelo extrajo la estructura disponible."

# Estacionalidad múltiple (diaria con ciclo semanal + mensual):
Arena(season_length=[7, 30]).compete(y_diaria, test_size=14, h=14)
# agrega automáticamente al luchador Harmonic (Fourier + ARIMA en errores)
  • Intervalos de predicción (cap. 5.5): cada modelo con su método — fórmulas cerradas (naive), analíticos (SARIMA/ETS/Harmonic) y simulación bootstrap con residuales honestos de holdout para el híbrido ML. Con Box-Cox se destransforman exactamente (quedan asimétricos, como debe ser).
  • Diagnóstico de residuales (cap. 5.4): Ljung-Box + chequeo de sesgo sobre el campeón, con veredicto legible.
  • Estacionalidad múltiple (cap. 12.1): regresión armónica dinámica vía el luchador Harmonic, activado solo al pasar season_length=[m1, m2].

Detección automática (v0.3): periodicidad y transformación

Ya no necesitas decirle nada a la Arena — pero puedes:

result = Arena().compete(y, test_size=12, h=12)   # cero configuración

print(result.season_length)    # 12
print(result.season_source)    # "mensual, por frecuencia del índice"
print(result.transformation)   # "Box-Cox λ=-0.29 — serie multiplicativa
                               #  (la varianza crece con el nivel)"

Periodicidad (season_length="auto", default): primero por la frecuencia del índice de fechas (mensual→12, trimestral→4, semanal→52, diaria→7, hábiles→5, horaria→24, anual→1); si no hay fechas o no se infiere, por periodograma (FFT) sobre la serie sin tendencia, confirmado con ACF significativa al 1% y prominencia del pico espectral (sin falsos positivos en ruido). Si nada confirma → m=1 y todos los modelos degradan limpiamente a sus versiones no estacionales.

Transformación (transform="auto", default): detecta si la serie es multiplicativa midiendo si la dispersión crece con el nivel (Spearman entre media y desviación estándar por bloques de un ciclo). Si lo es, el λ de Box-Cox se elige por el método de Guerrero (el de fable/fpp3, cap. 3.1); con λ≈0 aplica logaritmo por interpretabilidad. Si es aditiva, no toca nada. El resultado siempre te dice qué hizo y por qué. Las métricas del leaderboard siguen reportándose en la escala original.

Opciones manuales: transform="boxcox" (Guerrero forzado), "log", None, y season_length=12 fijo. El flag box_cox=True/False de v0.1 sigue funcionando.

Variables exógenas (v0.2) — fpp3 cap. 10

Tres modos, según qué tanto sabes del futuro de tus regresores:

# 1. CONOCIDAS (ex-post): calendario, promociones planeadas, precios fijados.
result = arena.compete(y, h=6, X=X_hist, X_future=X_conocidas)

# 2. DESCONOCIDAS (ex-ante): cada columna se pronostica en su propia
#    mini-arena (naive/snaive/drift/ETS) y el campeón alimenta a y.
result = arena.compete(y, h=6, X=X_hist, forecast_X=True)

# 3. MIXTO: las columnas en X_future van directas; el resto, en cascada.
result = arena.compete(y, h=6, X=X_hist, X_future=X_promos, forecast_X=True)

Quién usa las exógenas: sarima (se vuelve SARIMAX) y xgb (features junto a los lags). ets, naive, snaive y drift las ignoran pero siguen compitiendo — si tus exógenas no aportan, un modelo ciego ganará el leaderboard y te enteras solito.

Backtesting honesto: en cada ventana, las exógenas desconocidas se pronostican usando solo datos hasta el origen — nunca sus valores reales del tramo de test. El leaderboard refleja el error real de producción, error de cascada incluido. Las conocidas sí usan sus valores reales (en producción también los tendrás: son deterministas o planeadas).

El resultado incluye:

  • result.exog_report: campeón y MASE en holdout por exógena pronosticada. MASE > 1 delata una exógena "ruidosa" cuyo error contamina el pronóstico — candidata a salir.
  • result.X_future_used: la tabla exacta (dadas + pronosticadas) que alimentó el pronóstico final.

Helpers para exógenas de calendario (siempre conocidas):

from forecast_arena import fourier_terms, calendar_features
X_cal = calendar_features(y.index)               # mes, trimestre, diciembre...
X_fourier = fourier_terms(y.index, period=12, K=3)  # armónicos (cap. 10.5)

Cambios vs. el Forecasting.py original

  • Bugs corregidos: typo star_Q ignorado por auto_arima; .values sobre ndarray en la rama sin estacionalidad; Box-Cox truena con ceros/negativos (ahora shift automático reversible); comparación R² in-sample donde el polinomio nunca podía perder (ahora se compara en cola de validación); estacionalidad espuria de STL en ruido con pocos ciclos (guarda ACF); el diagnóstico de estacionalidad usaba period=12 fijo, degradando silenciosamente series trimestrales/semanales (v0.3).
  • Dependencias eliminadas: skforecast (su ForecasterAutoreg fue deprecado; el forecaster recursivo está implementado internamente) y PyAstronomy (generalized ESD implementado con scipy).
  • Nuevo: benchmarks naive/snaive/drift, MASE/sMAPE/RMSE, backtesting rolling-origin multiventana, perfil de serie reutilizable, manejo de series cortas sin crashes, API extensible, suite de 16 tests.
  • Typos de API corregidos: TranformationBoxCoxTransformer, perfomancescore_table.

Tests

pip install -e ".[dev]" && pytest

Limitaciones conocidas / roadmap

  • Los intervalos del Ensemble son el promedio de los de sus miembros (equivale a asumir correlación perfecta: la combinación conservadora).
  • La propagación de incertidumbre de exógenas usa método delta (primer orden); simulación completa sería más exacta para efectos no lineales.
  • La reconciliación jerárquica no incluye MinT con covarianza muestral (solo OLS, su caso identidad-ponderado).
  • La cascada pronostica cada exógena de forma independiente (sin correlaciones entre ellas).
  • El Ensemble es media simple; ponderar por inverso del error sería mejorable.
  • Publicación en PyPI, CI y documentación web pendientes para v1.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

forecast_arena-1.0.0.tar.gz (66.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

forecast_arena-1.0.0-py3-none-any.whl (58.3 kB view details)

Uploaded Python 3

File details

Details for the file forecast_arena-1.0.0.tar.gz.

File metadata

  • Download URL: forecast_arena-1.0.0.tar.gz
  • Upload date:
  • Size: 66.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for forecast_arena-1.0.0.tar.gz
Algorithm Hash digest
SHA256 a1b31cda26550806c5bf8d9f53cc1b2b5d9d14df78083e33f4fac49057247853
MD5 0c88547782910f5a7d9fa9ce838ee2f8
BLAKE2b-256 c70e38e8a154d01c8aa0ba3a130c096db2b7457b84c8362d2dc6129f2209a9de

See more details on using hashes here.

Provenance

The following attestation bundles were made for forecast_arena-1.0.0.tar.gz:

Publisher: publish.yml on Zurishadday/forecast-arena

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file forecast_arena-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: forecast_arena-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 58.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for forecast_arena-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d24bedbebc85b69251d017b7140e3a260e36e9748c71604bf34ed3bef07c5ddb
MD5 bdea0af4eae2e684202804fa432f836f
BLAKE2b-256 0578ef1e0177b114153abd71d996b8522277e6a0a12791d13ebd0560ec7f84d4

See more details on using hashes here.

Provenance

The following attestation bundles were made for forecast_arena-1.0.0-py3-none-any.whl:

Publisher: publish.yml on Zurishadday/forecast-arena

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page