lista_1

Exercício sobre parte 1 - Forecasting

Bibliotecas

Importando Pacotes utilizados neste documento.

library(fpp3)
── Attaching packages ────────────────────────────────────────────── fpp3 0.5 ──
✔ tibble      3.2.1     ✔ tsibble     1.1.4
✔ dplyr       1.1.4     ✔ tsibbledata 0.4.1
✔ tidyr       1.3.1     ✔ feasts      0.3.2
✔ lubridate   1.9.3     ✔ fable       0.3.4
✔ ggplot2     3.5.0     ✔ fabletools  0.4.2
── Conflicts ───────────────────────────────────────────────── fpp3_conflicts ──
✖ lubridate::date()    masks base::date()
✖ dplyr::filter()      masks stats::filter()
✖ tsibble::intersect() masks base::intersect()
✖ tsibble::interval()  masks lubridate::interval()
✖ dplyr::lag()         masks stats::lag()
✖ tsibble::setdiff()   masks base::setdiff()
✖ tsibble::union()     masks base::union()
library(tseries)
Registered S3 method overwritten by 'quantmod':
  method            from
  as.zoo.data.frame zoo 
library(forecast)
library(prophet)
Carregando pacotes exigidos: Rcpp
Carregando pacotes exigidos: rlang
library(xts)
Carregando pacotes exigidos: zoo

Attaching package: 'zoo'
The following object is masked from 'package:tsibble':

    index
The following objects are masked from 'package:base':

    as.Date, as.Date.numeric

######################### Warning from 'xts' package ##########################
#                                                                             #
# The dplyr lag() function breaks how base R's lag() function is supposed to  #
# work, which breaks lag(my_xts). Calls to lag(my_xts) that you type or       #
# source() into this session won't work correctly.                            #
#                                                                             #
# Use stats::lag() to make sure you're not using dplyr::lag(), or you can add #
# conflictRules('dplyr', exclude = 'lag') to your .Rprofile to stop           #
# dplyr from breaking base R's lag() function.                                #
#                                                                             #
# Code in packages is not affected. It's protected by R's namespace mechanism #
# Set `options(xts.warn_dplyr_breaks_lag = FALSE)` to suppress this warning.  #
#                                                                             #
###############################################################################

Attaching package: 'xts'
The following objects are masked from 'package:dplyr':

    first, last
library(modeltime)
library(tidymodels)
── Attaching packages ────────────────────────────────────── tidymodels 1.2.0 ──
✔ broom        1.0.6      ✔ recipes      1.0.10
✔ dials        1.2.1      ✔ rsample      1.2.1 
✔ infer        1.0.7      ✔ tune         1.2.1 
✔ modeldata    1.3.0      ✔ workflows    1.1.4 
✔ parsnip      1.2.1      ✔ workflowsets 1.1.0 
✔ purrr        1.0.2      ✔ yardstick    1.3.1 
── Conflicts ───────────────────────────────────────── tidymodels_conflicts() ──
✖ purrr::%@%()          masks rlang::%@%()
✖ yardstick::accuracy() masks forecast::accuracy(), fabletools::accuracy()
✖ purrr::discard()      masks scales::discard()
✖ dplyr::filter()       masks stats::filter()
✖ xts::first()          masks dplyr::first()
✖ purrr::flatten()      masks rlang::flatten()
✖ purrr::flatten_chr()  masks rlang::flatten_chr()
✖ purrr::flatten_dbl()  masks rlang::flatten_dbl()
✖ purrr::flatten_int()  masks rlang::flatten_int()
✖ purrr::flatten_lgl()  masks rlang::flatten_lgl()
✖ purrr::flatten_raw()  masks rlang::flatten_raw()
✖ infer::generate()     masks fabletools::generate()
✖ infer::hypothesize()  masks fabletools::hypothesize()
✖ purrr::invoke()       masks rlang::invoke()
✖ dplyr::lag()          masks stats::lag()
✖ xts::last()           masks dplyr::last()
✖ parsnip::null_model() masks fabletools::null_model()
✖ rsample::populate()   masks Rcpp::populate()
✖ purrr::splice()       masks rlang::splice()
✖ recipes::step()       masks stats::step()
• Search for functions across packages at https://www.tidymodels.org/find/

Importando Base de Dados

Para a resolução dos exercícios, será utilizada a base de dados aus_retail, que consiste em dados sobre as vendas no varejo australiano, uma série temporal mensal de diferentes categorias de varejo.

Abaixo esta listada todas as categorias presentes na base de dados:

unique(aus_retail$Industry)
 [1] "Cafes, restaurants and catering services"                         
 [2] "Cafes, restaurants and takeaway food services"                    
 [3] "Clothing retailing"                                               
 [4] "Clothing, footwear and personal accessory retailing"              
 [5] "Department stores"                                                
 [6] "Electrical and electronic goods retailing"                        
 [7] "Food retailing"                                                   
 [8] "Footwear and other personal accessory retailing"                  
 [9] "Furniture, floor coverings, houseware and textile goods retailing"
[10] "Hardware, building and garden supplies retailing"                 
[11] "Household goods retailing"                                        
[12] "Liquor retailing"                                                 
[13] "Newspaper and book retailing"                                     
[14] "Other recreational goods retailing"                               
[15] "Other retailing"                                                  
[16] "Other retailing n.e.c."                                           
[17] "Other specialised food retailing"                                 
[18] "Pharmaceutical, cosmetic and toiletry goods retailing"            
[19] "Supermarket and grocery stores"                                   
[20] "Takeaway food services"                                           

Para realizar o trabalho iremos utilizar a categoria de “Department stores”:

# Realizando Filtro
department_stores_data <- aus_retail %>%
  filter(Industry == "Department stores")

Estes dados referem-se a lojas de departamentos, que são grandes estabelecimentos de varejo que oferecem uma ampla variedade de mercadorias em diferentes seções ou departamentos dentro da mesma loja.

Pre-Precessamento

Da forma que o dataframe está organizado, existem diferentes estados:

unique(department_stores_data$State)
[1] "Australian Capital Territory" "New South Wales"             
[3] "Queensland"                   "South Australia"             
[5] "Victoria"                     "Western Australia"           

Para a resolução dos exercícios da lista, serão utilizados os dados da Austrália inteira, sendo necessária a seguinte transformação:

department_stores_group <- department_stores_data %>%
  index_by(Month) %>%
  summarise(Turnover_total = sum(Turnover, na.rm = TRUE))

Com esta transformação, os dados são agrupados pela coluna “Month”, somando os turnovers de todos os estados conforme essa coluna. O resultado final é a receita total de lojas de departamentos para toda a Austrália.

Resolução:

Questão 1

Plote os gráficos da série e das ACF/PACF e discorra sobre o aspecto da série. Faça o teste de raíz unitária e explique as conclusões. Existe tendência estocástica?

O primeiro gráfico análisado será o Time Plot:

department_stores_group %>%
  autoplot(Turnover_total) +
  labs(y="Turnover", title="Australian Department Stores Turnover")

O gráfico plotado acima demonstra um aparente padrão sazonal na série, com um pico de receita que se repete ao longo dos anos. Além disso, parece haver uma tendência de aumento de receita ao longo da série.

Outro gráfico analisado será o Seasonal Plot:

department_stores_group %>%
  gg_season(Turnover_total, period = "year")

Observando o gráfico plotado, é possível identificar um padrão sazonal de aumento de vendas no mês de dezembro, representado pelo pico no gráfico. Além disso, após esse pico de vendas, há uma queda aparente em fevereiro, que se mostra mais acentuada nos anos mais recentes.

Para uma melhor compreensão sobre a possível tendência de aumento de receita ao longo dos anos, podemos observar o gráfico de subplots abaixo:

department_stores_group %>%
  gg_subseries(Turnover_total)

Como podemos observar pelo gráfico discriminado por meses acima, a sazonalidade de aumento de vendas em dezembro está bem representada. Além disso, nota-se que, de maneira geral, houve um aparente aumento acentuado de receita em todos os meses entre os anos de 1990 e o início dos anos 2000, estabilizando-se nos anos seguintes.

Para reforçar essas premissas, podemos analisar as funções de autocorrelação e autocorrelação parcial.

# Gráfico de Autocorrelação
department_stores_group %>%
  ACF(Turnover_total, lag_max = 100) %>%
  autoplot()

Como esperado, a função de autocorrelação revela fortes correlações entre os lags múltiplos de 12 (12, 24, 36…) indicando a sazonalidade associada aos meses de dezembro. Além disso, é possível observar uma aparente tendência positiva, evidenciada pela significativa autocorrelação nos lags menores, que diminui gradualmente à medida que os lags aumentam.

# Gráfico de Autocorrelação Parcial
department_stores_group %>%
  PACF(Turnover_total, lag_max = 100) %>%
  autoplot()

