Seasonality and Trends in Time Series
A shop climbs all year then spikes every December. The climb is the trend, the December spike is seasonality, and mixing them up ruins forecasts.
Why Does This Exist?
Daily shop sales look chaotic. One week you sell 100 units, next week 152, then back to 100. If you hand that raw line to a model, it tries to learn two forces at once: the slow climb as your brand grows, and the calendar bumps that return every year. It blends them, so a normal December looks like explosive growth and a normal June looks like collapse.
Split the forces and each becomes easy. The trend tells you where the baseline drifts. Seasonality tells you what December or Monday adds on top. You forecast each on its own, then add them back.
This page teaches the split: what counts as trend, what counts as season, and how to tell which combining rule fits. The full teardown into trend plus season plus leftovers lives in time-series decomposition. Why the split matters for modelling lives in stationarity testing. The model that handles season directly is SARIMA.
Basics first: a time series is values in time order, written for the value at time . If that notation is new, read time series fundamentals first. Everything below assumes you know why order matters.
Think of It Like This
Walking uphill while your dog loops
You walk steadily uphill. Your dog loops around you in the same wide circle, sniffing the same bushes each lap. A drone tracking only the dog sees a wild zigzag. A drone tracking only you sees a clean climb.
Your path is the trend. The dog loop is seasonality. The leash position you actually observe is the sum of both.
Where it stops: your dog chooses its loops and can change them. Seasonality follows the calendar and repeats at a fixed period. And your hill keeps rising only while business grows. Trends flatten, dog loops do not ask permission.
How It Actually Works
Call the observed value , meaning sales at month . Call the trend , meaning the baseline if seasons vanished. Call the seasonal offset , meaning what the calendar adds at . Call the leftover , meaning noise you cannot explain.
The additive rule says . Use it when December adds about the same units each year. The multiplicative rule says . Use it when December adds about the same percent each year, so spikes grow with the brand. Plot yearly peaks: flat height means additive, growing height means multiplicative.
Seasonality needs a fixed period , meaning the cycle length in steps. Daily data with weekly rhythm has . Monthly data with yearly rhythm has . A business boom with no fixed length is a cycle, not a season, and seasonal tools will misfire on it. When in doubt, check autocorrelation: a true season spikes at lags , , .
Worked example
Twelve months, trend , so month 1 starts at 100 and month 12 reaches 122. Season adds 30 in December and subtracts 10 in June, with 0 leftover.
- December: . Then .
- June: . Then .
- The raw jump from June to December is , but the trend only rose . The other 40 is calendar, not growth.
Miss that split and you tell your team demand quadrupled. Split it and you order 30 extra units for December while planning 2 extra per month of real growth.
Code
This snippet recovers yearly means and a fixed seasonal profile with plain lists. Same idea as the diagram, smaller fixture so you can check each step:
def yearly_means(values: list[float], period: int) -> list[float]: """Mean of each full period block.""" blocks: list[float] = [values[i:i + period] for i in range(0, len(values), period)] return [sum(b) / len(b) for b in blocks]
def seasonal_profile(values: list[float], period: int) -> list[float]: """Average offset of each position from its yearly mean.""" means: list[float] = yearly_means(values, period) offsets: list[list[float]] = [[] for _ in range(period)] for year, m in enumerate(means): for pos in range(period): offsets[pos].append(values[year * period + pos] - m) return [sum(col) / len(col) for col in offsets]
sales: list[float] = [100.0, 110.0, 90.0, 120.0, 104.0, 114.0, 94.0, 124.0]print(yearly_means(sales, 4)) # -> [105.0, 109.0]print(seasonal_profile(sales, 4)) # -> [-5.0, 5.0, -15.0, 15.0]print(105.0 + 15.0) # -> 120.0print(109.0 + 15.0) # -> 124.0Yearly means rise 105.0 to 109.0, so the trend gains 4. The fourth-quarter offset repeats at plus 15.0 in both years, so the 120.0 and 124.0 peaks are season plus trend, not a surprise.
Watch Out For
Treating a growing spike as fixed units
You fit the additive rule while peaks grow with the brand. The symptom is a fan shape: residuals look fine early, then December misses by 20, then by 40, always low. The model learned plus 30 units when the truth is plus 25 percent.
Fix it by plotting peak heights against the yearly mean. Flat means additive. Rising together means multiplicative, so take a log or switch models before you trust the intervals.
Calling any wobble seasonal
You fit a 12-month pattern to a business cycle that runs 9 months once and 15 the next. The symptom is a season that never repeats: this January is high, next January is flat, and the forecast confidently picks the wrong one.
Fix it by demanding a calendar reason and a fixed . Day of week, month of year, hour of day all qualify. A wobble with no fixed is trend or cycle, so model it there instead of forcing a seasonal term.
The Quick Version
- Trend is the slow baseline drift. Seasonality is the fixed-period calendar bump.
- Additive means season adds units. Multiplicative means season scales with the level.
- A fixed period tied to the calendar is what makes a pattern seasonal.
- Twelve months from 100 to 122 plus a December 30 gives 152, not runaway growth.
- Check peak heights against the yearly mean to pick additive or multiplicative.
- A wobble with no fixed period is a cycle, and seasonal tools will misfire on it.