Financial Analytics

Previsão com ARIMA e Prophet

Paloma Vaissman Uribe

Insper, PADS

Julio Trecenti

2026

🎯 Objetivos desta Aula

Ao final você será capaz de:

  • Identificar a ordem de um modelo ARIMA via ACF/PACF
  • Estimar e diagnosticar modelos ARIMA com Python
  • Gerar previsões com intervalos de confiança
  • Modelar sazonalidade com SARIMA, Prophet e ARIMAX
  • Comparar os três métodos e escolher o mais adequado

📌 Estrutura da Aula

Parte 1: Modelagem ARIMA

  • Revisão: AR, MA, ARMA
  • Processo integrado I(\(d\))
  • Simulação ARIMA(1,1,0) e IMA(1,1)
  • Metodologia Box-Jenkins
  • Estimação e diagnóstico
  • Previsão com IC e AutoARIMA

Parte 2: Sazonalidade

  • SARIMA, Prophet, ARIMAX
  • Prophet com regressores
  • Aditivo vs Multiplicativo
  • Comparação: AirPassengers

Parte 1 · Modelagem ARIMA

Do processo integrado à previsão

1 · Revisão: AR, MA e ARMA

AR(\(p\)): valor atual depende de \(p\) valores passados:

\[y_t = \phi_0 + \phi_1 y_{t-1} + \cdots + \phi_p y_{t-p} + \varepsilon_t, \quad \varepsilon_t \sim RB(0,\sigma^2)\]

MA(\(q\)): valor atual depende de \(q\) perturbações passadas:

\[y_t = \theta_0 + \varepsilon_t - \theta_1\varepsilon_{t-1} - \cdots - \theta_q\varepsilon_{t-q}, \quad \varepsilon_t \sim RB(0,\sigma^2)\]

ARMA(\(p\),\(q\)): combinação com operador de defasagem \(B\): \[\Phi(B)\, y_t = \alpha + \Theta(B)\, \varepsilon_t\]

Padrão ACF PACF
AR(\(p\)) Decaimento exponencial Corte em \(p\)
MA(\(q\)) Corte em \(q\) Decaimento exponencial
ARMA(\(p\),\(q\)) Decaimento Decaimento

2 · Processo Integrado I(\(d\))

Definição: se \(\Delta^d y_t = (1-B)^d y_t\) é estacionário para \(d \geq 1\), escrevemos \(y_t \sim I(d)\).

ARIMA(\(p\), \(d\), \(q\)): aplica \(d\) diferenças antes de modelar com ARMA(\(p\),\(q\)):

\[\underbrace{(1-B)^d}_{\text{diferenciação}} y_t \sim \text{ARMA}(p,q)\]

  • \(d = 0\): série já estacionária → ARMA direto
  • \(d = 1\): trabalha com variações \(\Delta y_t\) (mais comum)
  • \(d = 2\): variações das variações (raro)

Dica

Use o teste ADF para determinar \(d\).
Sinal visual: ACF no nível que não decai mesmo nos lags altos → I(1).

3 · Simulando ARIMA(1,1,0)

import numpy as np, pandas as pd
from statsmodels.tsa.arima_process import ArmaProcess
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

np.random.seed(123)

# AR(1) estacionário com phi=0.5
ar_params = [1, -0.5]  # [1, -phi1, -phi2, ...]
arma_p = ArmaProcess(ar=ar_params, ma=[1])
diff_y = arma_p.generate_sample(nsample=500)

# Integrar (I=1) → ARIMA(1,1,0)
y  = np.cumsum(diff_y)   # serie nao estacionaria
dy = np.diff(y)           # primeira diferenca → AR(1)

Nota

PACF da 1ª diferença corta no lag 1 → confirma AR(1) após diferenciação.

4 · Simulando IMA(1,1) = ARIMA(0,1,1)

np.random.seed(123)

# MA(1) com theta=0.9
arma_p2 = ArmaProcess(ar=[1], ma=[1, 0.9])
diff_ima = arma_p2.generate_sample(nsample=500)

# Integrar → IMA(1,1)
dates = pd.date_range('2020-01-01', periods=500, freq='D')
ima1  = pd.Series(np.cumsum(diff_ima), index=dates)

Dica

ACF da 1ª diferença corta após lag 1 → MA(1).
\(\Delta y_t \sim\) MA(1) → \(y_t \sim\) ARIMA(0,1,1)

5 · Metodologia Box-Jenkins

