Relatório de Modelo: Seasonal Naive Baseline

Análise de Desempenho e Previsão Base

Authors

UIVS - Unidade de Inteligência em Vigilância em Saúde

Caio Sain Vallio

Published

11/01/2026

1. Introdução

1.1 Objetivo

Este relatório analisa o desempenho do modelo Seasonal Naive, que serve como baseline (referência base) para o sistema de previsão de dengue. Este modelo pressupõe que “o futuro repetirá o passado recente seguindo a sazonalidade”, utilizando o valor observado na mesma semana do ano anterior como previsão.

1.2 Metodologia

  • Modelo: Seasonal Naive (SNaive)
  • Estratégia: Rolling Replication (Janela deslizante)
  • Horizontes de Previsão: 4, 6 e 8 semanas
  • Métricas: RMSE, MAE, MAPE, RMSLE, MASE
  • Interpretabilidade: O modelo captura puramente a sazonalidade anual (lag 52).

2. Dados e Preparação

Code
# 1. Carregar dados brutos
df_raw <- load_raw_data()
#> [2026-01-11 14:46:09] INFO: Carregando dados de: /Users/caiosainvallio/ses-sp/forecast_dengue/data/raw/dengue.RData 
#> [2026-01-11 14:46:09] INFO: Dados carregados: 248865 linhas, 23 colunas
Code
# 2. Agregar por estado 
df_state <- aggregate_state(df_raw)
#> [2026-01-11 14:46:09] INFO: Agregando dados por estado... 
#> [2026-01-11 14:46:09] INFO: Dados agregados: 626 semanas
Code
# 3. Preprocessamento (limpeza, imputacao, transformacao)
df <- preprocess_data(df_state)
#> [2026-01-11 14:46:09] INFO: ========== INICIO: Preprocessamento ========== 
#> [2026-01-11 14:46:09] INFO: Removidas 3 linhas da semana 53 
#> [2026-01-11 14:46:09] WARN: Semanas faltantes detectadas: 2 
#> [2026-01-11 14:46:09] WARN: ATENCAO: Alvo nao sera imputado (sera mantido como NA) 
#> [2026-01-11 14:46:09] INFO: Imputados 8 NAs em mean_temp (fill) 
#> [2026-01-11 14:46:09] INFO: Imputados 8 NAs em mean_max_temp (fill) 
#> [2026-01-11 14:46:09] INFO: Imputados 8 NAs em mean_min_temp (fill) 
#> [2026-01-11 14:46:09] INFO: Imputados 8 NAs em mean_precip (fill) 
#> [2026-01-11 14:46:09] INFO: Dados preprocessados: 625 linhas 
#> [2026-01-11 14:46:09] INFO: ========== FIM: Preprocessamento ==========
Code
# NOTA: Seasonal Naive NÃO precisa de features externas
# Usa apenas o valor defasado de 52 semanas (sazonalidade)
# Por isso, NÃO chamamos make_features() aqui

df_features <- df  # Usar dados preprocessados diretamente

# Visualizar ultimas semanas
tail(df_features |> select(data_iniSE, est_inc100k), 5) |>
  kable(caption = "Dados mais recentes da série")
Dados mais recentes da série
data_iniSE est_inc100k
621 2025-11-23 25.519079
622 2025-11-30 25.899642
623 2025-12-07 23.605036
624 2025-12-14 20.172107
625 2025-12-21 6.802992

3. Definição do Modelo

Code
# Carregar definicao do modelo do registro
model_name <- "seasonal_naive"
model <- get_model(model_name)

cat(sprintf("Modelo: %s\nFamília: %s\nDescrição: %s", 
            model$name, model$family, model$description))
#> Modelo: seasonal_naive
#> Família: baseline
#> Descrição: Previsao usando valor da mesma semana do ano anterior

4. Backtesting (Validação Histórica)

Realizamos um backtesting robusto utilizando a abordagem de origem deslizante (rolling origin). O modelo é reavaliado a cada passo de tempo para garantir que as métricas reflitam o desempenho esperado em produção.

Code
# Configuracao do Backtest
# Vamos definir horizontes explicitos para este relatorio
backtest_config <- list(
  backtest = list(
    horizons = c(4, 6, 8),
    initial_window = 52 * 5, # 5 anos de treino inicial
    step = 4                 # Avancar a cada 4 semanas
  ),
  data = list(
    date_col = "data_iniSE",
    target = "est_inc100k"
  )
)

