Código
import subprocess, sysfor pkg in ['statsmodels', 'statsforecast', 'prophet']: subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', pkg])print("✅ Pacotes instalados!")Autores: Paloma Vaissman Uribe & Julio Trecenti
Instituição: Insper, PADS | Ano: 2026
Este notebook acompanha os slides da Aula 2:
Série de referência: AirPassengers, passageiros mensais de aviação (1949-1960)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.tsa.arima_process import ArmaProcess
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
from statsmodels.stats.diagnostic import acorr_ljungbox
import warnings, logging
warnings.filterwarnings('ignore')
logging.getLogger('prophet').setLevel(logging.WARNING)
logging.getLogger('cmdstanpy').disabled = True
plt.style.use('default')
plt.rcParams.update({
'figure.facecolor':'#FFFFFF','axes.facecolor':'#F9F9F9',
'axes.edgecolor':'#444444','axes.labelcolor':'#1C1C1C',
'xtick.color':'#444444','ytick.color':'#444444',
'text.color':'#1C1C1C','grid.color': '#DDDDDD',
'grid.linestyle':'--','grid.alpha':0.5,'figure.dpi':110,
})
RED,BLUE,GREEN,GRAY,YELLOW,PURPLE = '#E4002B','#1a6fc4','#1a9b57','#555555','#cc8800','#cc44ff'
print("✅ Imports OK!")| Processo | ACF | PACF |
|---|---|---|
| AR(p) | Decaimento exponencial | Corte em \(p\) |
| MA(q) | Corte em \(q\) | Decaimento exponencial |
| ARMA(p,q) | Decaimento | Decaimento |
ARIMA(p,d,q): aplica \(d\) diferenças antes do ARMA: \[\underbrace{(1-B)^d}_{\text{diferenciação}} y_t \sim \text{ARMA}(p,q)\]
np.random.seed(123)# AR(1) com phi=0.5, integrado uma vez → ARIMA(1,1,0)arma_p = ArmaProcess(ar=[1, -0.5], ma=[1])diff_y = arma_p.generate_sample(nsample=500)y = np.cumsum(diff_y) # série I(1)dy = np.diff(y) # 1ª diferença → AR(1)fig, axes = plt.subplots(2, 3, figsize=(14, 7))axes[0,0].plot(y, color=RED, lw=1.2); axes[0,0].set_title('Série y (não estacionária)'); axes[0,0].grid(True)plot_acf( y, ax=axes[0,1], lags=40, zero=False, title='ACF: Nível')plot_pacf(y, ax=axes[0,2], lags=40, zero=False, title='PACF: Nível')axes[1,0].plot(dy, color=BLUE, lw=1.2); axes[1,0].axhline(0,color='#888',ls='--',lw=0.8)axes[1,0].set_title('1ª Diferença Δy'); axes[1,0].grid(True)plot_acf( dy, ax=axes[1,1], lags=40, zero=False, title='ACF: 1ª Diferença')plot_pacf(dy, ax=axes[1,2], lags=40, zero=False, title='PACF: 1ª Diferença (corte lag 1 → AR(1) ✓)')plt.suptitle('Simulação ARIMA(1,1,0)', fontsize=13, y=1.01)plt.tight_layout(); plt.show()np.random.seed(123)# MA(1) com theta=0.9, integrado → IMA(1,1)arma_p2 = ArmaProcess(ar=[1], ma=[1, 0.9])diff_ima = arma_p2.generate_sample(nsample=500)dates = pd.date_range('2020-01-01', periods=500, freq='D')ima1 = pd.Series(np.cumsum(diff_ima), index=dates)dima = ima1.diff().dropna()fig, axes = plt.subplots(2, 2, figsize=(13, 6))axes[0,0].plot(ima1, color=RED, lw=1.2); axes[0,0].set_title('IMA(1,1): Série'); axes[0,0].grid(True)plot_acf( ima1, ax=axes[0,1], lags=30, zero=False, title='ACF: Nível (decaimento lento)')axes[1,0].plot(dima, color=BLUE, lw=1.2); axes[1,0].axhline(0,color='#888',ls='--',lw=0.8)axes[1,0].set_title('1ª Diferença'); axes[1,0].grid(True)plot_acf( dima, ax=axes[1,1], lags=30, zero=False, title='ACF: 1ª Diferença (corte lag 1 → MA(1) ✓)')plt.suptitle('Simulação IMA(1,1) = ARIMA(0,1,1)', fontsize=13, y=1.01)plt.tight_layout(); plt.show()# Etapa 3: Diagnóstico dos resíduosres = results.residfig, axes = plt.subplots(1, 3, figsize=(14, 4))axes[0].plot(res.values, color=BLUE, lw=0.9, alpha=0.85)axes[0].axhline(0, color='#888', ls='--', lw=0.8)axes[0].set_title('Resíduos no tempo'); axes[0].grid(True)axes[1].hist(res, bins=30, color=BLUE, alpha=0.7, edgecolor='#333')axes[1].set_title('Distribuição dos resíduos'); axes[1].grid(True, axis='y')plot_acf(res, ax=axes[2], lags=24, zero=False, title='ACF dos resíduos (deve estar no IC)')plt.tight_layout(); plt.show()lb = acorr_ljungbox(res, lags=[24], model_df=2, return_df=True)print(f"Ljung-Box p-value: {lb['lb_pvalue'].values[0]:.4f}", "✅ ruído branco" if lb['lb_pvalue'].values[0] > 0.05 else "⚠️ revisar!")# Etapa 4: Previsão com ICforecast_arima = results.get_forecast(steps=30)fc_mean = forecast_arima.predicted_meanfc_ci = forecast_arima.conf_int(alpha=0.05)fig, ax = plt.subplots(figsize=(12, 4))ax.plot(ima1.iloc[-80:], color=RED, lw=1.5, label='Histórico')ax.plot(fc_mean, color=GREEN, lw=2, ls='--', label='Previsão ARIMA(0,1,1)')ax.fill_between(fc_ci.index, fc_ci.iloc[:,0], fc_ci.iloc[:,1], alpha=0.2, color=GREEN, label='IC 95%')ax.axvline(ima1.index[-1], color=YELLOW, lw=1, ls=':', alpha=0.8)ax.set_title('Previsão: ARIMA(0,1,1) com IC 95%')ax.legend(fontsize=9); ax.grid(True)plt.tight_layout(); plt.show()from statsforecast import StatsForecastfrom statsforecast.models import AutoARIMAsf = StatsForecast(models=[AutoARIMA(season_length=1)], freq='D', n_jobs=1)df_nixtla = pd.DataFrame({'unique_id':'ima1','ds':ima1.index,'y':ima1.values})sf.fit(df=df_nixtla)# Inspecionar modelo selecionadomodelo_info = sf.fitted_[0, 0].model_arma = modelo_info['arma'] # [p, q, P, Q, s, d, D]p, q, P, Q, s, d, D = armaprint(f"Modelo selecionado: ARIMA({p},{d},{q})({P},{D},{Q})[{s}]")print(f"AIC : {modelo_info['aic']:.3f}")print(f"BIC : {modelo_info['bic']:.3f}")print(f"sigma: {modelo_info['sigma2']**0.5:.4f}")O ARIMAX adiciona regressores externos ao ARIMA:
\[y_t = \beta x_t + \phi_1 y_{t-1} + \cdots + \varepsilon_t - \theta_1\varepsilon_{t-1} - \cdots\]
Se \(y_t\) e \(x_t\) são integradas, diferencie ambas para evitar regressão espúria.
# Dados reais do AirPassengers (1949-1960), hardcoded para garantir reprodutibilidadeap_values = [ 112,118,132,129,121,135,148,148,136,119,104,118, 115,126,141,135,125,149,170,170,158,133,114,140, 145,150,178,163,172,178,199,199,184,162,146,166, 171,180,193,181,183,218,230,242,209,191,172,194, 196,196,236,235,229,243,264,272,237,211,180,201, 204,188,235,227,234,264,302,293,259,229,203,229, 242,233,267,269,270,315,364,347,312,274,237,278, 284,277,317,313,318,374,413,405,355,306,271,306, 315,301,356,348,355,422,465,467,404,347,305,336, 340,318,362,348,363,435,491,505,404,359,310,337, 360,342,406,396,420,472,548,559,463,407,362,405, 417,391,419,461,472,535,622,606,508,461,390,432,]dates_ap = pd.date_range('1949-01-01', periods=144, freq='MS')AP = pd.Series(ap_values, index=dates_ap, name='Passengers')AP_train = AP[:'1959-12-01'] # 132 obs: jan/1949-dez/1959AP_test = AP['1960-01-01':] # 12 obs: jan/1960-dez/1960real = AP_test.values # valores reais de 1960print(f"Treino: {len(AP_train)} obs | Teste: {len(AP_test)} obs")print(f"Real 1960: {real}")AP.plot(figsize=(12, 4), color=RED, title='AirPassengers: série completa', grid=True)plt.ylabel('Passageiros (mil)'); plt.show()# Decomposição multiplicativadecomp = seasonal_decompose(AP, model='multiplicative', period=12)fig, axes = plt.subplots(4, 1, figsize=(12, 9), sharex=True)for ax, (comp, color, label) in zip(axes, [ (decomp.observed, RED, 'Série Original'), (decomp.trend, BLUE, 'Tendência'), (decomp.seasonal, GREEN, 'Sazonalidade'), (decomp.resid, GRAY, 'Resíduo'),]): ax.plot(comp, color=color, lw=1.3) ax.set_title(label, pad=2); ax.grid(True)plt.suptitle('Decomposição Multiplicativa: AirPassengers', fontsize=13, y=1.01)plt.tight_layout(); plt.show()print("💡 Amplitude sazonal cresce com tendência → modelo MULTIPLICATIVO")AP_log = np.log(AP) # log transforma multiplicativo → aditivofig, axes = plt.subplots(1, 2, figsize=(13, 4))axes[0].plot(AP.index, AP.values, color=RED, lw=1.4)axes[0].set_title('Escala original: amplitude cresce'); axes[0].grid(True)axes[1].plot(AP_log.index, AP_log.values, color=BLUE, lw=1.4)axes[1].set_title('Log: amplitude estabilizada'); axes[1].grid(True)plt.suptitle('AirPassengers: Original vs Log', fontsize=12, y=1.02)plt.tight_layout(); plt.show()# Trabalhar com log do treinoAP_log_train = np.log(AP_train)AP_diff = AP_log_train.diff().dropna() # d=1AP_diff_seas = AP_diff.diff(12).dropna() # D=1fig, axes = plt.subplots(2, 2, figsize=(13, 7))plot_acf( AP_diff, ax=axes[0,0], lags=36, zero=False, title='ACF: 1ª Diff (log)')plot_pacf(AP_diff, ax=axes[0,1], lags=36, zero=False, title='PACF: 1ª Diff (log)')plot_acf( AP_diff_seas, ax=axes[1,0], lags=36, zero=False, title='ACF: Diff + Diff Sazonal')plot_pacf(AP_diff_seas, ax=axes[1,1], lags=36, zero=False, title='PACF: Diff + Diff Sazonal')for ax in axes.flat: ax.axvline(12, color=YELLOW, lw=0.8, ls=':', alpha=0.7) ax.axvline(24, color=YELLOW, lw=0.8, ls=':', alpha=0.5)plt.suptitle('AirPassengers: Identificação SARIMA', fontsize=13, y=1.01)plt.tight_layout(); plt.show()# Seleção por AICcfrom itertools import productresultados_aic = []for p, q in product([0, 1, 2], repeat=2): try: m = ARIMA(AP_log_train, order=(p,1,q), seasonal_order=(0,1,0,12)).fit() resultados_aic.append({'Modelo': f'SARIMA({p},1,{q})(0,1,0)₁₂', 'AIC': round(m.aic,2), 'AICc': round(m.aicc,2), 'BIC': round(m.bic,2)}) except: passdf_aic = pd.DataFrame(resultados_aic).sort_values('AICc')print(df_aic.head(5).to_string(index=False))melhor_modelo = df_aic.iloc[0]['Modelo']print(f"\n✅ Melhor modelo: {melhor_modelo}")# Estimação e previsão SARIMA(1,1,0)(0,1,0)₁₂fit_sarima = ARIMA(AP_log_train, order=(1,1,0), seasonal_order=(0,1,0,12)).fit()print(fit_sarima.summary())# Diagnósticolb_s = acorr_ljungbox(fit_sarima.resid, lags=[24], model_df=2, return_df=True)print(f"\nLjung-Box p-value: {lb_s['lb_pvalue'].values[0]:.4f}", "✅" if lb_s['lb_pvalue'].values[0] > 0.05 else "⚠️ revisar!")# Previsão na escala original (exp do log)fc_log_s = fit_sarima.get_forecast(12).predicted_meanfc_orig = np.exp(fc_log_s.values) # array de 12 valoresci_log_s = fit_sarima.get_forecast(12).conf_int()ci_orig = np.exp(ci_log_s.values)print(f"Previsão SARIMA 1960: {fc_orig.round(1)}")print(f"Real 1960 : {real}")print(f"MAE SARIMA: {np.abs(fc_orig - real).mean():.2f}")from prophet import Prophet# Preparar dadosdf_prophet = pd.DataFrame({'ds': AP.index, 'y': AP.values})# Ajustar apenas no treino (jan/1949-dez/1959)m_prophet = Prophet(seasonality_mode='multiplicative')m_prophet.fit(df_prophet[df_prophet['ds'] < '1960-01-01'])# Previsão para 12 meses após o treinofuture = m_prophet.make_future_dataframe(periods=12, freq='MS')forecast = m_prophet.predict(future)# Componentesfig = m_prophet.plot_components(forecast)plt.show()# Extrair apenas as 12 previsões de 1960fc_prophet = forecast[forecast['ds'] >= '1960-01-01']['yhat'].valuesprint(f"Previsão Prophet 1960 ({len(fc_prophet)} vals): {fc_prophet.round(1)}")print(f"MAE Prophet (mult.): {np.abs(fc_prophet - real).mean():.2f}")# Criar dummies mensais (janeiro = referência) + tendência linearmeses = pd.get_dummies(AP.index.month, prefix='mes', drop_first=True)meses.index = AP.indextrend_var = pd.Series(range(1, len(AP)+1), index=AP.index, name='trend')X = pd.concat([meses, trend_var], axis=1).astype(float)X_train = X[:'1959']X_test = X['1960':]# Ajustar ARIMAX no treinofit_arimax = ARIMA(AP_train, order=(1,1,0), exog=X_train).fit()fc_arimax = fit_arimax.get_forecast(12, exog=X_test).predicted_mean.valuesprint(f"Previsão ARIMAX 1960: {fc_arimax.round(1)}")print(f"MAE ARIMAX: {np.abs(fc_arimax - real).mean():.2f}")# Diagnósticolb_ax = acorr_ljungbox(fit_arimax.resid, lags=[24], model_df=2, return_df=True)print(f"Ljung-Box p-value: {lb_ax['lb_pvalue'].values[0]:.4f}", "✅" if lb_ax['lb_pvalue'].values[0] > 0.05 else "⚠️")add_regressor())# Usar df_prophet já criado na célula do Prophet# Adicionar dummies de mês como regressoresdf_proph2 = df_prophet.copy()df_proph2['mes'] = df_proph2['ds'].dt.monthregressores = [f'mes_{mes}' for mes in range(2, 13)]for mes in range(2, 13): df_proph2[f'mes_{mes}'] = (df_proph2['mes'] == mes).astype(int)# Ajustar apenas no treinom_reg = Prophet(seasonality_mode='multiplicative')for reg in regressores: m_reg.add_regressor(reg)m_reg.fit(df_proph2[df_proph2['ds'] < '1960-01-01'])# Previsão: dummies devem estar no dataframe futurofuture_reg = m_reg.make_future_dataframe(periods=12, freq='MS')future_reg['mes'] = future_reg['ds'].dt.monthfor reg in regressores: future_reg[reg] = (future_reg['mes'] == int(reg.split('_')[1])).astype(int)forecast_reg = m_reg.predict(future_reg)fc_prophet_reg = forecast_reg[forecast_reg['ds'] >= '1960-01-01']['yhat'].valuesprint(f"Previsão Prophet+Dummies 1960: {fc_prophet_reg.round(1)}")print(f"MAE Prophet+Dummies: {np.abs(fc_prophet_reg - real).mean():.2f}")# Verificação de consistência antes da comparaçãoprint("Valores reais 1960:", real)print()print(f"fc_orig shape: {fc_orig.shape} (SARIMA)")print(f"fc_prophet shape: {fc_prophet.shape} (Prophet mult.)")print(f"fc_prophet_reg shape:{fc_prophet_reg.shape} (Prophet+Dummies)")print(f"fc_arimax shape: {fc_arimax.shape} (ARIMAX)")# Comparação: MAE e MAPE ordenados do melhor ao piorresultados_finais = { '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_finais.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}%")# Visualização: 3 painéis: previsões, MAE, MAPEcores_mod = [GREEN, BLUE, PURPLE, YELLOW]fig, axes = plt.subplots(1, 3, figsize=(16, 5))# Painel 1: previsões no tempoaxes[0].plot(AP.index, AP.values, color=GRAY, lw=1.5, label='Histórico')axes[0].plot(AP_test.index, real, color='white', lw=2.5, ls=':', label='Real 1960', zorder=6)for (nome, fc), cor in zip(resultados_finais.items(), cores_mod): axes[0].plot(AP_test.index, fc, color=cor, lw=2, ls='--', label=nome)axes[0].axvline(AP_train.index[-1], color='#666', lw=1, ls=':', alpha=0.7)axes[0].set_title('Previsões: 1960'); axes[0].set_ylabel('Passageiros')axes[0].legend(fontsize=7.5, loc='upper left'); axes[0].grid(True)# Painel 2: MAEnomes_mod = list(resultados_finais.keys())vals_mae = [np.abs(fc - real).mean() for fc in resultados_finais.values()]bars = axes[1].bar(nomes_mod, vals_mae, color=cores_mod, alpha=0.85, edgecolor='#333', width=0.55)axes[1].set_title('MAE: Erro Médio Absoluto'); axes[1].set_ylabel('MAE (passageiros)')axes[1].grid(True, axis='y'); axes[1].tick_params(axis='x', labelsize=8)for bar, val in zip(bars, vals_mae): axes[1].text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.3, f'{val:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold')axes[1].set_ylim(0, max(vals_mae)*1.22)# Painel 3: MAPEvals_mape = [np.abs((fc - real)/real*100).mean() for fc in resultados_finais.values()]bars2 = axes[2].bar(nomes_mod, vals_mape, color=cores_mod, alpha=0.85, edgecolor='#333', width=0.55)axes[2].set_title('MAPE: Erro Percentual'); axes[2].set_ylabel('MAPE (%)')axes[2].grid(True, axis='y'); axes[2].tick_params(axis='x', labelsize=8)for bar, val in zip(bars2, vals_mape): axes[2].text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.05, f'{val:.1f}%', ha='center', va='bottom', fontsize=10, fontweight='bold')axes[2].set_ylim(0, max(vals_mape)*1.22)plt.suptitle('AirPassengers: SARIMA · Prophet · Prophet+Dummies · ARIMAX', fontsize=12, y=1.02)plt.tight_layout(); plt.show()# ── Salvar gráfico para usar nos slides ──────────────────────────────────────# Execute esta célula após rodar todas as anteriores# O gráfico gerado terá os valores reais do seu ambientefig, axes = plt.subplots(1, 3, figsize=(16, 5))cores_mod = [GREEN, BLUE, PURPLE, YELLOW]nomes_mod = list(resultados_finais.keys())vals_mae = [np.abs(fc - real).mean() for fc in resultados_finais.values()]vals_mape = [np.abs((fc-real)/real*100).mean() for fc in resultados_finais.values()]axes[0].plot(AP.index, AP.values, color=GRAY, lw=1.5, label='Histórico')axes[0].plot(AP_test.index, real, color='white', lw=2.5, ls=':', label='Real 1960', zorder=6)for (nome, fc), cor in zip(resultados_finais.items(), cores_mod): axes[0].plot(AP_test.index, fc, color=cor, lw=2, ls='--', label=nome)axes[0].axvline(AP_train.index[-1], color='#666', lw=1, ls=':', alpha=0.7)axes[0].set_title('Previsões: 1960'); axes[0].set_ylabel('Passageiros')axes[0].legend(fontsize=7.5, loc='upper left'); axes[0].grid(True)bars = axes[1].bar(nomes_mod, vals_mae, color=cores_mod, alpha=0.85, edgecolor='#333', width=0.55)axes[1].set_title('MAE: Erro Médio Absoluto'); axes[1].set_ylabel('MAE (passageiros)')axes[1].grid(True, axis='y'); axes[1].tick_params(axis='x', labelsize=8)for bar, val in zip(bars, vals_mae): axes[1].text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.3, f'{val:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold')axes[1].set_ylim(0, max(vals_mae)*1.22)bars2 = axes[2].bar(nomes_mod, vals_mape, color=cores_mod, alpha=0.85, edgecolor='#333', width=0.55)axes[2].set_title('MAPE: Erro Percentual'); axes[2].set_ylabel('MAPE (%)')axes[2].grid(True, axis='y'); axes[2].tick_params(axis='x', labelsize=8)for bar, val in zip(bars2, vals_mape): axes[2].text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.05, f'{val:.1f}%', ha='center', va='bottom', fontsize=10, fontweight='bold')axes[2].set_ylim(0, max(vals_mape)*1.22)plt.suptitle('AirPassengers: SARIMA · Prophet · Prophet+Dummies · ARIMAX', fontsize=12, y=1.02)plt.tight_layout()plt.savefig('a2_g8_comparacao.png', dpi=130, bbox_inches='tight', facecolor='#1C1C1C')plt.show()print("✅ Gráfico salvo como 'a2_g8_comparacao.png'")print(" Substitua o arquivo de mesmo nome na pasta dos slides para atualizar!")Substitua o AirPassengers pelo IBC-Br ou Desemprego e refaça o pipeline:
import requestsdef bcb_serie(codigo, start='2003-01-01'): """Carrega série do BCB via API REST, sem dependências extras.""" di = pd.Timestamp(start).strftime('%d/%m/%Y') url = (f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.{codigo}/dados" f"?formato=json&dataInicial={di}") df = pd.DataFrame(requests.get(url, timeout=30).json()) df['data'] = pd.to_datetime(df['data'], dayfirst=True) df['valor'] = pd.to_numeric(df['valor'], errors='coerce') return df.set_index('data').rename(columns={'valor': f'bcb_{codigo}'}).dropna()# Carregar IBC-Br ou desemprego:# ibc = bcb_serie(24363) # IBC-Br mensal# desemp = bcb_serie(24369) # Taxa de desemprego mensal# serie = ibc['bcb_24363']# serie_train = serie.iloc[:-12]# serie_test = serie.iloc[-12:]print("✅ Função bcb_serie() pronta. Descomente as linhas acima para carregar.")