15. Modelos automáticos do StatsForecast¶
Os modelos AutoARIMA, AutoETS, AutoTheta e AutoCES do StatsForecast (Nixtla) entram como modelos-base opcionais: sf_auto_arima, sf_auto_ets, sf_auto_theta e sf_auto_ces. Este notebook compara esses modelos com os equivalentes do statsmodels e com baselines nas mesmas origens, mostra que os parâmetros ficam congelados depois do ajuste (requisito da variante B) e reproduz os híbridos estatístico-ANN do projeto de replicação.
Requisitos: extra nixtla-stats (pip install "autohybts[nixtla-stats]"). O StatsForecast exige pandas<3, então o extra instala o pandas 2.x; num ambiente com pandas 3, use um segundo ambiente virtual. Guia: user_guide/nixtla.
import time
import warnings
import matplotlib.pyplot as plt
import pandas as pd
import autohybts
from autohybts import AutoHybridExperiment
from autohybts.datasets import get_data
from autohybts.domain.fit_count import count_fits
from autohybts.residuals.generation import local_context
warnings.filterwarnings("ignore")
pd.set_option("display.width", 160)
pd.set_option("display.max_columns", 20)
def clean(ax, grid_axis="both"):
"""Recessive grid and axes; text in neutral ink."""
ax.grid(axis=grid_axis, color="#e4e3df", linewidth=0.8)
ax.set_axisbelow(True)
for side in ("top", "right"):
ax.spines[side].set_visible(False)
ax.tick_params(colors="#52514e")
import statsforecast # noqa: E402
print(
"AutoHybTS",
autohybts.__version__,
"| StatsForecast",
statsforecast.__version__,
"| pandas",
pd.__version__,
)
AutoHybTS 0.0.1 | StatsForecast 2.1.1 | pandas 2.3.3
1. Dados e protocolo¶
Série sintética diária (tendência, sazonalidade semanal, ruído AR(0,7)), fh=14, 3 origens em V_search e 3 em V_select, semente 42. O teste selado (últimos 14 dias) só é lido no fim.
y = get_data("synthetic_daily", n=500)
exp = AutoHybridExperiment(fh=14, fold=3, session_id=42).fit(y)
exp.splits
| layer | train_start | origin | first_target | last_target | n_train | horizon | |
|---|---|---|---|---|---|---|---|
| fold_id | |||||||
| search-0 | search | 2024-01-01 | 2025-02-05 | 2025-02-06 | 2025-02-19 | 402 | 14 |
| search-1 | search | 2024-01-01 | 2025-02-19 | 2025-02-20 | 2025-03-05 | 416 | 14 |
| search-2 | search | 2024-01-01 | 2025-03-05 | 2025-03-06 | 2025-03-19 | 430 | 14 |
| select-0 | select | 2024-01-01 | 2025-03-19 | 2025-03-20 | 2025-04-02 | 444 | 14 |
| select-1 | select | 2024-01-01 | 2025-04-02 | 2025-04-03 | 2025-04-16 | 458 | 14 |
| select-2 | select | 2024-01-01 | 2025-04-16 | 2025-04-17 | 2025-04-30 | 472 | 14 |
2. Cada modelo nas mesmas origens¶
Os quatro do StatsForecast com season_length=7, contra os modelos do statsmodels (arima, exponential_smoothing com Holt-Winters semanal) e o seasonal_naive. MASE média por camada e segundos de avaliação (as 6 origens).
candidates = {
"seasonal_naive": {},
"exponential_smoothing": {"season_length": 7},
"arima": {},
"sf_auto_ets": {"season_length": 7},
"sf_auto_arima": {"season_length": 7},
"sf_auto_theta": {"season_length": 7},
"sf_auto_ces": {"season_length": 7},
}
rows = []
for name, params in candidates.items():
started = time.perf_counter()
frame = exp.create_model(name, **params).evaluation.to_frame()
rows.append(
{
"modelo": name,
"MASE V_search": frame.loc[frame["layer"] == "search", "MASE"].mean(),
"MASE V_select": frame.loc[frame["layer"] == "select", "MASE"].mean(),
"segundos": time.perf_counter() - started,
}
)
scores = pd.DataFrame(rows).set_index("modelo")
scores.round(3).sort_values("MASE V_select")
| MASE V_search | MASE V_select | segundos | |
|---|---|---|---|
| modelo | |||
| arima | 0.916 | 0.877 | 47.166 |
| exponential_smoothing | 0.914 | 0.881 | 8.885 |
| sf_auto_arima | 0.874 | 0.888 | 162.075 |
| sf_auto_ets | 0.888 | 0.891 | 13.675 |
| sf_auto_ces | 0.914 | 0.912 | 0.382 |
| sf_auto_theta | 0.908 | 0.964 | 0.638 |
| seasonal_naive | 1.288 | 1.522 | 0.022 |
order = scores.sort_values("MASE V_select").index[::-1]
colors = ["#2a78d6" if name.startswith("sf_") else "#c3c2b7" for name in order]
fig, ax = plt.subplots(figsize=(8, 3.6))
ax.barh(order, scores.loc[order, "MASE V_select"], color=colors, height=0.6)
for row, value in enumerate(scores.loc[order, "MASE V_select"]):
ax.text(value, row, f" {value:.2f}", va="center", fontsize=8, color="#0b0b0b")
ax.set_xlabel("MASE média em V_select (azul: StatsForecast; cinza: referências)", color="#52514e")
ax.set_title("Mesmas origens, mesmo horizonte", loc="left", fontsize=10)
clean(ax, grid_axis="x")
fig.tight_layout()
3. Parâmetros congelados¶
Na variante B e nos resíduos fora da amostra, um modelo ajustado prevê a partir de contextos mais longos sem ser reajustado. O adaptador usa forward do StatsForecast: no próprio treino ele reproduz a previsão do ajuste; com 20 observações a mais, só os estados mudam (nenhum ajuste é contado), e o resultado difere de um reajuste completo.
values = y.to_numpy()
rows = []
for name in ["sf_auto_ets", "sf_auto_arima"]:
model = exp.registry_.make_base(name, {"season_length": 7}).fit(values[:400])
with count_fits() as fits:
frozen = model.predict(7, local_context(values, 420))
refit = exp.registry_.make_base(name, {"season_length": 7}).fit(values[:420])
rows.append(
{
"modelo": name,
"ajustes ao prever": fits.total,
"previsão congelada (h=1)": frozen[0],
"após reajuste (h=1)": refit.predict(7, local_context(values, 420))[0],
"observado": values[420],
}
)
pd.DataFrame(rows).set_index("modelo").round(3)
| ajustes ao prever | previsão congelada (h=1) | após reajuste (h=1) | observado | |
|---|---|---|---|---|
| modelo | ||||
| sf_auto_ets | 0 | 64.702 | 64.738 | 64.164 |
| sf_auto_arima | 0 | 64.239 | 64.267 | 64.164 |
4. Os híbridos estatístico-ANN da replicação¶
O projeto de replicação usa AutoETS, AutoARIMA e AutoSARIMA com um MLP de 5 neurônios logísticos sobre os resíduos fora da amostra. As mesmas configurações viram especificações da AutoHybTS; scale="minmax" e feature_order="lag" reproduzem o corretor do projeto (a paridade exata é testada em tests/regression/test_legacy_parity.py).
ann = {
"scale": "minmax",
"feature_order": "lag",
"random_state": 42,
"estimator_params": {
"hidden_layer_sizes": [5],
"activation": "logistic",
"solver": "lbfgs",
"max_iter": 2000,
"alpha": 0.0001,
"tol": 0.0001,
},
}
bases = {
"ETS-ANN": ("sf_auto_ets", {"season_length": 7, "model_params": {"model": "ZZZ"}}),
"ARIMA-ANN": ("sf_auto_arima", {"season_length": 1, "model_params": {"seasonal": False}}),
"SARIMA-ANN": ("sf_auto_arima", {"season_length": 7, "model_params": {"seasonal": True}}),
}
rows = []
for label, (base, params) in bases.items():
alone = exp.create_model(base, **params).evaluation.to_frame()
hybrid = exp.create_hybrid(base, "mlp", window=7, base_params=params, corrector_params=ann)
frame = hybrid.evaluation.to_frame()
rows.append(
{
"híbrido": label,
"MASE base (V_select)": alone.loc[alone["layer"] == "select", "MASE"].mean(),
"MASE híbrido (V_select)": frame.loc[frame["layer"] == "select", "MASE"].mean(),
"variabilidade": hybrid.evaluation.trials[0].diagnostics["correction_std_ratio"],
}
)
pd.DataFrame(rows).set_index("híbrido").round(3)
| MASE base (V_select) | MASE híbrido (V_select) | variabilidade | |
|---|---|---|---|
| híbrido | |||
| ETS-ANN | 0.891 | 0.882 | 0.303 |
| ARIMA-ANN | 1.284 | 1.317 | 0.293 |
| SARIMA-ANN | 0.888 | 0.875 | 0.280 |
5. Busca com os modelos do StatsForecast¶
Passados em base_models, entram na busca como os demais: o período sazonal vem do experimento.
search = AutoHybridExperiment(
fh=14,
fold=3,
session_id=42,
budget="small",
base_models=["seasonal_naive", "linear", "sf_auto_ets", "sf_auto_theta", "sf_auto_ces"],
correctors=["linear", "mlp"],
residual_windows=[14],
alphas=[1.0],
).fit(y)
result = search.compare_models()
print(f"candidatas: {result.search.trial_count}, ajustes: {result.search.fit_count}")
print("campeão:", result.best.label)
result.decisions[
[
"family",
"champion_before",
"challenger",
"accepted",
"reason_codes",
"median_gain",
"win_rate",
]
]
candidatas: 13, ajustes: 93
campeão: sf_auto_ets(season_length=7)
| family | champion_before | challenger | accepted | reason_codes | median_gain | win_rate | |
|---|---|---|---|---|---|---|---|
| 0 | linear | sf_auto_ets | linear | False | (median_gain_below_threshold, win_rate_below_t... | -0.636219 | 0.333333 |
| 1 | residual | sf_auto_ets | sf_auto_ets-linear | False | (median_gain_below_threshold,) | 0.009861 | 0.666667 |
6. Avaliação externa¶
Uma única leitura do teste selado, depois de congelar a escolha.
final = search.finalize_model(result.best)
search.evaluate_external(final).metrics.round(3)
| MASE | MAE | RMSE | sMAPE | |
|---|---|---|---|---|
| model | ||||
| sf_auto_ets(season_length=7) | 0.68 | 1.064 | 1.529 | 0.016 |
7. Leitura¶
Nesta execução (500 dias, fh=14, 3 origens por camada — fold=3 explícito, para o notebook ser rápido —, StatsForecast 2.1.1 com pandas 2.3):
Os automáticos do StatsForecast empatam com os do statsmodels. Em
V_select:arima(statsmodels) 0,877,exponential_smoothing0,881,sf_auto_arima0,888,sf_auto_ets0,891,sf_auto_ces0,912 esf_auto_theta0,964, todos muito à frente doseasonal_naive(1,52). Diferenças de 1% com 3 origens não separam os modelos.Custo. O
sf_auto_arimalevou 162 s nas 6 origens (busca de ordem sazonal em cada uma), contra 47 s doarimado statsmodels;sf_auto_etseexponential_smoothinglevaram 9–14 s;sf_auto_cesesf_auto_theta, menos de 1 s. Os tempos foram medidos com outras execuções em paralelo; servem para comparar os modelos entre si.Parâmetros congelados. Prevendo 20 dias depois do treino, nenhum ajuste é feito; a previsão congelada difere da de um reajuste completo em ~0,03 (ETS 64,70 contra 64,74; observado 64,16): o modelo incorpora as observações novas pelos estados, como a variante B exige.
Híbridos estatístico-ANN da replicação. O MLP residual melhorou pouco as bases sazonais (ETS 0,891 → 0,882; SARIMA 0,888 → 0,875, ~1–1,5%) e piorou a não sazonal (ARIMA 1,284 → 1,317): sem o termo sazonal, o erro semanal sobra para um corretor de janela 7 que não o recupera.
Na busca (13 candidatas, 93 ajustes), o campeão foi
sf_auto_ets. O híbridosf_auto_ets-linearvenceu 2 de 3 unidades, mas com ganho mediano de 0,99%, abaixo do mínimo de 5% da regra padrão (ADR-0008): recusado. No teste selado, o campeão teve MASE 0,68.
Os modelos do StatsForecast ampliam as bases estatísticas sem mudar o protocolo; nesta série, não superaram os equivalentes já presentes. Use-os quando a busca automática de ordem e de componentes compensar o custo, sempre comparados com as referências nas mesmas origens.