Playground: Chapter 16

Time Series Forecasting

This page practises the ideas of Chapter 16 in four parts. Practise the chapter works through the book’s exercises, with starter code, hints, and solutions. Go further goes beyond the book, with new questions and some of R’s built-in time series. Check your understanding asks short questions whose answers open with a click. Do it yourself gives open tasks with an empty code space and no answers: you forecast and report yourself, as you will in your own thesis.

The fable packages are large for a browser, so this page works with functions that come with R: ts() for time series, acf() for autocorrelation, stl() for decomposition, and forecasts built by hand. One exercise needs fable’s simulation, and the chapter’s STL and ETS model needs fable too; they are in the download project, which uses tsibble, fable, and feasts exactly as the chapter does.

In the setup, counselling holds the weekly counselling visits, visits is the same series as a ts object with 52 weeks per year, year_of(k) returns the 52 weeks of academic year k (1 is 2020/21, 5 is 2024/25), and mae() calculates the mean absolute error.

Practise the chapter

These are the exercises at the end of Chapter 16, with the same numbers.

Exercise 1: Benchmarks for a series with a trend

Predict which of the three benchmarks (mean, naive, seasonal naive) would be best for a series with a trend but no seasonality. Then test your prediction on the chapter’s clinic series after adding a steady increase of one visit a day. To score the forecasts, assume the third week follows the same weekly pattern as the second, with the trend continuing.

NoteHint

The seasonal naive method repeats the last full cycle: here the last seven days of the series.

TipSolution
snaive = clinic_trend[8:14]

For a series with a trend and no seasonality, the naive method is best of the three: the last value is closer to the future than the average of a rising series, and there is no cycle for the seasonal naive method to copy. The clinic series has a strong weekly pattern as well as the added trend, and there the seasonal naive method wins (an MAE of 7.0, against 10.7 for naive and 11.0 for the mean), because the weekly pattern is much larger than a week’s growth. But its error is exactly 7 every day: it repeats last week without the 7 extra visits a week the trend adds. No benchmark handles both trend and season, which is why fable offers a drift version of the naive method, and why the chapter’s models estimate both.

Exercise 2: The seasonally adjusted series

Decompose the series, then plot the seasonally adjusted series (the series minus its seasonal component), and describe what it shows that the original series hides.

NoteHint

The series is the sum of trend, season, and remainder; removing the seasonal part means subtracting it.

TipSolution
adjusted <- visits - decomposition$time.series[, "seasonal"]

Without the huge seasonal swings, the trend is plain to see: the average seasonally adjusted level rises every year, from 30.7 visits a week in 2020/21 to 33.5, 34.6, 37.0, and 37.6, growing more slowly in the last year. The awareness week of October 2023 stands out as a single spike of about 63, which in the original series was partly hidden among the busy autumn weeks. The seasonally adjusted series answers the question “is demand growing?”, which the raw series, dominated by the academic calendar, cannot. (fable’s STL, in the download project, gives almost the same yearly averages.)

Exercise 3: Fewer or more waves

Refit the Fourier model with K = 2 and K = 12, and explain how and why the test accuracy changes. On this page, the Fourier terms are built by hand and used in an ordinary regression with a trend, trained on the first four years and tested on the fifth.

NoteHint

Each pair of waves repeats a whole number of times per seasonal period; the period is the number of weeks in a year.

TipSolution
cbind(sin(2 * pi * k * t / 52), cos(2 * pi * k * t / 52))

With K = 2, the MAE is 10.5 visits a week; with 6, 7.8; with 12, 7.0, the same as the seasonal naive benchmark. Two pairs of smooth waves can only draw a gentle curve through the year, and cannot follow the sharp steps of the counselling calendar, such as the week-to-week drop into summer. More waves can bend more sharply and follow the steps better. But each pair adds two coefficients, and with only four years of training data, many waves start to fit the noise of those years. fable’s ARIMA version in the download project shows exactly that trade-off: 10.2 for K = 2, 7.8 for 6, and 9.2 for 12, where the extra waves no longer pay. Either way, the Fourier model never beats the chapter’s STL and ETS model (5.6).

Exercise 4: Three years of training

Train on the first three years and test on the fourth. On this page, compare the benchmarks and an average of years 2 and 3 with 6% growth; the chapter’s STL and ETS model, which needs fable, is compared in the download project, and its result is given in the solution.

NoteHint

The seasonal naive forecast for year 4 is a copy of the year before it.

TipSolution
snaive = mae(actual_4, year_of(3))

For year 4, the seasonal naive forecast has an MAE of 6.6 visits a week and the mean 14.3; averaging years 2 and 3 and adding 6% growth gives 6.0. In the download project, fable’s STL and ETS model trained on three years gives 5.9, still the best, ahead of the seasonal naive benchmark (6.6) and the Fourier model (8.7). The ranking holds with a different training period, which makes the choice of model more convincing than a single test year could. Averaging two years with growth does almost as well because it does by hand what the model does from the data: it smooths away one-off weeks and adds the trend.