# Executar Backtest
# Nota: O SNaive eh muito rapido, nao precisa de cache complexo
bt_result <- run_backtest(
  model_name = model_name,
  data = df_features,
  config = backtest_config,
  verbose = FALSE
)

# Resumo da execucao
print(bt_result)
#> 
#> === Resultado de Backtest ===
#> 
#> Modelo: seasonal_naive 
#> Origens: 90 
#> Previsoes: 270 
#> Duracao: 0.1 s
#> 
#> Metricas por Horizonte:
#>  h      mae     rmse     mape    smape     mase     rmsle  n
#>  4 42.74004 96.31014 66.14327 68.11956 1.261964 0.9683374 90
#>  6 41.76718 89.45208 66.88110 67.60992 1.233238 0.9715382 90
#>  8 42.75303 96.31281 66.41829 67.49840 1.262347 0.9609409 90

5. Avaliação de Desempenho

5.1 Métricas Gerais

Abaixo apresentamos as métricas de erro agregadas por horizonte de previsão.

Code
# Formatar tabela de metricas
bt_result$metrics |>
  mutate(
    across(where(is.numeric), \(x) round(x, 4)),
    h_desc = paste(h, "semanas")
  ) |>
  select(Horizonte = h_desc, RMSE = rmse, MAE = mae, MAPE = mape, RMSLE = rmsle, MASE = mase, N_Obs = n) |>
  kable() |>
  kable_styling(bootstrap_options = c("striped", "hover"))
Horizonte RMSE MAE MAPE RMSLE MASE N_Obs
4 semanas 96.3101 42.7400 66.1433 0.9683 1.2620 90
6 semanas 89.4521 41.7672 66.8811 0.9715 1.2332 90
8 semanas 96.3128 42.7530 66.4183 0.9609 1.2623 90

Interpretação das Métricas

  • RMSE (Root Mean Squared Error): Penaliza erros grandes. Valor na escala da variável.
  • MAE (Mean Absolute Error): Erro médio absoluto. Mais interpretável diretamente.
  • MAPE (Mean Absolute Percentage Error): Erro percentual.
  • RMSLE (Root Mean Squared Logarithmic Error): Aproximação do erro percentual, penaliza menos erros em valores altos e mais em valores baixos. Importante para dados epidemiológicos com crescimento exponencial.
  • MASE (Mean Absolute Scaled Error): Comparativo com o SNaive in-sample. Valores próximos a 1 são esperados para o próprio SNaive no out-of-sample.

5.2 Análise Visual das Previsões

Comparação entre valores previstos e observados para o horizonte de 4 semanas.

Code
# Extrair previsoes de multiplos horizontes
preds_multi <- bind_rows(
  extract_predictions(bt_result, horizon = 4) |> mutate(horizon = "4 semanas"),
  extract_predictions(bt_result, horizon = 6) |> mutate(horizon = "6 semanas"),
  extract_predictions(bt_result, horizon = 8) |> mutate(horizon = "8 semanas")
)

# Plot estatico para ser convertido em interativo
p_backtest <- ggplot() +
  geom_line(data = preds_multi, aes(x = target_date, y = actual), color = "black", size = 0.5) +
  geom_line(data = preds_multi, aes(x = target_date, y = predicted, color = horizon), size = 0.5, alpha = 0.8) +
  scale_color_manual(values = c("4 semanas" = "#E74C3C", "6 semanas" = "#3498DB", "8 semanas" = "#2ECC71")) +
  labs(title = "Backtest: Previsões Multi-Horizonte",
       x = "Data", y = "Incidência") +
  theme_minimal()

ggplotly(p_backtest) |>
  layout(legend = list(orientation = "h", x = 0.5, xanchor = "center", y = -0.2))

5.3 Erros por Regime Epidemiológico

É importante avaliar se o modelo erra mais em picos epidêmicos ou em períodos de baixa transmissão.

Code
# Adicionar classificacao de regime aos resultados
preds_all <- bt_result$predictions
preds_all$regime <- classify_regime(preds_all$actual)

# Calcular metricas por regime
metrics_regime <- compute_metrics_by_regime(preds_all)

