Autocorrelation and the ACF
Today let's understand autocorrelation and the ACF, using simple and practical examples.
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.
Look at the line. It rises and falls again and again, and it never quite settles at one level for long. That raises the question this whole lesson answers: does each quarter's value actually depend on the quarters that came before it, or is that rise and fall just noise with no memory at all?
What autocorrelation asks: does a value depend on its own past?
Build that same series properly, as a tsibble, and you can start asking sharper questions of it than a single line chart can answer.
A lag is just "how many periods back". Lag 1 means one quarter earlier, lag 2 means two quarters earlier, and so on.
Autocorrelation asks a very specific question using that idea: does the value at time \(t\) depend on the value some lag \(k\) periods earlier, at time \(t-k\)? And the way you check that is not new at all. You already know how to check whether two things move together: correlation. Autocorrelation is nothing more than the ordinary Pearson correlation you already know, computed between a series and its own lagged copy.
Written with subscripts, autocorrelation at lag \(k\), called \(r_k\), is the correlation between \(y_t\) (the value now) and \(y_{t-k}\) (the value \(k\) periods earlier).
Here's the beer series built as a tsibble, with a new column holding each row's own lag-1 value next to it.
Look at the Beer_lag1 column. Row 2's value there, 443, is exactly row 1's Beer value. Every row's lag-1 column is just the row above its own Beer value, shifted down by one. The first row has nothing before it, so its lag is NA (not available).
That's all a lag is: the same series, copied and shifted down by \(k\) rows. Autocorrelation at lag \(k\) is just the correlation between the Beer column and this shifted copy.
The lag-k autocorrelation coefficient, computed by hand
You could hand Beer and Beer_lag1 straight to R's cor() function and get something close to \(r_1\). That would work, roughly. But the actual formula behind ACF() is slightly more careful than that, and it's worth seeing why.
\[ r_k = \frac{\sum_{t=k+1}^{n}(y_t - \bar y)(y_{t-k} - \bar y)}{\sum_{t=1}^{n}(y_t - \bar y)^2} \]
Here \(y_t\) is the value at time \(t\), \(\bar y\) is the mean of the whole series (all 74 quarters), and \(n\) is 74. The numerator multiplies each value's deviation from the mean by the deviation of the value \(k\) quarters earlier, and adds those products up over the \(n - k\) pairs that actually overlap once you shift by \(k\). The denominator, though, always sums the squared deviations over all \(n\) values, no matter what \(k\) is.
That fixed denominator is the detail worth noticing. It never changes as \(k\) changes, so \(r_1\), \(r_2\), all the way to \(r_{12}\), are divided by the exact same number. That's what makes different lags comparable to each other: every one of them is measured against the same total variance in the series, not each against its own private slice of it.
Compute \(r_1\) directly from that formula, on beer's full 74-quarter series, and compare it with what cor() gives when handed the two shifted vectors directly.
r1 comes out to -0.1019, which is what ACF() will report too, as you'll see in a moment. r1_cor_naive comes out slightly different, -0.1033. It isn't wrong exactly, it's just answering a subtly different question. cor() centres and scales each of the two vectors using only their own 73 values, not the full 74-quarter series. The ACF formula centres everything around the one mean of the whole series instead, and always divides by that same full-series sum of squares, which is exactly why ACF() can report every lag on one shared scale.
ACF() computes every lag at once, and where its significance band comes from
Computing \(r_1\) by hand once is useful, so you know exactly what the number means. But nobody recomputes \(r_2\) through \(r_{12}\) this way by hand every time. R's feasts package has a function, ACF(), that does the same computation at every lag in one call.
Run it on the beer tsibble out to lag 12, three full years of quarters.
The first value, -0.102, matches \(r_1\) computed by hand a moment ago. ACF() repeats that exact computation at every lag from 1 to 12, all divided by the same fixed denominator.
Twelve numbers on their own don't tell you much, though. Which of them count as real dependence, and which are just noise wandering around zero? For that you need a threshold, called the significance band.
If a series really were white noise, with no dependence on its own past at all, its sample autocorrelation at any lag would still wander a little away from zero just by chance, even though the true autocorrelation is exactly zero. The band \(\pm 2/\sqrt{n}\) marks the region that wandering would rarely leave, about 95% of the time, if the series truly had no autocorrelation. Here \(n\) is the number of observations, 74.
Compute that band for the 74-quarter beer series.
So any \(|r_k|\) bigger than 0.2325 sits outside where pure noise would typically land, and is worth treating as real dependence rather than chance.
Chart all twelve lags as bars, coloured by whether each one falls inside or outside that band.
Six of the twelve lags, every even one, land outside the band. Something is clearly going on in this series beyond pure chance. What that pattern actually means is exactly where the next few steps go.
Quick check: does the hand-computed r_1 match ACF(), and is it real?
A slow, one-way decay: what trend looks like in an ACF
Beer's ACF, so far, is just one shape. To learn to read ACF shapes in general, it helps to see a very different one next to it.
Here's a second real series: Australia's annual GDP, from 1960 to 2017, 58 years, measured in billions of dollars, originally published in tsibbledata's global_economy dataset. Unlike beer, GDP has no season. It rises for most of the run, from 18.6 billion in 1960 up to a peak of 1,573.7 billion in 2013, before easing back to 1,323.4 billion by 2017.
Build the GDP tsibble and compute its ACF out to lag 10.
Look at the shape of that column. It starts high, 0.945 at lag 1, and falls smoothly, lag after lag, all the way down to 0.274 at lag 10. It never dips to zero, never goes negative, and never bounces back up. It just fades, steadily, in one direction.
That shape is the signature of a trend. A trending series carries its level from one year almost unchanged into the next: if GDP was high last year, it is very likely to still be high this year, and only a little less certain to still be high 10 years from now. Nearby values stay strongly correlated, and that correlation only fades slowly as the gap between them grows.
A repeating peak and trough: what seasonality looks like in an ACF
GDP's ACF fades in one direction. Beer's ACF, from a few steps back, does something completely different.
Look back at those twelve numbers: -0.102, -0.657, -0.060, 0.869, -0.089, -0.635, -0.054, 0.832, -0.108, -0.574, -0.055, 0.774.
Notice the pattern in where the big numbers land. The peaks, the largest positive values, sit at lags 4, 8 and 12: 0.869, 0.832 and 0.774. The troughs, the largest negative values, sit at lags 2, 6 and 10: -0.657, -0.635 and -0.574. Both sets are spaced exactly 4 lags apart.
That spacing is not a coincidence. Beer production is measured quarterly, and 4 quarters make one year. A quarter's value stays close to the same quarter a year earlier (lag 4, lag 8, lag 12: strong positive correlation) and sits furthest from the opposite quarter two quarters away (lag 2, lag 6, lag 10: strong negative correlation), because a quarter that runs high every year sits opposite a quarter that tends to run low.
A repeating peak-and-trough pattern like this, spaced at a fixed number of lags, is the signature of seasonality. The spacing itself tells you the season length: beer repeats every 4 lags, so its season is 4 quarters, one year.
White noise: the shape when nothing is left to explain
You've now seen two real shapes: a slow one-way decay (trend) and a repeating peak-and-trough (seasonality). There's a third shape worth knowing, and it's the most important one of all, because it's the shape every other ACF gets compared against.
White noise is a series where every value is drawn independently, with nothing about one value telling you anything about the next: no trend, no season, no memory of any kind.
Simulate 100 independent values and compute their ACF out to lag 12, to see what pure independence looks like in practice.
Every one of those twelve values sits between -0.184 and 0.149, well inside the \(\pm 0.2\) band for \(n = 100\). None of them show anything a trend or a season would produce: no steady one-way decay, no repeating spacing, just small numbers scattered on both sides of zero with no pattern.
That's what white noise looks like on an ACF: every bar inside the band, no shape to read at all. It's the reference point. When you look at a real series' ACF and ask whether anything is going on, this all-inside-the-band shape is the "no" answer you're comparing it against.
The Ljung-Box test: one number for "is this white noise overall?"
So far you've been reading ACF shapes by eye: does it decay steadily, does it repeat at a fixed spacing, does it sit inside the band everywhere. That works, but eyeballing twelve bars isn't a decision procedure. Sometimes you want one number that says, across several lags at once, whether a series behaves like white noise or not.
That number comes from the Ljung-Box test. It combines the squared autocorrelations from several lags into a single statistic, and tests the null hypothesis that the series is white noise up to that many lags. A small p-value means the series is not behaving like white noise: there's real dependence in there somewhere among those lags.
Run the Ljung-Box test at lag 8 on beer and on the simulated white-noise series from the last step, side by side.
Beer's Ljung-Box statistic comes out at 189, with a p-value that rounds to zero. That rejects the null hypothesis: beer is definitely not white noise, matching everything you already saw in its ACF, the strong seasonal peaks and troughs.
The simulated white-noise series gives a Ljung-Box statistic of 4.87, with p = 0.771. That p-value is nowhere near small, so there's no reason to reject the null here: this series behaves exactly like what it is, white noise, which also matches its ACF, every bar inside the band.
One number, one clean verdict, instead of eyeballing twelve bars each time.
Why it matters: what ignoring autocorrelation does to your intervals
Everything so far has been about spotting autocorrelation. Here's why it's worth spotting in the first place.
Ordinary regression and most standard confidence intervals assume the errors are independent of each other, one observation carrying no information about the next. Autocorrelation breaks that assumption directly: when errors are correlated with their own past, your data does not carry as much independent information as its raw count suggests.
The widget below runs that idea as an experiment. It fits many regressions on data built with a chosen amount of autocorrelation between the errors, and measures two things every time it runs: how often the model's 95% interval actually contains the true value, called coverage, and how good the fit looks, R-squared. It uses a fresh simulated regression, not the beer series itself, but the same coverage problem applies to any time series with real autocorrelation left in its errors, beer included.
Drag the dial from independent to severe. Coverage, the share of intervals that actually contain the truth, collapses well below the nominal 95% as the errors become more strongly autocorrelated. Now watch R-squared while you do it: it does not get worse. If anything, it rises, because a smooth, slowly-drifting error series happens to flatter the fit at the exact same time it's destroying the interval.
That is the whole reason the checks from the last several steps matter. A model can look just as good, or even better, by its fit statistic, while the confidence interval built around it has become worthless. ACF and Ljung-Box are how you catch that before you trust an interval you shouldn't.
Quick check: matching an ACF shape to what it means
Here's an ACF from a series you haven't seen yet: it decays smoothly toward zero across many lags, and it never once crosses into negative territory.
Your turn: test a series for leftover autocorrelation
aus_gdp, the GDP tsibble from a few steps back, is still sitting in your session. Its ACF decayed slowly and never crossed zero, the trend shape. Now put a number on that: run the Ljung-Box test on it at lag 8, the same way you just saw done for beer and the white-noise series, and see whether it agrees with what the ACF already showed.
Show answer
# Test Australia's GDP series for leftover autocorrelation with the Ljung-Box test
features(aus_gdp, GDP_b, ljung_box, lag = 8)
#> # A tibble: 1 × 2
#> lb_stat lb_pvalue
#> <dbl> <dbl>
#> 1 254. 0References
- Forecasting: Principles and Practice (3rd ed.) - Hyndman and Athanasopoulos, the free online textbook. Sections 2.8 and 2.9 cover the ACF and white noise; section 5.4 covers the Ljung-Box test used in this lesson.
- feasts package reference - documentation for ACF() and the ljung_box feature used inside features().
- Ljung, G.M. and Box, G.E.P., "On a Measure of Lack of Fit in Time Series Models", Biometrika (1978) - the paper the Ljung-Box test comes from.
- 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 global_economy dataset behind Australia's GDP series.
Quick recap
- \(r_k\), the autocorrelation at lag \(k\), is the ordinary Pearson correlation of a series with its own lag-k self, computed from one fixed-denominator formula and reproduced exactly by ACF().
- The band \(\pm 2/\sqrt{n}\) marks which lags are real and which are noise. Beer's \(r_1\) sits inside it; six of its twelve lags sit outside.
- A smooth, one-directional decay in the ACF means trend, the shape GDP showed. A repeating peak-and-trough spaced at a fixed number of lags means seasonality, the shape beer showed, spaced at 4. Every bar inside the band means white noise, the shape the simulated series showed.
- The Ljung-Box test turns that whole shape into one p-value: close to zero rejects white noise, and nowhere near zero fails to reject it.
- A real, undetected autocorrelation does not make a model's fit look worse. It quietly breaks the confidence interval's coverage instead, while the fit statistic can even look a little better than it should.