Exercise 5: Visits before the first exam period

Use generate() to estimate how many visits to expect in the four weeks before the first exam period of 2025/26, with a 95% interval.

This exercise needs fable’s simulation, so it is done in the download project.

set.seed(2026)
futures <- final_fit |> generate(h = 52, times = 1000)
four_weeks <- futures |>
  as_tibble() |>
  group_by(.rep) |>
  slice(12:15) |>
  summarise(total = sum(.sim))
quantile(four_weeks$total, c(0.025, 0.5, 0.975))

The forecast shows the first exam peak beginning in week 16 of the academic year (mid-December 2025), so the four weeks before it are weeks 12 to 15, from 17 November to 14 December. The simulated futures give a median of about 193 visits in those four weeks, with a 95% interval from about 151 to 247, compared with 169 in the same weeks of 2024/25. Adding the four weekly forecasts gives a similar central value (194), but only simulation gives the interval for the total, because the weeks do not vary independently.

Exercise 6: Explaining a prediction interval

Write a short paragraph for the counselling service explaining what a 95% prediction interval means. Write your answer first, then open the model answer.

“For each week, the forecast gives a most likely number of visits and a range. The 95% prediction interval is the range within which we expect the actual number of visits to fall in 19 weeks out of 20, if the patterns of the past five years continue. It is wide because demand varies from week to week for reasons no forecast can know, such as an awareness campaign or a difficult exam. Plan for the middle of the range, and keep in mind that about one week in 20 will fall outside it, most likely above it in a busy period.” The paragraph says what the interval covers, how often it can be wrong, why it is wide, and what it assumes.

Exercise 7: Twice the noise

In the artificial series at the start of the chapter, double the standard deviation of the noise. Describe how the sum changes, and explain what this would mean for the width of forecast intervals.

NoteHint

The chapter’s noise had a standard deviation of 3.

