Dinâmica: Mistério do PIB do 4º Trimestre

Notebook base para a aula 1 (Fundamentos de Séries Temporais).

Este é um scaffolding: os dados já estão carregados, mas cada grupo tem que preencher as células marcadas com # TODO. A ideia é descobrir intuitivamente a decomposição sazonal antes de ver a teoria formal de STL/SARIMA.

Roteiro completo em: https://padsinsper.github.io/202634-fa/dinamicas/sazonalidade-pib.html

Setup: carregar bibliotecas e dados do BCB

Se estiver no Colab, descomente a linha de pip install.

Código
# !pip install python-bcb statsmodels matplotlib pandas --quiet

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from bcb import sgs

plt.rcParams.update({
    'figure.figsize': (11, 4),
    'axes.spines.top': False,
    'axes.spines.right': False,
})

CORES = {
    'original': '#E50505',
    'dessaz':   '#3ACC9F',
    'tendencia':'#730D9F',
}
Código
# Códigos SGS/BCB das séries usadas na dinâmica
SERIES = {
    'ibcbr_original': 24363,   # IBC-Br sem ajuste sazonal (proxy PIB mensal)
    'ibcbr_dessaz':   24364,   # IBC-Br com ajuste sazonal
    'varejo':         1455,    # PMC - volume vendas varejo restrito
    'energia':        1406,    # Consumo de energia elétrica total
    'industria':      21867,   # Produção industrial geral
    'arrecadacao':    7547,    # Arrecadação federal bruta
}

dados = {}
for nome, codigo in SERIES.items():
    try:
        s = sgs.get({nome: codigo}, start='2003-01-01')
        dados[nome] = s.iloc[:, 0]
        print(f'{nome:15s} ✓  {len(s)} obs, {s.index.min().date()} → {s.index.max().date()}')
    except Exception as e:
        print(f'{nome:15s} ✗  {e}')

Cenário: o IBC-Br original vs. dessazonalizado

Este é o ponto de partida da dinâmica: projete isso para a turma antes de dividir em grupos.

Código
fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(dados['ibcbr_original'], color=CORES['original'], linewidth=0.9, label='IBC-Br original')
ax.plot(dados['ibcbr_dessaz'],   color=CORES['dessaz'],   linewidth=1.6, label='IBC-Br dessazonalizado')
ax.set_title('IBC-Br: série bruta vs. com ajuste sazonal', fontweight='bold')
ax.set_ylabel('Índice')
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

Pergunta da turma: por que a série vermelha tem aquele ‘zigue-zague’ todo ano?


Função auxiliar para os grupos

Cada grupo vai usar analise_setorial(serie, nome), que mostra:

  1. A série ao longo do tempo
  2. A média por mês (climatologia)
  3. A variação percentual do mesmo mês ano a ano
Código
def analise_setorial(serie: pd.Series, nome: str):
    """Plota série, climatologia mensal e variação YoY."""
    fig, axes = plt.subplots(1, 3, figsize=(16, 4))

    # Painel 1: série completa
    axes[0].plot(serie, color=CORES['original'], linewidth=0.7)
    axes[0].set_title(f'{nome}: série original')
    axes[0].grid(alpha=0.3)

    # Painel 2: climatologia mensal
    por_mes = serie.groupby(serie.index.month).mean()
    axes[1].bar(por_mes.index, por_mes.values, color=CORES['dessaz'])
    axes[1].set_title(f'{nome}: média por mês')
    axes[1].set_xticks(range(1, 13))
    axes[1].set_xticklabels(['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'])

    # Painel 3: variação ano-contra-ano no mesmo mês
    yoy = serie.pct_change(12) * 100
    axes[2].plot(yoy, color=CORES['tendencia'], linewidth=0.8)
    axes[2].axhline(0, color='#5B5B5B', linewidth=0.5)
    axes[2].set_title(f'{nome}: variação % ano-contra-ano')
    axes[2].grid(alpha=0.3)

    plt.tight_layout()
    plt.show()

    # Destaque do 4T → 1T
    media_Q4 = serie[serie.index.month.isin([10, 11, 12])].groupby(serie.index.year).mean()
    media_Q1 = serie[serie.index.month.isin([1, 2, 3])].groupby(serie.index.year).mean()
    comparacao = pd.DataFrame({'media_Q4': media_Q4, 'media_Q1_ano_seguinte': media_Q1.shift(-1)})
    comparacao['variacao_pct'] = (comparacao['media_Q1_ano_seguinte'] / comparacao['media_Q4'] - 1) * 100
    return comparacao.dropna()

Grupo A: Varejo (PMC)

