Um pipeline completo de previsão da carga do Sistema Interligado Nacional: features sem vazamento, validação cruzada temporal, um canário de ruído que poda features inúteis, baselines que precisam ser batidos e um holdout intocado de 2026. E a parte viva: toda segunda-feira o modelo registra suas previsões em público, antes de o realizado ser publicado.
48.144 horas de carga do SIN (jan/2021 a jun/2026, dados abertos do ONS) · holdout: todas as origens de 2026, jamais vistas em nenhuma escolha · LightGBM global multi-horizonte com quantis P10/P50/P90 · ciclo semanal gratuito via GitHub Actions · todos os números desta página saem dos artefatos de src/evaluate.py.
O operador do sistema elétrico precisa saber a carga da próxima hora (despacho fino) e a de amanhã cedo (planejamento de geração). São perguntas com físicas diferentes: em 1h, a inércia da série domina; em 24h, quem manda é o calendário. Um bom sistema de previsão precisa ser honesto sobre como o erro cresce com o horizonte, e é exatamente isso que este projeto mede.
É a série temporal aplicada por excelência: sazonalidade tripla (dia, semana, ano), feriados que mudam tudo, e um custo real para cada MW errado, para cima (desperdício) ou para baixo (risco operacional).
Um único modelo global recebe o horizonte como feature e prevê de 1h a 24h. As métricas são reportadas por horizonte: a média esconderia que prever 1h é fácil e 24h é outro esporte.
Operação decide com margem de segurança. O modelo produz P10/P50/P90 e a página mede se o intervalo cumpre o prometido (cobertura nominal de 80%) fora da amostra.
Médias de 3h, nas últimas quatro semanas do período avaliado. O ciclo diário (vale de madrugada, pico à tarde) e o degrau dos fins de semana saltam aos olhos.
Eixo vertical em GW (milhares de MW). É essa regularidade que torna o baseline sazonal-semanal tão competitivo, e é por isso que ele, e não a persistência, é a régua que o modelo precisa bater.
A série vem do portal de dados abertos do ONS (parquet anual, somando os 4 subsistemas por hora). Cada decisão de limpeza é contada e vai para um relatório versionado: a regra é nunca "consertar" dado em silêncio.
Cada linha do dataset é um par (origem t, horizonte h). O contrato: features da série usam só o passado da origem (shifts ≥ 0); do instante-alvo, entra apenas calendário, que é conhecido com antecedência infinita. Quebrar essa regra infla o backtest e quebra em produção.
| Grupo | Features | Papel |
|---|---|---|
| Autorregressivas (origem) | lags 1-6h, 24h, 48h, 168h, 336h e carga na origem | inércia de curto prazo e memória semanal |
| Janelas (origem) | média/mín/máx/desvio 24h, média 168h, variações 1h/24h/168h | nível e tendência recentes |
| Referências do alvo | carga na mesma hora ontem e há 7 dias (shifts 24-h e 168-h, sempre ≥ 0) | o "sazonal ingênuo" embutido como feature |
| Calendário do alvo | hora, dia da semana, mês, seno/cosseno de hora e dia do ano, fim de semana | as três sazonalidades |
| Feriados | é feriado, véspera, pós-feriado + nome do feriado | quedas de 10-20% que o calendário comum não vê |
| Estrutura | horizonte h | um modelo global para os 24 horizontes |
| Controle | ruido_aleatorio ~ N(0,1) | o canário: régua de corte de importância |
Importância por ganho no modelo tunado, com o ruído incluído de propósito. A linha tracejada vermelha é a régua: feature que informa menos que ruído puro não merece o posto.
Veredito desta rodada: o ruído terminou com ganho exatamente zero (nunca foi escolhido em split algum) e nenhuma das 34 features ficou abaixo dele, então o corte removeu apenas o próprio canário. O modelo enxuto foi re-validado nos mesmos folds: MAE de 1.394,4 MW (todas) contra 1.394,7 MW (enxuto). Empate técnico, como esperado quando não há o que podar: a régua está no pipeline e agirá quando features fracas aparecerem (e a regularização da config vencedora já segura o resto).
A ordem importa: primeiro o holdout é trancado, depois a validação cruzada escolhe os hiperparâmetros, e só no fim o modelo enfrenta 2026. Nenhum número desta página veio de dado que participou de alguma escolha.
Todas as origens de 2026 (jan a jun) ficam fora de tudo: 103.380 pares (origem, horizonte) que o modelo só viu uma única vez, na avaliação final.
4 folds com validação trimestral em 2025 e treino usando apenas o passado de cada fold. Embaralhar linhas em séries temporais é vazamento: o modelo "veria o futuro" dos vizinhos.
20 configurações de LightGBM sorteadas (folhas, taxa de aprendizado, amostragem, regularização L2), early stopping por fold, seleção por MAE médio. Reproduzível de ponta a ponta.
Seis meses de 2026 que o modelo nunca viu: 103.380 previsões avaliadas. A comparação justa é contra o sazonal-semanal, o baseline que qualquer operador usaria de graça.
Erro absoluto médio em MW para cada horizonte de 1h a 24h, no holdout de 2026.
Leituras: a persistência morre rápido (em 12h está 11.440 MW de erro: prever "igual a agora" não atravessa o ciclo diário); o sazonal é estável (~3.567 MW) mas ignora o estado atual do sistema; o modelo combina os dois mundos e fica entre 836 MW (1h) e 1.588 MW (24h), com skill de 76,5% no primeiro horizonte e 55,6% no último.
Origem real do holdout: as últimas 48h observadas, o leque P10-P90 das 24h seguintes, o realizado e o que o baseline sazonal teria dito.
Eixo em GW. O leque alarga com o horizonte, como deve: incerteza honesta cresce com a distância. Bastidor importante: os quantis brutos do boosting saíram estreitos (cobertura de 72,3% no holdout, contra 80% prometidos). Em vez de aceitar ou alargar no olho, o intervalo passa por calibração conformal (CQR, Romano et al. 2019) por horizonte, aprendida em nov-dez/2025, fatia que o ajuste nunca viu. Resultado no holdout de 2026: 82,8% de cobertura, entre 80,0% e 85,7% em todos os 24 horizontes.
| Média 1h-24h no holdout | Modelo (P50) | Sazonal-semanal | Persistência |
|---|---|---|---|
| MAE | 1.373 MW | 3.567 MW | 8.815 MW |
| MAPE | 1,72% | 4,5% | 11,1% |
| Redução de erro vs sazonal | 61,5% | referência | -147% |
Toda segunda-feira, um workflow gratuito do GitHub Actions baixa o dado mais novo do ONS, gera a previsão das próximas 24h e a registra neste repositório, antes de o realizado ser publicado (a divulgação do ONS tem ~1 semana de defasagem). Quando o realizado chega, o confronto entra no placar abaixo. Sem servidor, sem custo, sem como trapacear: o carimbo do commit é a prova.
Carregando o track record...
Previsto (P50) vs realizado, com a banda P10-P90, para as previsões semanais já confrontadas com o dado oficial. Fonte: previsoes.json · track_record.json (públicos e versionados).
O Actions roda src/score_live.py: baixa o parquet do ano corrente, reconstrói a série com o MESMO pipeline do treino e prevê as 24h após o último instante publicado.
As previsões entram em docs/live/previsoes.json num commit assinado pelo bot. Histórico completo, com data de geração, disponível para auditoria de qualquer pessoa.
Na semana seguinte, o realizado publicado pelo ONS é confrontado com o que foi previsto: MAE, MAPE e cobertura do intervalo atualizam o placar automaticamente.
Seis princípios que valem para qualquer forecasting sério, não só para carga elétrica.
O sazonal-semanal custa zero e erra ~4,5%. Todo MW de melhoria reportado aqui é medido contra ele, não contra um espantalho. Modelo que não bate baseline honesto é complexidade sem causa.
O MAE médio esconderia que 1h à frente é inércia e 24h é calendário. A curva de erro por horizonte é o retrato honesto do que o modelo sabe e de onde ele degrada.
Uma feature N(0,1) define a régua: o que informa menos que ruído puro sai. É barato, objetivo e evita o acúmulo de features de estimação que só adicionam variância.
Toda feature declara sua disponibilidade: passado da origem (shift ≥ 0) ou calendário do alvo. Validação temporal expansiva e holdout trancado fecham as outras portas.
Os quantis brutos cobriam 72,3% quando prometiam 80%. A calibração conformal por horizonte corrigiu para 82,8% no holdout, com um conjunto de calibração que o ajuste nunca viu. Prometer intervalo, medir cobertura e calibrar quando falha: isso separa previsão probabilística de enfeite.
Agendamento, scoring, registro imutável e monitoramento de erro real: o ciclo completo de um modelo em operação, implementado com GitHub Actions + Pages a custo zero. MLOps é disciplina, não fatura de cloud.
python src/data.py → python src/train.py → python src/evaluate.py → python src/build_charts.py --splice. O ciclo vivo é python src/score_live.py, o mesmo que o Actions executa toda segunda. Evolução natural documentada no repositório: clima como exógena (exigiria previsão meteorológica no score) e horizontes de 25-48h.