Lesson 2 - A Trend That Changes Pace

Welcome to A Trend That Changes Pace

Lesson 1 left two open questions: is Lantern & Vine’s seasonal swing additive or multiplicative, and did its growth rate genuinely change, or does that just look that way in three yearly totals? Both questions get real, computed answers in this lesson, using tools built in Module 2, applied here for the first time to a series that does not hand you a clean answer the way Cyclepath did.

By the end of this lesson, you will be able to:

  • Use STL’s seasonal component to diagnose multiplicative structure on a series with a non-constant trend
  • Confirm that diagnosis with a classical multiplicative decomposition
  • Fit separate growth rates to two segments of a trend and quantify how much they differ
  • Explain why a trend regime change complicates simpler, year-by-year diagnostics

Let’s settle both questions with evidence.


Diagnosing the Structure: STL’s Growing Seasonal Swing

Module 2’s simplest additive-versus-multiplicative test compared a raw peak-to-trough swing against the average level, year by year. That test assumed a constant trend, which Cyclepath had and Lantern & Vine does not, so applying it directly here gives a confusing, stepped pattern rather than a clean answer. STL sidesteps the problem, because it estimates a smooth, locally-adapting trend rather than one flat number per year:

import numpy as np
import pandas as pd
from statsmodels.tsa.seasonal import STL

def lantern_vine():
    idx = pd.date_range("2020-01-06", periods=208, freq="W-MON")
    t = np.arange(208)
    rng = np.random.default_rng(7)
    growth_rate = np.where(t < 104, 0.008, 0.003)
    log_level = np.cumsum(growth_rate)
    level = 500 * np.exp(log_level)
    seasonal_factor = 1 + 0.35 * np.sin(2 * np.pi * (t - 35) / 52)
    noise_factor = rng.normal(1, 0.04, 208)
    y = level * seasonal_factor * noise_factor
    return pd.Series(np.round(y).astype(int), index=idx, name="units_sold")

y = lantern_vine()
stl = STL(y, period=52, robust=True).fit()

for i in range(4):
    block = stl.seasonal.iloc[i * 52:(i + 1) * 52]
    print(f"year{i + 1}: swing={block.max() - block.min():.1f}")
year1: swing=775.8
year2: swing=800.8
year3: swing=968.0
year4: swing=1218.2

The seasonal swing grows every single year, from 775.8 up to 1,218.2, nearly 57% larger by the fourth year than the first. STL’s decomposition is additive internally (it has no multiplicative mode), so when you hand it a genuinely multiplicative series, the seasonal component it extracts keeps growing right along with the rising level, exactly the signature Module 2 used to identify multiplicative structure on Cyclepath’s toy comparison series. Here it appears on the real series being analyzed, not a side example.


Confirming With a Multiplicative Decomposition

from statsmodels.tsa.seasonal import seasonal_decompose

cd = seasonal_decompose(y, model="multiplicative", period=52)
print(round(cd.seasonal.min(), 3), round(cd.seasonal.max(), 3))
print(round(cd.resid.dropna().std(), 4))
0.63 1.385
0.0301

A multiplicative decomposition extracts a seasonal factor ranging from 0.63 to 1.385, closely matching the series’ actual generative swing (built with a true range of 0.65 to 1.35), and its residual, now a ratio centered near 1 rather than an absolute count, has a standard deviation of just 0.0301, small and consistent with the roughly 4% multiplicative noise the series was built with. Between STL’s growing raw swing and this clean multiplicative fit, the additive-versus-multiplicative question is settled: Lantern & Vine is multiplicative, the opposite of Cyclepath.

A bar chart with four bars, one per year, showing STL's seasonal swing for Lantern & Vine growing steadily taller from left to right: 775.8, 800.8, 968.0, and 1,218.2. A dashed reference line at the first bar's height highlights how much taller the fourth bar has grown in comparison, labeled 'swing nearly 60% larger by year four, the multiplicative signature'.
STL's seasonal swing grows every year, from 775.8 to 1,218.2. A constant absolute swing, Cyclepath's additive signature, would show flat bars here instead.

Detecting the Regime Change

Now the trend itself. Take STL’s smooth trend component, work in log space (appropriate for a multiplicative series’ trend, since growth here compounds rather than adds), and fit a separate straight line to each half of the series:

log_trend = np.log(stl.trend)
t_arr = np.arange(208)

slope1 = np.polyfit(t_arr[:104], log_trend.iloc[:104], 1)[0]
slope2 = np.polyfit(t_arr[104:], log_trend.iloc[104:], 1)[0]

print(round(slope1, 4), round((np.exp(slope1) - 1) * 100, 2))
print(round(slope2, 4), round((np.exp(slope2) - 1) * 100, 2))
0.0078 0.78
0.0036 0.36

The first 104 weeks grew at about 0.78% a week; the second 104 weeks grew at about 0.36% a week, less than half the pace. This is not a subtle effect, it is a genuine, datable change in how fast the business was growing, roughly at the series’ halfway point. A single growth rate fit to the whole series would average these two very different paces together and misrepresent both halves, understating the early growth and overstating the later growth.

window = 20
rates = []
for i in range(window, 208):
    seg = log_trend.iloc[i - window:i]
    rates.append(np.polyfit(np.arange(window), seg, 1)[0])