Analisando o gráfico gerado pela PACF, percebemos um pico positivo seguido por um pico negativo, ambos muito acima das bandas do intervalo de confiança, nos lags 12 e 13. Isso indica uma forte sazonalidade neste período do ano, conforme observado nos gráficos anteriores.

Ao que tudo indica, esta série temporal possui raiz unitária e não é estacionária. Para obtermos mais uma medida que confirme esta hipótese, podemos realizar o teste de raiz unitária de Dickey-Fuller abaixo:

# Realizar o teste ADF
adf.test(department_stores_group$Turnover_total, alternative = c("stationary", "explosive"), k = trunc((length(department_stores_group$Turnover_total)-1)^(1/3)))
Warning in adf.test(department_stores_group$Turnover_total, alternative =
c("stationary", : p-value smaller than printed p-value

    Augmented Dickey-Fuller Test

data:  department_stores_group$Turnover_total
Dickey-Fuller = -8.9811, Lag order = 7, p-value = 0.01
alternative hypothesis: stationary

O resultado do teste realizado, com p-valor menor do que o nível de 0,05, indica que não existem evidências significativas para rejeitar a hipótese nula de que a série não é estacionária. Este resultado contraria os resultados obtidos anteriormente pelos gráficos da série e pela função ACF. Um dos motivos para isso pode ser o fato de a série possuir uma tendência fraca que não foi capturada pelo teste ADF. Nesse caso, podemos dizer que a série possui uma fraca estacionariedade e não tem uma tendência estocástica evidente. Contudo, esses resultados podem estar sendo afetados pela forte sazonalidade que a série apresenta.

Questão 2

Use um método de decomposição estudado e explique os componentes, tendo em vista o aspecto da série. Existe sazonalidade? Qual periodicidade?

Aplicando método de decomposição dos componentes:

department_stores_group %>%
  model(
    classical_decomposition(Turnover_total, type = "additive")
  ) |>
  components() |>
  autoplot() +
  labs(title = "Decomposição Série Departaments Stores")
Warning: Removed 6 rows containing missing values or values outside the scale range
(`geom_line()`).

Como podemos observar no gráfico de decomposição, existe um padrão sazonal de picos de venda no final de cada ano da série, fenômeno que já podia ser observado nos gráficos gerados anteriormente.

Ao contrário do resultado do teste ADF realizado na questão anterior, a decomposição da série temporal sugere a presença de uma tendência positiva de aumento nas vendas ao longo dos anos, o que também pode ser observado nos gráficos ACF e Seasonal plots realizados anteriormente.

Assim, podemos concluir que existe uma sazonalidade de 12 meses ao final de cada ano.

Questão 3

Identifique um modelo SARIMA apropriado. Justifique o modelo escolhido através de critérios de informação e/ou usando a etapa de identificação da abordagem Box-Jenkins. Nesta etapa é possível utilizar um método automático de seleção das ordens do modelo.

Como foi observado anteriormente, existe uma sazonalidade anual na série. Por conta disso, iremos diferenciar a série, componente d, com base nesta sazonalidade anual para tornar a série ainda mais estacionária.

# Aplicando diferenciação de 12 meses
department_stores_diff <- diff(department_stores_group$Turnover_total, lag=12)

# Plotar ACF
acf(department_stores_diff, main="ACF da Série Diferenciada Anualmente")

# Plotando PACf
pacf(department_stores_diff, main="PACF da Série Diferenciada Anualmente")

Ao aplicar a diferenciação, percebe-se que os componentes de tendência e sazonalidade observados anteriormente foram reduzidos e que a série apresentou um resultado de ACF muito mais próximo de um padrão estacionário.

Com isso, podemos rodas os modelos abaixo:

# Definindo os dados
y <- ts(department_stores_group$Turnover_total, frequency = 12) 

# Definindo os parâmetros para testar
orders <- list(c(1,1,0), c(1,1,1), c(0,1,0), c(0,1,1))
seasonal_orders <- list(c(1,1,0), c(1,1,1), c(0,1,0))

# Inicializando listas para armazenar os resultados
models <- list()
aic <- numeric()
aicc <- numeric()
bic <- numeric()

# Loop para ajustar os modelos
for (i in seq_along(orders)) {
    for (j in seq_along(seasonal_orders)) {
        model <- Arima(y, order = orders[[i]],
                       seasonal = list(order = seasonal_orders[[j]], period = 12),
                       method = "ML")
        model_label <- paste("fit", i, j, sep="_")
        models[[model_label]] <- model
        aic <- c(aic, model$aic)
        aicc <- c(aicc, model$aicc)
        bic <- c(bic, model$bic)
    }
}

# Criando uma tabela com os resultados
results <- cbind(Model = names(models), AIC = aic, AICC = aicc, BIC = bic)
results <- as.data.frame(results)


# Mostrando a tabela de resultados
print(results[order(results$AICC),]  )
     Model              AIC             AICC              BIC
5  fit_2_2 4574.88795574596 4575.03013584074 4595.18357172387
4  fit_2_1 4584.15908735852 4584.25365000627 4600.39558014085
11 fit_4_2  4588.4096674886 4588.50423013635 4604.64616027092
10 fit_4_1 4602.68868186884 4602.74528564242 4614.86605145558
6  fit_2_3 4614.97563404118 4615.03223781477 4627.15300362793
12 fit_4_3 4636.39915387812 4636.42738917224 4644.51740026928
2  fit_1_2 4733.80991195571 4733.90447460346 4750.04640473803
1  fit_1_1 4746.62013091522 4746.67673468881 4758.79750050197
3  fit_1_3 4776.26472192322 4776.29295721734 4784.38296831438
8  fit_3_2 4890.38560726148 4890.44221103506 4902.56297684822
7  fit_3_1 4915.98535299739 4916.01358829151 4924.10359938855
9  fit_3_3 4961.56717455822 4961.57656422958  4965.6262977538

De todos os modelos testados o que obteve melhor desempenho de acordo com a métrica AICC foi o modelo fit_2_2:

best_model <- models[["fit_2_2"]]

# Mostrando um resumo do modelo
summary(best_model)
Series: y 
ARIMA(1,1,1)(1,1,1)[12] 

Coefficients:
          ar1      ma1    sar1     sma1
      -0.2217  -0.8239  0.1506  -0.4830
s.e.   0.0548   0.0320  0.1098   0.0958

sigma^2 = 2514:  log likelihood = -2282.44
AIC=4574.89   AICc=4575.03   BIC=4595.18

Training set error measures:
                     ME     RMSE      MAE        MPE     MAPE      MASE
Training set -0.6230457 49.16252 36.80342 -0.4086745 3.528757 0.7334425
                  ACF1
Training set -0.024329

Ajustando o modelo otimizado:

fit = Arima(y, order=c(1,1,1), seasonal=list(order=c(1,1,1), period=12), method="ML")

Questão 4

Faça a verificação de diagnóstico residual de seu modelo ARIMA. Os resíduos são ruído branco?

Com o modelo otimizado ajustado, podemos realizar o diagnostico de desempenho.

par(mfrow=c(1,2))
acf(residuals(fit), 96, main="")
pacf(residuals(fit), 96, main="")

Como pode ser observado, os resíduos apresentam caracteristicas de ruído branco.

Questão 5

Use o modelo SARIMA de sua escolha para fazer previsões h (livre escolha) passos à frente.

Realizando predição de 24 períodos (2 anos) com o modelo otimizado.

plot(forecast(fit, 24))

Questão 6

Repita os passos acima ajustando um modelo que use o método do Prophet.

Faça a previsão para o mesmo período e compare com o modelo SARIMA.

Rodando o Prophet com sazonalidade multiplicativa:

df <- data.frame(ds = zoo::index(as.xts(y)),y = coredata(as.xts(y)))
m <- prophet(df, seasonality.mode = "multiplicative") 
Disabling weekly seasonality. Run prophet with weekly.seasonality=TRUE to override this.
Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
future <- make_future_dataframe(m, 24, freq = 'm')
forecast <- predict(m, future)
plot(m, forecast)

Comparando SARIMA e Prophet no dataset inteiro:

sarima_model <- fit
forecast_sarima <- forecast(fit, 24)
prophet_model <- m
forecast_prophet <- forecast


# Combine forecasts for comparison
combined_forecast <- data.frame(
  ds = future$ds,
  actual = c(y, rep(NA, 24)), # Adjust length based on your data
  sarima = c(fitted(sarima_model), forecast_sarima$mean),
  prophet = forecast_prophet$yhat
)

# Plot combined forecasts
ggplot(combined_forecast, aes(x = ds)) +
  geom_line(aes(y = actual, color = "Actual")) +
  geom_line(aes(y = sarima, color = "SARIMA")) +
  geom_line(aes(y = prophet, color = "Prophet")) +
  ggtitle("Comparison of SARIMA and Prophet Forecasts") +
  labs(y = "Values", color = "Legend")
Warning: Removed 24 rows containing missing values or values outside the scale range
(`geom_line()`).

Comparando SARIMA e Prophet fazendo o split treino-teste:

dados <- data.frame(date=as.Date(zoo::index(as.xts(y))), value=coredata(as.xts(y)))

split_forecast <- timetk::time_series_split(
  dados,
  date_var = date,
  initial = "410 months",
  assess = "24 months"
)
split_forecast |>
  timetk::tk_time_series_cv_plan() |>
  timetk::plot_time_series_cv_plan(date, value)
model_sarima <- arima_reg(
        seasonal_period          = 12,
        non_seasonal_ar          = 1,
        non_seasonal_differences = 1,
        non_seasonal_ma          = 1,
        seasonal_ar              = 1,
        seasonal_differences     = 1,
        seasonal_ma              = 1
    ) |>
  set_engine("arima") |>
  fit(value ~ date, training(split_forecast))


model_prophet <- prophet_reg(seasonality_yearly = TRUE) |>
  set_engine("prophet") |>
  fit(value ~ date, training(split_forecast))
Disabling weekly seasonality. Run prophet with weekly.seasonality=TRUE to override this.
Disabling daily seasonality. Run prophet with daily.seasonality=TRUE to override this.
mtable <- modeltime_table(
  model_sarima,
  model_prophet
)

dados_para_teste <- modeltime_calibrate(
  mtable,
  testing(split_forecast)
)

dados_para_teste |>
  modeltime_accuracy() |>
  table_modeltime_accuracy(.interactive = FALSE)
Accuracy Table
.model_id .model_desc .type mae mape mase smape rmse rsq
1 ARIMA(1,1,1)(1,1,1)[12] Test 34.84 2.41 0.12 2.39 43.00 0.99
2 PROPHET Test 81.32 5.25 0.29 5.15 112.07 0.99

O SARIMA apresenta resultados melhores.

Questão 7

Faça um backtest e verifique qual modelo tem melhor poder preditivo (calcule medidas de performance que sejam comparáveis entre os dois modelos usando diferentes janelas de tempo). Nesta etapa você pode usar apenas um split treino/teste temporal ou vários (cross-validation). Justifique qual dos modelos você usaria.

Vamos começar divindo em 10 splits:

splits <- timetk::time_series_cv(
  dados,
  date_var = date,
  initial = "60 months",
  assess = "24 months",
  skip = 12,
  slice_limit = 10,
  cumulative = FALSE
)

splits
# Time Series Cross Validation Plan 
# A tibble: 10 × 2
   splits          id     
   <list>          <chr>  
 1 <split [60/24]> Slice01
 2 <split [60/24]> Slice02
 3 <split [60/24]> Slice03
 4 <split [60/24]> Slice04
 5 <split [60/24]> Slice05
 6 <split [60/24]> Slice06
 7 <split [60/24]> Slice07
 8 <split [60/24]> Slice08
 9 <split [60/24]> Slice09
10 <split [60/24]> Slice10

Verificando se os splits foram feitos corretamente:

splits |>
  timetk::tk_time_series_cv_plan() |>
  timetk::plot_time_series_cv_plan(date, value)

Fit dos modelos nos 10 splits:

resamples <- mtable |>
  modeltime.resample::modeltime_fit_resamples(
    resamples = splits
  )

Comparar resultado de cada split:

resamples |>
  modeltime.resample::plot_modeltime_resamples(
    .point_size  = 3,
    .point_alpha = 0.8,
    .interactive = FALSE
  )

Comparar resultado do SARIMA x Prophet:

resamples |>
  modeltime.resample::modeltime_resample_accuracy(summary_fns = mean) |>
  table_modeltime_accuracy(.interactive = FALSE)
Warning: There was 1 warning in `dplyr::summarise()`.
ℹ In argument: `dplyr::across(.fns = summary_fns, ...)`.
Caused by warning:
! Using `across()` without supplying `.cols` was deprecated in dplyr 1.1.0.
ℹ Please supply `.cols` instead.
Accuracy Table
.model_id .model_desc .type n mae mape mase smape rmse rsq
1 ARIMA(1,1,1)(1,1,1)[12] Resamples 10 61.39 4.20 0.22 4.14 73.52 0.98
2 PROPHET Resamples 10 71.32 4.95 0.26 4.86 84.53 0.98

Apesar do cross-validation ter piorado o resultado do SARIMA, ele continua melhor que o resultado do Prophet.

O modelo escolhido é o SARIMA.