Trend and seasonal dummy variables

Today let's learn how to put both a rising trend and a repeating yearly pattern into one regression model, using a gym chain's quarterly signups as the running example.

Meridian Fitness is a mid-size gym chain. Every quarter since 2018, it has logged how many new members signed up. Below are all 24 quarters, from 2018 Q1 to 2023 Q4, quarter 1 on the left and quarter 24 on the right.

Look at the shape. Signups climb year over year, but every Q1 sits higher than the quarter before it, and every Q3 dips lower than the quarter before that.

Trend alone: what TSLM(y ~ trend()) fits

TSLM() fits an ordinary linear regression, except its predictors are built from time itself instead of from other measured columns. The simplest one is trend(), a straight count of each quarter's position in the data: 1 for the first quarter, 2 for the second, and so on up to 24.

Fit signups on trend() alone, and read the coefficients report() returns.

RInteractive R
# Build the 24-quarter tsibble and fit a trend-only regression library(fable) library(fabletools) library(tsibble) library(dplyr) meridian <- tsibble( quarter = yearquarter("2018 Q1") + 0:23, signups = c(232, 224, 200, 242, 297, 289, 270, 299, 362, 338, 329, 354, 426, 379, 365, 391, 432, 415, 384, 401, 462, 445, 413, 429), index = quarter ) fit_trend <- meridian |> model(TSLM(signups ~ trend())) report(fit_trend) #> Series: signups #> Model: TSLM #> #> Residuals: #> Min 1Q Median 3Q Max #> -53.9099 -22.2512 -0.5651 14.1941 71.9075 #> #> Coefficients: #> Estimate Std. Error t value Pr(>|t|) #> (Intercept) 223.8551 12.6429 17.71 1.67e-14 *** #> trend() 10.0183 0.8848 11.32 1.20e-10 *** #> --- #> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 #> #> Residual standard error: 30.01 on 22 degrees of freedom #> Multiple R-squared: 0.8535, Adjusted R-squared: 0.8469 #> F-statistic: 128.2 on 1 and 22 DF, p-value: 1.2037e-10

  

Two coefficients, and they are read exactly like any other regression. The intercept, 223.86, is where the line sits at quarter 0, just before the data starts. The slope on trend(), 10.02, says signups grow by about 10 members a quarter, on average, across all 24 quarters.

Drag the line below over the same 24 points and watch the residual squares shrink as you approach that same intercept and slope. TSLM() found the line that makes the sum of those squares as small as possible; this widget lets you find it by hand.

Settle near intercept 223.86 and slope 10.02 and the squares bottom out, at the same least-squares line report() gave you. Now look at which points sit far from that line even at the best fit: 2018 Q1 lands almost exactly on it, but 2019 Q1 through 2023 Q1 all sit clearly above it, while every single Q3, all six of them, sits below it. A single straight line cannot bend to follow that yearly wiggle. It can only run through the middle of it.