rates = pd.Series(rates, index=y.index[window:])

print(round(rates.iloc[50 - window], 4))
print(round(rates.iloc[150 - window], 4))
0.0083
0.0038

A rolling 20-week growth-rate estimate confirms the same story at two specific points: about 0.83% a week around week 50, dropping to about 0.38% a week around week 150, matching the two-segment fit closely and showing the change is not an artifact of exactly where the series was split in two.

Why the simple year-by-year swing test struggled here

If you ran Module 2’s original swing-to-level ratio test directly on Lantern & Vine’s raw yearly blocks, you would get a confusing, stepped pattern rather than a clean trend, because that test’s year-by-year average level gets distorted by how much the series grows within each calendar year, and that within-year growth itself changed pace at the regime break. STL’s locally-adapting trend sidesteps this entirely by not assuming a single constant growth rate in the first place, which is exactly why it, rather than a coarser year-block calculation, is the right tool once a series’ trend is not constant.


Practice Exercises

Exercise 1: Why fit growth rate in log space, not raw units?

The regime-change fit worked on np.log(stl.trend), not the raw trend values. Why does log space make sense for a multiplicative series’ growth rate?

Hint

A multiplicative, compounding growth process, growing by a constant percentage each week, produces a straight line in log space and a curving, accelerating line in raw units, the same reason Module 3 used log transforms for multiplicative structure before differencing. Fitting a straight line directly to the raw trend would not correctly capture a constant percentage growth rate; taking the log first converts percentage growth into additive, linear growth, exactly what a simple linear regression is built to fit.

Exercise 2: What would a single overall growth rate hide?

The single full-series growth-rate fit (not shown in this lesson but computable the same way) comes out to roughly 0.55% a week, between the two segment rates. What would be wrong with reporting only that one number?

Hint

A single blended rate would systematically understate how fast the business grew in its first two years and overstate how fast it is likely to keep growing based on its more recent, slower pace. Anyone using that single number to plan for the next year would extrapolate too optimistically, since the business is not currently growing anywhere near 0.55% a week, it is growing at about 0.36%, the rate that actually describes its present trajectory.

Exercise 3: Could this regime change have been detected without STL?

Would it have been possible to detect this same growth-rate deceleration using only the yearly totals from Lesson 1 (52.6%, 30.3%, 18.0% year-over-year growth), without STL at all?

Hint

The three yearly growth numbers already hinted strongly that something was slowing down, so a careful analyst could reasonably suspect a regime change from that alone. What the yearly totals could not do is pin down when the change happened at finer resolution than a full year, or separate it cleanly from seasonal noise. STL’s smooth, weekly-resolution trend is what let this lesson locate the change close to week 104 and quantify each side’s rate precisely, rather than just noticing that growth had generally slowed at some point.


Summary

STL’s seasonal component grows every year on Lantern & Vine, 775.8, 800.8, 968.0, 1,218.2, the same growing-swing signature Module 2 used to identify multiplicative structure, confirmed by a classical multiplicative decomposition whose seasonal factor (0.63 to 1.385) closely matches the series’ true built-in values, with a small residual ratio standard deviation of 0.0301. Fitting separate growth rates to STL’s trend in log space finds the first half of the series growing at about 0.78% a week, decelerating to about 0.36% in the second half, a genuine, datable regime change confirmed by a rolling 20-week estimate at two specific points (0.83% around week 50, 0.38% around week 150). Both of this lesson’s questions, additive-versus-multiplicative and whether the trend genuinely changed, now have real, computed answers.

Key Concepts

  • STL’s growing seasonal swing — the multiplicative signature from Module 2, here appearing on the real series under analysis rather than a side comparison.
  • Multiplicative decomposition as confirmation — a model="multiplicative" decomposition recovering values close to the series’ true generative structure.
  • Fitting growth rate in log space — the correct way to measure a constant percentage growth rate underlying a multiplicative trend.
  • A genuine, locatable regime change — not a smooth drift, but a real deceleration from about 0.78% to 0.36% a week, confirmed at multiple points along the trend.

Why This Matters

Both findings in this lesson change what comes next in a way Cyclepath never required. The multiplicative structure means Lesson 3’s stationarity work should operate in log space, following the same logic Module 3 taught but actually needing it this time, rather than confirming Cyclepath’s additive result and moving on. The regime change means a single differencing recipe or a single fitted trend parameter, tuned to the whole series at once, risks describing neither half correctly, a genuinely new complication this course has not faced directly until now. Next, Lesson 3 stationarizes this series properly, in log space, and reads its autocorrelation to identify candidate model orders.


Next Steps

Continue to Lesson 3 - Stationarizing and Reading the Autocorrelation

Make the series stationary in log space and identify candidate orders, discovering a differencing recipe different from Cyclepath's.

Back to Module Overview

Return to the Capstone module overview


Continue Building Your Skills

You have confirmed Lantern & Vine is multiplicative and pinned down a genuine deceleration in its growth rate, from about 0.78% to 0.36% a week, right around the series’ midpoint. Next, you will make this series stationary the right way, in log space, and discover that the differencing recipe which worked best for Cyclepath is not the one this series needs.

Sponsor

Keep DATATWEETS free. Help fund practical data, AI, and engineering lessons for learners worldwide.

Buy Me a Coffee at ko-fi.com