5 · Etapa 1: Identificação

  1. Plotar a série: há tendência? Variância crescente?
  2. Teste ADF → determinar \(d\)
  3. ACF da série estacionarizada → corta em \(q\) → MA(\(q\))
  4. PACF da série estacionarizada → corta em \(p\) → AR(\(p\))
  5. AICc: comparar candidatos e selecionar o melhor

Aviso

Os padrões de ACF/PACF raramente são tão limpos na prática.
Use-os como ponto de partida e valide pelos critérios de informação.

5 · Etapa 2: Estimação

from statsmodels.tsa.arima.model import ARIMA

model   = ARIMA(endog=ima1, order=(0, 1, 1))
results = model.fit()
print(results.summary())

5 · Etapa 2: Saída do Modelo

ARIMA(0, 1, 1)          Obs: 500
─────────────────────────────────────
         coef   std err     z   P>|z|
─────────────────────────────────────
ma.L1   0.909     0.019   48.9  0.000
sigma²  1.008     0.066   15.4  0.000
─────────────────────────────────────
AIC = 1425.8    BIC = 1434.2

Nota

\(\hat{\theta}_1 = 0.909\), muito próximo do valor verdadeiro (\(\theta_1 = 0.9\)). Boa estimação!

5 · Etapa 3: Diagnóstico dos Resíduos

from statsmodels.stats.diagnostic import acorr_ljungbox

res = results.resid

# Visualizar ACF dos resíduos
fig, ax = plt.subplots(figsize=(10, 4))
plot_acf(res, lags=24, zero=False, ax=ax,
         title='ACF dos Resíduos: ARIMA(0,1,1)')
plt.show()

5 · Etapa 3: Teste Ljung-Box

# Teste Ljung-Box (H₀: resíduos são ruído branco)
lb = acorr_ljungbox(res, lags=[24], model_df=2, return_df=True)
# p-value > 0.05 → não rejeitamos H₀ → modelo OK ✓

5 · Teste Ljung-Box

\[H_0: \rho_1 = \rho_2 = \cdots = \rho_k = 0 \quad \text{(resíduos = ruído branco)}\]

\[Q = n(n+2)\sum_{j=1}^{k} \frac{\hat{\rho}_j^2}{n-j} \sim \chi^2_{k-m}\]

onde \(m\) = número de parâmetros estimados e \(k\) = número de lags testados.

Resultado Interpretação
p-value > 0.05 Não rejeita H₀ → resíduos são RB → modelo OK ✓
p-value ≤ 0.05 Rejeita H₀ → há autocorrelação → revisar modelo

Dica

Regra prática: use \(k = 2s\) (duas vezes o período sazonal).
Para dados mensais: \(k = 24\). Para trimestrais: \(k = 8\).

6 · Previsão IMA(1,1): 30 Passos à Frente

# Previsão 30 passos à frente com IC 95%
forecast = results.get_forecast(steps=30)
fc_mean  = forecast.predicted_mean
fc_ci    = forecast.conf_int(alpha=0.05)

fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(ima1.iloc[-80:], color='#E4002B', label='Histórico')
ax.plot(fc_mean, color='#1a9b57', linestyle='--', label='Previsão')
ax.fill_between(fc_ci.index,
                fc_ci.iloc[:,0], fc_ci.iloc[:,1],
                alpha=0.18, color='#1a9b57', label='IC 95%')
ax.axvline(ima1.index[-1], color='gray', lw=1, ls=':')
ax.legend(); plt.tight_layout(); plt.show()

Dica

O IC do IMA(1,1) alarga progressivamente: sem sazonalidade, a incerteza cresce com o horizonte. Isso é esperado para processos integrados.

🎲 Dinâmica: Box-Jenkins na Prática

Importante

Atividade em duplas (~15 minutos)

Cada dupla recebe uma série diferente (disponível no notebook da aula):

Série Fonte Frequência
IPCA mensal BCB código 433 Mensal
Ibovespa retornos yfinance Diário
Exportações brasileiras BCB código 22707 Mensal
Selic diária BCB código 11 Diário

Tarefa:

  1. Plote a série e aplique o teste ADF → determine \(d\)
  2. Plote ACF e PACF da série estacionarizada → proponha \(p\) e \(q\)
  3. Estime o ARIMA com statsmodels e leia o summary
  4. Verifique os resíduos (ACF + Ljung-Box)
  5. Apresente em 2 minutos: qual modelo escolheu e por quê?

Dica

Na sequência vamos ver como o AutoARIMA chega (ou não!) ao mesmo modelo que você escolheu.

6 · AutoARIMA: Busca Automática

from statsforecast import StatsForecast
from statsforecast.models import AutoARIMA

