Spoiler: Depende!

Neste post, falaremos sobre: Os pacotes prophet, forecast e mafs, três pacotes para previsão de Séries temporais e o pacote cranlogs é para baixar os dados de downloads do CRAN.

library(mafs)
library(prophet)
library(cranlogs)
library(tidyverse)
library(lubridate)

 

 

Pacotes de previsão de séries temporais no R

Prophet é um pacote para R e Python que implementa o algoritmo de previsão de séries temporais usado em produção no Facebook. Ele foi programado para detectar automaticamente os padrões sazonais de uma série de input, sem precisar de ajustes manuais. Contudo, é possível customizar alguns inputs de parâmetros, como indicar a presença de períodos sazonais (semanal ou anual), feriados e changepoints. O método é descrito por inteiro neste paper. Os pacotes para R e Python são apenas uma simples interface para cálculos realizados em Stan. Segundo a própria equipe de desenvolvimento, o Prophet funciona melhor com séries temporais de frequência diária, com pelo menos um ano de dado, sendo robusto a dados ausentes (NA), mudanças na tendência e outliers.
forecast é um pacote para R criado por Rob Hyndmann, um dos maiores especialistas em Séries Temporais do mundo e autor do livro online gratuito Forecasting: principles and practice, uma excelente referência no tema. O pacote, além de funções muito úteis de visualização e tratamento de séries temporais, possui funções para ajustar dezenas de diferentes tipos de modelos de séries temporais, como ARIMA, suavização exponencial, Croston e Redes Neurais. Fácil de usar, possui também funções de previsão e avaliação de acurácia.
mafs é um pacote criado por mim, durante a elaboração do meu TCC na graduação de Engenharia de Produção. Eu queria fazer algo relacionado a previsão de demanda em larga escala, mas não sabia direito qual modelo escolher para cada série (uma tarefa das mais difícieis em séries temporais). A partir desse problema, desenvolvi um método automatizado de seleção do melhor modelo de previsão, que acabou virando o mafs. Resumidamente, sua principal função, select_forecast(), recebe uma série temporal de input, divide-a em séries de treino e de teste, ajusta 17 (ou menos, de acordo com a opção do usuário) modelos de previsão contidos no pacote forecast na série de treino, obtem previsões para cada modelo e as compara com a série de teste por meio de uma métrica de erro (como o MAPE) escolhida pelo usuário. O modelo de melhor erro é então selecionado para prever valores futuros para a série.
Faremos então um exercício de comparar a acurácia dos pacotes prophet e mafs (e por tabela o forecast) usando a série temporal de downloads diários do pacote forecast (o mais popular dos três).

Coleta dos dados

Vamos definir os parâmetros de data de nossa query:

data_inicio <- as.Date("2015-09-30")
data_fim <- as.Date("2017-09-30")
df_dls <- cran_downloads(packages = "forecast", from = data_inicio, to = data_fim)
knitr::kable(head(df_dls))

 

date count package
2015-09-30 639 forecast
2015-10-01 770 forecast
2015-10-02 644 forecast
2015-10-03 486 forecast
2015-10-04 501 forecast
2015-10-05 670 forecast

Vemos que o dataframe df_dls possui três colunas: a primeira indica a data, a segunda a quantidade de downloads do pacote naquele dia e a terceira a qual pacote os dados se referem.
Primeiramente, será que tem algum buraco nos dados? Vamos fazer uma verificação:

vetor_datas <- seq.Date(from = min(df_dls$date), to = max(df_dls$date), by = "1 day")
length(vetor_datas) == nrow(df_dls)
## [1] TRUE

O TRUE acima indica que não temos nenhum buraco nos dados. Isto é, caso haja algum dia onde ninguém baixou o forecast, o dado informado será 0 ao invés de NA.
A melhor maneira de visualizar os dados que temos é por meio de um gráfico de linha do ggplot2:

ggplot(df_dls, aes(x = date, y = count)) +
  geom_line() +
  theme_minimal() +
  labs(x = NULL, y = NULL,
       title = "Quantidade de downloads diários do pacote forecast") +
  scale_x_date(date_labels = "%m/%Y", date_breaks = "3 months")

 