TipSolution
for (noise_sd in c(3, 6)) {

With twice the noise, the trend and the seasonal wave are still there, but the series is far more jagged: the weekly ups and downs are larger, and the wave is harder to see by eye. The standard deviation of the whole series grows only from 9.0 to 10.6, because most of its spread comes from the seasonal wave, but the part that cannot be forecast has doubled. Forecast intervals depend mainly on that unpredictable part, so they would be roughly twice as wide: the most likely values stay the same, but the range of plausible values doubles. More noise does not change what to expect; it changes how sure one can be.

Go further

These exercises go beyond the book.

Exercise 8: Build a series and forecast it

Build a weekly series of your own for four years from a trend (50 visits rising by 0.1 a week), a seasonal wave of 15, and noise with a standard deviation of 4. Forecast the fourth year from the first three with the mean, the seasonal naive method, and the seasonal naive method plus one year’s growth.

NoteHint

The trend adds 0.1 visits every week; a year later, every week is higher by 0.1 times the number of weeks in a year.

TipSolution
snaive_growth = mae(actual, last_year + 0.1 * 52)

The mean forecast has an MAE of 13.0, the seasonal naive 7.5, and the seasonal naive with growth 4.5, close to the noise itself (the typical size of noise with a standard deviation of 4 is about 3.2). Because you built the series, you know its parts exactly: the seasonal naive method gets the season right but copies last year’s noise and misses a year’s growth of 5.2 visits; adding the growth removes one of the two errors. The noise can never be removed, which sets a floor on how good any forecast can be.

Exercise 9: Autocorrelation by hand

Calculate the autocorrelation of the counselling series at lags 1 and 52 by hand with cor(), pairing each week with the week one step and 52 steps earlier, and compare with acf().

NoteHint

For lag 52, drop the first 52 weeks from one copy of the series and the last 52 from the other, so that each week is paired with the same week a year earlier.

TipSolution
lag_52 = cor(x[-(1:52)], x[1:(n - 52)])

At lag 1, cor() gives 0.69 and acf() 0.69: the same. At lag 52, cor() gives 0.87 but acf() only 0.68. The difference is in the definition: acf() uses the mean of the whole series and divides by the full length of 260 weeks, although only 208 pairs exist at lag 52, which shrinks long-lag values by roughly 208/260. It is the standard definition, chosen because it behaves well mathematically, but it means that acf() understates how strongly a week resembles the same week a year earlier. Both agree on the message: the series is strongly seasonal.

Exercise 10: Air passengers and growing swings

R’s built-in AirPassengers series gives the monthly number of international airline passengers, in thousands, from 1949 to 1960. Plot it on the original scale and on a log scale, and compare the size of the seasonal swing in the first and last years.

NoteHint

The second plot shows the natural logarithm of the series.

TipSolution
plot(log(AirPassengers), ylab = "log(passengers)")

On the original scale, the gap between the busiest and quietest month grows from 44 thousand passengers in 1949 to 232 thousand in 1960: the seasonal swings grow with the level of the series, as air travel nearly quadrupled. On the log scale, the swings are almost constant (0.35 and 0.47): the seasonal pattern is a roughly constant percentage of the level. This is the situation the chapter described for the counselling data, where the model was fitted to log(visits); for AirPassengers, a log transformation is essential, since a model with constant swings would underestimate the summer peaks of later years badly.

Exercise 11: Carbon dioxide and the missing trend

R’s built-in co2 series gives monthly atmospheric CO₂ concentrations at Mauna Loa from 1959 to 1997. Forecast the last two years from the years before, with the mean, the seasonal naive method, and the seasonal naive method plus the growth of the last year.

NoteHint

The monthly series has 12 observations per year; the rise over the last year compares the last month with the same month a year earlier.

TipSolution
growth <- y[n_train] - y[n_train - 12]

The mean forecast is useless (an MAE of 27.6 ppm), because CO₂ has risen steadily for decades; the seasonal naive forecast is off by 2.3 ppm; adding the growth of the last year (1.7 ppm) brings the error down to 0.4 ppm. The CO₂ series has a small, very regular seasonal cycle, from plants taking up and releasing carbon, on top of a strong trend, so the trend matters far more than the season. The best benchmark depends on the series: here, a seasonal naive method with drift, which fable provides as SNAIVE(y ~ drift()).

Check your understanding

Answer each question in your own words first, then click to see a model answer.

1. Why are the observations of a time series not independent?

Because each observation carries over much of what shaped the one before it: the same students, the same term, the same circumstances. A busy week tends to be followed by a busy week, and the same week of the year tends to look alike from year to year. This dependence, measured by the autocorrelation, is what makes forecasting possible, and what makes methods that assume independent observations inappropriate.

2. What is the difference between trend, season, and noise?

The trend is the long-term direction of the series, such as demand growing year by year. The season is a pattern that repeats with a fixed period, such as the academic calendar every 52 weeks. The noise, or remainder, is what is left: irregular variation that follows no pattern. Forecasts extend the trend and the season; the noise cannot be forecast and sets the width of the prediction intervals.

3. Why must the test period come after the training period?

Because a real forecast can only use the past. If test weeks were scattered among the training weeks, as a random split would do, the model would be judged partly on filling gaps between weeks it had seen, which is far easier than forecasting ahead, and its accuracy would be overestimated. Training on the earlier period and testing on the later one imitates how the forecast will be used.

4. What is a prediction interval?

A range within which a future observation is expected to fall with a given probability, such as 95%, if the model’s assumptions hold. Unlike a confidence interval for a mean, it covers a single future value, including its noise, so it is much wider. Prediction intervals usually widen as forecasts reach further into the future.

5. Why should every forecast be compared with benchmarks?

Because an error of, say, 5.6 visits a week means nothing on its own. The benchmarks show what the simplest possible methods achieve: if a sophisticated model cannot beat repeating last year, it adds nothing but complexity. In the chapter, the Fourier model did not beat the seasonal naive benchmark, which only the comparison could reveal.

Do it yourself

These tasks have no starter code and no answers. Each code space below is empty and ready to run: write your own code, as you would for a thesis. The data (counselling, visits, and year_of()), the function mae(), and R’s built-in time series are available.

1. Choose one of R’s built-in series: nottem (monthly temperatures in Nottingham), UKgas (quarterly gas consumption in the UK), or ldeaths (monthly deaths from lung disease in the UK). Plot it, decompose it with stl(), hold out its last two years, forecast them with the mean, the seasonal naive method, and a benchmark of your own design, and compare their errors. (In the download project, add fable’s ETS model and report its prediction intervals.)

2. Write the one-page report for the counselling service about next year’s demand: the expected total, the busiest and quietest periods, the uncertainty, and what the forecast assumes, with every number calculated by code. (The download project has the chapter’s model and generate() for the total and its interval.)

3. Describe three things that could make next year’s forecast of counselling visits wrong, and for each, how the service would notice early in the year that the forecast was going wrong. No code is needed; write it on paper or in a document.

Work on your own computer

NoteDownload the Chapter 16 project

The project contains the data and all four parts of this page as an R script. Its Practise part uses tsibble, fable, and feasts exactly as the chapter does, including the exercises that cannot run in a browser; the other parts use base R, as on this page. Answers to the questions are in solutions.R, and the open tasks have space to write your code.

  • Download chapter16.zip, unzip it, and double-click chapter16.Rproj.
  • Or type this one line in RStudio’s Console:
usethis::use_course("https://polla-fattah.github.io/data2thesis_r/playground/chapter16.zip")