sf = StatsForecast(
    models=[AutoARIMA(season_length=1)],  # sem sazonalidade
    freq='D', n_jobs=1,
)

df = pd.DataFrame({
    'unique_id': 'ima1',
    'ds': ima1.index,
    'y':  ima1.values
})

sf.fit(df=df)
previsao = sf.predict(h=30, level=[80, 95])
# Inspecionar o modelo selecionado
modelo = sf.fitted_[0, 0].model_
arma   = modelo['arma']        # [p, q, P, Q, s, d, D]
p, q, P, Q, s, d, D = arma
print(f"Modelo selecionado: ARIMA({p},{d},{q})({P},{D},{Q})[{s}]")
print(f"AIC  : {modelo['aic']:.3f}")
print(f"BIC  : {modelo['bic']:.3f}")
print(f"sigma: {modelo['sigma2']**0.5:.4f}")

Output esperado para IMA(1,1):

Modelo selecionado: ARIMA(0,1,1)(0,0,0)[0]
AIC  : 1425.831
BIC  : 1434.219
sigma: 1.0041

Dica

season_length=1 → sem sazonalidade
season_length=12 → sazonalidade mensal
season_length=4 → sazonalidade trimestral
O campo arma retorna [p, q, P, Q, s, d, D]: inspecione sempre para confirmar o modelo!

Parte 2 · Sazonalidade e Comparação

SARIMA, Prophet e ARIMAX: quando usar cada um?

6 · ARIMAX: Variáveis Exógenas

O ARIMAX adiciona regressores externos \(x_t\) ao modelo ARIMA:

\[y_t = \beta x_t + \phi_1 y_{t-1} + \cdots + \phi_p y_{t-p} + \varepsilon_t - \theta_1\varepsilon_{t-1} - \cdots - \theta_q\varepsilon_{t-q}\]

Aviso

Cuidado: \(\beta\) só pode ser interpretado condicionalmente aos valores passados de \(y_t\).
Alternativa mais intuitiva: regressão com erros ARMA:

\[y_t = \beta x_t + \eta_t, \quad \eta_t \sim \text{ARMA}(p,q)\]

Se \(y_t\) e \(x_t\) são integradas, diferencie ambas para evitar regressão espúria.

7 · Tratamento da Sazonalidade

Três abordagens para modelar a sazonalidade:

Modelo Como modela Melhor quando
SARIMA Diff sazonal + AR/MA sazonais Série simples, IC rigoroso
Prophet Sazonalidade multiplicativa + changepoints Feriados, tendência flexível
ARIMAX Variáveis dummy mensais Simples, interpretável

Série de referência: AirPassengers, passageiros mensais (1949-1960)

7 · AirPassengers: Decomposição

from statsmodels.tsa.seasonal import seasonal_decompose
import statsmodels.api as sm

data = sm.datasets.get_rdataset('AirPassengers', 'datasets').data
AP   = pd.Series(data['value'].values,
                 index=pd.date_range('1949-01', periods=144, freq='MS'))

decomp = seasonal_decompose(AP, model='multiplicative', period=12)
decomp.plot(); plt.show()

Nota

Amplitude sazonal cresce com a tendência → use modelo multiplicativo (ou log + aditivo).

8 · Aditivo vs Multiplicativo

# Transformação log converte multiplicativo em aditivo
AP_log = np.log(AP)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# Aditivo: amplitude constante (inadequado para AirPassengers)
# Multiplicativo/log: amplitude proporcional à tendência (correto)

Dica

Regra prática: se a “bigodada” sazonal cresce visivelmente → multiplicativo.
Aplicar log e usar aditivo dá o mesmo resultado e facilita a estimação.

9 · SARIMA: Identificação para AirPassengers

AP_log  = np.log(AP[:'1959'])      # excluir 1960 para teste
AP_diff      = AP_log.diff().dropna()    # d=1
AP_diff_seas = AP_diff.diff(12).dropna() # D=1

fig, axes = plt.subplots(2, 2, figsize=(12, 6))
plot_acf( AP_diff,      ax=axes[0,0], lags=36, zero=False)
plot_pacf(AP_diff,      ax=axes[0,1], lags=36, zero=False)
plot_acf( AP_diff_seas, ax=axes[1,0], lags=36, zero=False)
plot_pacf(AP_diff_seas, ax=axes[1,1], lags=36, zero=False)
plt.show()

9 · SARIMA: Seleção por AICc

from itertools import product