unnamed chunk 4 1

Existem alguns outliers na série. Como além de ser difícil prever esses picos é improvável que eles aconteçam novamente, vamos os retirar da série:

df_dls <- df_dls %>% filter(date >= as.Date("2017-02-01"))

 

Obtendo previsões para a série

Para este post, vamos simular que o objetivo é prever o mês de Setembro da série, usando o restante como conjunto de treino.

# definir conjuntos de treino e teste
data_treino <- as.Date("2017-09-01")

 

Prophet

A função de ajuste de modelo prophet::prophet() exige que o data frame de input possua duas colunas: uma chamada ds, com o vetor de datas, e uma chamada y, com o vetor numérico da variável que se deseja prever. Aliás, uma crítica pessoal minha ao prophet é a de eles usarem dataframes como objetos de input, e não objetos do tipo ts, que é o normal no R para séries temporais.

df_dls <- df_dls %>% select(ds = date, y = count)
df_treino <- df_dls %>% filter(ds < data_treino)
df_teste <- df_dls %>% filter(ds >= data_treino)
nn <- nrow(df_teste)

As principais funções do prophet são mostradas abaixo:

# fitar modelo prophet
mod_prophet <- prophet(df_treino)
## Initial log joint probability = -2.90115
## Optimization terminated normally:
##   Convergence detected: absolute parameter change was below tolerance
fcast_prophet <- predict(mod_prophet,
                         make_future_dataframe(mod_prophet, periods = nn))

É possível visualizar as previsões fornecidas pelo prophet:

plot(mod_prophet, fcast_prophet)

 

unnamed chunk 9 1

A tabela abaixo mostra uma pequena parte do dataframe de output:

knitr::kable(head(fcast_prophet))

 

ds trend seasonal seasonalities seasonalities_lower seasonalities_upper seasonal_lower seasonal_upper weekly weekly_lower weekly_upper yhat_lower yhat_upper trend_lower trend_upper yhat
2017-02-01 2630.864 264.016251 264.016251 264.016251 264.016251 264.016251 264.016251 264.016251 264.016251 264.016251 2084.346 3657.371 2630.864 2630.864 2894.880
2017-02-02 2634.808 393.889195 393.889195 393.889195 393.889195 393.889195 393.889195 393.889195 393.889195 393.889195 2226.090 3791.494 2634.808 2634.808 3028.697
2017-02-03 2638.751 8.692494 8.692494 8.692494 8.692494 8.692494 8.692494 8.692494 8.692494 8.692494 1860.844 3467.157 2638.751 2638.751 2647.444
2017-02-04 2642.695 -645.710391 -645.710391 -645.710391 -645.710391 -645.710391 -645.710391 -645.710391 -645.710391 -645.710391 1149.036 2773.226 2642.695 2642.695 1996.984
2017-02-05 2646.638 -524.249067 -524.249067 -524.249067 -524.249067 -524.249067 -524.249067 -524.249067 -524.249067 -524.249067 1339.384 2951.047 2646.638 2646.638 2122.389
2017-02-06 2650.582 201.997594 201.997594 201.997594 201.997594 201.997594 201.997594 201.997594 201.997594 201.997594 2056.022 3700.804 2650.582 2650.582 2852.579

Vemos que o dataframe resultante é bem verboso, possuindo 16 colunas. Para este post, precisamos apenas da coluna yhat, que se refere à previsão obtida pelo prophet, além da coluna de data.

# retornar previsoes
fcast_prophet <- fcast_prophet %>%
  filter(ds >= data_treino) %>%
  select(ds, yhat) %>%
  mutate(ds = as.Date(ds), yhat = round(yhat))

 

mafs

A sintaxe do mafs é diferente. Como ele foi feito em cima do pacote forecast, o objeto de input deve ser um objeto da classe ts. Por isso, precisamos transformar os dados nesse formato:

# transformar em objeto ts
ts_dls <- ts(df_treino$y, start = lubridate::decimal_date(data_inicio),
             frequency = 365)

Assim, já podemos obter os modelos com o mafs. Nos testes que eu fiz, os modelos StructTS (modelo estrutural) e tslm (modelo de regressão que usa a tendência e a sazonalidade como regressores) não funcionam nuito bem para séries diárias (o StructTS demora uma eternidade para rodar para séries diárias).

modelo_mafs <- select_forecast(ts_dls, test_size = nn, horizon = nn,
                               error = "MAPE", verbose = TRUE,
                               dont_apply = c("StructTS", "tslm"))
## Warning in nnetar(x, p = 12, size = 12, repeats = 24): Series too short for
## seasonal lags
## Warning in forecastHybrid::hybridModel(x, verbose = FALSE): frequency(y) >=
## 24. The ets model will not be used.
## Warning in forecastHybrid::hybridModel(x, verbose = FALSE): frequency(y) >=
## 24. The Theta model will not be used.
## Warning in forecastHybrid::hybridModel(x, verbose = FALSE): The stlm model
## requres a series more than twice as long as the seasonal period. The stlm
## model will not be used.
## Warning in forecastHybrid::hybridModel(x, verbose = FALSE): The nnetar
## model requres a series more than twice as long as the seasonal period. The
## nnetar model will not be used.
## Warning in mean.default(x, na.rm = TRUE): argument is not numeric or
## logical: returning NA
## Warning in mean.default(x, na.rm = TRUE): argument is not numeric or
## logical: returning NA
## Warning in mean.default(x, na.rm = TRUE): argument is not numeric or
## logical: returning NA
## Warning in trainingaccuracy(f, test, d, D): test elements must be within
## sample
## Warning in trainingaccuracy(f, test, d, D): test elements must be within
## sample
## Warning in trainingaccuracy(f, test, d, D): test elements must be within
## sample
prev_mafs <- round(modelo_mafs$best_forecast$mean)

Vemos que alguns dos modelos aplicados pelo mafs produziram alguma mensagem de aviso ou não puderam ser obtidos. De fato, o dataframe modelo_mafs$df_models retorna apenas 13 modelos:

knitr::kable(modelo_mafs$df_models)

 

model ME RMSE MAE MPE MAPE MASE ACF1 best_model runtime_model
auto.arima 430.0275 543.5366 471.5111 11.938668 13.422700 NaN 0.3145553 ets 0.377
bats 339.0677 473.3145 428.6866 9.189533 12.378723 NaN 0.3059720 ets 1.882
croston 216.5770 396.5251 375.9404 5.472501 11.130077 NaN 0.3041755 ets 1.886
ets -145.3627 362.5698 260.4997 -5.475541 8.614132 NaN 0.3041755 ets 0.075
hybrid 26257.8410 37720.2071 26257.8410 786.376373 786.376373 NaN 0.8728045 ets 9.695
meanf 441.9520 552.8545 482.2700 12.289703 13.732511 NaN 0.3041755 ets 0.001
naive -145.3667 362.5714 260.5000 -5.475662 8.614149 NaN 0.3041755 ets 0.004
nnetar 136.5865 557.6698 472.8066 3.481630 14.652416 NaN 0.4416121 ets 1.285
rwf -145.3667 362.5714 260.5000 -5.475662 8.614149 NaN 0.3041755 ets 0.003
rwf_drift -239.1374 409.9504 283.8696 -8.303362 9.516029 NaN 0.3038044 ets 0.005
snaive 595.5000 1254.7320 984.1000 18.379700 29.369605 NaN 0.2117650 ets 0.002
splinef -3506.0761 3982.5196 3506.0761 -106.818788 106.818788 NaN 0.8778869 ets 0.440
tbats 339.0677 473.3145 428.6866 9.189533 12.378723 NaN 0.3059720 ets 4.372

Vamos então obter a previsão futura produzida pelo mafs e a juntar com a previsão do prophet no dataframe de teste:

prev_mafs <- round(modelo_mafs$best_forecast$mean)
fcast_prophet$yhat_mafs <- as.numeric(prev_mafs)
# mudar nome das colunas
names(fcast_prophet) <- c("ds", "previsao_prophet", "previsao_mafs")
# juntar dataframe de resultado com o de previsao
df_teste <- df_teste %>%  left_join(fcast_prophet, by = "ds")
# plotar previsoes vs resultados reais
df_teste %>%
  gather(metodo, previsao, -(1:2)) %>%
  ggplot(aes(x = ds, y = y)) +
    geom_line() +
    geom_line(aes(y = previsao, color = metodo))

 

unnamed chunk 15 1
mafs produziu uma previsão de linha reta. Apenas como forma de demonstrar o uso do meu pacote, vamos remover o modelo ets da lista de modelos usados e rever os resultados:

modelo_mafs <- select_forecast(ts_dls, test_size = nn, horizon = nn,
                               error = "MAPE", verbose = FALSE,
                               dont_apply = c("StructTS", "ets", "tslm"))
## Warning in nnetar(x, p = 12, size = 12, repeats = 24): Series too short for
## seasonal lags
## Warning in forecastHybrid::hybridModel(x, verbose = FALSE): frequency(y) >=
## 24. The ets model will not be used.
## Warning in forecastHybrid::hybridModel(x, verbose = FALSE): frequency(y) >=
## 24. The Theta model will not be used.
## Warning in forecastHybrid::hybridModel(x, verbose = FALSE): The stlm model
## requres a series more than twice as long as the seasonal period. The stlm
## model will not be used.
## Warning in forecastHybrid::hybridModel(x, verbose = FALSE): The nnetar
## model requres a series more than twice as long as the seasonal period. The
## nnetar model will not be used.
## Warning in mean.default(x, na.rm = TRUE): argument is not numeric or
## logical: returning NA
## Warning in mean.default(x, na.rm = TRUE): argument is not numeric or
## logical: returning NA
## Warning in mean.default(x, na.rm = TRUE): argument is not numeric or
## logical: returning NA
## Warning in trainingaccuracy(f, test, d, D): test elements must be within
## sample
## Warning in trainingaccuracy(f, test, d, D): test elements must be within
## sample
## Warning in trainingaccuracy(f, test, d, D): test elements must be within
## sample
prev_mafs <- round(modelo_mafs$best_forecast$mean)
fcast_prophet$previsao_mafs <- as.numeric(prev_mafs)
# mudar nome das colunas
names(fcast_prophet) <- c("ds", "previsao_prophet", "previsao_mafs")
# juntar dataframe de resultado com o de previsao
df_teste <- df_dls %>% filter(ds >= data_treino)
df_teste <- df_teste %>%  left_join(fcast_prophet, by = "ds")
df_teste %>%
  gather(metodo, previsao, -(1:2)) %>%
  ggplot(aes(x = ds, y = y)) +
    geom_line() +
    geom_line(aes(y = previsao, color = metodo))

unnamed chunk 17 1

Por mais incrível que pareça, mais uma vez uma linha reta foi fornecida como previsão pelo mafs, enquanto o prophet conseguiu prever com muita eficácia a sazonalidade da série.
Numericamente, o erro médio absoluto de downloads é de:
real <- df_teste$y
prev_prophet <- df_teste$previsao_prophet
prev_mafs <- df_teste$previsao_mafs
mean(abs(real - prev_mafs))
## [1] 1010.867
mean(abs(real - prev_prophet))
## [1] 837.0333

 

Considerações finais

Sobre o título (meio sensacionalista) do post: Em meus estudos sobre séries temporais, é comum encontrar livros e papers afirmando que é impossível determinar que o modelo X sempre será melhor que Y. Cada série temporal possui suas próprias características: sazonalidade, outliers, ciclos de negócios, tendência, frequência, etc. O recomendável é estudar a teoria de cada modelo que se deseja usar, variar seus parâmetros e pesquisar em artigos benchmarks para séries temporais de um determinado contexto (por exemplo, para vendas de produtos de demanda intermitente costuma-se usar Croston).