Variável: volume de vendas no varejo restrito (PMC, IBGE via BCB SGS 1455).

Perguntas guia: 1. Qual mês do ano sistematicamente tem o maior volume de vendas? E o menor? 2. Isso faz sentido do ponto de vista do calendário do varejo brasileiro? 3. Quanto, em média, o Q1 fica abaixo do Q4 anterior? Isso é “crise” ou efeito sazonal?

Código
# TODO Grupo A: rodar a análise e discutir
comp_varejo = analise_setorial(dados['varejo'], 'Varejo (PMC)')
comp_varejo.tail(10)
Código
# TODO Grupo A: escrever aqui em 2 frases a conclusão do grupo
# - Pico em: ___
# - Causa: ___
# - Variação Q4→Q1 é explicada por: ___

Grupo B: Energia elétrica

Variável: consumo de energia elétrica total, sistema interligado nacional (BCB SGS 1406).

Perguntas guia: 1. Em quais meses o consumo é maior? Por quê? 2. A amplitude sazonal está crescendo, estável ou diminuindo ao longo dos anos? 3. Existem outliers óbvios (ex.: crise de 2001, pandemia de 2020)?

Código
# TODO Grupo B
comp_energia = analise_setorial(dados['energia'], 'Energia elétrica')
comp_energia.tail(10)
Código
# TODO Grupo B: conclusão em 2 frases

Grupo C: Indústria

Variável: produção industrial geral (BCB SGS 21867).

Perguntas guia: 1. O Carnaval (fev ou mar) aparece no gráfico? Como? 2. E as férias coletivas de julho/dezembro? 3. O choque da pandemia (mar/abr 2020) é visível? Ele é um outlier ou mudou a tendência?

Código
# TODO Grupo C
comp_industria = analise_setorial(dados['industria'], 'Indústria')
comp_industria.tail(10)
Código
# TODO Grupo C: conclusão em 2 frases

Grupo D: Arrecadação federal

Variável: arrecadação federal bruta (BCB SGS 7547).

Perguntas guia: 1. Qual mês do ano tem sistematicamente a maior arrecadação? Por quê? 2. Existe um padrão trimestral (IRPJ, CSLL)? Mensal (IRPF em abril)? 3. Como você explicaria para o diretor financeiro da sua empresa por que a arrecadação de janeiro ‘caiu’ em relação a dezembro?

Código
# TODO Grupo D
comp_arrec = analise_setorial(dados['arrecadacao'], 'Arrecadação federal')
comp_arrec.tail(10)
Código
# TODO Grupo D: conclusão em 2 frases

Chave de ouro: o choque da pandemia

Agora que cada grupo apresentou suas conclusões, vamos aplicar STL robusto no IBC-Br e destacar o que acontece em 2020.

Pergunta provocativa: quando acontece um choque gigantesco como o da pandemia, o que o modelo de sazonalidade faz? Ele “aprende” que abril é sempre ruim?

Código
from statsmodels.tsa.seasonal import STL

serie = dados['ibcbr_original'].asfreq('MS').interpolate()

stl = STL(serie, period=12, robust=True).fit()

fig, axes = plt.subplots(4, 1, figsize=(13, 10), sharex=True)
axes[0].plot(serie, color='#E50505', linewidth=0.7);           axes[0].set_ylabel('Original')
axes[1].plot(stl.trend, color='#730D9F', linewidth=1.5);       axes[1].set_ylabel('Tendência')
axes[2].plot(stl.seasonal, color='#FFCC00', linewidth=0.7);    axes[2].set_ylabel('Sazonalidade')
axes[3].plot(stl.resid, color='#5B5B5B', linewidth=0.7);       axes[3].set_ylabel('Resíduo')
axes[3].axvspan(pd.Timestamp('2020-03-01'), pd.Timestamp('2020-08-01'), color='red', alpha=0.15,
                label='Pandemia')
axes[3].legend()
for ax in axes: ax.grid(alpha=0.3)
fig.suptitle('STL robusto no IBC-Br: notar o choque pandêmico nos resíduos',
             fontweight='bold', y=1.01)
plt.tight_layout()
plt.show()

Discussão final

  • O STL robusto (robust=True) dá peso menor a observações extremas: isso impede que o choque de 2020 “contamine” o fator sazonal dos outros anos.
  • Se você rodasse STL sem robust=True, o modelo poderia “aprender” que abril é sistematicamente ruim, o que estragaria previsões futuras.
  • Para tratar choques grandes de forma mais explícita, adicionamos dummies de intervenção (variáveis exógenas): isso vira parte natural do SARIMAX na Aula 3 e da modelagem de risco na Parte 2.
De volta ao topo