Sección 10 : Forecasting with Exponential Smoothing Models
En este capítulo, exploraremos más a fondo las funciones de suavizado y su uso en pronósticos. Los modelos que veremos pueden abordar diversas series temporales, con o sin tendencias y componentes estacionales. Iniciaremos con el modelo de media móvil y el suavizado exponencial simple, para luego incrementar la complejidad del modelo y su capacidad para tratar con series temporales más complejas.
The simple moving average
La función de media móvil simple (SMA), usada en el capítulo 5 para suavizar datos de series temporales, puede convertirse en un modelo de pronóstico con algunos ajustes.
Vamos a personalizar la función SMA para pronosticar los precios mensuales del café Robusta durante los próximos 12 meses. Los precios del café Robusta, que no siguen una tendencia o patrón estacional específico, presentan un componente cíclico con ciclos de magnitud y duración variables. Estos datos forman parte del conjunto Coffee_Prices disponible en el paquete TSstudio.
## The Coffee_Prices series is a mts object with 2 variables and 701 observations
## Frequency: 12
## Start time: 1960 1
## End time: 2018 5
Este conjunto de datos es un objeto de tipo mt s y contiene los precios mensuales (USD por kg) tanto del café Robusta como del café Arábica entre 1960 y 2018. Extraeremos los precios mensuales del café Robusta del objeto Coffee_Prices.
robusta <- Coffee_Prices[,1]
ts_plot (robusta, title = "The Robusta Coffee Monthly Prices",
Ytitle = "Price in USD",
Xtitle = "Year") A continuación, crearemos una función básica de SMA (Media Móvil Simple) utilizando algunas de las funcionalidades del paquete tidyr.
library(tidyr)
sma_forecast <- function(df, h, m, w = NULL){
# Error handling
if(h > nrow(df)){
stop("The length of the forecast horizon must be shorter than the length of the series")}
if(m > nrow(df)){
stop("The length of the rolling window must be shorter than the length of the series")}
if(!is.null(w)){
if(length(w) != m){
stop("The weight argument is not aligned with the length of the rolling window")
} else if(sum(w) !=1){
stop("The sum of the average weight is different than 1")
}
}
# Setting the average weigths
if(is.null(w)){
w <- rep(1/m, m)
}
# Setting the data frame
#-----------------------
# Changing the Date object column name
names(df)[1] <- "date"
# Setting the training and testing partition
# according to the forecast horizon
df$type <- c(rep("train", nrow(df) - h),
rep("test", h))
# Spreading the table by the partition type
df1 <- df %>% spread(key = type, value = y)
# Create the target variable
df1$yhat <- df1$train
# Simple moving average function
for(i in (nrow(df1) - h + 1):nrow(df1)){
r <- (i-m):(i-1)
df1$yhat[i] <- sum(df1$yhat[r] * w)
}
# dropping from the yhat variable the actual values
# that were used for the rolling window
df1$yhat <- ifelse(is.na(df1$test), NA, df1$yhat)
df1$y <- ifelse(is.na(df1$test), df1$train, df1$test)
return(df1)
}Vamos a utilizar esta función para demostrar el rendimiento de la función SMA. Vamos a predecir los últimos 24 meses de la serie Robusta utilizando una ventana móvil de 3, 6, 12, 24 y 36 meses
robusta_df <- ts_to_prophet(robusta)
robusta_fc_m1 <- sma_forecast(robusta_df, h = 24, m = 1)
robusta_fc_m6 <- sma_forecast(robusta_df, h = 24, m = 6)
robusta_fc_m12 <- sma_forecast(robusta_df, h = 24, m = 12)
robusta_fc_m24 <- sma_forecast(robusta_df, h = 24, m = 24)
robusta_fc_m36 <- sma_forecast(robusta_df, h = 24, m = 36) ## Loading required package: ggplot2
##
## Attaching package: 'plotly'
## The following object is masked from 'package:ggplot2':
##
## last_plot
## The following object is masked from 'package:stats':
##
## filter
## The following object is masked from 'package:graphics':
##
## layout
plot_ly(data = robusta_df[650:nrow(robusta_df),], x = ~ ds, y = ~ y,
type = "scatter", mode = "lines",
name = "Actual") %>%
add_lines(x = robusta_fc_m1$date, y = robusta_fc_m1$yhat,
name = "SMA - 1", line = list(dash = "dash")) %>%
add_lines(x = robusta_fc_m6$date, y = robusta_fc_m6$yhat,
name = "SMA - 6", line = list(dash = "dash")) %>%
add_lines(x = robusta_fc_m12$date, y = robusta_fc_m12$yhat,
name = "SMA - 12", line = list(dash = "dash")) %>%
add_lines(x = robusta_fc_m24$date, y = robusta_fc_m24$yhat,
name = "SMA - 24", line = list(dash = "dash")) %>%
add_lines(x = robusta_fc_m36$date, y = robusta_fc_m36$yhat,
name = "SMA - 36", line = list(dash = "dash")) %>%
layout(title = "Forecasting the Robusta Coffee Monthly Prices",
xaxis = list(title = ""),
yaxis = list(title = "USD per Kg."))Las principales observaciones del gráfico anterior son las siguientes: - Si la longitud de la ventana móvil es más corta: - El rango de la predicción está bastante cerca de las observaciones más recientes de la serie. - La predicción converge más rápido a algún valor constante. - Si la longitud de la ventana es más larga: - Toma más tiempo hasta que la predicción converge a algún valor constante. - Puede manejar mejor los shocks y los valores atípicos. - Un modelo de pronóstico SMA con una ventana móvil de una longitud de 1 es equivalente al modelo de pronóstico Naive.
Aunque la función SMA es bastante simple de usar y consume poco poder de cálculo, tiene algunas limitaciones: - El poder de pronóstico de la función SMA está limitado a un horizonte corto y puede tener un rendimiento deficiente a largo plazo. - Este método está limitado para datos de series temporales, sin tendencia o patrones estacionales. Esto afecta principalmente al promedio aritmético que suaviza el patrón estacional y se vuelve plano a largo plazo.
Weighted moving average
El promedio móvil ponderado (WMA) es una versión extendida de la función SMA, y se basa en el uso del promedio ponderado (en lugar del promedio aritmético). La principal ventaja de la función WMA, con respecto a la función SMA, es que te permite distribuir el peso de los retrasos en la ventana móvil. Esto puede ser útil cuando la serie tiene una alta correlación con algunos de sus retrasos
data(USgas)
USgas_df <- ts_to_prophet(USgas)
# -------- Code Chank 8 --------
USgas_fc_m12a <- sma_forecast(USgas_df,
h = 24,
m = 12,
w = c(1, rep(0,11)))
USgas_fc_m12b <- sma_forecast(USgas_df,
h = 24,
m = 12,
w = c(0.8, rep(0,10), 0.2))plot_ly(data = USgas_df[190:nrow(USgas_df),], x = ~ ds, y = ~ y,
type = "scatter", mode = "lines",
name = "Actual") %>%
add_lines(x = USgas_fc_m12a$date, y = USgas_fc_m12a$yhat,
name = "WMA - Seasonal Lag", line = list(dash = "dash")) %>%
add_lines(x = USgas_fc_m12b$date, y = USgas_fc_m12b$yhat,
name = "WMA - 12 (0.2/0.8)", line = list(dash = "dash")) %>%
layout(title = "Forecasting the Monthly Consumption of Natural Gas in the US",
xaxis = list(title = ""),
yaxis = list(title = "Billion Cubic Feet"))Como se puede ver en el gráfico anterior, ambos modelos capturaron hasta cierto punto la oscilación estacional de la serie. Establecer todo el peso en el retraso estacional es equivalente al modelo Naive estacional. Esta estrategia podría ser útil para una serie con un patrón estacional dominante, como Usgas. En el segundo ejemplo, ponderamos el promedio entre el retraso más reciente y el retraso estacional. Tendría sentido distribuir los pesos entre los diferentes retrasos cuando la serie tiene una alta correlación con esos retrasos.
Aunque WMA puede capturar el componente estacional de una serie, no puede capturar la tendencia de la serie (debido al efecto promedio). Por lo tanto, este método comenzará a perder su efectividad una vez que el horizonte de pronóstico cruce la longitud de la frecuencia de la serie (por ejemplo, más de un año para series mensuales).
Simple exponential smoothing model
Es el modelo de pronóstico más sencillo de la familia de alisado exponencial. La principal suposición de este modelo es que la serie se mantiene en el mismo nivel (es decir, la media local de la serie es constante) a lo largo del tiempo, y por lo tanto, este modelo es adecuado para series sin componentes de tendencia ni estacionales.
El modelo SES comparte algunos de los atributos del modelo WMA, ya que ambos modelos pronostican los valores futuros de la serie mediante un promedio ponderado de las observaciones pasadas de la serie. Por otro lado, la principal distinción entre los dos es que el modelo SES utiliza todas las observaciones anteriores, mientras que el modelo WMA solo utiliza las m observaciones más recientes (para un modelo con una ventana móvil de una longitud de #7).
El principal atributo del modelo SES es la técnica del promedio ponderado, que se basa en la disminución exponencial de los pesos de observación según su distancia cronológica (es decir, índice de serie o marca de tiempo) desde los primeros valores pronosticados. La tasa de disminución de los pesos de observación
alpha_df <- data.frame(index = seq(from = 1, to = 15, by = 1),
power = seq(from = 14, to = 0, by = -1))
alpha_df$alpha_0.01 <- 0.01 * (1 - 0.01) ^ alpha_df$power
alpha_df$alpha_0.2 <- 0.2 * (1 - 0.2) ^ alpha_df$power
alpha_df$alpha_0.4 <- 0.4 * (1 - 0.4) ^ alpha_df$power
alpha_df$alpha_0.6 <- 0.6 * (1 - 0.6) ^ alpha_df$power
alpha_df$alpha_0.8 <- 0.8 * (1 - 0.8) ^ alpha_df$power
alpha_df$alpha_1 <- 1 * (1 - 1) ^ alpha_df$power
plot_ly(data = alpha_df) %>%
add_lines(x = ~ index, y = ~ alpha_0.01, name = "alpha = 0.01") %>%
add_lines(x = ~ index, y = ~ alpha_0.2, name = "alpha = 0.2") %>%
add_lines(x = ~ index, y = ~ alpha_0.4, name = "alpha = 0.4") %>%
add_lines(x = ~ index, y = ~ alpha_0.6, name = "alpha = 0.6") %>%
add_lines(x = ~ index, y = ~ alpha_0.8, name = "alpha = 0.8") %>%
add_lines(x = ~ index, y = ~ alpha_1, name = "alpha = 1") %>%
layout(title = "Decay Rate of the SES Weights",
xaxis = list(title = "Index"),
yaxis = list(title = "Weight"))Forecasting with the ses function
El paquete de pronóstico proporciona un modelo SES personalizado con la función ses. Los principales argumentos de esta función son los siguientes:
initial: Define el método para inicializar el valor de ¥, que puede calcularse utilizando las primeras observaciones de la serie al establecer el argumento en simple, o estimándolo con el modelo et s (una versión avanzada del modelo Holt-Winters del paquete de pronóstico) al establecerlo en óptimo. alpha: Define el valor del parámetro de suavizado del modelo. Si se establece en NULL, la función lo estimará. h: Establece el horizonte de pronóstico. Vamos a utilizar la función ses para pronosticar nuevamente los precios mensuales del café Robusta. Dejaremos los últimos 12 meses de la serie como un conjunto de pruebas para evaluar el rendimiento del modelo.
robusta_par <- ts_split(robusta, sample.out = 12)
train <- robusta_par$train
test <- robusta_par$test
library(forecast)## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
## Simple exponential smoothing
##
## Call:
## ses(y = train, h = 12, initial = "optimal")
##
## Smoothing parameters:
## alpha = 0.9999
##
## Initial states:
## l = 0.6957
##
## sigma: 0.161
##
## AIC AICc BIC
## 1989.646 1989.681 2003.252
test_forecast(actual = robusta,
forecast.obj = fc_ses,
test = test) %>%
layout(title = "Robusta Coffee Prices Forecast vs. Actual",
xaxis = list(range = c(2010, max(time(robusta)))),
yaxis = list(range = c(1, 3)))Como podemos ver en el gráfico de pronóstico anterior, la función ses utiliza el conjunto de entrenamiento para identificar el nivel de la serie estimando el parámetro alfa y el nivel inicial del modelo (o Y1). El nivel del valor de pronóstico está bastante cerca del valor de la última observación de la serie, ya que el valor & en este caso, está cerca de 1. Dado que el objetivo del modelo SES es pronosticar el nivel de la serie, el modelo no capturará ninguna oscilación a corto plazo
plot_forecast(fc_ses) %>%
add_lines(x = time(test) + deltat(test),
y = as.numeric(test),
name = "Testing Partition") %>%
layout(title = "Robusta Coffee Prices Forecast vs. Actual",
xaxis = list(range = c(2010, max(time(robusta)) +
deltat(robusta))),
yaxis = list(range = c(0, 4)))En el caso de un pronóstico plano, los intervalos de confianza del modelo juegan un papel crítico, ya que el nivel de incertidumbre es mayor. Por lo tanto, será útil evaluar si los valores de pronóstico están dentro de los límites del intervalo de confianza del modelo.
Como puedes ver en el gráfico de pronóstico anterior, el conjunto de pruebas está dentro del rango del intervalo de confianza del 80%.
Model optimization with grid search
La función ses optimiza los valores de los parámetros del modelo (« y l0) que minimizan la suma de los errores cuadrados (SSE) del modelo en el conjunto de entrenamiento. Un enfoque de optimización alternativo es utilizar una búsqueda en cuadrícula. Una búsqueda en cuadrícula es un enfoque simplista pero poderoso que se utiliza para identificar los valores de los parámetros del modelo que minimizan el error del modelo. En el caso del modelo SES, aplicaremos una búsqueda en cuadrícula para identificar el valor óptimo de ¢ que minimiza alguna métrica de error del modelo
robusta_par1 <- ts_split(robusta, sample.out = 24)
train1 <- robusta_par1$train
test1 <- ts_split(robusta_par1$test, sample.out = 12)$train
robusta_par2 <- ts_split(robusta, sample.out = 12)
train2 <- robusta_par2$train
valid <- robusta_par2$test
# -------- Code Chank 15 --------
alpha <- seq(from = 0, to = 1, by = 0.01)
# -------- Code Chank 16 --------
alpha[1] <- 0.001##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
ses_grid <- lapply(alpha, function(i){
md1 <- md_accuracy1 <- md2 <- md_accuracy2 <- results <- NULL
md1 <- ses(train1, h = 12, alpha = i, initial = "simple")
md_accuracy1 <- accuracy(md1, test1)
md2 <- ses(train2, h = 12, alpha = i, initial = "simple")
md_accuracy2 <- accuracy(md2, valid)
resutls <- data.frame(alpha = i,
train = md_accuracy1[9],
test = md_accuracy1[10],
valid = md_accuracy2[10])
}) %>% bind_rows()plot_ly(data = ses_grid, x = ~ alpha, y = ~ train,
line = list(color = 'rgb(205, 12, 24)'),
type = "scatter",
mode = "lines",
name = "Training") %>%
add_lines(x = ~ alpha, y = ~ test, line = list(color = "rgb(22, 96, 167)", dash = "dash"), name= "Testing") %>%
add_lines(x = ~ alpha, y = ~ valid, line = list(color = "green", dash = "dot"), name = "Validation") %>%
layout(title = "SES Model Grid Search Results",
yaxis = list(title = "MAPE (%)"))Forecasting with the holt function
El paquete de pronóstico proporciona una implementación del modelo Holt con la función holt. Esta función inicializa automáticamente los valores de L1 y T’1, e identifica los valores de a y 4 que minimizan el SSE del modelo en el conjunto de entrenamiento. En el siguiente ejemplo, recuperaremos los datos trimestrales del Producto Interno Bruto (PIB) de EE.UU. de la API de Datos Económicos de la Reserva Federal (FRED)
## Loading required package: xts
## Loading required package: zoo
##
## Attaching package: 'zoo'
## 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
## The gdp series is a ts object with 1 variable and 48 observations
## Frequency: 4
## Start time: 2010 1
## End time: 2021 4
# -------- Code Chank 20 --------
ts_plot(gdp,
title = "Gross Domestic Product",
Ytitle = "Billions of Dollars",
Xtitle = "Source: U.S. Bureau of Economic Analysis / fred.stlouisfed.org")gdp_par <- ts_split(gdp, sample.out = 8)
train <- gdp_par$train
test <- gdp_par$test
# -------- Code Chank 22 --------
fc_holt <- holt(train, h = 8, initial = "optimal")
fc_holt$model## Holt's method
##
## Call:
## holt(y = train, h = 8, initial = "optimal")
##
## Smoothing parameters:
## alpha = 0.9985
## beta = 0.0941
##
## Initial states:
## l = 14603.0487
## b = 161.9411
##
## sigma: 80.9869
##
## AIC AICc BIC
## 504.8838 506.6485 513.3282
## ME RMSE MAE MPE MAPE MASE
## Training set 10.99164 76.83093 61.7626 0.05241543 0.3460477 0.08790283
## Test set -665.87355 1141.19097 854.8069 -3.29699460 4.0874667 1.21659297
## ACF1 Theil's U
## Training set 0.0192117 NA
## Test set 0.2975704 1.08055
Como puedes ver en la salida de la función de precisión, la relación entre la tasa de error en el conjunto de pruebas y entrenamiento es más de 5 veces para el RMSE y 4.5 para el MAPE. Esta gran proporción en las métricas de error se deriva principalmente de las siguientes dos razones:
Los valores ajustados del modelo en el conjunto de entrenamiento no están limitados por una línea lineal (a diferencia de la salida del pronóstico). El crecimiento de la tendencia en los últimos trimestres cambia de una tasa de crecimiento lineal a una tasa exponencial. Los cambios en el crecimiento de la tendencia y el pronóstico pueden observarse con la función test_forecast.
fc_holt_exp <- holt(train,
h = 8,
beta = 0.75 ,
initial = "optimal",
exponential = TRUE)
fc_holt_exp$model## Holt's method with exponential trend
##
## Call:
## holt(y = train, h = 8, initial = "optimal", exponential = TRUE,
##
## Call:
## beta = 0.75)
##
## Smoothing parameters:
## alpha = 0.781
## beta = 0.75
##
## Initial states:
## l = 14572.2291
## b = 1.0142
##
## sigma: 0.0053
##
## AIC AICc BIC
## 515.9291 517.0720 522.6847
## ME RMSE MAE MPE MAPE MASE
## Training set -2.792636 90.20753 74.14323 -0.01756593 0.4165413 0.1055234
## Test set -703.014806 1151.21262 858.97402 -3.46212317 4.1130726 1.2225238
## ACF1 Theil's U
## Training set -0.1541725 NA
## Test set 0.2834645 1.090637
Aunque el modelo Holt fue diseñado para manejar series de tiempo con tendencia lineal, el argumento exponencial de la función holt proporciona la opción de manejar series con tendencias exponenciales o decrecientes cuando se establece en TRUE. Para el ejemplo anterior, podemos utilizar el argumento exponencial para modificar el patrón de crecimiento de la tendencia. Como puedes ver, la tasa de error del segundo modelo Holt está más equilibrada, donde la relación entre el error en el conjunto de pruebas y entrenamiento es 2.5 y 2.1 para las métricas RMSE y MAPE, respectivamente.
Holt-Winters model
Cerraremos este capítulo con el tercer modelo más avanzado entre la familia de modelos de pronóstico de suavizado exponencial: el modelo Holt-Winters. El modelo Holt-Winters (HW) es una versión extendida del modelo Holt y puede manejar datos de series de tiempo con componentes de tendencia y estacionales. La predicción del componente estacional requirió un tercer parámetro y ecuación de suavizado, además de los del nivel y la tendencia.
La implementación más común del modelo HW en R es las funciones HoltWinters y hw de los paquetes stats y forecast. La principal diferencia entre las dos funciones es que la función hw puede manejar series de tiempo con una tendencia exponencial o amortiguada (similar al modelo Holt). En el siguiente ejemplo, utilizaremos la función Holtwinters para pronosticar los últimos 12 meses de la serie USgas.
Podemos observar en el gráfico anterior que tanto la tendencia como los componentes estacionales de la serie tienen una estructura aditiva. Al igual que hicimos anteriormente, crearemos particiones de entrenamiento y prueba utilizando los últimos 12 meses de la serie para evaluar el rendimiento del modelo:
## Holt-Winters exponential smoothing with trend and additive seasonal component.
##
## Call:
## HoltWinters(x = train)
##
## Smoothing parameters:
## alpha: 0.371213
## beta : 0
## gamma: 0.4422456
##
## Coefficients:
## [,1]
## a 2491.9930104
## b -0.1287005
## s1 32.0972651
## s2 597.1088003
## s3 834.9628439
## s4 353.8593860
## s5 318.1927058
## s6 -173.0721496
## s7 -346.6229990
## s8 -329.7169608
## s9 -112.1664217
## s10 -140.3186476
## s11 -324.5343787
## s12 -243.9334551
## ME RMSE MAE MPE MAPE MASE
## Training set 7.828361 115.2062 87.67420 0.2991952 4.248131 0.7614222
## Test set 51.013877 115.1555 98.06531 1.7994297 3.766099 0.8516656
## ACF1 Theil's U
## Training set 0.21911103 NA
## Test set -0.01991923 0.3652142
Al graficar los valores ajustados y pronosticados, obtenemos una mejor comprensión del rendimiento del modelo. Como podemos ver en el gráfico anterior, el modelo HW hace un buen trabajo capturando ambos patrones estacionales de la serie. Por otro lado, el modelo no está capturando el pico del año durante el mes de enero en la mayoría de los casos.
Alternativamente, el modelo puede ser entrenado con una búsqueda en cuadrícula de manera similar a lo que hicimos con el modelo SES
Resumen
En este capítulo, exploramos cómo usar un promedio ponderado de observaciones anteriores para predecir datos de series de tiempo. Comenzamos con el enfoque de pronóstico simple y directo del promedio móvil. A pesar de sus limitaciones, proporciona un marco para las funciones de suavizado exponencial. Estos modelos usan parámetros de suavizado para modelar los componentes clave de los datos de series de tiempo: nivel, tendencia y estacionalidad. Su simplicidad, eficiencia computacional y modularidad los hacen útiles para manejar diferentes tipos de datos de series de tiempo. En el próximo capítulo, nos adentraremos en un enfoque de pronóstico más avanzado con los modelos de pronóstico ARIMA.