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")
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)
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))
O 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))
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).