Lecture slides

Time Series Forecasting

Time Series Forecasting

When the order of observations carries information

Chapter 16

Polla Fattah

By the end of today you can

  • explain dependence over time, and why forecasts are ranges;
  • name the parts of a series: trend, seasonality, noise;
  • store a series as a tsibble, and plot it;
  • read time plots, seasonal plots, and autocorrelation;
  • decompose a series with STL;
  • forecast with benchmarks, ETS, and ARIMA in fable;
  • evaluate forecasts on a test period, and report prediction intervals.

A different way of thinking

Survey data Time series
observations independent this week relates to last week
order does not matter order carries information
one value per case trend, season, and noise together

A forecast of the future is honestly a range, never a single number.

The counselling service’s question

Five years of weekly visit counts: overwhelmed before exams, quiet in summer, staffing planned by guesswork.

How many visits should the service expect next year, week by week?

An internal analysis, done for the service rather than for publication.

A tsibble knows which column is time

clinic <- tsibble(
  day    = 1:14,
  visits = c(12, 10, 9, 11, 7, 3, 2,   14, 11, 10, 12, 8, 4, 2),
  index  = day
)

Two weeks of daily visits to a small clinic, from a Monday.

The index keeps observations in order and each time unique. Busy early in the week, quiet at the weekend.

Three parts of a pattern

parts <- tibble(
  week     = 1:156,
  trend    = 30 + 0.05 * week,
  seasonal = 12 * sin(2 * pi * week / 52),
  noise    = rnorm(156, sd = 3)
) |>
  mutate(series = trend + seasonal + noise)

An artificial weekly series for three years, built from its parts.

Only the sum is observed

Forecasting extends trend and season. Noise cannot be forecast: the main reason every forecast has a range.

The counselling data as a tsibble

visits <- counselling_visits |>
  mutate(week = yearweek(week_start)) |>
  as_tsibble(index = week)

Five academic years, September 2020 to August 2025: 260 weeks.

yearweek() turns each Monday’s date into a week; the header reads [1W].

Plot the series first

A seasonal pattern each academic year, a gentle upward trend, noise, and one unusual week in autumn 2023.

A seasonal plot

Every year has the same shape; later years lie slightly higher. The period is 52 weeks.

Busy weeks follow busy weeks

Knowing last week’s visits says a good deal about this week’s.

Autocorrelation

The correlation between the series and itself \(k\) steps earlier.

visits_acf <- visits |> ACF(visits, lag_max = 60)
Lag 1 26 52
autocorrelation 0.69 -0.24 0.68

A week resembles the same week a year earlier. Half a year apart, a busy term week often meets a quiet summer week.

The autocorrelation function

A peak at the seasonal lag is the signature of seasonality. No autocorrelation would mean nothing to forecast beyond the average.

Decomposition with STL

visits_stl <- visits |>
  model(STL(visits ~ season(period = 52), robust = TRUE)) |>
  components()
Component Is
trend the slow movement
seasonal the repeating academic-year pattern
remainder what is left: the noise

robust = TRUE stops unusual weeks from distorting trend and season.

The recovered parts

The trend rises from about 28 to 38 visits a week, levelling off in the last year.

The unusual week

visits_stl |> as_tibble() |>
  slice_max(abs(remainder), n = 3) |>
  select(week, visits, trend, season_52, remainder)

The week of 23 October 2023: 71 visits, about 28 more than trend and season predict.

It was the mental health awareness week. A real week, not an error: kept, and the robust fit stops it distorting forecasts.

Growing seasonal swings

The peaks get higher as the trend rises, while the summer lows stay low.

When the season grows with the level, model the logarithm of the series.

In fable, write log(visits) in the model; forecasts are transformed back automatically.

Benchmarks first

Method Forecasts
mean the average of all past observations
naive the last observed value
seasonal naive the value from the same season last cycle

Every forecasting method must beat these, as every model in Chapters 11–13 had to beat a baseline.

Benchmarks on the clinic

clinic |>
  model(mean   = MEAN(visits),
        naive  = NAIVE(visits),
        snaive = SNAIVE(visits ~ lag(7))) |>
  forecast(h = 7)
Method Forecast for next week
mean 8.2 every day
naive 2 every day: a quiet Sunday repeated
seasonal naive last week’s pattern repeated

Two families of models

Family Forecasts with
ETS, exponential smoothing weighted averages, recent weeks counting most; tracks level, trend, season
ARIMA how each value depends on earlier values and shocks

ETS() and ARIMA() choose their details automatically.

Both work best with short seasons: 4 quarters, 7 days, 12 months. A 52-week season is long.

Two strategies for weekly data

flowchart LR
    A["Weekly series"] --> B["STL: remove<br>the season"] --> C["ETS on the<br>adjusted series"] --> D["Add the<br>season back"]
    A --> E["ARIMA with<br>Fourier waves"]

decomposition_model() does the first in one step. fourier(period = 52, K = 6) uses six pairs of smooth waves.

Training and test periods

visits_train <- visits |> filter(week_start <  as.Date("2024-09-01"))
visits_test  <- visits |> filter(week_start >= as.Date("2024-09-01"))

The test period must come after the training period: a forecast can only use the past.

