04. Variante B: previsão rolling de um passo¶
Na variante B cada previsão é de um passo, e o valor real chega logo depois. Os parâmetros ficam congelados:
prever
t+1usando só o que foi observado atét;observar
y[t+1];atualizar a janela de resíduos com o erro da base naquele instante;
avançar, sem chamar
fit.
A seleção usa o erro de um passo na validação. A seleção dinâmica de corretores só é permitida nesta variante.
import autohybts
print("AutoHybTS", autohybts.__version__)
AutoHybTS 1.0.0rc2
1. Experimento na variante B¶
from autohybts import AutoHybridExperiment
from autohybts.datasets import get_data
y = get_data("synthetic_daily")
exp = AutoHybridExperiment(fh=1, variant="B", test_size=28, session_id=42).fit(y)
result = exp.compare_models()
result.leaderboard
| name | model | family | strategy | base | corrector | residual_source | window | alpha | mean_rank | ... | MAE | RMSE | sMAPE | coverage | n_units | n_failed | n_skipped | fit_seconds | predict_seconds | is_champion | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| candidate_id | |||||||||||||||||||||
| f197a4a852eefc42b77e9d063dff3c84cf252feda2deb62c878faf84713a4449 | arima | arima | statistical | standalone | arima | NaN | NaN | NaN | NaN | 1.0 | ... | 0.883120 | 0.883120 | 0.012429 | 1.0 | 7 | 0 | 0 | 16.643486 | 0.012792 | True |
| dcc94aa07bedae7630b1f38c2570937852793c439f560bfd9098cbedf9650690 | arima-linear | arima + linear [w=7, alpha=0.5, historical_oof... | residual | residual_additive | arima | linear | historical_oof_1step | 7.0 | 0.5 | 2.0 | ... | 0.885318 | 0.885318 | 0.012459 | 1.0 | 7 | 0 | 0 | 4.903926 | 0.012789 | False |
| 0ec158f3b6ec517f088da25b7c2f75c10bfe627d7bf5437f0622b8ff36cca85a | arima-linear | arima + linear [w=7, alpha=0.5, in_sample] | residual | residual_additive | arima | linear | in_sample | 7.0 | 0.5 | 3.0 | ... | 0.886370 | 0.886370 | 0.012471 | 1.0 | 7 | 0 | 0 | 0.262579 | 0.013400 | False |
| 50c3b10b84c20c32b6b898008596592cb4f651f3f73110b54230461381267f3e | arima-linear | arima + linear [w=7, alpha=1.0, historical_oof... | residual | residual_additive | arima | linear | historical_oof_1step | 7.0 | 1.0 | 4.0 | ... | 0.891898 | 0.891898 | 0.012552 | 1.0 | 7 | 0 | 0 | 0.383705 | 0.014258 | False |
| c51a658f4db43de6316044da15702a7d11e9e319df05363c4688b9ba00547929 | mlp | mlp(lags=7) | neural | standalone | mlp | NaN | NaN | NaN | NaN | 5.0 | ... | 0.892754 | 0.892754 | 0.012454 | 1.0 | 7 | 0 | 0 | 0.654276 | 0.002061 | False |
| 4a92d125bd10f30ce4cdf0fc07e114c1e0d07bbd03ca06dd5a8c04c2781bfd4b | svm | svm(lags=7) | kernel | standalone | svm | NaN | NaN | NaN | NaN | 6.0 | ... | 0.926790 | 0.926790 | 0.013126 | 1.0 | 7 | 0 | 0 | 0.102579 | 0.002965 | False |
| c05fbfc4191206898692474cb6edb6c95948dcd38fd203e4d6be47b604035efe | linear | linear(lags=7) | linear | standalone | linear | NaN | NaN | NaN | NaN | 7.0 | ... | 0.939771 | 0.939771 | 0.013160 | 1.0 | 7 | 0 | 0 | 1.025291 | 0.001306 | False |
| 3e1cf077f43789200b40b04ab5b6098eb7f916132b72ca9bbd6f6f8eba5f3d02 | arima-linear | arima + linear [w=7, alpha=0.5, historical_oof... | residual | residual_additive | arima | linear | historical_oof_multistep | 7.0 | 0.5 | 8.0 | ... | 0.958163 | 0.958163 | 0.013517 | 1.0 | 7 | 0 | 0 | 9.948506 | 0.012775 | False |
| 1984f459ddd73973f8dee0312933d0c5d4a4ae9ff5e3e8a400a30bd0b87baff5 | linear-linear | linear(lags=7) + linear [w=7, alpha=0.5, histo... | residual | residual_additive | linear | linear | historical_oof_1step | 7.0 | 0.5 | 9.0 | ... | 1.002286 | 1.002286 | 0.013990 | 1.0 | 7 | 0 | 0 | 0.509810 | 0.002312 | False |
| a02b2e3d287a824a49ab59fb6ba512b63b07cdd1f99e4dcfaef443617e0f77f2 | knn | knn(lags=7) | kernel | standalone | knn | NaN | NaN | NaN | NaN | 10.0 | ... | 1.235898 | 1.235898 | 0.017431 | 1.0 | 7 | 0 | 0 | 0.023858 | 0.003429 | False |
| 17770e7b1955f67c1ede0c43e5f28a54c77852422c234d23ec6475da52e8e57f | seasonal_naive-linear | seasonal_naive(season_length=7) + linear [w=7,... | residual | residual_additive | seasonal_naive | linear | historical_oof_1step | 7.0 | 0.5 | 11.0 | ... | 1.300770 | 1.300770 | 0.018164 | 1.0 | 7 | 0 | 0 | 0.232959 | 0.001532 | False |
| c4029ff35105b1163f36fe6625239a1a03d36530d6c10868247d7c6805cbd2dc | random_forest | random_forest(lags=7) | tree | standalone | random_forest | NaN | NaN | NaN | NaN | 12.0 | ... | 1.304235 | 1.304235 | 0.018459 | 1.0 | 7 | 0 | 0 | 1.621146 | 0.034359 | False |
| 76a20a6a4683ebf0031f39dfbf4c574bfad03f3308fc2bd37794a8357bb49fe3 | mlp-linear | mlp(lags=7) + linear [w=7, alpha=1.0, historic... | residual | residual_additive | mlp | linear | historical_oof_1step | 7.0 | 1.0 | 13.0 | ... | 1.367125 | 1.367125 | 0.019047 | 1.0 | 7 | 0 | 0 | 1.732240 | 0.003291 | False |
| 49ccd436b98e419cc4ca07ac9cd7fdcefc5781cd060799f3dbf68a665a7b933f | mlp-linear | mlp(lags=7) + linear [w=7, alpha=0.5, historic... | residual | residual_additive | mlp | linear | historical_oof_1step | 7.0 | 0.5 | 14.0 | ... | 1.376205 | 1.376205 | 0.019121 | 1.0 | 7 | 0 | 0 | 1.822151 | 0.003041 | False |
| c295647b2c1fbb87604afc4149027b096db438f6718f8b9412ffedd8aad29157 | seasonal_naive | seasonal_naive(season_length=7) | statistical | standalone | seasonal_naive | NaN | NaN | NaN | NaN | 15.0 | ... | 1.477611 | 1.477611 | 0.020579 | 1.0 | 7 | 0 | 0 | 0.000117 | 0.000214 | False |
| 7f28194267d2be6365845089c32eeca9ca2fb3704620ade51e8058aeed4565a2 | naive | naive | statistical | standalone | naive | NaN | NaN | NaN | NaN | 16.0 | ... | 1.536732 | 1.536732 | 0.021733 | 1.0 | 7 | 0 | 0 | 0.000151 | 0.000211 | False |
| 76e16eb5ecda291262338e982ea0ed8f13261f5a94fd8c79f009b769d5221f15 | tree | tree(lags=7) | tree | standalone | tree | NaN | NaN | NaN | NaN | 17.0 | ... | 2.901353 | 2.901353 | 0.041320 | 1.0 | 7 | 0 | 0 | 0.028939 | 0.002017 | False |
| c8badc4b2e97c34ef97f28939bc5fcb4e4c42719296d9904f9b123a4323c69d8 | linear-linear | linear(lags=7) + linear [w=7, alpha=0.5, histo... | residual | residual_additive | linear | linear | historical_oof_multistep | 7.0 | 0.5 | 18.0 | ... | 3.842352 | 3.842352 | 0.052033 | 1.0 | 7 | 0 | 0 | 0.469302 | 0.003070 | False |
| 98b19ebecc5279fb4a58c00a60fb3fe8cf909807de0d606c01da4aeb1c709ea9 | random_forest-linear | random_forest(lags=7) + linear [w=7, alpha=0.5... | residual | residual_additive | random_forest | linear | historical_oof_1step | 7.0 | 0.5 | 19.0 | ... | 4.703941 | 4.703941 | 0.063774 | 1.0 | 7 | 0 | 0 | 16.516583 | 0.045515 | False |
| 7b24b87c2c136c2d8a3169062b443453476a1d6a1af538d70df1703ef02d07fb | knn-linear | knn(lags=7) + linear [w=7, alpha=0.5, historic... | residual | residual_additive | knn | linear | historical_oof_1step | 7.0 | 0.5 | 20.0 | ... | 5.246151 | 5.246151 | 0.071193 | 1.0 | 7 | 0 | 0 | 1.258855 | 0.005130 | False |
| 18af1beb3e968d119f44a9d569f1e925a8c602f81f9c4f9a7a84c2beadc51b87 | tree-linear | tree(lags=7) + linear [w=7, alpha=0.5, histori... | residual | residual_additive | tree | linear | historical_oof_1step | 7.0 | 0.5 | 21.0 | ... | 5.824911 | 5.824911 | 0.078668 | 1.0 | 7 | 0 | 0 | 0.739781 | 0.003367 | False |
| 59ef79be199fa121bbc4e6932275347ac6c2f278530a9a6d325f71f5e7835d70 | svm-linear | svm(lags=7) + linear [w=7, alpha=0.5, historic... | residual | residual_additive | svm | linear | historical_oof_1step | 7.0 | 0.5 | 22.0 | ... | 8.065861 | 8.065861 | 0.107547 | 1.0 | 7 | 0 | 0 | 0.771446 | 0.003421 | False |
| 6d01dc8782681bfe3e40abb2ee3cd570dd64a065ed22ff051823470d8d542bbc | svm-linear | svm(lags=7) + linear [w=7, alpha=1.0, historic... | residual | residual_additive | svm | linear | historical_oof_1step | 7.0 | 1.0 | 23.0 | ... | 15.960524 | 15.960524 | 0.202087 | 1.0 | 7 | 0 | 0 | 0.760382 | 0.003359 | False |
23 rows × 21 columns
2. Finalizar e avaliar no teste selado¶
final = exp.finalize_model(result.best)
exp.evaluate_external(final).metrics
| MASE | MAE | RMSE | sMAPE | |
|---|---|---|---|---|
| model | ||||
| arima | 0.532694 | 0.83562 | 1.022446 | 0.011846 |
3. Streaming em produção¶
O mesmo modelo, recebendo observações novas uma a uma. Aqui simulamos a chegada dos dados com as últimas 28 observações da série.
import pandas as pd
stream = final.stream()
rows = []
for timestamp, actual in y.iloc[-28:].items():
ticket = stream.predict_next()
rows.append({"timestamp": timestamp, "y_pred": ticket.prediction, "y_true": actual})
stream.observe(actual) # só agora o valor real fica disponível
stream_log = pd.DataFrame(rows).set_index("timestamp")
stream_log["abs_error"] = (stream_log["y_true"] - stream_log["y_pred"]).abs()
stream_log
| y_pred | y_true | abs_error | |
|---|---|---|---|
| timestamp | |||
| 2025-12-03 | 73.163889 | 72.429605 | 0.734284 |
| 2025-12-04 | 70.867025 | 71.246072 | 0.379047 |
| 2025-12-05 | 68.793807 | 70.065617 | 1.271810 |
| 2025-12-06 | 68.582059 | 67.422345 | 1.159713 |
| 2025-12-07 | 68.022263 | 67.361921 | 0.660342 |
| 2025-12-08 | 69.636127 | 70.737740 | 1.101613 |
| 2025-12-09 | 73.005390 | 73.033376 | 0.027986 |
| 2025-12-10 | 73.587046 | 74.455786 | 0.868740 |
| 2025-12-11 | 72.897800 | 72.770706 | 0.127095 |
| 2025-12-12 | 70.278648 | 70.848351 | 0.569702 |
| 2025-12-13 | 69.320746 | 67.974139 | 1.346606 |
| 2025-12-14 | 68.572199 | 68.709667 | 0.137468 |
| 2025-12-15 | 71.028243 | 70.974923 | 0.053320 |
| 2025-12-16 | 73.286105 | 72.268050 | 1.018055 |
| 2025-12-17 | 72.854259 | 71.772094 | 1.082164 |
| 2025-12-18 | 70.232700 | 71.654518 | 1.421817 |
| 2025-12-19 | 69.187838 | 69.077403 | 0.110435 |
| 2025-12-20 | 67.534704 | 66.810274 | 0.724430 |
| 2025-12-21 | 67.376158 | 67.873867 | 0.497709 |
| 2025-12-22 | 70.155440 | 71.791572 | 1.636132 |
| 2025-12-23 | 74.080296 | 71.837857 | 2.242439 |
| 2025-12-24 | 72.402862 | 71.600799 | 0.802063 |
| 2025-12-25 | 70.079941 | 69.988975 | 0.090966 |
| 2025-12-26 | 67.562980 | 69.786655 | 2.223674 |
| 2025-12-27 | 68.301897 | 68.674617 | 0.372720 |
| 2025-12-28 | 69.227426 | 70.105403 | 0.877978 |
| 2025-12-29 | 72.291891 | 71.148468 | 1.143423 |
| 2025-12-30 | 73.327526 | 72.611886 | 0.715640 |
4. Previsto × observado¶
Cada ponto laranja foi previsto antes de o valor preto ser observado; o modelo só viu o passado de cada ponto.
import matplotlib.pyplot as plt
fig, (top, bottom) = plt.subplots(2, 1, figsize=(10, 5.2), sharex=True)
top.plot(stream_log.index, stream_log["y_true"], color="#333333", label="observado")
top.plot(
stream_log.index,
stream_log["y_pred"],
"o-",
color="#DD8452",
markersize=4,
label="previsto (1 passo)",
)
top.set_title("Variante B: previsão de um passo, valor revelado depois")
top.legend()
bottom.bar(stream_log.index, stream_log["abs_error"], color="#4C72B0")
bottom.set_ylabel("erro absoluto")
plt.show()
5. Base e correção a cada passo¶
O campeão aqui é um modelo individual, sem corretor. Para ver o que a variante B faz num híbrido, o arima-linear é avaliado nas mesmas origens, finalizado e transmitido pelos mesmos 28 dias. Cada ForecastTicket traz a previsão da base e a correção; o resíduo observado y − b entra na janela do corretor só depois da observação.
hybrid = exp.create_hybrid("arima", "linear")
hybrid_stream = exp.finalize_model(hybrid).stream()
steps = []
for timestamp, actual in y.iloc[-28:].items():
ticket = hybrid_stream.predict_next()
steps.append(
{
"timestamp": timestamp,
"base": ticket.base_prediction,
"correction": ticket.correction,
"y_pred": ticket.prediction,
"y_true": actual,
}
)
hybrid_stream.observe(actual)
steps = pd.DataFrame(steps).set_index("timestamp")
fig, (top, bottom) = plt.subplots(2, 1, figsize=(10, 5.2), sharex=True)
top.plot(steps.index, steps["y_true"], color="#333333", label="observado")
top.plot(steps.index, steps["base"], "--", color="#4C72B0", label="base (arima)")
top.plot(steps.index, steps["y_pred"], "o-", color="#DD8452", markersize=4, label="base + correção")
top.set_title("Híbrido arima-linear na variante B")
top.legend()
bottom.bar(steps.index, steps["correction"], color="#55A868")
bottom.axhline(0, color="gray", linewidth=0.8)
bottom.set_ylabel("correção")
plt.show()
errors = {
"base": (steps["y_true"] - steps["base"]).abs().mean(),
"base + correção": (steps["y_true"] - steps["y_pred"]).abs().mean(),
}
pd.Series(errors, name="MAE nos 28 dias").round(3)
base 0.836
base + correção 0.803
Name: MAE nos 28 dias, dtype: float64
6. Variantes A e B no mesmo trecho¶
A mesma estrutura campeã, agora na variante A: finalizada no mesmo histórico, ela prevê os 28 dias de uma vez a partir do fim do treino, sem ver nenhum valor desse trecho. Na variante B, cada dia foi previsto com os dias anteriores já observados. É a diferença descrita na página “Variantes A e B” do guia do usuário.
from dataclasses import replace
exp_a = AutoHybridExperiment(fh=28, test_size=28, session_id=42).fit(y)
final_a = exp_a.finalize_model(replace(result.best, variant="A")) # mesma estrutura, variante A
forecast_a = exp_a.predict_model(final_a).predictions["y_pred"]
comparison = pd.DataFrame(
{
"y_true": stream_log["y_true"],
"A (28 passos)": forecast_a.to_numpy(),
"B (1 passo)": stream_log["y_pred"],
}
)
error_a = (comparison["y_true"] - comparison["A (28 passos)"]).abs()
error_b = (comparison["y_true"] - comparison["B (1 passo)"]).abs()
fig, (top, bottom) = plt.subplots(2, 1, figsize=(10, 5.2), sharex=True)
top.plot(comparison.index, comparison["y_true"], color="#333333", label="observado")
top.plot(
comparison.index,
comparison["A (28 passos)"],
"s-",
color="#4C72B0",
markersize=3,
label="variante A",
)
top.plot(
comparison.index,
comparison["B (1 passo)"],
"o-",
color="#DD8452",
markersize=3,
label="variante B",
)
top.set_title(f"{result.best.label}: variante A × variante B")
top.legend()
bottom.plot(comparison.index, error_a, color="#4C72B0", label="erro A")
bottom.plot(comparison.index, error_b, color="#DD8452", label="erro B")
bottom.set_ylabel("erro absoluto")
bottom.legend()
plt.show()
pd.Series({"A (28 passos)": error_a.mean(), "B (1 passo)": error_b.mean()}, name="MAE").round(3)
A (28 passos) 0.791
B (1 passo) 0.836
Name: MAE, dtype: float64
7. Leitura¶
Streaming. O campeão (
arima) previu os 28 dias um passo por vez, sem reajuste, com MAE 0,836 — o mesmo valor da avaliação externa, que na variante B também é feita em rolagem de um passo.Base e correção. No híbrido
arima-linear, a correção ficou abaixo de 0,5 em valor absoluto em todos os dias e baixou o MAE dos 28 dias de 0,836 (só a base) para 0,803; cada correção usou só resíduos já observados.A × B. Neste trecho a variante A teve MAE menor (0,791 contra 0,836). O
arimaescolhido projeta bem o ciclo semanal e a tendência 28 dias à frente, e o erro de A não cresceu com o horizonte; a previsão de um passo reage ao último valor, que carrega o ruído AR(1). A variante B não garante menor erro: ela serve quando cada valor chega antes da próxima previsão. Em séries com componente pouco previsível, o erro de A tende a crescer com o horizonte.Limites. Um único trecho de 28 dias de uma série sintética; a comparação ilustra a mecânica, não estima qual variante erra menos em geral.