forecast-arena 🥊
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
- 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. - Backtesting rolling-origin (fpp3 cap. 5.10):
n_windowsventanas detest_sizepasos que avanzan hacia el presente. Un modelo que falla en una ventana no detiene la pelea. - Leaderboard en escala original (aunque uses Box-Cox), ordenado por la
métrica que elijas (
metric="MAE"default; también"MASE","RMSE"...). - El campeón reentrena con toda la serie y pronostica
hpasos.
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 condetect_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ón — result.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 ponderado —
ensemble_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=Nonela Arena exige serie completa. NaN en los extremos solo se recortan. - Paralelización —
n_jobs=-1entrena 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 pasarseason_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_Qignorado por auto_arima;.valuessobre 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(suForecasterAutoregfue deprecado; el forecaster recursivo está implementado internamente) yPyAstronomy(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:
Tranformation→BoxCoxTransformer,perfomance→score_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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a1b31cda26550806c5bf8d9f53cc1b2b5d9d14df78083e33f4fac49057247853
|
|
| MD5 |
0c88547782910f5a7d9fa9ce838ee2f8
|
|
| BLAKE2b-256 |
c70e38e8a154d01c8aa0ba3a130c096db2b7457b84c8362d2dc6129f2209a9de
|
Provenance
The following attestation bundles were made for forecast_arena-1.0.0.tar.gz:
Publisher:
publish.yml on Zurishadday/forecast-arena
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
forecast_arena-1.0.0.tar.gz -
Subject digest:
a1b31cda26550806c5bf8d9f53cc1b2b5d9d14df78083e33f4fac49057247853 - Sigstore transparency entry: 2306417749
- Sigstore integration time:
-
Permalink:
Zurishadday/forecast-arena@04786e7623c58d9eea06cf6973e49ff1f9c4218b -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Zurishadday
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@04786e7623c58d9eea06cf6973e49ff1f9c4218b -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d24bedbebc85b69251d017b7140e3a260e36e9748c71604bf34ed3bef07c5ddb
|
|
| MD5 |
bdea0af4eae2e684202804fa432f836f
|
|
| BLAKE2b-256 |
0578ef1e0177b114153abd71d996b8522277e6a0a12791d13ebd0560ec7f84d4
|
Provenance
The following attestation bundles were made for forecast_arena-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on Zurishadday/forecast-arena
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
forecast_arena-1.0.0-py3-none-any.whl -
Subject digest:
d24bedbebc85b69251d017b7140e3a260e36e9748c71604bf34ed3bef07c5ddb - Sigstore transparency entry: 2306417829
- Sigstore integration time:
-
Permalink:
Zurishadday/forecast-arena@04786e7623c58d9eea06cf6973e49ff1f9c4218b -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Zurishadday
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@04786e7623c58d9eea06cf6973e49ff1f9c4218b -
Trigger Event:
release
-
Statement type: