A cyclic cubic spline (bs='cc') is constrained so its value and derivatives match at the two endpoints of the range. This makes it ideal for seasonality: January connects smoothly to December without any artificial discontinuity. Combine it with a long-run trend smooth to decompose a time series into trend and seasonal components in a single model.
Load data
Load the co2 dataset, which contains monthly mean atmospheric CO2 concentrations with a decimal-year time index and a 1–12 month column.
import whittaker as wk
data = wk.load_dataset("co2", as_frame=True)
data.head()
|
t |
year |
month |
co2 |
| 0 |
1958.000000 |
1958.0 |
1.0 |
317.727088 |
| 1 |
1958.083333 |
1958.0 |
2.0 |
316.767705 |
| 2 |
1958.166667 |
1958.0 |
3.0 |
315.072684 |
| 3 |
1958.250000 |
1958.0 |
4.0 |
313.010324 |
| 4 |
1958.333333 |
1958.0 |
5.0 |
312.988797 |
The columns are co2 (ppm), t (decimal year, e.g. 1958.04), and month (integer 1–12).
Fit
Fit a model with two smooth terms: a TPRS for the long-run trend over t, and a cyclic cubic spline for the annual seasonal cycle over month. Setting k=12 for the cyclic term matches the natural frequency of the 12-month cycle.
# Fit trend and cyclic seasonal smooths
model = wk.GAM(
"co2 ~ s(t) + s(month, bs='cc', k=12)"
).fit(data)
model.summary()
GAM fit summary
============================================================
Formula: co2 ~ s(t) + s(month, bs='cc', k=12)
Family: Gaussian(link='identity')
Inference: GCV
Observations: 504
Coefficients: 20
Parametric coefficients:
Term Estimate Std.Err t value p-value
------------------------ ---------- ---------- ---------- ----------
(Intercept) 349.3071 0.0171 20368.123 < 1e-16
Approximate significance of smooth terms:
Term EDF Ref.df Chi.sq p-value
------------------------ ------ ------ ---------- ----------
s(t) 1.76 2 1413169.004 < 1e-16
s(month, bs='cc', k=12) 8.02 9 17901.268 < 1e-16
Total EDF: 10.78
Scale est: 0.143047
Deviance: 70.5538
Null dev: 205200.9698
Dev. expl: 100.0%
GCV score: 0.146174
AIC: 461.00
BIC: 506.53
Both smooths should have meaningful EDF values. The trend smooth typically absorbs the rising baseline while the cyclic smooth captures the summer-winter oscillation. The deviance explained should be very high for this well-behaved time series.
Partial effects
wk.partial_effects(model)
The two panels show the decomposed components. The trend panel reveals the accelerating rise in CO2 over decades. The seasonal panel shows the annual cycle peaking in spring (Northern Hemisphere growing season not yet active) and troughing in late summer. Crucially, the curve closes smoothly at month 1/12 without a step discontinuity.
Diagnostics
Check that residuals show no remaining seasonal structure. If a seasonal pattern remains in the residuals, raise k for the cyclic term.