metrics_regime |>
  mutate(across(where(is.numeric), \(x) round(x, 3))) |>
  datatable(options = list(dom = 't'))

6. Previsão Futura

Gerando previsões para as próximas semanas com o modelo treinado em todos os dados disponíveis até 2 semanas atrás, simulando o atraso real de notificação do SINAN.

Code
# Definir atraso de notificação (semanas a ignorar)
delay_weeks <- 2
last_reliable_date <- max(df_features$data_iniSE) - (delay_weeks * 7)

# Filtrar dados para treino final (removendo ultimas semanas incompletas)
df_final <- df_features |> 
  filter(data_iniSE <= last_reliable_date)

cat(sprintf("Ignorando últimas %d semanas devido ao atraso de notificação.\n", delay_weeks))
#> Ignorando últimas 2 semanas devido ao atraso de notificação.
Code
cat(sprintf("Treinando modelo com dados até: %s\n", last_reliable_date))
#> Treinando modelo com dados até: 2025-12-07
Code
# Treinar com dados confiaveis
model_fit <- model$fit(df_final, list(season = 52)) # Sazonalidade anual

# Datas para previsão (seguindo padrão SARIMA: iniciando após o último dado disponível, T+1)
# O modelo foi treinado até T-2, mas projetamos 8 semanas à frente para cobrir 
# o período de nowcast (T-1, T) e futuro (T+1..T+6), rotulando como T+1..T+8
last_date <- max(df_features$data_iniSE)
future_dates <- seq(
  from = last_date + 7,
  by = "week",
  length.out = 8
)

# Prever 8 semanas (2 nowcast + 6 futuro)
forecast_h8 <- model$predict(model_fit, h = 8)

# Intervalos de confianca
intervals <- model$predict_interval(model_fit, h = 8, level = 0.95)

# Tabela de Previsão
forecast_df <- data.frame(
  Data = future_dates,
  Semana_Epidem = lubridate::epiweek(future_dates),
  Previsao = round(forecast_h8, 2),
  Inferior_95 = round(intervals$lower, 2),
  Superior_95 = round(intervals$upper, 2)
)

forecast_df |>
  kable(caption = "Previsão Seasonal Naive - Próximas 8 Semanas") |>
  kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
Previsão Seasonal Naive - Próximas 8 Semanas
Data Semana_Epidem Previsao Inferior_95 Superior_95
2025-12-28 53 40.53 -123.44 204.49
2026-01-04 1 43.58 -120.38 207.54
2026-01-11 2 75.91 -88.05 239.88
2026-01-18 3 99.27 -64.69 263.24
2026-01-25 4 114.16 -49.81 278.12
2026-02-01 5 135.88 -28.08 299.84
2026-02-08 6 147.50 -16.47 311.46
2026-02-15 7 153.39 -10.58 317.35
Code
# Dados históricos usados no treino (até T-2)
history_train <- df_features |> 
  filter(data_iniSE <= last_reliable_date) |>
  tail(104) |> 
  select(Date = data_iniSE, Value = est_inc100k) |>
  mutate(Type = "Histórico", Lower = NA, Upper = NA)

# Dados recentes ignorados no treino (T-2 a T)
history_ignored <- df_features |> 
  filter(data_iniSE > last_reliable_date) |>
  select(Date = data_iniSE, Value = est_inc100k) |>
  mutate(Type = "Provisório (Ignorado)", Lower = NA, Upper = NA)

# Previsão do modelo (T-2 a T+6)
future_viz <- data.frame(
  Date = future_dates,
  Value = forecast_h8,
  Lower = intervals$lower,
  Upper = intervals$upper,
  Type = "Previsão (SNaive)"
)

# Combinar para plot
viz_df <- bind_rows(history_train, history_ignored, future_viz)

