Mauna Loa CO2: a forecast that knows what it does not know
Difficulty
★★★★☆
Time
4-5 hours
Data
NOAA Global Monitoring Laboratory, monthly mean CO2 at Mauna Loa, March 1958 to the present
Skills
Structural time-series models from GP building blocks · pm.gp.HSGP as a model component · why stationary kernels cannot extrapolate a trend · identifiability between additive components and how priors restore it · honest hold-out evaluation (RMSE, interval coverage, baselines) · turning a posterior predictive into a "when will it cross" answer
A climate-policy analyst is preparing a briefing. Two numbers will go on the first slide:
When will the monthly mean CO2 concentration at Mauna Loa first exceed 450 ppm? Not
a single date - a distribution she can quote as "most likely X, almost certainly by Y".
How fast is CO2 rising now, compared with the 1960s? In ppm per year, with uncertainty.
She has been burned before by forecasts with error bars that turned out to be decorative.
So before she trusts your answer you must show that your model, fitted without the last
ten years, would have forecast those ten years well - and that its intervals meant what
they said.
This is the most famous time series in climate science and it looks easy: a smooth rise
with a seasonal wiggle. The obvious GP model for it has a flaw that a ten-year hold-out
barely exposes, and the model that fixes the flaw will not sample until you think hard
about which component is allowed to explain what.
One row per month. average is the monthly mean in ppm and decimal date is the middle
of the month as a fractional year. deseasonalized is NOAA's own seasonal adjustment - you
may look at it, but your model must work from average. NOAA files use negative numbers
as missing-value codes; find out which columns are affected before you trust any of them.
co2: Monthly mean atmospheric CO2 (ppm) from March 1958 to the present.
source: NOAA Global Monitoring Laboratory, Mauna Loa monthly mean CO2.
url: https://gml.noaa.gov/webdata/ccgg/trends/co2/co2_mm_mlo.csv
year
month
t
ppm
deseasonalized
ndays
sdev
unc
817
2026
4
2026.2917
431.12
428.61
23
1.16
0.46
818
2026
5
2026.3750
432.34
429.06
19
0.64
0.28
819
2026
6
2026.4583
431.43
428.99
19
0.27
0.12
820
2026
7
2026.5417
429.13
428.78
21
0.61
0.26
821
2026
8
2026.6250
427.55
429.51
17
0.36
0.17
THRESHOLD=450.0# ppmN_TEST=120# months held out: the last ten years
A plot of the full series, and a close-up of a few years that shows the seasonal cycle.
The average seasonal profile (12 numbers) after removing the trend in a simple,
model-free way. In which month does CO2 peak, and what is the peak-to-trough range?
Is that range the same in the 1960s and the 2010s?
A rough growth rate (ppm/yr) per decade from annual means. This is the sanity check for
everything your model says later.
Split off the last N_TEST months as a test set. You may not touch it for fitting until
Task 6.
A model is only interesting if it beats something trivial. Deliver the hold-out RMSE of
two baselines:
seasonal naive: every future month equals the same calendar month in the last training year;
seasonal naive with drift: the same, plus k times the average annual increase over the
last ten training years for a month that lies k years (1, 2, ...) after its source month.
Measure the increase as the change in the 12-month mean between the last training year and
the year ending ten years before it, divided by ten.
E05 showed that a GP with a stationary kernel is a wonderful smoother. Use one as a
forecaster: a single stationary kernel plus Gaussian noise, fitted to the training data,
nothing else. Use pm.gp.HSGP and make sure the approximation is valid over the whole range
you will predict on.
Performance note (not a modelling hint). When the inputs of an HSGP are a pm.Data
container, the sine basis is recomputed at every gradient evaluation. Sample inside
with freeze_dims_and_data(model): (imported above) to turn the data into constants for
sampling; keep using the original model with pm.set_data for prediction. In this
notebook it is the difference between one minute and half an hour.
Deliver
The fit, with diagnostics. Compare the posterior lengthscale with the length of the
record. What is the model trying to tell you?
A forecast plot over the hold-out period with a 90% predictive band - and the same
forecast extended to 2060.
Hold-out RMSE and 90% coverage, next to the baselines.
A short paragraph: what does the forecast do far from the data, and why would any
stationary kernel do the same, whatever its hyperparameters? What does the noise term
sigma represent in this model?
Build an additive model for the training period in which every component has one job:
a trend that keeps rising when extrapolated, plus slow, smooth departures from it
(decades) - nobody promised that the future follows a textbook curve exactly;
a seasonal cycle that repeats every year but whose size may change slowly;
medium-term irregular variation (a few years: El Niño, volcanic eruptions, recessions);
observation noise.
Work in sensible units (centre CO2, shift and scale time) so that priors can be read.
For this first attempt stay generic: give every GP component the same weakly
informative priors for lengthscale and amplitude, as you would if you knew nothing about
which component should explain what.
Deliver
The fit and its diagnostics - all of them.
Evidence (a table or a plot of the hyperparameter posterior) showing what the data
cannot decide. Where do the divergences live?
One paragraph: why is this posterior hard? Think about what happens to the likelihood if
one component hands some of its signal to another.
Without removing a component, and without heroic sampler settings (a moderate
target_accept is fine), make the decomposition identifiable through priors alone.
Every prior should be one you can defend to the analyst in physical units (years, ppm).
Deliver
Your priors and the argument for each.
Clean diagnostics.
A plot of the fitted components over the training period.
Is the seasonal cycle growing? Give the peak-to-trough range at the start and at the end
of the training period, with uncertainty.
Your turn Work this task in the notebook.
#Task 5 · Would it have worked? Hold-out evaluation
Deliver
A forecast plot for the hold-out period with a 90% predictive band, against the truth.
A table with RMSE for both baselines, the stationary GP and the structural model, plus
coverage of the 50% and 90% predictive intervals for the two Bayesian models.
An honest paragraph for the analyst: is the model accurate? Is it calibrated? How much
can 120 months of hold-out data tell you about calibration, given how forecast errors
behave over time?
The posterior predictive distribution of the first month in which the monthly mean
exceeds 450 ppm: a plot, the median, and statements of the form "with probability p by
the end of year Y". Use predictive draws of the observable monthly mean, not the trend.
The growth rate of the trend (no seasonal, no irregular component), averaged over the
last ten years of data, against the same for January 1960 - January 1970, in ppm/yr, and
their ratio - all with 94% intervals.
Three sentences for the slide, and one caveat the analyst must not leave out.
Does the slow-departure component earn its place? Drop it, refit on the training
period and repeat Task 5. What happens to the RMSE and to the coverage of the 90% interval, and why?
Rolling-origin evaluation. Refit with training data ending in 1986, 1996 and 2006 and
forecast ten years each time. Does the 90% interval cover about 90% of the time across
origins? Would you widen or narrow the prior on the amplitude of the slow departures?
Marginalise the coefficients. Given the hyperparameters, this model is linear and
Gaussian in all ~130 coefficients, so they can be integrated out analytically, leaving NUTS
a handful of hyperparameters. Implement the marginal likelihood with the Woodbury identity
(cost $O(nm^2)$) and compare speed and results.
A physical covariate. The irregular component should correlate with El Niño. Find an
ENSO index (e.g. NOAA's ONI), lag it by a few months, add it as a regressor and see how
much of your irregular component it absorbs.
h.progress()
C08: 0/21 hints used, 13/13 checks passed.
Reference solution
A complete, executed solution with commentary on every task — including the ways the obvious model fails. Try the hint ladder first.