resultados = []
for p, q in product([0, 1, 2], repeat=2):
    try:
        m = ARIMA(AP_log, order=(p,1,q),
                  seasonal_order=(0,1,0,12)).fit()
        resultados.append({'Modelo': f'SARIMA({p},1,{q})(0,1,0)',
                           'AIC': m.aic, 'AICc': m.aicc, 'BIC': m.bic})
    except: pass

pd.DataFrame(resultados).sort_values('AICc').head(4)

9 · SARIMA: Resultado da Seleção

Modelo AICc
SARIMA(1,1,0)(0,1,0)₁₂ 1020.4
SARIMA(0,1,1)(0,1,0)₁₂ 1020.5
SARIMA(1,1,1)(0,1,0)₁₂ 1022.4

Dica

O AICc penaliza modelos com muitos parâmetros, preferido ao AIC quando \(n/k < 40\). Os dois melhores têm AICc quase idêntico: SARIMA(1,1,0) e SARIMA(0,1,1) são equivalentes aqui.

9 · SARIMA: Estimação e Previsão

fit = ARIMA(AP_log[:'1959'],
            order=(1, 1, 0),
            seasonal_order=(0, 1, 0, 12)).fit()

# Previsão em escala original (exp do log)
fc_log  = fit.get_forecast(12).predicted_mean
fc_orig = np.exp(fc_log)
ci_log  = fit.get_forecast(12).conf_int()
ci_orig = np.exp(ci_log)

print(fit.summary())
print(f"\nMAE (1960): {np.abs(fc_orig - AP['1960':]).mean():.1f}")

Nota

Verificar resíduos com ACF + Ljung-Box antes de confiar nas previsões!

10 · Prophet: Código Python

from prophet import Prophet  # pip install prophet

df_prophet = pd.DataFrame({
    'ds': AP.index,
    'y':  AP.values
})

# Sazonalidade multiplicativa (amplitude cresce com tendência)
m = Prophet(seasonality_mode='multiplicative')
m.fit(df_prophet[df_prophet['ds'] < '1960-01-01'])

# Previsão
future   = m.make_future_dataframe(periods=12, freq='MS')
forecast = m.predict(future)

# Visualizar componentes
fig = m.plot_components(forecast)
plt.show()

# Extrair previsão para 1960 e calcular MAE
real       = AP['1960':]
fc_prophet = forecast[forecast['ds'] >= '1960-01-01']['yhat'].values
print(f"MAE Prophet: {np.abs(fc_prophet - real.values).mean():.1f}")

11 · ARIMAX: Dummies Sazonais

# Criar dummies mensais (janeiro = referência)
meses = pd.get_dummies(AP.index.month, prefix='mes', drop_first=True)
meses.index = AP.index
trend = pd.Series(range(1, len(AP)+1), index=AP.index, name='trend')
X = pd.concat([meses, trend], axis=1).astype(float)

X_train = X[:'1959']; X_test = X['1960':]

fit_arimax = ARIMA(AP[:'1959'],
                   order=(1, 1, 0),
                   exog=X_train).fit()

fc_arimax = fit_arimax.get_forecast(12, exog=X_test).predicted_mean
print(f"MAE ARIMAX: {np.abs(fc_arimax - AP['1960':]).mean():.1f}")

11 · Prophet com Regressores Exógenos

O Prophet também aceita variáveis exógenas via add_regressor(), alternativa às dummies do ARIMAX dentro do framework Prophet:

from prophet import Prophet
import pandas as pd

# Criar dummies de mês
df_prophet['mes'] = df_prophet['ds'].dt.month
for m in range(2, 13):  # janeiro = referência
    df_prophet[f'mes_{m}'] = (df_prophet['mes'] == m).astype(int)

regressores = [f'mes_{m}' for m in range(2, 13)]

# Modelo Prophet com dummies como regressores
m = Prophet(seasonality_mode='multiplicative')
for reg in regressores:
    m.add_regressor(reg)

m.fit(df_prophet[df_prophet['ds'] < '1960-01-01'])

# Previsão: dummies devem estar no dataframe futuro também
future = m.make_future_dataframe(periods=12, freq='MS')
future['mes'] = future['ds'].dt.month
for reg in regressores:
    future[reg] = (future['mes'] == int(reg.split('_')[1])).astype(int)

forecast = m.predict(future)

# Extrair previsão para 1960
fc_prophet_reg = forecast[forecast['ds'] >= '1960-01-01']['yhat'].values
print(f"MAE Prophet+Dummies: {np.abs(fc_prophet_reg - AP['1960':].values).mean():.1f}")

Dica