p_future <- ggplot(viz_df, aes(x = Date, y = Value, color = Type)) +
  geom_line(aes(linetype = Type)) +
  geom_ribbon(aes(ymin = Lower, ymax = Upper, fill = Type), alpha = 0.2, color = NA) +
  geom_point(data = filter(viz_df, Type == "Provisório (Ignorado)"), size = 2) +
  scale_color_manual(values = c(
    "Histórico" = "black", 
    "Previsão (SNaive)" = "#E74C3C",
    "Provisório (Ignorado)" = "gray"
  )) +
  scale_fill_manual(values = c(
    "Histórico" = NA, 
    "Previsão (SNaive)" = "#E74C3C",
    "Provisório (Ignorado)" = NA
  )) +
  scale_linetype_manual(values = c(
    "Histórico" = "solid",
    "Previsão (SNaive)" = "solid",
    "Provisório (Ignorado)" = "dashed"
  )) +
  labs(title = "Previsão SNaive - Próximas 8 Semanas",
       subtitle = "Previsão gerada ignorando as últimas 2 semanas (simulação)",
       y = "Incidência / 100k hab") +
  theme_minimal() +
  theme(legend.position = "bottom")

ggplotly(p_future) |>
  layout(legend = list(orientation = "h", x = 0.5, xanchor = "center"))

7. Salvamento e Registro

7.1 Salvar Modelo

Salvando o objeto do modelo ajustado para uso posterior.

Code
# Diretorio de modelos
models_dir <- file.path(PROJECT_ROOT, "data", "models")
if (!dir.exists(models_dir)) dir.create(models_dir, recursive = TRUE)

# Salvar modelo
model_path <- file.path(models_dir, "seasonal_naive.rds")
saveRDS(model_fit, model_path)
cat(sprintf("Modelo salvo em: %s\n", model_path))
#> Modelo salvo em: /Users/caiosainvallio/ses-sp/forecast_dengue/data/models/seasonal_naive.rds

7.2 Salvar Metadados (Tabela de Comparação)

Atualizando a tabela mestre de métricas para comparação entre modelos.

Code
# Caminho da tabela de metricas
metrics_path <- file.path(PROJECT_ROOT, "data", "model_metrics.RData")

# Preparar nova linha de registro
new_entry <- data.frame(
  model_name = "seasonal_naive",
  execution_date = Sys.time(),
  horizon_4w_rmse = bt_result$metrics[bt_result$metrics$h == 4, "rmse"],
  horizon_4w_mae = bt_result$metrics[bt_result$metrics$h == 4, "mae"],
  horizon_4w_mape = bt_result$metrics[bt_result$metrics$h == 4, "mape"],
  horizon_4w_rmsle = bt_result$metrics[bt_result$metrics$h == 4, "rmsle"],
  
  horizon_8w_rmse = bt_result$metrics[bt_result$metrics$h == 8, "rmse"],
  horizon_8w_mae = bt_result$metrics[bt_result$metrics$h == 8, "mae"],
  horizon_8w_mape = bt_result$metrics[bt_result$metrics$h == 8, "mape"],
  horizon_8w_rmsle = bt_result$metrics[bt_result$metrics$h == 8, "rmsle"],
  
  avg_rmse = mean(bt_result$metrics$rmse),
  avg_mape = mean(bt_result$metrics$mape)
)

# Carregar ou criar tabela
if (file.exists(metrics_path)) {
  load(metrics_path)
  # Se ja existe entrada para este modelo, remover antiga (ou manter historico?)
  # Vamos manter historico por data, mas para dashboard costuma-se pegar a mais recente
  model_metrics <- rbind(model_metrics, new_entry)
} else {
  model_metrics <- new_entry
}

# Salvar
save(model_metrics, file = metrics_path)

cat("Tabela de métricas atualizada com sucesso.\n")
#> Tabela de métricas atualizada com sucesso.
Code
model_metrics |> tail(5) |> kable()
model_name execution_date horizon_4w_rmse horizon_4w_mae horizon_4w_mape horizon_4w_rmsle horizon_8w_rmse horizon_8w_mae horizon_8w_mape horizon_8w_rmsle avg_rmse avg_mape
seasonal_naive 2026-01-11 14:46:10 96.31014 42.74004 66.14327 0.9683374 96.31281 42.75303 66.41829 0.9609409 94.02501 66.48089

8. Conclusão

O modelo Seasonal Naive estabelece o baseline de performance. Qualquer modelo preditivo mais complexo (ARIMA, Machine Learning, Redes Neurais) deve obrigatoriamente superar as métricas apresentadas neste relatório para justificar sua complexidade.

Resultados Chave: - RMSE (4 sem): 96.31 - MAPE (4 sem): 66.14%

Próximos Passos

Utilizar estes valores como referência mínima (“floor”) para os próximos modelos a serem desenvolvidos.