Moving averages and classical decomposition
Today let's understand how a time series gets pulled apart into a trend and a repeating seasonal pattern, using one real series.
AirPassengers is a classic dataset built into R: the monthly totals of international airline passengers worldwide, from January 1949 to December 1960, 144 months in all. Monthly totals range from 104 in the quietest month to 622 in the busiest.
Here is the whole series, plotted in the order the months actually happened.
The line climbs across all twelve years, and every summer it rises to a peak, each one higher than the summer before. That climbing, repeating peak is the whole puzzle this lesson solves: how do you pull the steady climb apart from the repeating summer bump, so each can be looked at on its own?
What a centred moving average is, and how to compute one by hand
The trend in a series is its slow-moving overall level, with the season and the noise averaged out. The simplest way to estimate it is a moving average: replace each raw value with the average of the values around it.
A centred moving average of order m, written m-MA, averages the m raw values sitting symmetrically around a given month t. For m = 3, the 3-MA at month t averages month t-1, month t itself, and month t+1, one month on each side.
Take July 1949. Its raw value is 148. June 1949 is 135 and August 1949 is 148. Average those three and you pull the value down to about 143.67, a little below the raw 148 because June drags it lower.
Build this in R. First turn AirPassengers into a tsibble, the data structure feasts and tsibble expect, then compute the 3-MA by hand for the months around July 1949.
The table rounds its display to fit the column, so July's exact value shows up as 144. Pull that one number out on its own and the full figure comes back: 143.67. The loop above only runs from month 2 to month 143, one short of each end, because month 1 has no month before it to average with, and month 144 has no month after.
How much smoothing is too much?
A longer window pulls in more neighbouring months, so it smooths harder. But it also needs more real months on each side to compute, so it leaves more months without a value at either end of the series.
Compute the 7-month centred average at the same July 1949 point, and compare all three numbers side by side.
The 7-MA pulls July down to 133.71, further from the raw 148 than the 3-MA's 143.67, because it now reaches three months out on each side instead of one. That is more smoothing. The cost sits at the edges: the 3-MA above only loses 1 month at the start and 1 at the end (it runs from month 2 to month 143 out of 144), while a 7-MA would lose 3 months at each end, because it needs 3 real months on each side to compute.
Window length in a moving average works exactly like the smoothness setting in another common smoothing tool, a spline. The widget below is a different, generic example, not AirPassengers itself, but it carries exactly the same trade-off you just computed by hand: too small a smoothing setting chases every wiggle in the data, too large a one flattens out the real bends underneath.
Move the slider from one end to the other. At the stiff end the fitted curve barely bends, the way a very long moving-average window would flatten a real turn in AirPassengers' level. At the flexible end it chases every wiggle, the way too short a window would. Somewhere in between it settles close to the true curve, the same middle ground a well-chosen window length gives a moving average.
Why an even order needs a second pass: the 2x12-MA
AirPassengers repeats its shape every 12 months, so the natural window for its trend is a 12-month average. But 12 is even, and that creates a problem odd windows like 3 and 7 never had.
Average months 1 through 12, all of 1949, and the result sits at time 6.5, halfway between June and July. It is not attached to any real month at all.
Slide the window forward by one month, months 2 through 13, and you get a second 12-month average, this one sitting at time 7.5, halfway between July and August.
Neither average lands on July. But average those two 12-month averages together, and the two half-months cancel out: 6.5 and 7.5 average to exactly 7, July itself.
This two-pass average is called a 2x12-MA: a 12-month average, taken twice and averaged together. It is the standard way to estimate a trend whenever the season repeats over an even number of periods, 12 months here (you would see a 2x4-MA for quarterly data, where the season repeats every 4 quarters).
The same 126.792 comes out of one 13-month weighted average instead, with the two end months getting half the weight of every month in between them.
\[ trend_t = \frac{1}{24}y_{t-6} + \frac{1}{12}\left(y_{t-5} + y_{t-4} + \cdots + y_{t+5}\right) + \frac{1}{24}y_{t+6} \]
And this is not just a coincidence of arithmetic. It is exactly what base R's own decompose() function computes for a trend.
Same number, to four decimal places. Whatever computes a classical trend, feasts included, is doing this same two-pass average underneath.
Quick check: why average two 12-month averages instead of one?
Running classical_decomposition() and reading trend, seasonal and random
That two-pass average is exactly what feasts' classical_decomposition() computes for you, along with the season and whatever is left over. Fit it on the tsibble you already built, with type = "additive" telling it to assume the season adds a fixed number of passengers to the trend every month, rather than scaling it.
components() hands back four columns beyond the ones you already have. trend and seasonal you have just built by hand. random is whatever is left once trend and seasonal are taken out of the observed value. season_adjust is the observed series with only the seasonal part removed, trend and random still inside it.
Row 7, July 1949: trend reads 127 (rounded to one decimal for display; it is the same 126.792 you computed by hand). Its seasonal value is 63.8. Add trend and seasonal and subtract the small random leftover, 127 + 63.8 - 42.6, and you land back close to the raw 148.
Notice the first six rows show NA for trend and random. The 2x12-MA needs 6 real months on each side of a point to compute (it is really a 13-month window), so the first month it can reach is July 1949, row 7. The same thing happens at the other end of the series, on its last 6 months.
Choosing additive or multiplicative from how the season behaves
The additive decomposition you just ran assumes the summer bump adds a fixed number of passengers to the trend every year. Check whether that is actually true by comparing each year's range, its tallest month minus its shortest, against its average level.
In 1949 the range between the busiest and quietest month was 44, against a mean level of 127. By 1960 the mean level had grown to 476, and the range had grown right along with it, to 232. The summer bump is not a fixed number of extra passengers. It scales with however big the series has grown: about a third of the mean level in 1949, and closing in on half of it by 1960.
That is the sign to use a multiplicative decomposition instead: Passengers = trend x seasonal x random, where the seasonal term scales the trend rather than adding to it. Refit with type = "multiplicative" and compare July's seasonal value against the additive one you already have.
Read those two numbers in their own terms. The additive value, 63.8, says July adds 63.8 to the trend, every year, no matter how big the trend has grown. The multiplicative value, 1.227, says July multiplies the trend by 1.227, a 22.7% lift, and that lift applies whether the trend is 127 or 476. For a series whose seasonal swings grow with its level, the multiplicative number is the one that stays true from 1949 to 1960; the additive number would understate the swing in 1960 and overstate it in 1949.
Seeing the four components side by side
Passengers, trend, seasonal and random are the same four columns you have just been reading one row at a time. Seeing all 144 months of each one at once, side by side, makes clear what each is actually doing.
The widget below plots the multiplicative fit's four columns, either combined onto one chart or split into four small multiples, one panel per component.
In the combined view, Passengers sits in the hundreds while Seasonal sits between about 0.8 and 1.23, so the two are hard to read against each other on one scale. Switch to facet_wrap(~Component) and each gets its own panel with its own scale. Trend climbs smoothly with no summer bumps left in it, because the moving average already averaged them away. Seasonal repeats the identical 12-month shape every single year, in effect one flat curve traced twelve times over. Random sits close to 1 the whole way through, the small multiplicative leftover once trend and seasonal are divided back out of Passengers.
What each component means, and where classical decomposition falls short
Put a name to each of the four columns you have now computed, plotted and compared.
- Trend is the smoothed level of the series, with season and noise averaged away by the 2x12-MA. It has no value for the first 6 or the last 6 months of any series, because the 2x12-MA needs 6 real months on each side of a point to compute.
- Seasonal is the repeating calendar effect. For AirPassengers it comes out to the exact same number every July, the exact same number every August, and so on, unchanged from 1949 all the way to 1960.
- Random is whatever trend and seasonal cannot explain, whatever is left over once both are divided (or subtracted) out.
- season_adjust is the observed series with only the seasonal part taken out, trend and random still inside it.
That fixed, repeating seasonal number is also the method's real limit. AirPassengers' summer bump keeping exactly the same shape for twelve straight years is a convenient property of a textbook series. A real series's season can drift, a slightly earlier peak one year, a slightly later one the next, and classical decomposition has no way to show that drift, because it assumes one fixed repeating shape for the whole series. And because the 2x12-MA needs 6 months on each side to compute, classical decomposition can never estimate a trend at either end of any series, however long that series runs.
Quick check: additive or multiplicative, and what each component says
Your turn: decompose the series and read one value
air still holds the AirPassengers tsibble, with columns Month and Passengers. Fit a multiplicative decomposition on it yourself, the same way you did two steps ago, then pull its components out and read off the trend value at row 7, July 1949. Check it against the 126.792 you already worked out by hand.
Show answer
# Fit the multiplicative decomposition and read off trend and seasonal at July 1949
fit_check <- air |> model(classical_decomposition(Passengers, type = "multiplicative"))
comp_check <- fit_check |> components()
comp_check$trend[7]
#> [1] 126.7917
comp_check$seasonal[7]
#> [1] 1.226556References
- Forecasting: Principles and Practice (3rd ed.), section 3.4 "Classical decomposition" - Hyndman, R.J. & Athanasopoulos, G., OTexts. The textbook treatment of the two-pass moving average this lesson builds by hand.
- stats::decompose() documentation - R Core Team, base R. The function this lesson's hand computation was checked against.
- feasts::classical_decomposition() reference - the feasts package documentation for the function fit throughout this lesson.
- Box, G.E.P., Jenkins, G.M. & Reinsel, G.C., "Time Series Analysis: Forecasting and Control" - the source of the AirPassengers series used throughout this lesson.
- tsibble::as_tsibble() documentation - the tsibble package documentation for converting a ts object to a tsibble.
Quick recap
You pulled AirPassengers apart into a trend, a season and a random leftover, and you know exactly how each number in that decomposition came to be.
- A centred moving average replaces each value with the average of its neighbours; a longer window smooths harder but loses more months at each end.
- An even season, 12 months here, needs two passes: two 12-month averages, themselves averaged, to land the result back on a real month. That 2x12-MA is exactly what
decompose()andclassical_decomposition()compute for a trend. - Additive assumes the season adds a fixed number every year; multiplicative assumes it scales the trend by a fixed proportion. AirPassengers' seasonal swing grew from 44 to 232 right alongside its mean, so multiplicative was the right call, a 22.7% July lift that held from 1949 to 1960.
trend,seasonal,randomandseason_adjustare the four columnscomponents()returns, and you can now name what each one measures.- Classical decomposition's season never changes shape year to year, and it cannot estimate a trend at either end of a series. The next part covers STL, which relaxes both of those limits.