Train on four academic years, test on the fifth.

Five models at once

visits_fit <- visits_train |>
  model(
    mean    = MEAN(visits),
    naive   = NAIVE(visits),
    snaive  = SNAIVE(visits ~ lag(52)),
    stl_ets = decomposition_model(
                STL(log(visits) ~ season(period = 52), robust = TRUE),
                ETS(season_adjust ~ season("N"))),
    fourier = ARIMA(log(visits) ~ fourier(period = 52, K = 6) + PDQ(0, 0, 0)))

season("N"): no season inside ETS, it comes from STL. PDQ(0, 0, 0): the Fourier terms handle the season.

Accuracy on the test year

model RMSE MAE
stl_ets 7.4 5.6
snaive 9.2 7.0
fourier 10.3 7.8
mean 18.7 16.3
naive 27.5 23.5

STL with ETS is best. The Fourier model struggles with sharp steps, such as the summer drop from one week to the next.

The two best, against reality

The seasonal naive copies last year’s noise, even the awareness-week spike. STL with ETS is smoother and closer.

Forecasting next year

final_fit <- visits |>
  model(stl_ets = decomposition_model(
    STL(log(visits) ~ season(period = 52), robust = TRUE),
    ETS(season_adjust ~ season("N"))))
next_year <- final_fit |> forecast(h = 52)

Refitted on all five years, so the forecasts use the most recent information.

Prediction intervals

80% and 95% ranges. First week: about 39 visits, anywhere from 24 to 61 unsurprising.

A total for the year needs simulation

set.seed(2026)
futures <- final_fit |> generate(h = 52, times = 1000)
yearly_totals <- futures |> as_tibble() |>
  summarise(total = sum(.sim), .by = .rep)
quantile(yearly_totals$total, c(0.025, 0.5, 0.975))

Weeks do not vary independently, so weekly intervals cannot simply be added.

generate() produces 1,000 possible futures; their totals give the range.

The report to the service

  • about 2,111 visits expected in 2025/26 (95% interval 1,952 to 2,286), against 1,954 in 2024/25;
  • the busiest weeks will again be just before and during the exam periods;
  • an awareness campaign can raise demand sharply for a week: plan extra capacity.

Forecasts assume the future resembles the past

A new booking system, a changed exam calendar, or a crisis like COVID-19 can make any forecast wrong.

  • report forecasts with their intervals;
  • say what they assume;
  • update them as new data arrives.

Prophet, once popular, is no longer actively developed; fable is well supported.

In your field: environmental science

co2_ts |>
  filter(index <= yearmonth("1990 Dec")) |>
  model(snaive = SNAIVE(value), ets = ETS(value), arima = ARIMA(value)) |>
  forecast(h = "7 years") |> accuracy(co2_ts)
Model MAE (ppm)
seasonal naive 5.3
ETS 1.0
ARIMA 1.2

Mauna Loa CO₂: a strong trend and a 12-month cycle that ETS and ARIMA model directly.

Practical lab: the Chapter 16 playground

Work through the playground exercises in your browser, with hints and solutions.

The browser version builds Fourier terms with lm(); the download uses fable.

Practical exercises 1–4: parts and models

  1. Which benchmark suits a trend without seasonality? Test it on the clinic.
  2. Plot the seasonally adjusted series: what does it reveal?
  3. Fourier terms with K = 2 and K = 12.
  4. Train on three years, test on the fourth.

Practical exercises 5–7: forecasts and communication

  1. Expected visits in the four weeks before the first exams, with an interval.
  2. Explain a 95% prediction interval to the service.
  3. Double the noise in the artificial series: what happens to forecast intervals?

Try this yourself

Find a series in your own field.

  • plot it, and name its trend, season, and noise;
  • choose the seasonal period;
  • pick a benchmark it must beat;
  • decide what event could make a forecast wrong.

Troubleshooting guide (Part 1)

Symptom Likely cause
“duplicated index” error the same time appears twice
ETS or ARIMA struggles with weekly data the 52-week season is long; decompose first
forecasts copy last year’s spikes seasonal naive repeats the noise
one week dominates the trend use robust = TRUE

Troubleshooting guide (Part 2)

Symptom Likely cause
test error suspiciously low test period not after training period
seasonal swings grow with the level model log() of the series
a yearly total without an interval weekly intervals cannot be added; simulate
forecasts wrong after a change the past no longer resembles the future

Completion checklist

Misconceptions to leave behind (Part 1)

Misconception Better mental model
a forecast predicts the exact value a forecast is a range
more history always helps only if old patterns still hold

Misconceptions to leave behind (Part 2)

Misconception Better mental model
any random subset can be the test set the test period comes after training
an unusual week should be removed keep it; use robust methods

The chapter in one sentence

Separate trend, season, and noise, beat the simple benchmarks on a later test period, and always report a forecast as a range with its assumptions.

Next: Chapter 17

The next chapter makes research reproducible:

  • reproducibility and credibility;
  • documents that contain their own analysis;
  • a results chapter in Quarto;
  • fixed package versions with renv;
  • interactive dashboards with Shiny, and open science.

Questions

What regular measurement in your field has a seasonal pattern?

What event could break its forecast?