Extending
broom
to time series forecasting
One of the most powerful benefits of sweep
is that it helps forecasting at scale within the “tidyverse”. There are two common situations:
In this vignette we’ll review how sweep
can help the second situation: Applying multiple models to a time series.
Before we get started, load the following packages.
library(forecast)
library(tidyquant)
library(timetk)
library(sweep)
To start, let’s get some data from the FRED data base using tidyquant
. We’ll use tq_get()
to retrieve the Gasoline Prices from 1990 through today (2017-07-25).
gas_prices_monthly_raw <- tq_get(
x = "GASREGCOVM",
get = "economic.data",
from = "1990-01-01",
to = "2016-12-31")
gas_prices_monthly_raw
## # A tibble: 316 x 2
## date price
## <date> <dbl>
## 1 1990-09-01 1.258
## 2 1990-10-01 1.335
## 3 1990-11-01 1.324
## 4 1990-12-01 NA
## 5 1991-01-01 NA
## 6 1991-02-01 1.094
## 7 1991-03-01 1.040
## 8 1991-04-01 1.076
## 9 1991-05-01 1.126
## 10 1991-06-01 1.128
## # ... with 306 more rows
Upon a brief inspection, the data contains 2 NA
values that will need to be dealt with.
summary(gas_prices_monthly_raw$price)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 0.900 1.138 1.615 1.974 2.697 4.002 2
We can use the fill()
from the tidyr
package to help deal with these data. We first fill down and then fill up to use the previous and then post days prices to fill in the missing data.
gas_prices_monthly <- gas_prices_monthly_raw %>%
fill(price, .direction = "down") %>%
fill(price, .direction = "up")
We can now visualize the data.
gas_prices_monthly %>%
ggplot(aes(x = date, y = price)) +
geom_line(color = palette_light()[[1]]) +
geom_point(color = palette_light()[[1]]) +
labs(title = "Gasoline Prices, Monthly", x = "", y = "USD") +
scale_y_continuous(labels = scales::dollar) +
theme_tq()
Monthly periodicity might be a bit granular for model fitting. We can easily switch periodicity to quarterly using tq_transmute()
from the tidyquant
package along with the periodicity aggregation function to.period
from the xts
package. We’ll convert the date to yearqtr
class which is regularized.
gas_prices_quarterly <- gas_prices_monthly %>%
tq_transmute(mutate_fun = to.period, period = "quarters")
gas_prices_quarterly
## # A tibble: 106 x 2
## date price
## <date> <dbl>
## 1 1990-09-01 1.258
## 2 1990-12-01 1.324
## 3 1991-03-01 1.040
## 4 1991-06-01 1.128
## 5 1991-09-01 1.109
## 6 1991-12-01 1.076
## 7 1992-03-01 1.013
## 8 1992-06-01 1.145
## 9 1992-09-01 1.122
## 10 1992-12-01 1.078
## # ... with 96 more rows
Another quick visualization to show the reduction in granularity.
gas_prices_quarterly %>%
ggplot(aes(x = date, y = price)) +
geom_line(color = palette_light()[[1]], size = 1) +
geom_point(color = palette_light()[[1]]) +
labs(title = "Gasoline Prices, Quarterly", x = "", y = "USD") +
scale_y_continuous(labels = scales::dollar) +
scale_x_date(date_breaks = "5 years", date_labels = "%Y") +
theme_tq()
In this section we will use three models to forecast gasoline prices:
Before we jump into modeling, let’s take a look at the multiple model process from R for Data Science, Chapter 25 Many Models. We first create a data frame from a named list. The example below has two columns: “f” the functions as text, and “params” a nested list of parameters we will pass to the respective function in column “f”.
df <- tibble(
f = c("runif", "rpois", "rnorm"),
params = list(
list(n = 10),
list(n = 5, lambda = 10),
list(n = 10, mean = -3, sd = 10)
)
)
df
## # A tibble: 3 x 2
## f params
## <chr> <list>
## 1 runif <list [1]>
## 2 rpois <list [2]>
## 3 rnorm <list [3]>
We can also view the contents of the df$params
column to understand the underlying structure. Notice that there are three primary levels and then secondary levels containing the name-value pairs of parameters. This format is important.
df$params
## [[1]]
## [[1]]$n
## [1] 10
##
##
## [[2]]
## [[2]]$n
## [1] 5
##
## [[2]]$lambda
## [1] 10
##
##
## [[3]]
## [[3]]$n
## [1] 10
##
## [[3]]$mean
## [1] -3
##
## [[3]]$sd
## [1] 10
Next we apply the functions to the parameters using a special function, invoke_map()
. The parameter lists in the “params” column are passed to the function in the “f” column. The output is in a nested list-column named “out”.
df_out <- df %>%
mutate(out = invoke_map(f, params))
df_out
## # A tibble: 3 x 3
## f params out
## <chr> <list> <list>
## 1 runif <list [1]> <dbl [10]>
## 2 rpois <list [2]> <int [5]>
## 3 rnorm <list [3]> <dbl [10]>
And, here’s the contents of “out”, which is the result of mapping a list of functions to a list of parameters. Pretty powerful!
df_out$out
## [[1]]
## [1] 0.59102984 0.20873637 0.87339386 0.24989660 0.24076947 0.20023536
## [7] 0.81866094 0.88582756 0.91040161 0.07579531
##
## [[2]]
## [1] 6 14 13 17 9
##
## [[3]]
## [1] 11.645398 2.280693 12.513776 5.753345 -10.439134 2.414632
## [7] -27.233099 11.065930 7.532832 -9.286955
Take a minute to understand the conceptual process of the invoke_map
function and specifically the parameter setup. Once you are comfortable, we can move on to model implementation.
We’ll need to take the following steps to in an actual forecast model implementation:
This is easier than it sounds. Let’s start by coercing the univariate time series with tk_ts()
.
gas_prices_quarterly_ts <- gas_prices_quarterly %>%
tk_ts(select = -date, start = c(1990, 3), freq = 4)
gas_prices_quarterly_ts
## Qtr1 Qtr2 Qtr3 Qtr4
## 1990 1.258 1.324
## 1991 1.040 1.128 1.109 1.076
## 1992 1.013 1.145 1.122 1.078
## 1993 1.052 1.097 1.050 1.014
## 1994 1.008 1.078 1.144 1.060
## 1995 1.059 1.186 1.108 1.066
## 1996 1.129 1.243 1.200 1.233
## 1997 1.197 1.189 1.216 1.119
## 1998 1.014 1.048 0.994 0.923
## 1999 0.961 1.095 1.239 1.261
## 2000 1.498 1.612 1.525 1.418
## 2001 1.384 1.548 1.506 1.072
## 2002 1.221 1.341 1.363 1.348
## 2003 1.636 1.452 1.616 1.448
## 2004 1.689 1.910 1.841 1.800
## 2005 2.063 2.123 2.862 2.174
## 2006 2.413 2.808 2.501 2.284
## 2007 2.503 3.024 2.817 2.984
## 2008 3.215 3.989 3.709 1.669
## 2009 1.937 2.597 2.480 2.568
## 2010 2.742 2.684 2.678 2.951
## 2011 3.509 3.628 3.573 3.220
## 2012 3.774 3.465 3.801 3.256
## 2013 3.648 3.576 3.474 3.209
## 2014 3.474 3.626 3.354 2.488
## 2015 2.352 2.700 2.275 1.946
## 2016 1.895 2.303 2.161 2.192
Next, create a nested list using the function names as the first-level keys (this is important as you’ll see in the next step). Pass the model parameters as name-value pairs in the second level.
models_list <- list(
auto.arima = list(
y = gas_prices_quarterly_ts
),
ets = list(
y = gas_prices_quarterly_ts,
damped = TRUE
),
bats = list(
y = gas_prices_quarterly_ts
)
)
Now, convert to a data frame using the function, enframe()
that turns lists into tibbles. Set the arguments name = "f"
and value = "params"
. In doing so we get a bonus: the model names are the now convieniently located in column “f”.
models_tbl <- enframe(models_list, name = "f", value = "params")
models_tbl
## # A tibble: 3 x 2
## f params
## <chr> <list>
## 1 auto.arima <list [1]>
## 2 ets <list [2]>
## 3 bats <list [1]>
We are ready to invoke the map. Combine mutate()
with invoke_map()
as follows. Bada bing, bada boom, we now have models fitted using the parameters we defined previously.
models_tbl_fit <- models_tbl %>%
mutate(fit = invoke_map(f, params))
models_tbl_fit
## # A tibble: 3 x 3
## f params fit
## <chr> <list> <list>
## 1 auto.arima <list [1]> <S3: ARIMA>
## 2 ets <list [2]> <S3: ets>
## 3 bats <list [1]> <S3: bats>
It’s a good point to review and understand the model output. We can review the model parameters, accuracy measurements, and the residuals using sw_tidy()
, sw_glance()
, and sw_augment()
.
The tidying function returns the model parameters and estimates. We use the combination of mutate
and map
to iteratively apply the sw_tidy()
function as a new column named “tidy”. Then we unnest and spread to review the terms by model function.
models_tbl_fit %>%
mutate(tidy = map(fit, sw_tidy)) %>%
unnest(tidy) %>%
spread(key = f, value = estimate)
## # A tibble: 16 x 4
## term auto.arima bats ets
## * <chr> <dbl> <dbl> <dbl>
## 1 alpha NA 8.159321e-01 0.6770332202
## 2 ar.coefficients NA NA NA
## 3 b NA NA 0.0592275021
## 4 beta NA NA 0.0129535504
## 5 damping.parameter NA NA NA
## 6 gamma NA NA 0.0001718402
## 7 gamma.values NA -2.019471e-02 NA
## 8 l NA NA 1.0436968053
## 9 lambda NA 1.549104e-08 NA
## 10 ma.coefficients NA NA NA
## 11 phi NA NA 0.8000459550
## 12 s0 NA NA 1.0463243371
## 13 s1 NA NA 0.9764888261
## 14 s2 NA NA 0.9439714264
## 15 sar1 0.9551837 NA NA
## 16 sma1 -0.8299847 NA NA
Glance is one of the most powerful tools because it yields the model accuracies enabling direct comparisons between the fit of each model. We use the same process for used for tidy, except theres no need to spread to perform the comparison. We can see that the ARIMA model has the lowest AIC by far.
models_tbl_fit %>%
mutate(glance = map(fit, sw_glance)) %>%
unnest(glance, .drop = TRUE)
## # A tibble: 3 x 13
## f model.desc sigma logLik AIC
## <chr> <chr> <dbl> <dbl> <dbl>
## 1 auto.arima ARIMA(0,1,0)(1,0,1)[4] 0.3024768 -23.33351 52.66701
## 2 ets ETS(M,Ad,M) 0.1117591 -75.41773 170.83545
## 3 bats BATS(0, {0,0}, -, {4}) 0.1163602 160.49138 176.49138
## # ... with 8 more variables: BIC <dbl>, ME <dbl>, RMSE <dbl>, MAE <dbl>,
## # MPE <dbl>, MAPE <dbl>, MASE <dbl>, ACF1 <dbl>
We can augment the models to get the residuals following the same procedure. We can pipe (%>%
) the results right into ggplot()
for plotting. Notice the ARIMA model has the largest residuals especially as the model index increases whereas the bats model has relatively low residuals.
models_tbl_fit %>%
mutate(augment = map(fit, sw_augment, rename_index = "date")) %>%
unnest(augment) %>%
ggplot(aes(x = date, y = .resid, group = f)) +
geom_line(color = palette_light()[[2]]) +
geom_point(color = palette_light()[[1]]) +
geom_smooth(method = "loess") +
facet_wrap(~ f, nrow = 3) +
labs(title = "Residuals Plot") +
theme_tq()
Creating the forecast for the models is accomplished by mapping the forecast
function. The next six quarters are forecasted withe the argument h = 6
.
models_tbl_fcast <- models_tbl_fit %>%
mutate(fcast = map(fit, forecast, h = 6))
models_tbl_fcast
## # A tibble: 3 x 4
## f params fit fcast
## <chr> <list> <list> <list>
## 1 auto.arima <list [1]> <S3: ARIMA> <S3: forecast>
## 2 ets <list [2]> <S3: ets> <S3: forecast>
## 3 bats <list [1]> <S3: bats> <S3: forecast>
Next, we map sw_sweep
, which coerces the forecast into the “tidy” tibble format. We set fitted = FALSE
to remove the model fitted values from the output. We set timetk_idx = TRUE
to use dates instead of numeric values for the index.
models_tbl_fcast_tidy <- models_tbl_fcast %>%
mutate(sweep = map(fcast, sw_sweep, fitted = FALSE, timetk_idx = TRUE, rename_index = "date"))
models_tbl_fcast_tidy
## # A tibble: 3 x 5
## f params fit fcast sweep
## <chr> <list> <list> <list> <list>
## 1 auto.arima <list [1]> <S3: ARIMA> <S3: forecast> <tibble [112 x 7]>
## 2 ets <list [2]> <S3: ets> <S3: forecast> <tibble [112 x 7]>
## 3 bats <list [1]> <S3: bats> <S3: forecast> <tibble [112 x 7]>
We can unnest the “sweep” column to get the results of all three models.
models_tbl_fcast_tidy %>%
unnest(sweep)
## # A tibble: 336 x 8
## f date key price lo.80 lo.95 hi.80 hi.95
## <chr> <date> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 auto.arima 1990-09-01 actual 1.258 NA NA NA NA
## 2 auto.arima 1990-12-01 actual 1.324 NA NA NA NA
## 3 auto.arima 1991-03-01 actual 1.040 NA NA NA NA
## 4 auto.arima 1991-06-01 actual 1.128 NA NA NA NA
## 5 auto.arima 1991-09-01 actual 1.109 NA NA NA NA
## 6 auto.arima 1991-12-01 actual 1.076 NA NA NA NA
## 7 auto.arima 1992-03-01 actual 1.013 NA NA NA NA
## 8 auto.arima 1992-06-01 actual 1.145 NA NA NA NA
## 9 auto.arima 1992-09-01 actual 1.122 NA NA NA NA
## 10 auto.arima 1992-12-01 actual 1.078 NA NA NA NA
## # ... with 326 more rows
Finally, we can plot the forecasts by unnesting the “sweep” column and piping to ggplot()
.
models_tbl_fcast_tidy %>%
unnest(sweep) %>%
ggplot(aes(x = date, y = price, color = key, group = f)) +
geom_ribbon(aes(ymin = lo.95, ymax = hi.95),
fill = "#D5DBFF", color = NA, size = 0) +
geom_ribbon(aes(ymin = lo.80, ymax = hi.80, fill = key),
fill = "#596DD5", color = NA, size = 0, alpha = 0.8) +
geom_line(size = 1) +
facet_wrap(~f, nrow = 3) +
labs(title = "Gasoline Price Forecasts",
subtitle = "Forecasting multiple models with sweep: ARIMA, BATS, ETS",
x = "", y = "Price") +
scale_y_continuous(labels = scales::dollar) +
scale_x_date(date_breaks = "5 years", date_labels = "%Y") +
theme_tq() +
scale_color_tq()
The sweep
package can aid analysis of multiple forecast models. In the next vignette we will review time series object coercion with sweep
.