Forecast distributions and prediction intervals

Today let's understand forecast distributions and prediction intervals, using simple and practical examples.

Between 1821 and 1934, trappers in Canada's Mackenzie River district counted how many lynx pelts they trapped every year. That's 114 years of counts, as few as 39 pelts in a quiet year and as many as 6991 in a good one, ending at 3396 in 1934. It's a built-in R dataset called lynx.

Look at that swing, from 39 pelts some years to 6991 in others. Now fit the simplest forecasting model there is to this series, a NAIVE model, whose whole rule is to repeat the last observed count forever. Then forecast ten years past 1934.

RInteractive R
# Set the print width, build the lynx series as a tsibble, fit a NAIVE model and forecast 10 years ahead options(width = 200) library(tsibble) library(fable) library(fabletools) library(ggplot2) lynx_df <- data.frame(Year = 1821:1934, Trappings = as.numeric(lynx)) lx <- as_tsibble(lynx_df, index = Year) fit <- lx |> model(Naive = NAIVE(Trappings)) fc <- fit |> forecast(h = 10) suppressWarnings(autoplot(fc, lx) + labs(y = "Trappings", title = "Lynx pelts trapped, 1821-1934, plus a 10-year NAIVE forecast"))

  

Look at the plot. The point forecast, the line past 1934, sits perfectly flat at 3396 for all ten years. But the shaded band around it fans out wider the further out you look, even though the point forecast itself never moves. That flat line plus a widening band around it is exactly what a prediction interval looks like.

A forecast is a distribution, not one number

fc, the object forecast() just handed back, does not hold ten plain numbers. Print it and look at the Trappings column.

RInteractive R
# Print the fable and look at the Trappings column print(fc) #> # A fable: 10 x 4 [1Y] #> # Key: .model [1] #> .model Year Trappings .mean #> <chr> <dbl> <dist> <dbl> #> 1 Naive 1935 N(3396, 1409724) 3396 #> 2 Naive 1936 N(3396, 2819448) 3396 #> 3 Naive 1937 N(3396, 4229171) 3396 #> 4 Naive 1938 N(3396, 5638895) 3396 #> 5 Naive 1939 N(3396, 7e+06) 3396 #> 6 Naive 1940 N(3396, 8458343) 3396 #> 7 Naive 1941 N(3396, 9868067) 3396 #> 8 Naive 1942 N(3396, 1.1e+07) 3396 #> 9 Naive 1943 N(3396, 1.3e+07) 3396 #> 10 Naive 1944 N(3396, 1.4e+07) 3396

  

Every row of Trappings prints as N(mean, variance), R's shorthand for a Normal distribution: a bell curve centered on the first number, spread out according to the second, the variance. At h = 1, 1935, that's N(3396, 1409724). At h = 10, 1944, it's N(3396, 1.4e+07), the variance a full ten times bigger.

.mean pulls out just the center of each distribution for convenience, and it reads 3396 at every single horizon, since NAIVE always repeats the last observed count. But .mean is only ever half the story. forecast() actually hands back a full distribution at every horizon, and it's the variance climbing in that Trappings column, not anything in .mean, that is behind the fan you saw widen in the cover plot.