Diagnóstico da série¶
Antes de escolher modelos, vale saber se a série tem um ciclo regular. A sazonalidade
decide quais modelos fazem sentido: seasonal_naive, exponential_smoothing
sazonal, a decomposição stl e o sarima só ajudam quando o ciclo existe. Esta
página mostra o diagnóstico exploratório e como a busca pode usá-lo sem vazamento.
O notebook 16. Diagnóstico da série: sazonalidade, estacionariedade e diferenças percorre tudo com
figuras.
Outliers por intervenção: opção do usuário¶
A detecção é opcional: deixe de chamar outlier_report se não quiser usá-la.
Ela não altera a série nem ativa tratamento na busca.
exp = AutoHybridExperiment(fh=14).fit(y)
eventos = exp.outlier_report() # opcional: AO, LS e TC
eventos = exp.outlier_report(types=("AO", "TC"), threshold=4.0, decay=0.7)
AO é um pulso isolado; LS, uma mudança persistente de nível;
TC, uma mudança que decai geometricamente. O relatório informa série, posição
(a partir de zero), data, tipo candidato, efeito estimado e escore exploratório.
recommendation sugere revisar o evento antes de tratar. Sem candidatos,
retorna uma tabela vazia com as mesmas colunas. Em painéis, use series_id
para inspecionar uma série; sem esse argumento, o relatório reúne todas.
A triagem ajusta controles de tendência linear e posição no ciclo e acrescenta
iterativamente a intervenção com maior escore, até max_events=10 ou até não
haver escore acima de threshold. O escore usa a escala robusta dos resíduos;
não é um p-valor nem um teste ajustado por ARIMA ou por múltiplas comparações.
O método pode confundir autocorrelação, tendências não lineares e eventos reais
com outliers. Requer 20 observações e três ciclos; LS exige cinco observações
em cada lado. Eventos perto do fim podem ser difíceis de distinguir entre tipos.
Os coeficientes e escores registram o momento da seleção de cada candidato.
As formas das intervenções seguem a
documentação do tsoutliers;
esta triagem não implementa o procedimento ARIMA completo desse pacote.
Recomendação: use o relatório quando houver suspeita de erros de medição, choques pontuais ou mudanças de regime. Confirme os eventos pelo conhecimento do domínio: uma mudança de nível pode precisar ser modelada, preservando seu efeito futuro. O usuário escolhe manter os dados ou avaliar um tratamento. Compare as alternativas por validação temporal, detectando e ajustando qualquer tratamento apenas no treino de cada origem. Este relatório usa o histórico de desenvolvimento completo, excluindo o teste selado, e serve para exploração; não use seus eventos diretamente para tratar todas as dobras da validação.
Sazonalidade¶
exp = AutoHybridExperiment(fh=14).fit(y)
exp.seasonality_report() # período do experimento
exp.seasonality_report(periods=[7, 30])
Uma linha por série e período candidato, calculada no histórico de desenvolvimento (nunca no teste selado):
n_cyclesciclos completos disponíveis. Com menos de 3, não há teste.
strengthforça da sazonalidade,
F_s = max(0, 1 − Var(R)/Var(S + R)), de uma decomposição clássica (média móvel centrada de um ciclo para a tendência, médias por posição para a sazonalidade; Wang, Smith e Hyndman, 2006). Perto de 1: ciclo dominante; perto de 0: nenhum.autocorrelationautocorrelação da série sem tendência no lag do período.
p_valueteste de Friedman da série sem tendência: cada ciclo é um bloco e cada posição no ciclo, um tratamento, de modo que mudanças lentas entre ciclos não contam como sazonalidade.
seasonaldecisão da regra:
p_value < 0,05com ao menos 3 ciclos.seasonal_differencesdiferenças sazonais sugeridas: 1 quando há sazonalidade e
strength ≥ 0,64(a regra donsdiffsdo pacote Rforecast).
Nas séries sintéticas da biblioteca (730 dias, período 7):
Série |
|
|
|
|
|
|---|---|---|---|---|---|
|
102,3 |
0,87 |
1,6 × 10⁻¹¹⁴ |
sim |
1 |
|
102,3 |
0,01 |
0,81 |
não |
0 |
Em 200 séries simuladas por cenário, sem sazonalidade, a regra declarou um ciclo em 2–9 % das séries com ruído independente (com ou sem tendência linear) e em até 11 % com ruído persistente (AR(0,8) ou passeio aleatório), contra os 5 % nominais. Com um ciclo de amplitude igual ao desvio do ruído, detectou 68–100 % das séries a partir de 6 ciclos e quase nenhuma com 3. O poder depende do número de ciclos: com poucos, não rejeitar não é evidência de que o ciclo não existe.
Sazonalidade na busca: seasonal_period="auto"¶
Por padrão, o período vem da frequência (diária: 7, mensal: 12, anual: 1) e os modelos
sazonais o recebem sempre. Com seasonal_period="auto", a busca testa antes:
exp = AutoHybridExperiment(fh=14, seasonal_period="auto").fit(y)
exp.seasonal_period_ # 7: período das métricas (escala da MASE), sempre o da frequência
exp.search_period_ # 7 com ciclo; 1 sem evidência de ciclo
exp.seasonality_ # o teste que decidiu
O teste usa só o treino da primeira origem de validação: dados que todas as origens
já viram, então a decisão não depende de nenhum alvo avaliado. Se esse treino tem ao
menos 6 ciclos e o teste não mostra ciclo, search_period_ vale 1: seasonal_naive,
sarima e a STL ficam SKIPPED com o motivo no ledger, e os demais modelos recebem
a versão não sazonal. Com menos de 6 ciclos o teste quase não tem poder, e o período da
frequência fica (coluna keep_period de seasonality_). A escala da MASE
continua a da frequência, para os erros serem comparáveis com e sem "auto". Num
painel, o ciclo vale quando ao menos metade das séries o mostra.
Com os dados acima, synthetic_daily manteve o período 7 (p ≈ 3 × 10⁻⁸² no treino da
primeira origem) e white_noise_daily passou a 1 (p = 0,59).
Aviso
O teste é decidido no treino da primeira origem, que pode ser curto. No
air_passengers (mensal, fh=12), esse treino tem só 3 ciclos: o teste deu
p = 0,039 e o período 12 ficaria de qualquer forma, pela regra dos 6 ciclos. Em séries
curtas, "auto" quase sempre mantém o período da frequência; prefira um
seasonal_period explícito, escolhido pelo conhecimento do domínio.
Caracterização (regras do projeto de replicação)¶
exp.characterize() aplica ao histórico de desenvolvimento as duas regras do projeto de
replicação, com o mesmo resultado (teste de paridade com o código legado):
Estacionariedade (ADF), com constante e lags pelo AIC: a série é estacionária quando
p ≤ 0,05e a estatística fica abaixo do valor crítico de 5 %. Se não for,diff_orderé a menor ordem até 2 que passa. O KPSS de cada ordem também é informado, sem entrar na decisão.Sazonalidade, só para as séries não estacionárias: força
F_Sde uma STL robusta sobre a série transformada por Box-Cox (λ por máxima verossimilhança entre −1 e 2), para cada período candidato com ao menos dois ciclos. O de maior força é o período dominante, e a série é sazonal quandoF_S > 0,5.
exp.characterize() # período do experimento
exp.characterize(periods=[7, 365]) # diária: semana e ano, como na replicação
A categoria resume as duas regras: stationary, nonseasonal ou seasonal. Nas
séries sintéticas (histórico de 716 dias):
Série |
ADF (nível) |
p ADF d=0 |
p ADF d=1 |
|
|
categoria |
|---|---|---|---|---|---|---|
|
−0,75 |
0,83 |
< 0,001 |
1 |
0,84 |
|
|
−25,0 |
< 0,001 |
— |
0 |
— |
|
Com seasonal_period="auto", seasonality_test="strength" troca o teste de Friedman
pela regra 2: uma série que o ADF considera estacionária não tem sazonalidade, e as
demais precisam de F_S > 0,5. Sem dois ciclos a força é indefinida e o período da
frequência fica.
Gráficos¶
Todos usam o histórico de desenvolvimento, nunca o teste selado (extras plot e
stats):
exp.plot_data("stationarity")a série em nível, com uma e com duas diferenças, com os p-valores do ADF e do KPSS em cada painel e a ordem escolhida pelo ADF destacada.
exp.plot_data("acf_pacf")ACF e PACF na ordem escolhida, com a faixa ±1,96/√n. A PACF é a de Yule-Walker com autocovariâncias enviesadas (a
method="ywm"do statsmodels).exp.plot_data("decomposition")tendência, sazonalidade e resíduo da STL robusta da série Box-Cox, no período do experimento.
exp.plot_characterization()estatística ADF contra
F_S, um ponto por série, colorido pela categoria; útil num painel.
Estacionariedade: ADF e KPSS¶
exp.stationarity_report() testa o nível e as diferenças até max_d (padrão 2) com
os dois testes, que têm hipóteses nulas opostas: o ADF rejeita a raiz unitária quando
p ≤ 0,05 e a estatística fica abaixo do crítico de 5 %; o KPSS diz estacionária
quando não rejeita (p ≥ 0,05). consensus marca as ordens em que os dois
concordam, e attrs traz a menor ordem de cada regra (d_adf, d_kpss,
d_consensus). Na synthetic_daily:
d adf_stat adf_pvalue adf_stationary kpss_pvalue kpss_stationary consensus
0 -0.749 0.834 False 0.01 False False
1 -11.128 < 0.001 True 0.10 True True
2 -12.048 < 0.001 True 0.10 True True
{'d_adf': 1, 'd_kpss': 1, 'd_consensus': 1}
O KPSS informa p-valores só entre 0,01 e 0,10 (fora disso, o limite da tabela); com poucas observações, os dois testes têm pouco poder.
Decomposição em tabela¶
exp.decompose("stl") (STL robusta) ou exp.decompose("classical") (média móvel
centrada e índices sazonais médios) devolvem observed, trend, seasonal e
resid indexados pela data, no período do experimento (ou period=). Na clássica, a
tendência é NaN nas pontas. É exploratório: a estratégia decomposition refaz a
decomposição no treino de cada origem.
Resíduos de uma base: Ljung-Box¶
exp.residual_diagnostics("arima") ajusta a base na primeira metade do histórico de
desenvolvimento e prevê a outra metade um passo por vez, com parâmetros congelados: são
os resíduos fora da amostra de que os corretores aprendem. O teste de Ljung-Box diz se
sobrou autocorrelação neles; p pequeno indica espaço para um corretor residual. Lags
padrão: min(10, n/5) e duas vezes o período sazonal. Na synthetic_daily (358
resíduos):
Base |
p (lag 10) |
p (lag 14) |
|---|---|---|
|
< 0,001 |
< 0,001 |
|
< 0,001 |
< 0,001 |
|
0,070 |
0,110 |
Os resíduos do naive e do seasonal_naive carregam a estrutura que a base não
modelou; os do arima, quase nenhuma. Um teste sem rejeição não garante que um
corretor seja inútil: a regra de aceite decide nas origens de validação.
Diferenças automáticas¶
Modelos sobre defasagens (linear, KNN, SVR, árvores, MLP, boosting e as redes
registradas) aprendem melhor numa série sem tendência nem raiz unitária. Com
differencing="auto", cada origem escolhe, no seu próprio vetor de treino:
D: uma diferença sazonal (y[t] − y[t − m]) quando o teste acima encontra um ciclo significativo comstrength ≥ 0,64;d: a menor ordem de 0 a 2 em que a série (já com a diferença sazonal) passa no teste dedifferencing_test:"kpss"(padrão): o KPSS não rejeita a estacionariedade a 5 %, como ondiffsdoauto.arimae oarimadesta biblioteca;"adf": o ADF rejeita a raiz unitária (p ≤ 0,05e estatística abaixo do valor crítico de 5 %), a regra do projeto de replicação.
As ordens escolhidas ficam congeladas: a previsão integra de volta a partir dos valores
observados no contexto, sem refazer os testes (variante B incluída). Os testes vêm do
statsmodels (extra stats); sem ele, a candidata fica SKIPPED com o motivo.
# um modelo
exp.create_model("knn", differencing="auto", season_length=7)
exp.create_model("linear", differencing=1, seasonal_differencing=1, season_length=12)
# todos os modelos sobre defasagens embutidos da busca
exp = AutoHybridExperiment(fh=14, differencing="auto", differencing_test="adf")
A opção do experimento vale para os modelos embutidos da busca (compare_models) e usa
o período da busca (search_period_); create_model usa só os parâmetros que você
passa, e uma rede registrada recebe differencing="auto" no registro
(torch_model(LSTMNet, differencing="auto")). O padrão
(differencing=None) não diferencia, e os IDs das candidatas não mudam.
Pré-processamento opcional: Box-Cox e outliers¶
Desligados por padrão; o usuário liga por modelo ou para todos os modelos sobre defasagens da busca. Tudo é ajustado no treino de cada origem, e os alvos das métricas continuam os valores observados.
transform="boxcox"ou"log"λ por máxima verossimilhança no treino (com deslocamento quando há valores ≤ 0), congelado; o contexto da previsão passa pela mesma transformação e a previsão volta à escala original. Aplicado antes das diferenças.
outliers="winsorize"ou"interpolate"pontos com z robusto acima de 4 (em módulo) contra uma mediana móvel são cortados no limiar ou substituídos por interpolação, só nos valores com que o modelo aprende. O contexto da previsão não é alterado.
exp.create_model("linear", lags=12, transform="boxcox", differencing="auto", season_length=12)
AutoHybridExperiment(fh=12, transform="boxcox", outliers="winsorize") # toda a busca
No air_passengers (mensal, 4 origens), o linear sobre 12 lags teve MASE 0,822; com
transform="log", 0,881; com "boxcox", 0,927; com Box-Cox e differencing="auto",
0,773. A transformação sozinha não melhorou: combinada com as diferenças, sim. Use-a como
mais uma opção a validar, não como padrão.
Faltantes¶
Por padrão (missing="reject") uma série com valores ausentes é recusada no fit.
Com AutoHybridExperiment(missing="ffill"), cada lacuna recebe o último valor
observado (só o passado é usado, sem vazamento) e as lacunas do início são descartadas.
Os pontos preenchidos entram nas métricas como observados: prefira tratar faltantes
longos com o critério do seu domínio.
ARIMA sazonal¶
O sarima (extra stats) é o ARIMA sazonal do statsmodels com escolha automática no
treino de cada origem, como o auto.arima (Hyndman e Khandakar, 2008):
D(0 ou 1) pela regra acima: ciclo significativo estrength ≥ 0,64;d(até 2) por testes KPSS sucessivos na série com a diferença sazonal;(p, q, P, Q)por AICc numa busca stepwise a partir de quatro modelos iniciais, comp, q ≤ 2eP, Q ≤ 1.
exp = AutoHybridExperiment(fh=12, base_models=["seasonal_naive", "arima", "sarima"])
exp.create_model("sarima", season_length=12)
exp.create_model("sarima", season_length=12, order=(1, 1, 0), seasonal_order=(0, 1, 1))
A ordem e os parâmetros ficam congelados depois do ajuste: um contexto mais longo
(variante B, resíduos fora da amostra) só atualiza o estado do filtro de Kalman. O
sarima está fora da busca padrão (entra por base_models) e recusa períodos acima
de 24, lentos demais para uma busca; para ciclos longos use harmonic_regression. O
arima continua não sazonal.
Medido em 13 de outubro de 2026 com create_model e os padrões do experimento (MASE
médio nas origens de validação e tempo total):
Série |
|
|
|
|
|---|---|---|---|---|
|
1,27 · 0 s |
1,30 · 6 s |
0,68 · 0,5 s |
0,60 · 12 s |
|
1,13 · 0 s |
1,13 · 26 s |
1,06 · 1,5 s |
0,76 · 310 s |
O ganho veio com custo: numa série diária de 730 pontos, cada origem levou cerca de 22 s.
Por isso o sarima fica fora da busca padrão até ser medido em mais séries.