Seasonal, subseries and lag plots
Today let's understand three plots that check something a single line chart cannot show you: whether a season repeats the exact same way every year, whether one quarter is quietly drifting away from the rest, and whether a value depends on what came before it.
Here is the running example for the whole lesson: quarterly beer production in Australia, from 1992 Q1 to 2010 Q2, in megalitres (a megalitre is a million litres). That's 74 quarters, published by the Australian Bureau of Statistics. Here is the whole series, plotted once, in full.
It rises and falls four times a year, every single year, for eighteen years straight. Build that same series properly, as a tsibble, and you can ask sharper questions of it. Press Run.
yearquarter("1992 Q1") + 0:73 builds all 74 quarter labels at once, starting at 1992 Q1 and counting up one quarter at a time. autoplot() is a shortcut that picks a sensible plot for whatever kind of object you hand it. Given a tsibble, it draws exactly the line you just saw in the widget above, time along the bottom and the value up the side.
The open question is whether that up-and-down wiggle is the exact same shape every year, or whether it drifts.
What counts as a season in this series
A season, in time series terms, is a calendar unit that repeats on a fixed clock: the same position shows up again after a fixed number of steps. A day has 24 hours that repeat every day. A week has 7 days that repeat every week. This series is recorded once every three months, four times a year, so its season is the quarter, and the number of steps in one full season, called the period, is 4.
beer still holds the tsibble you just built, so look at what it actually contains.
This beer object is a tsibble, short for tidy time series table: a data frame that also keeps track of which column is time and how far apart the rows are spaced. [1Q] is that spacing, printed right next to the row count: one quarter, with no gaps. <qtr> is the special type Quarter is stored as, so it prints as "1992 Q1" instead of a plain number.
The full series runs 74 rows, from 1992 Q1 to 2010 Q2. That's 18 full years, 1992 through 2009, plus the first two quarters of 2010: 18 years and 2 quarters. Across all of it, Beer ranges from 374 to 532 megalitres.
Redrawing by year and reading whether the shape holds: gg_season()
The cover's line chart packs 74 quarters into one long strip, which makes it hard to compare one year's shape against another. gg_season() fixes that: it keeps the quarter on the x-axis and draws one separate line for each calendar year on top of it, all lined up on the same four-point scale.
Eighteen lines appear, one per year from 1992 to 2009, plus a short one for 2010 that stops at Q2, since that's where the series ends. If those eighteen lines all rise and fall the same way, quarter to quarter, the seasonal shape is stable from year to year. If some of them cut across the others in a different order, the shape itself is changing.
One simple summary makes the shared shape concrete: the average Beer value for each quarter, across every year.
Q4 averages 502 megalitres, the highest of the four, and Q2 averages 394, the lowest. Every one of the eighteen lines in the plot above rises toward Q4 and dips at Q2 the same way, which is exactly what "a stable seasonal shape" looks like: the same up-and-down order, year after year, even while the overall level drifts a little.
One panel per season: gg_subseries()
gg_season() is still eighteen overlapping lines squeezed onto one small axis, which gets hard to read once you're hunting for a slow drift inside just one quarter. gg_subseries() solves that by giving each quarter its own panel.
Four panels appear, one for Q1, one for Q2, one for Q3 and one for Q4. Inside each panel the x-axis is no longer quarter, it is year, running from 1992 up to 2010. The Q1 and Q2 panels each hold 19 points, one per year from 1992 to 2010. The Q3 and Q4 panels hold only 18, because the series stops at 2010 Q2, so 2010 never reaches a Q3 or a Q4.
Every panel also carries one flat horizontal line: the mean of that quarter alone, the same four numbers, 429, 394, 411 and 502, you just read off the table in the last step. That line is a private reference for that one quarter. It answers a question gg_season() cannot: has this quarter's own typical level held steady over the years, or has it moved?
For this series, the answer is different from one quarter to the next. Q3's panel is the flattest of the four: 420 megalitres in 1992 and 419 in 2009, hugging its own mean line the whole way. Q4's panel is not: it opens near 532 in 1992 and closes near 488 in 2009, drifting further below its own mean line as the years go by, even while its mean of 502 is still the highest of the four quarters overall.
That is the reading skill a subseries plot gives you that a season plot does not: not just "what is this quarter's typical level", but "has that level been holding, or moving".
Turning one chart into small multiples by quarter
gg_subseries() does that quarter-by-quarter split automatically, but it is worth seeing the mechanic underneath it once, because it is the same mechanic ggplot2's facet_wrap() uses for splitting any chart by any category, not just quarters. The widget below plots the same 74 quarters, split by quarter, and lets you toggle between one combined chart and four small multiples.
In "One chart" mode, all 74 points sit on one Year axis, colored by quarter, which is crowded and hard to read on its own. Press facet_wrap(~Quarter) and the same 74 points split into four separate panels, one per quarter, each on its own small Year axis. That split, one panel per level of a variable, is exactly what gg_subseries() already built for you in the last step, using quarter as the splitting variable and adding each quarter's own mean line on top.
Quick check: what does a rising subseries mean line say?
You just read Q3's panel as flat and Q4's as drifting down, below its own mean line, in the later years. Now flip that around: suppose a different quarter's panel showed its yearly values climbing further and further above its own mean line as the years went by. What would that be telling you?
Defining a lag: the value k quarters earlier
A lag is one of the simplest ideas in time series, once you see it written down. Lag k of a series is just its value k steps earlier: today's value paired with the value from k steps before it. This series has one step per quarter, so lag 4 means four quarters ago, a full year back.
Written with symbols, if \(y_t\) is the value at time t, then lag k of that series is \(y_{t-k}\), and a lag plot always compares \(y_t\) against \(y_{t-k}\) for one fixed k at a time.
Here is lag 1 made concrete: Beer next to Beer shifted down by one row, using dplyr::lag().
Row 2's lag1 is 443, which is exactly row 1's Beer. Row 3's lag1 is 410, row 2's Beer. Every lag1 value is just its own row's Beer value, moved down one position. Row 1 has no earlier row to pull from, so its lag1 comes back NA.
Plotting lag against value and matching each panel to a strength: gg_lag()
A lag plot turns that idea into nine small scatterplots at once, one panel per lag from 1 to 9. Panel k plots \(y_{t-k}\) on the x-axis against \(y_t\) on the y-axis, so every point in that panel is one quarter, paired with its own value from k quarters earlier.
Here are all nine panels for this series.
Read the panels left to right, top to bottom: lag 1, lag 2, and on up to lag 9. Some panels are a loose, round cloud with barely any visible shape. Others are a tight diagonal band, rising from bottom left to top right. The tighter and more diagonal a panel looks, the stronger the relationship between a value and the value that many quarters earlier.
You do not have to judge tightness by eye. Correlate Beer against Beer shifted back by 1, 2, 4 and 8 quarters, and the same story shows up as numbers.
Lag 1 is close to zero, -0.10: one quarter's value tells you almost nothing about the next quarter's value. Lag 2 is a strong -0.67: two quarters apart lands you on close to opposite sides of the yearly pattern, so a high value tends to pair with a low one. Lag 4 and lag 8 are the strong positive ones, 0.93 and 0.94: four quarters is a full year and eight is two full years, so the same quarter of the pattern lines up with itself, and the values track each other closely.
That interpretation only works because this code passed geom = "point". gg_lag()'s own default is not "point", it is "path": draw a line through the points in the order the quarters actually occurred, then scatter them on top of it. Two quarters that sit right next to each other in time are not necessarily anywhere near each other on this plot, so that default line tends to bury the exact diagonal shapes you just read. The next step shows you exactly that.
Point cloud versus connected path at lag 4
See it on the tightest pair you found, lag 4, r about 0.93: the same 70 pairs of \(y_{t-4}\) against \(y_t\) that fed into the correlation table above.
In point mode, the widget draws those same 70 pairs and computes the same Pearson r you already read off the table: about 0.93, a tight, rising cloud. Switch to line, and the same 70 points connect to their nearest neighbour along the x-axis instead of standing alone. The segments cross back and forth across the whole cloud, and the clean diagonal shape you could read a moment ago disappears under them.
That is what connecting a lag plot's points does to it, even sorted along one axis. gg_lag()'s real default, "path", is worse still, because it connects the points in the order the quarters occurred rather than in order along an axis, so the line jumps around even more. That is the whole reason the code a moment ago overrode the default and asked for geom = "point" instead.
Quick check: which plot answers which question?
Suppose you are handed a new series and asked: "is Q3 alone pulling away from the other quarters?" Which of the three plots from this lesson answers that directly?
Your turn: predict and compute the lag-2 correlation
Before you compute anything, predict the sign. Look back at the quarter pattern you read earlier: production troughs at Q2, 394 megalitres, and peaks at Q4, 502 megalitres, two quarters apart. Two quarters back from any point in this series lands you close to the opposite side of that same up-and-down pattern, so a high value tends to pair with a low value two quarters earlier, and a low value pairs with a high one. That alone should push the lag-2 correlation negative, before you run a single line of code.
beer still holds the 74 quarters of Beer production from before. Compute the lag-2 correlation the same way the last step did, one lag at a time: shift Beer back two quarters with dplyr::lag(), then correlate it against Beer with cor().
Show answer
# Shift Beer back two quarters, then correlate it against Beer
beer_lag2 <- beer |>
mutate(lag2 = dplyr::lag(Beer, 2))
cor(beer_lag2$Beer, beer_lag2$lag2, use = "complete.obs")
#> [1] -0.6682That matches the prediction: negative, and close to the -0.67 you already saw two steps back. The seasonal swing between a trough quarter and a peak quarter, two steps away, is strong enough to show up clearly in a single correlation number.
References
- Forecasting: Principles and Practice (3rd ed.) - Hyndman and Athanasopoulos, the free online textbook. Chapter 2 covers seasonal plots, subseries plots and lag plots in the same order used in this lesson.
- feasts package reference - documentation for gg_season(), gg_subseries() and gg_lag().
- Cleveland, W.S., "Visualizing Data" (Hobart Press, 1993) - the book that introduced the seasonal subseries plot.
- Australian Bureau of Statistics, catalogue 8301.0.55.001, table 1 - the source of the beer production series used throughout this lesson.
- tsibbledata package reference - documentation for the aus_production dataset.
Quick recap
- gg_season() checks whether the whole seasonal shape holds from year to year: quarter on the x-axis, one line per year.
- gg_subseries() checks whether one season is drifting apart from the rest: one panel per quarter, each with its own mean line.
- gg_lag() plots \(y_t\) against \(y_{t-k}\) for a range of lags, a first visual look at dependence, before any formal statistic measures it.
- On this series, Q4's mean line drifted down while Q3's stayed flat, and lag 4 and lag 8 showed the strongest positive correlations, 0.93 and 0.94, because a full year and two full years both land you back on the same quarter.