---
title: "Time series"
output: rmarkdown::html_vignette
vignette: >
%\VignetteIndexEntry{Time series}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
bibliography: ../inst/REFERENCES.bib
csl: apa.csl
link-citations: true
---
```{r setup, include = FALSE}
knitr::opts_chunk$set(collapse = FALSE, comment = "",
fig.width = 7, fig.height = 4, dpi = 96,
dev.args = list(bg = "transparent"))
# Console colour carries no meaning on a rendered page. pkgdown turns it on for
# its own build, and the escape sequences then reach the reader as literal text,
# so colour is switched off here for a plain vignette render and a site build
# alike. The fixed width keeps printed output inside the documentation column.
options(cli.num_colors = 1, cli.hyperlink = FALSE, crayon.enabled = FALSE,
width = 80)
# Figures on the package website sit on a warm off-white page in light mode and
# are inverted by pkgdown in dark mode, so an opaque background would read as a
# pale slab one way and a black plate the other. Two things paint one. The
# device
# canvas is made transparent by `dev.args` above, and theme_depictr() then
# inherits theme_minimal()'s white plot.background, which is drawn over that
# canvas, so it is cleared as each figure is printed. This is deliberately a
# vignette-level choice: theme_depictr() keeps its opaque background, which is
# what a figure saved for a paper wants.
transparent_bg <- ggplot2::theme(
plot.background = ggplot2::element_rect(fill = NA, colour = NA),
panel.background = ggplot2::element_rect(fill = NA, colour = NA)
)
knit_print.ggplot <- function(x, ...) knitr::normal_print(x + transparent_bg)
knit_print.patchwork <- function(x, ...) knitr::normal_print(x & transparent_bg)
library(depictr)
```
depictr provides a small, consistent set of time-series plots. The examples use
the bundled `monthly_sales` dataset: two product lines (`indoor` and `outdoor`),
each with six years of monthly observations carrying a trend, a twelve-month
seasonal cycle and noise.
## Plotting one or several series
`timeseries_plot()` accepts a `ts` object, a numeric vector or a data frame with
time, value and (optionally) group columns. Passing the long-form data frame with
a grouping column draws both series at once, each in its own colour, and a
moving-average overlay is one argument away.
```{r}
timeseries_plot(monthly_sales, time = date, value = sales, group = series,
rolling = 12, title = "Monthly sales by product line",
y_lab = "Units")
```
Twelve months is an even window, so the overlay is the 2×12 moving average
that the caption names [@hyndman2021, sec. 3.3]. It is a twelve-month average
followed by a two-month one, which centres it on each month.
For the single-series views (decomposition, autocorrelation, the seasonal plot
and forecasting) we extract one line as a monthly `ts` object of frequency 12.
```{r}
indoor <- subset(monthly_sales, series == "indoor")
indoor <- indoor[order(indoor$date), ]
indoor_ts <- ts(indoor$sales, start = c(2018, 1), frequency = 12)
```
## Decomposition
`decompose_plot()` separates a seasonal series into trend, seasonal and
remainder components. Use `method = "stl"` (the default, loess-based) or
`method = "classical"`. Setting `confidence = TRUE` shades a band around the
smoothed trend, from the spread of the remainder, so the scale of the
unexplained variation is visible rather than implied by the line alone.
```{r, fig.height = 6}
decompose_plot(indoor_ts, confidence = TRUE, title = "Indoor sales, decomposed")
```
Classical decomposition holds the seasonal component fixed across the whole
series, and so does STL [@cleveland1990] with its default
`s_window = "periodic"`.
```{r, fig.height = 6}
decompose_plot(indoor_ts, method = "classical",
title = "Indoor sales, classical decomposition")
```
A numeric `s_window` lets the STL seasonal component change from year to year.
It is the number of consecutive years used to estimate each seasonal value, an
odd number of at least 7, so a smaller window lets the pattern change faster.
The bundled series has a fixed seasonal pattern by construction, so the effect
shows better on `AirPassengers`, whose seasonal swing grows with the number of
passengers.
```{r, fig.height = 6}
decompose_plot(AirPassengers, s_window = 13,
title = "Air passengers, with a 13-year seasonal window")
```
## Autocorrelation
`acf_plot()` draws the autocorrelation (or, with `type = "partial"`, the partial
autocorrelation) function, with approximate significance bounds. The spikes at
multiples of twelve are the annual seasonality.
```{r, fig.height = 3.4}
acf_plot(indoor_ts)
```
```{r, fig.height = 3.4}
acf_plot(indoor_ts, type = "partial")
```
## The seasonal pattern up close
`seasonal_plot()` draws a seasonal-subseries (cycle) plot: one small panel per
month, with the value traced across successive years and a reference line at each
month's mean. This shows the seasonal shape (differences *between* panels) and
the year-on-year trend within each month (the slope *inside* each panel) at the
same time, something a single overlaid line cannot do.
```{r, fig.height = 3.8}
seasonal_plot(indoor_ts, title = "Indoor sales: monthly subseries")
```
With `style = "season"` every year becomes its own line over the months on a
shared axis, which is handy for spotting an unusual year.
```{r, fig.height = 4}
seasonal_plot(indoor_ts, style = "season",
title = "Indoor sales: one line per year")
```
## Forecasting
`ts_forecast()` forecasts by decomposition [@hyndman2021, sec. 5.7]. It removes
the seasonal component that STL estimates, forecasts what remains with a random
walk with drift and adds the last year's seasonal pattern back. Its prediction
intervals are those of the random walk with drift, which widen with the horizon
[@hyndman2021, sec. 5.5].
```{r}
fc <- ts_forecast(indoor_ts, h = 18, level = 0.9)
head(fc)
```
Passing an integer horizon straight to `timeseries_plot()` overlays that forecast
on the history: the point forecast continues the line and the shaded ribbon shows
the (growing) 90% prediction interval, named in the caption.
```{r, fig.height = 4}
timeseries_plot(indoor_ts, forecast = 18, level = 0.9,
title = "Indoor sales with an 18-month forecast",
y_lab = "Units")
```
The model assumes a seasonal pattern that adds to the level and stays the same
from year to year. Its band treats that pattern as known, which leaves out the
uncertainty in estimating it. In a rolling-origin backtest on five classic
series whose seasonal swing does not grow with their level, the nominal 95%
band covered 92% of one-step-ahead outcomes. From three to 12 steps ahead, it
covered 99%. The band therefore runs somewhat narrow at the shortest horizons
and wide further out, where the uncertainty of a random walk keeps growing
while these series stay close to their trend. When the swing does grow with the
level, as it does for `AirPassengers`, set `lambda = 0` to forecast the
logarithm. The forecast comes back on the original scale, where the point
forecast is the median of the forecast distribution [@hyndman2021, sec. 5.6].
```{r, fig.height = 4}
timeseries_plot(AirPassengers, forecast = 24, level = 0.9, lambda = 0,
title = "Air passengers with a 24-month forecast",
y_lab = "Passengers")
```
For a different model, fit it yourself (for example with
`forecast::forecast()`) and pass the resulting `time`/`fit`/`lwr`/`upr` columns
to `timeseries_plot(forecast = )` as a data frame.
## References