Quando usar regressores no Prophet?
Use add_regressor() para variáveis que você conhece antecipadamente (dummies, feriados personalizados, preços de insumos).
Use add_regressor() quando os regressores são conhecidos antecipadamente e têm interpretação clara de negócio.

12 · Comparação Final: 4 Modelos

# fc_orig        → SARIMA           (slide 9)
# fc_prophet     → Prophet mult.    (slide 10)
# fc_prophet_reg → Prophet+Dummies  (slide 11)
# fc_arimax      → ARIMAX           (slide 11)

real = AP_test.values   # 12 valores reais de 1960

resultados = {
    'SARIMA':            fc_orig,
    'Prophet (mult.)':   fc_prophet,
    'Prophet + Dummies': fc_prophet_reg,
    'ARIMAX':            fc_arimax,
}

print(f"{'Modelo':25s}  {'MAE':>8}  {'MAPE':>8}")
print('-' * 48)
for modelo, fc in sorted(resultados.items(),
                          key=lambda x: np.abs(x[1] - real).mean()):
    mae_val  = np.abs(fc - real).mean()
    mape_val = np.abs((fc - real) / real * 100).mean()
    print(f"{modelo:25s}  {mae_val:8.1f}  {mape_val:7.1f}%")

12 · Comparação: Visualização

fig, axes = plt.subplots(1, 2, figsize=(13, 4.5))
cores = ['#1a9b57', '#1a6fc4', '#cc44ff', '#cc8800']

axes[0].plot(AP.index, AP.values, color='#aaaaaa', lw=1.5, label='Histórico')
axes[0].plot(AP_test.index, real, color='#1C1C1C', lw=2.5, ls=':', label='Real 1960', zorder=6)
for (nome, fc), cor in zip(resultados.items(), cores):
    axes[0].plot(AP_test.index, fc, color=cor, lw=2, ls='--', label=nome)
axes[0].axvline(AP_train.index[-1], color='#888', lw=1, ls=':', alpha=0.7)
axes[0].set_title('Previsões: 1960'); axes[0].set_ylabel('Passageiros')
axes[0].legend(fontsize=8, loc='upper left')
plt.tight_layout(); plt.show()

12 · Comparação: Métricas

vals_mae  = [np.abs(fc - real).mean()            for fc in resultados.values()]
vals_mape = [np.abs((fc - real)/real*100).mean() for fc in resultados.values()]
x = np.arange(len(resultados)); w = 0.35
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(x - w/2, vals_mae,  width=w, color=cores, alpha=0.85, label='MAE')
ax.bar(x + w/2, vals_mape, width=w, color=cores, alpha=0.5,
       edgecolor=cores, linewidth=1.5, fill=False, label='MAPE (%)')
ax.set_xticks(x); ax.set_xticklabels(resultados.keys(), fontsize=9)
ax.set_title('MAE e MAPE% por modelo'); ax.legend(); plt.tight_layout(); plt.show()

Nota

Os valores de MAE/MAPE do Prophet variam a cada execução (componente estocástico). Execute o notebook para resultados reproduzíveis.

12 · Quando usar cada modelo?

Situação Recomendação
Série simples, sem feriados SARIMA: IC bem calibrado
Sazonalidade multiplicativa Prophet (multiplicativo): amplitude crescente
Feriados e eventos especiais Prophet: componente \(h(t)\)
Sazonalidade + regressores pontuais Prophet + Dummies: add_regressor()
Regressores externos contínuos ARIMAX: interpretável
Série curta (\(n < 50\)) SARIMA simples
Produção em escala AutoARIMA: rápido e robusto

Aviso

Não existe modelo universalmente melhor.
Sempre avalie o diagnóstico dos resíduos e valide em amostra de teste holdout.

🏋️ Exercício Proposto

  1. Carregue o IBC-Br ou Taxa de Desemprego via API do BCB
  2. Ajuste um SARIMA manualmente via Box-Jenkins
  3. Compare com o AutoARIMA: chegaram ao mesmo modelo?
  4. Instale e ajuste um Prophet (pip install prophet)
  5. Separe os últimos 12 meses como amostra de teste
  6. Calcule MAE e RMSE para cada modelo: qual venceu?
  7. Verifique: os resíduos do melhor modelo são ruído branco?

📖 Referências

Forecasting: Principles and Practice (3ª ed.)
Hyndman & Athanasopoulos · 🔗 otexts.com/fpppy

Material do Curso
🔗 padsinsper.github.io/202634-fa

Prophet: Forecasting at Scale, Taylor & Letham (2018)
🔗 facebook.github.io/prophet

Dúvidas?

Financial Analytics · PADS · Insper · 2026