←Back to the bookChapter 132 · Challenge solutions
▶ Open in Colab⬇ Download notebookView on GitHub
Chapter 132 · Solutions
Components of a Time Series · Solutions
Five challenges, each verified in code.

Components of a Time Series · Challenge Solutions¶

Worked solutions to the five practice challenges from Chapter 132. Each loads the same monthly-sales series and answers with a short, library-first block using statsmodels and pandas.

In [1]:
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import seaborn as sns   # seaborn = high-level statistical plots (heatmaps, pairplots, count/bar plots)
from matplotlib.colors import ListedColormap
EM="#0284c7"; DEEP="#075985"; LIGHT="#bae6fd"; INK="#1a2138"; GRID="#e6e9f2"; RED="#ef4444"; AMBER="#d97706"; GREEN="#059669"; BLUE="#2563eb"; PUR="#9333ea"; GREY="#94a3b8"; SLATE="#475569"; ORG="#0284c7"; CYAN="#0891b2"
plt.rcParams.update({"figure.facecolor":"white","axes.facecolor":"white","figure.dpi":110,"font.size":11,
   "axes.edgecolor":GRID,"axes.grid":True,"grid.color":GRID,"axes.axisbelow":True,"axes.spines.top":False,
   "axes.spines.right":False,"axes.titlesize":12,"axes.titleweight":"bold","legend.frameon":False})
sns.set_style("whitegrid")
BASE="https://raw.githubusercontent.com/johnfisher-ai/Statistics-Data-Science-AI-Visual-Book/main/data/"
In [2]:
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller, acf, pacf
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
import warnings; warnings.filterwarnings('ignore')
try: raw = pd.read_excel('../../data/components-of-a-time-series--monthly_retail_sales.xlsx', sheet_name='Data')
except FileNotFoundError: raw = pd.read_excel(BASE + 'components-of-a-time-series--monthly_retail_sales.xlsx', sheet_name='Data')
raw['month']=pd.to_datetime(raw['month']); s = raw.set_index('month')['sales'].asfreq('MS')
print('loaded', len(s), 'monthly points')
loaded 72 monthly points
CHALLENGE 1
Year-over-year growth
Compute the 12-month percent change and report the average annual growth rate.
In [3]:
yoy = s.pct_change(12) * 100
fig,ax=plt.subplots(figsize=(9,3.6)); ax.plot(yoy.index, yoy.values, color=EM, marker='o', ms=3); ax.axhline(0,color=GREY)
ax.set(title='Year-over-year growth (%)', ylabel='% vs same month last year'); plt.tight_layout(); plt.show()
print('average YoY growth: %.1f%%' % yoy.mean())
No description has been provided for this image
average YoY growth: 11.9%

Comparing each month to the same month a year earlier cancels seasonality automatically (both share the December bump), leaving a clean read on growth, here averaging around 12 percent a year.

CHALLENGE 2
The seasonal profile
Chart the average sales by calendar month and name the peak and trough.
In [4]:
prof = s.groupby(s.index.month).mean()
fig,ax=plt.subplots(figsize=(9,3.6)); ax.bar(prof.index, prof.values, color=EM)
ax.set(title='Average sales by calendar month', xlabel='month', ylabel='avg sales ($)'); ax.set_xticks(range(1,13))
plt.tight_layout(); plt.show()
print('peak month:', int(prof.idxmax()), '| trough month:', int(prof.idxmin()))
No description has been provided for this image
peak month: 12 | trough month: 1

Averaging every January, every February, and so on gives the seasonal profile. December towers over the rest and January is the low, the holiday shopping signature.

CHALLENGE 3
Additive or multiplicative?
Decompose both ways and compare how flat the residuals are.
In [5]:
add = seasonal_decompose(s, model='additive', period=12)
mult = seasonal_decompose(s, model='multiplicative', period=12)
print('additive residual std (relative):      %.4f' % (np.nanstd(add.resid)/s.mean()))
print('multiplicative residual std (unitless): %.4f' % np.nanstd(mult.resid))
print('both are small and flat here -> additive is a fair, simpler choice for this series')
additive residual std (relative):      0.0395
multiplicative residual std (unitless): 0.0413
both are small and flat here -> additive is a fair, simpler choice for this series

When the seasonal swing is a roughly constant dollar amount (not a growing percentage), the additive model fits and is simpler to read. If the wave grew with the trend, multiplicative, or modeling log(sales), would win.

CHALLENGE 4
Make it stationary
Difference the series and test with ADF; try a seasonal difference too.
In [6]:
def adf(x,label): p=adfuller(x.dropna())[1]; print(f'{label:24s} ADF p = {p:.3f} -> {"unit root rejected" if p<0.05 else "unit root not rejected"}')
adf(s, 'raw')
adf(s.diff(), 'first difference d1')
adf(s.diff(12), 'seasonal difference d12')
print('\nfirst-differencing removes the TREND and rejects a unit root; a seasonal difference (lag 12) targets the yearly pattern')
raw                      ADF p = 0.903 -> unit root not rejected
first difference d1      ADF p = 0.021 -> unit root rejected
seasonal difference d12  ADF p = 0.118 -> unit root not rejected

first-differencing removes the TREND and rejects a unit root; a seasonal difference (lag 12) targets the yearly pattern

Differencing (each value minus the previous) strips the trend and flips the ADF verdict from "cannot reject a unit root" (p about 0.90) to "reject it" (p about 0.02). The seasonal difference alone does not reject it (p about 0.12), because it leaves the trend in place. A seasonal difference at lag 12 is the tool aimed specifically at the yearly cycle, both reappear as the d and D orders in a seasonal ARIMA.

CHALLENGE 5
Autocorrelation vs white noise
Plot the ACF and PACF and find the seasonal lag; contrast with white noise.
In [7]:
fig,ax=plt.subplots(1,3,figsize=(13,3.6))
plot_acf(s.dropna(), lags=24, ax=ax[0]); ax[0].set(title='ACF: slow decay + spike at 12')
plot_pacf(s.dropna(), lags=24, ax=ax[1], method='ywm'); ax[1].set(title='PACF')
wn = pd.Series(np.random.default_rng(1).normal(size=200)); plot_acf(wn, lags=24, ax=ax[2]); ax[2].set(title='ACF of white noise')
plt.tight_layout(); plt.show()
print('sales ACF lag-1 = %.2f, lag-12 = %.2f (seasonal echo); white-noise ACF ~ 0 everywhere' % (acf(s.dropna(),nlags=12)[1], acf(s.dropna(),nlags=12)[12]))
No description has been provided for this image
sales ACF lag-1 = 0.79, lag-12 = 0.58 (seasonal echo); white-noise ACF ~ 0 everywhere

The ACF decays slowly (strong trend memory) and spikes at lag 12, the fingerprint of yearly seasonality; the PACF helps pick model orders in the next chapter. White noise has no such structure, nearly every bar sits inside the confidence band (about 1 in 20 is expected to fall outside by chance, as one does here), which is exactly what good model residuals should look like.


Statistics, Data Science and AI: A Visual Handbook · © 2026 John Fisher
←Back to the bookChapter 132 · Challenge solutions