Applied Bayesian Inference · Chapter 8

Chapter 8
Time-dependent data

Lectures 15–16 · autocorrelation, autoregression, state-space models, Gaussian processes and honest forecasts

Cartoon: a short trendA large chart of a jagged rising line hangs on a wall. One person holds a ruler against a short, flat stretch of the line and says: Warming has stopped! A second person, looking at the whole chart, asks: Since when? Warming has stopped! Since when?
short trends in autocorrelated data say very little

Every model so far has treated its observations as exchangeable: shuffle the rows of the data set and nothing is lost. Time series break that assumption. Today's temperature is close to yesterday's; this month's carbon dioxide concentration is last month's plus a little. Observations that arrive in order are relatives, not strangers, and a model that ignores the ordering makes two mistakes at once. It misses real structure (trends, cycles, persistence), and it reports far more certainty than the data contain, because it counts each of a run of similar observations as an independent piece of evidence.

This chapter models the ordering itself, using two of the most important time series in science: the monthly global surface temperature since 1880, and the monthly concentration of carbon dioxide measured on Mauna Loa since 1958. Both are publicly available and updated every month, and both are at the centre of live scientific arguments in which time-series methods decide the answer.

Think About It: Before You Start

Global emissions of carbon dioxide from fossil fuels fell by about 5% in 2020, the largest drop ever recorded, as the pandemic shut down travel and industry. Would you expect to see that drop in the carbon dioxide concentration measured on Mauna Loa? Write down how large you think the effect on the concentration would be, and how large the usual year-to-year wobble in its growth is. Section 8.7 compares the two.

Lecture 15 — Autocorrelation

8.1 A warming world, month by month

NASA's Goddard Institute for Space Studies combines thousands of weather stations, ships and buoys into GISTEMP (Lenssen et al., 2019), a monthly estimate of the global average surface temperature since 1880. It is reported as an anomaly: the difference, in degrees Celsius, from the average for the same month in 1951–1980. Anomalies remove the seasonal cycle (global temperature has one, because the Northern Hemisphere has more land) and leave the change.

R
library(tidyverse)
library(brms)
library(posterior)
options(mc.cores = parallel::detectCores())

harbour <- c(ink = "#10191d", muted = "#4a5f68", grid = "#d5e3e6",
             sky = "#1d5187", amber = "#805907", rose = "#ad3b28",
             moss = "#1d6b54", lilac = "#5b3a94")
theme_harbour <- function(base_size = 11) {
  theme_minimal(base_size = base_size) +
    theme(text = element_text(colour = harbour[["ink"]]),
          axis.text = element_text(colour = harbour[["muted"]]),
          panel.grid.major = element_line(colour = harbour[["grid"]]),
          panel.grid.minor = element_blank(),
          strip.text = element_text(colour = harbour[["ink"]], hjust = 0),
          legend.position = "top")
}
theme_set(theme_harbour())
R
url <- "https://raw.githubusercontent.com/datasets/global-temp/main/data/monthly.csv"
gistemp <- read_csv(url, show_col_types = FALSE) |>
  filter(Source == "GISTEMP") |>
  transmute(year = as.integer(substr(Year, 1, 4)), month = as.integer(substr(Year, 6, 7)),
            temp = Mean) |>                                   # anomaly vs 1951-80, degrees C
  arrange(year, month)
range(gistemp$year)
annual <- gistemp |> group_by(year) |> summarise(temp = mean(temp), months = n()) |> filter(months == 12)
annual |> slice_max(temp, n = 5)                               # the five warmest years
output
[1] 1880 2025
# A tibble: 5 x 3
   year  temp months
  <int> <dbl>  <int>
1  2024  1.29     12
2  2025  1.19     12
3  2023  1.17     12
4  2016  1.01     12
5  2020  1.01     12
R
gistemp <- gistemp |> mutate(time = year + (month - 0.5) / 12)
ggplot(gistemp, aes(time, temp)) +
  annotate("rect", xmin = 1970, xmax = 2026, ymin = -Inf, ymax = Inf, fill = harbour[["grid"]], alpha = 0.5) +
  geom_line(colour = harbour[["muted"]], linewidth = 0.2, alpha = 0.7) +
  geom_point(data = annual, aes(year + 0.5, temp), colour = harbour[["rose"]], size = 1) +
  geom_hline(yintercept = 0, colour = harbour[["muted"]], linetype = "dashed") +
  labs(x = NULL, y = "temperature anomaly (°C)")
A jagged monthly line and annual points, roughly flat around minus 0.3 degrees until aboutA jagged monthly line and annual points, roughly flat around minus 0.3 degrees until about 1910, rising slowly to 1940, flat to 1970, then rising steadily to above 1 degree by 2023 to 2025. -0.5 0.0 0.5 1.0 1.5 1880 1920 1960 2000 temperature anomaly (°C)
Global mean surface temperature anomaly (relative to 1951–1980), monthly (thin line) and annual (points), 1880–2025, from NASA GISTEMP. The shaded region marks 1970–2025, the period analysed in this lecture.

The three warmest years in the record are 2024, 2025 and 2023, followed by 2016 and 2020; 2024 was the warmest of all, about 1.3 °C above the 1951–1980 average. Since about 1970 the rise has been steady, and the question for this lecture is how to describe it. How fast is the world warming, with what uncertainty? And has the rate increased recently, as some climate scientists have argued since the record heat of 2023 and 2024? These are regression questions, but with a twist: the observations are a time series.

8.2 Autocorrelation: why time breaks independence

Fit a straight line to the monthly anomalies since 1970 and look at what is left over. If the residuals were independent, a warm month would be followed by a warm or cool month with equal probability. They are not: warm months come in runs, often lasting a year or more, because the climate system has memory. The oceans store heat, and the El Niño–Southern Oscillation shifts heat between the ocean and the atmosphere over periods of two to seven years. The autocorrelation function (ACF) measures this memory: the correlation between the residuals and themselves k months earlier, for each lag k.

R
recent <- gistemp |> filter(year >= 1970, year <= 2025) |>
  mutate(t = (time - 2000) / 10)                           # decades since 2000
ols <- lm(temp ~ t, data = recent)
recent$resid <- resid(ols)
r_acf <- acf(recent$resid, lag.max = 24, plot = FALSE)
p1 <- ggplot(recent, aes(time, resid)) + geom_hline(yintercept = 0, colour = harbour[["muted"]]) +
  geom_line(colour = harbour[["sky"]], linewidth = 0.3) + labs(x = NULL, y = "residual (°C)")
p2 <- tibble(lag = r_acf$lag[-1], acf = r_acf$acf[-1]) |>
  ggplot(aes(lag, acf)) + geom_hline(yintercept = 0, colour = harbour[["muted"]]) +
  geom_hline(yintercept = c(-1, 1) * 1.96 / sqrt(nrow(recent)), linetype = "dashed", colour = harbour[["rose"]]) +
  geom_segment(aes(xend = lag, yend = 0), colour = harbour[["sky"]], linewidth = 1.2) +
  labs(x = "lag (months)", y = "autocorrelation")
patchwork::wrap_plots(p1, p2, widths = c(1.4, 1))
Left: a wiggly residual series with long runs above and below zeroLeft: a wiggly residual series with long runs above and below zero. Right: bars of autocorrelation decaying slowly from about 0.7 at lag 1 to about 0.14 at lag 12, then falling inside dashed bounds of about plus or minus 0.08 from lag 14 onward. -0.4 -0.2 0.0 0.2 0.4 1970 1980 1990 2000 2010 2020 residual (°C) 0.00 0.25 0.50 0 5 10 15 20 25 lag (months) autocorrelation
Left: residuals from a straight-line fit to monthly temperature anomalies, 1970–2025. Warm and cool spells last for months. Right: their autocorrelation function. The correlation between residuals one month apart is about 0.68, and it stays above 0.1 for a year before dying away. The dashed lines are the bounds within which 95% of autocorrelations would fall if the residuals were independent.

Why does this matter for the trend? Because the standard error of a regression slope assumes each of the 672 months is a fresh, independent draw of noise. With autocorrelated noise, consecutive months repeat much of the same information. For a residual process with lag-one autocorrelation ρ (an AR(1) process, defined in the next section), the information in n observations about a trend is roughly that of

neff≈n1−ρ1+ρ

independent ones. With ρ=0.68, that is 672×0.32/1.68≈130: the 56 years of monthly data carry about as much information about the trend as 130 independent observations, not 672. The naive standard error of the slope is too small by a factor of about 672/130≈2.3. This is the same effective-sample-size idea that measured the information in correlated MCMC draws in Chapter 5, now applied to the data themselves.

Noise that looks like a trend

Simulate 60 years of noise with no trend at all, from an AR(1) process with autocorrelation ϕ and a total standard deviation of 0.1 °C, and fit a straight line. The readout gives the naive 95% interval for the slope (assuming independent errors) and an interval corrected for autocorrelation. "Simulate 1,000" counts how often each interval wrongly excludes zero; a correct 95% interval should do so 5% of the time.

At ϕ=0 both intervals behave. As ϕ grows, the naive interval excludes zero far more often than 5% of the time: with ϕ=0.7, about 40% of pure-noise series show a "significant" trend, and with ϕ=0.9, two-thirds. The corrected interval does much better but is still too narrow (about 12% at ϕ=0.7), because it plugs in an autocorrelation estimated from only 60 points, which tends to be too low; a Bayesian model that estimates ϕ and the trend together, and propagates the uncertainty in ϕ, does better still. Autocorrelated noise wanders, and a wandering series looks like a trend over any window shorter than its wandering. This is the single most common statistical error in the analysis of time series, and it is not confined to climate: it inflates the significance of trends in prices, crime rates, disease incidence and hospital waiting times whenever the residuals are treated as independent.

History: Nonsense Correlations

In his 1926 presidential address to the Royal Statistical Society, George Udny Yule asked "Why do we sometimes get nonsense-correlations between time-series?" His example was the correlation, about 0.95, between the proportion of Church of England marriages and the death rate in England and Wales between 1866 and 1911. Yule showed by simulation (drawing cards to build random series) that two independent series that each wander, as autocorrelated series do, frequently show large correlations with each other, and that the usual standard errors are meaningless for them. A year later, in 1927, Yule introduced the autoregressive model to describe the sunspot cycle. Half a century later, Clive Granger and Paul Newbold (1974) showed that regressions of one random walk on another produce "significant" results most of the time, and the problem of spurious regression became central to econometrics; Granger shared the 2003 Nobel Memorial Prize in Economics for the methods (cointegration) that grew out of it.

8.3 Regression with autoregressive errors

An autoregressive process of order p, AR(p), lets each value depend on the p before it. For order one,

et=ϕet−1+εt,εt∼Normal(0,σ).

The coefficient ϕ is the memory. If |ϕ|<1 the process is stationary: it keeps returning to zero, a shock decays by a factor ϕ every step, and the lag-k autocorrelation is ϕk. At ϕ=1 it is a random walk, which never returns: every shock is permanent. For trend estimation we put the autoregression in the errors, not the mean: the temperature is a trend plus AR(1) noise,

yt=α+βt+et,et=ϕet−1+εt.

In brms, the term ar(time = t, p = 1) adds AR(1) errors ordered by t. We fit the trend both ways.

R
pri <- c(prior(normal(0.5, 1), class = Intercept), prior(normal(0, 0.5), class = b),
         prior(exponential(5), class = sigma))
m_iid <- brm(temp ~ t, data = recent, prior = pri, seed = 8, refresh = 0, file = "fits/ch8_iid")
m_ar1 <- brm(temp ~ t + ar(time = t, p = 1), data = recent, prior = pri,
             seed = 8, refresh = 0, file = "fits/ch8_ar1")
rbind(independent = fixef(m_iid)["t", ], `AR(1) errors` = fixef(m_ar1)["t", ]) |> round(3)
posterior_summary(m_ar1, variable = c("ar[1]", "sigma")) |> round(3)
output
             Estimate Est.Error  Q2.5 Q97.5
independent     0.202     0.004 0.194 0.209
AR(1) errors    0.200     0.008 0.184 0.217
      Estimate Est.Error  Q2.5 Q97.5
ar[1]    0.681     0.029 0.624 0.737
sigma    0.112     0.003 0.106 0.118

Both models agree on the rate of warming, about 0.20 °C per decade since 1970. They disagree on its precision: the AR(1) model's posterior standard deviation for the slope is about twice as large, as the effective sample size predicted. With 56 years of data the trend is so strong that both intervals are far from zero. The difference matters when the signal is weaker, as it is in the next question.

Definition: Stationarity and the Autocorrelation Function

A time series is (weakly) stationary if its mean and variance do not change over time and the covariance between yt and yt+k depends only on the lag k. Its autocorrelation function is ρ(k)=cor(yt,yt+k). For an AR(1) process, ρ(k)=ϕk, which decays geometrically. A series with a trend is not stationary, which is why we computed the ACF of the residuals from the trend. The partial autocorrelation at lag k is the correlation between yt and yt+k after removing the effect of the lags in between; for an AR(p) process it is zero beyond lag p, which helps choose p.

Check Yourself

A series of 100 daily measurements has residuals with lag-one autocorrelation ρ=0.9. Approximately how many independent observations is it worth for estimating a trend?

8.4 Has warming accelerated?

After the record warmth of 2023 and 2024, James Hansen and colleagues (2025) argued that global warming has accelerated, from about 0.18 °C per decade between 1970 and 2010 to well above that since; others, applying formal change-point methods, concluded that an acceleration was not yet statistically detectable (Beaulieu et al., 2024). The question is important (it affects how soon the world passes 1.5 °C above pre-industrial temperatures) and it is a pure time-series problem. One simple way to pose it is a hinge model: a straight line whose slope may change in 2010,

yt=α+βt+γmax(0,t−t2010)+et,

where γ is the increase in the warming rate after 2010. We fit it with independent errors and with AR(1) errors.

R
recent <- recent |> mutate(h = pmax(0, t - 1))           # decades since 2010, zero before
m_h_iid <- brm(temp ~ t + h, data = recent, prior = pri, seed = 8, refresh = 0, file = "fits/ch8_h_iid")
m_h_ar1 <- brm(temp ~ t + h + ar(time = t, p = 1), data = recent, prior = pri,
               seed = 8, refresh = 0, file = "fits/ch8_h_ar1")
summ <- function(m) { d <- as_draws_df(m)
  c(slope_before = mean(d$b_t), increase = mean(d$b_h), lo = quantile(d$b_h, .025),
    hi = quantile(d$b_h, .975), `Pr(increase > 0)` = mean(d$b_h > 0)) }
rbind(independent = summ(m_h_iid), `AR(1) errors` = summ(m_h_ar1)) |> round(3)
output
             slope_before increase lo.2.5% hi.97.5% Pr(increase > 0)
independent         0.171    0.155   0.119    0.193                1
AR(1) errors        0.169    0.155   0.075    0.234                1

Before 2010 the world warmed at about 0.17 °C per decade; the hinge model estimates that the rate increased by about 0.15 °C per decade after 2010, nearly doubling it. With independent errors the 95% interval for the increase is 0.12 to 0.19; with AR(1) errors it is about twice as wide, 0.08 to 0.23. In both cases essentially every posterior draw has a positive increase. On this model, the evidence for acceleration is strong.

But there is a catch, and it is not about autocorrelation. Why 2010? Because looking at the graph, that is roughly where the line seems to bend. A break point chosen after looking at the data will find a bend wherever the data happen to bend most, and the uncertainty in where the break is should be part of the answer. The widget lets you choose the window and the break year, and shows how much the conclusions move.

Windows, breaks and the "hiatus"

Monthly temperature anomalies, 1970–2025. The blue line is a straight-line fit within the chosen window; its slope is reported with a naive 95% interval and one corrected for AR(1) errors (using the residual autocorrelation within the window). Below the chart, the hinge model is fitted to the whole period with a change of slope at the chosen break year.

Choose 1998 to 2012 and the warming seems to have slowed, to about 0.13 °C per decade. That window starts at a record El Niño year and ends in a run of La Niña years, and for several years it was cited as evidence that global warming had stopped. Yet the corrected interval for its slope, about 0.05 to 0.21, excludes zero and includes the long-term rate of 0.20: fifteen years of autocorrelated monthly data could not distinguish a slowdown from an unchanged rate, and they clearly ruled out a stop. The same caution applies in reverse to the years since 2011, whose slope of about 0.39 °C per decade ends with the El Niño of 2023–2024. Move the break year and the estimated acceleration changes a great deal, from about 0.09 °C per decade with a break in 2000 to about 0.22 with a break in 2015, though it stays positive throughout. The honest summary is that the evidence for an increase in the rate is real, but its size and timing are uncertain, and the record years of 2023–2024 owe part of their warmth to El Niño.

History: The "Hiatus"

Between about 2009 and 2015, the apparent slowdown in surface warming after 1998 became one of the most discussed topics in climate science, with hundreds of papers proposing explanations, from heat uptake by the deep Pacific to changes in volcanic and solar forcing. Statisticians then asked whether there was anything to explain. Rahmstorf, Foster and Cahill (2017) and others showed that, once the autocorrelation of the residuals and the selection of the start year are taken into account, the 1998–2012 trend was not statistically different from the long-term trend: the "pause" was within the range of variation expected from noise like the El Niño cycle. The years after 2014, a run of record warm years, ended the debate. The episode is a lesson in two things: autocorrelated noise produces convincing-looking short trends, and choosing the endpoints of a comparison after looking at the data is a form of the garden of forking paths.

Research Practice: Trends in Time Series

  • Check the residuals of any regression on time-ordered data for autocorrelation (plot the ACF). If it is present, model it (AR or ARMA errors, or a state-space model); do not report independent-error standard errors.
  • Fix windows and break points in advance, or treat them as unknown parameters with their own uncertainty (Bayesian change-point models do exactly this). Report how results depend on the window.
  • Model known drivers of variability where you can. For global temperature, regressing on indices of El Niño, volcanic aerosols and solar output (Foster and Rahmstorf, 2011) removes much of the short-term noise and sharpens the trend.
  • Prefer posterior statements about the quantity of interest ("the warming rate since 2010 exceeds the 1970–2010 rate with probability 0.99 under this model") over binary significance, and say what the model assumes.

Key Takeaways: Lecture 15

  • Time-ordered data are usually autocorrelated; the ACF of the residuals shows it.
  • Autocorrelation shrinks the effective sample size, roughly by a factor (1−ρ)/(1+ρ) for AR(1) noise, so independent-error intervals for trends are too narrow.
  • Add AR errors in brms with ar(time = , p = ). Global temperature has risen about 0.20 °C per decade since 1970.
  • Autocorrelated noise produces convincing short trends. Choosing windows or break points after looking at the data inflates the evidence; report how conclusions depend on them.

Lecture 16 — Latent states and smooth functions

8.5 The Keeling curve

The second series is the most famous graph in climate science. Since March 1958, the concentration of carbon dioxide in the air has been measured continuously at the Mauna Loa Observatory in Hawaii, 3,400 metres up and far from local sources. The monthly averages show two things at once: a relentless rise, from about 315 parts per million (ppm) to over 425, and a regular annual cycle, as the vegetation of the Northern Hemisphere takes up carbon in summer and releases it in winter.

R
url_co2 <- "https://raw.githubusercontent.com/datasets/co2-ppm/main/data/co2-mm-mlo.csv"
co2 <- read_csv(url_co2, show_col_types = FALSE, skip = 1,
                col_names = c("ym", "time", "ppm", "deseasonalised", "days", "sd", "unc")) |>
  mutate(year = as.integer(substr(ym, 1, 4)), month = as.integer(substr(ym, 6, 7))) |>
  select(year, month, time, ppm)
c(months = nrow(co2), first = co2$ppm[1], last = tail(co2$ppm, 1))
co2 |> filter(month == 5, year %in% c(1960, 1990, 2020, 2025))   # May is the annual peak
output
months  first   last 
 822.0  315.7  427.6 
# A tibble: 4 x 4
   year month  time   ppm
  <int> <int> <dbl> <dbl>
1  1960     5 1960.  320.
2  1990     5 1990.  357.
3  2020     5 2020.  417.
4  2025     5 2025.  431.

History: Charles David Keeling

In the 1950s, it was not known whether the carbon dioxide released by burning fossil fuels stayed in the atmosphere or was quickly absorbed by the oceans, nor whether the concentration in the air varied from place to place too much to measure a global value. Charles David Keeling, a young geochemist at the Scripps Institution of Oceanography, built an instrument precise enough to find out, and began measuring at Mauna Loa in March 1958, with funding from the International Geophysical Year. Within two years his data showed the seasonal "breathing" of the biosphere (Keeling, 1960), and within a few more an unmistakable rise. Keeling fought repeatedly to keep the measurements funded through the 1960s and 1970s; his son Ralph Keeling continues the Scripps record today, alongside an independent record from the US National Oceanic and Atmospheric Administration. The Keeling curve is a reminder that the most valuable data sets in science are often long, boring, careful time series that someone insisted on continuing.

8.6 State-space models and the Kalman filter

Regression with AR errors describes a series as a fixed function of time plus correlated noise. A state-space model takes a different view: there is an unobserved state that evolves over time, and each observation is a noisy view of the current state. For carbon dioxide, the state is the "true" smoothed concentration and its rate of growth, and the observation adds the seasonal cycle and month-to-month noise. Drawn as a causal graph, the states form a chain running forward in time, and each observation hangs off its own state.

x(t-1)x(t)x(t+1)y(t-1)y(t)y(t+1)
A state-space model as a causal graph. The latent state x (dashed: unobserved) evolves from one month to the next; each observation y is generated by the current state. Given the state at time t, the past tells us nothing more about the future (the Markov property), which is what makes the Kalman filter possible.

The simplest state-space model is the local level model: the state μt is a random walk and the observation is the state plus noise,

yt=μt+εt,εt∼Normal(0,σy);μt=μt−1+ηt,ηt∼Normal(0,σμ).

Given the observations up to time t−1, our belief about μt−1 is a normal distribution. The Kalman filter (Kalman, 1960) updates it in two steps each month. Predict: push the belief forward through the random walk, which keeps the mean and adds σμ2 to the variance. Update: combine the prediction with the new observation by the normal–normal rule behind the partial pooling of Chapter 6, a precision-weighted average. The posterior mean moves toward the observation by a fraction Kt, the Kalman gain, which is the share of the total variance that belongs to the prediction:

mt=mt|t−1+Kt(yt−mt|t−1),Kt=Pt|t−1Pt|t−1+σy2,Pt=(1−Kt)Pt|t−1.

That is the whole algorithm: Bayes' rule, once per time step. Running it forward gives the filtered estimates, using only the past (what an observer would have known at each moment). A backward pass, the smoother, then revises every state using the whole series. The filter also delivers, as a by-product, the probability of each observation given the past, and their product is the likelihood of the hyperparameters σμ and σy with the states integrated out. Here is the filter in ten lines of R, applied to annual global temperature.

R
kalman_level <- function(y, s_mu, s_y, m0 = y[1], P0 = 1) {
  n <- length(y); m <- P <- numeric(n); loglik <- 0; mp <- m0; Pp <- P0
  for (t in 1:n) {
    if (t > 1) { mp <- m[t - 1]; Pp <- P[t - 1] + s_mu^2 }      # predict
    K <- Pp / (Pp + s_y^2)                                       # Kalman gain
    loglik <- loglik + dnorm(y[t], mp, sqrt(Pp + s_y^2), log = TRUE)
    m[t] <- mp + K * (y[t] - mp); P[t] <- (1 - K) * Pp           # update
  }
  list(m = m, P = P, loglik = loglik)
}
fit_ml <- optim(c(log(0.05), log(0.1)), function(p)
  -kalman_level(annual$temp, exp(p[1]), exp(p[2]))$loglik)
c(s_mu = exp(fit_ml$par[1]), s_y = exp(fit_ml$par[2]))           # maximum likelihood
output
   s_mu     s_y 
0.06473 0.06854 

The maximum-likelihood estimates are σμ≈0.065 °C (how much the underlying level moves in a year) and σy≈0.069 °C (how far a single year strays from it): the two are about equal, which in steady state gives a Kalman gain of about 0.6: each new year moves the estimated level about 60% of the way from the prediction toward the observation. The widget lets you explore them.

The Kalman filter by hand

Annual global temperature anomalies, 1880–2025 (points), with the local level model's filtered estimate (orange, using only past years) and smoothed estimate (blue, using all years) and 90% bands for the smoothed level. Set the two standard deviations: how much the true level can move each year, and how noisy each year is around it. The readout gives the Kalman gain in the last year and the log likelihood; find the values that maximise it.

Make σμ large relative to σy and the gain approaches one: the filter believes each new observation almost completely, and the level follows every wiggle. Make it small and the gain falls toward zero: the filter trusts its prediction and the level becomes a slow average. The likelihood is highest in between, and that compromise, chosen by the data, is the state-space model's version of the shrinkage of Chapter 6. Notice also that the filtered estimate lags behind the smoothed one during rapid warming: in real time, an observer cannot tell a new trend from a run of warm years until several years have passed.

8.7 Is carbon dioxide still accelerating?

For carbon dioxide we want three components: a smooth level, a slowly changing growth rate (the slope of the level, so that the level can rise faster and faster), and a seasonal cycle that may drift slowly. Each evolves by its own random walk, and the observation is their sum plus noise. This is a structural time-series model (Harvey, 1989); the growth rate is the quantity of interest. Stan can fit it efficiently because it has the Kalman filter built in: the distribution gaussian_dlm_obs computes the likelihood with every latent state integrated out, so the sampler explores only the four standard deviations. The states themselves are then recovered, for each posterior draw, by the Kalman smoother in the dlm package.

R
dlm_code <- "
data { int<lower=2> N; matrix[1, N] y; }
transformed data {
  matrix[6, 1] F = [[1], [0], [1], [0], [1], [0]];          // level + two seasonal harmonics
  matrix[6, 6] G = rep_matrix(0, 6, 6);
  real w1 = 2 * pi() / 12;  real w2 = 4 * pi() / 12;
  G[1, 1] = 1; G[1, 2] = 1; G[2, 2] = 1;                    // level grows by the slope
  G[3, 3] = cos(w1); G[3, 4] = sin(w1); G[4, 3] = -sin(w1); G[4, 4] = cos(w1);
  G[5, 5] = cos(w2); G[5, 6] = sin(w2); G[6, 5] = -sin(w2); G[6, 6] = cos(w2);
  vector[6] m0 = [315, 0.07, 0, 0, 0, 0]';
  matrix[6, 6] C0 = diag_matrix([100, 0.01, 10, 10, 10, 10]');
}
parameters { real<lower=0> s_level; real<lower=0> s_slope; real<lower=0> s_seas; real<lower=0> s_y; }
model {
  s_level ~ exponential(10); s_slope ~ exponential(100); s_seas ~ exponential(20); s_y ~ exponential(2);
  y ~ gaussian_dlm_obs(F, G, [s_y^2]',
        diag_matrix([s_level^2, s_slope^2, s_seas^2, s_seas^2, s_seas^2, s_seas^2]'), m0, C0);
}"
fit_dlm <- rstan::stan(model_code = dlm_code, data = list(N = nrow(co2), y = matrix(co2$ppm, nrow = 1)),
                       seed = 8, refresh = 0)
print(fit_dlm, digits = 4, probs = c(.05, .95))
output
Inference for Stan model: anon_model.
4 chains, each with iter=2000; warmup=1000; thin=1; 
post-warmup draws per chain=1000, total post-warmup draws=4000.

            mean se_mean     sd       5%      95% n_eff   Rhat
s_level   0.1621  0.0002 0.0118   0.1432   0.1821  3581 0.9996
s_slope   0.0029  0.0000 0.0010   0.0017   0.0048  3232 1.0007
s_seas    0.0083  0.0000 0.0016   0.0060   0.0112  3655 0.9998
s_y       0.2157  0.0002 0.0092   0.2006   0.2312  3746 0.9994
lp__    477.3341  0.0306 1.4253 474.5831 478.9962  2172 1.0023

Samples were drawn using NUTS(diag_e) at Fri Oct 23 10:57:08 2026.
For each parameter, n_eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor on split chains (at 
convergence, Rhat=1).
R
library(dlm)
draws_dlm <- as.data.frame(fit_dlm)
build <- function(p) dlmModPoly(2, dV = p$s_y^2, dW = c(p$s_level^2, p$s_slope^2),
                                m0 = c(315, 0.07), C0 = diag(c(100, 0.01))) +
                     dlmModTrig(s = 12, q = 2, dV = 0, dW = p$s_seas^2, m0 = rep(0, 4), C0 = diag(10, 4))
set.seed(8)
ids <- sample(nrow(draws_dlm), 200)
smooth_draws <- lapply(ids, function(i) dlmSmooth(co2$ppm, build(draws_dlm[i, ]))$s[-1, ])
growth <- sapply(smooth_draws, function(s) 12 * s[, 2])          # ppm per year, months x draws
level  <- sapply(smooth_draws, function(s) s[, 1])
season <- sapply(smooth_draws, function(s) s[, 3] + s[, 5])
gr <- co2 |> mutate(g = apply(growth, 1, median), g_lo = apply(growth, 1, quantile, .05),
                    g_hi = apply(growth, 1, quantile, .95), seas = apply(season, 1, median))
gr |> filter(month == 7, year %in% c(1960, 1970, 1980, 1990, 2000, 2010, 2015, 2019, 2020, 2025)) |>
  select(year, g, g_lo, g_hi) |> mutate(across(-year, ~ round(.x, 2)))
output
# A tibble: 10 x 4
    year     g  g_lo  g_hi
   <int> <dbl> <dbl> <dbl>
 1  1960  0.87  0.82  0.95
 2  1970  1.1   1.09  1.14
 3  1980  1.46  1.42  1.52
 4  1990  1.51  1.41  1.56
 5  2000  1.84  1.82  1.85
 6  2010  2.16  2.13  2.16
 7  2015  2.38  2.3   2.47
 8  2019  2.44  2.36  2.48
 9  2020  2.44  2.37  2.47
10  2025  2.46  2.4   2.47
R
lv <- co2 |> mutate(level = apply(level, 1, median))
p1 <- ggplot(lv, aes(time)) + geom_point(aes(y = ppm), colour = harbour[["muted"]], size = 0.3) +
  geom_line(aes(y = level), colour = harbour[["sky"]], linewidth = 0.8) + labs(x = NULL, y = "CO2 (ppm)")
p2 <- ggplot(gr, aes(time)) + geom_ribbon(aes(ymin = g_lo, ymax = g_hi), fill = harbour[["sky"]], alpha = 0.25) +
  geom_line(aes(y = g), colour = harbour[["sky"]], linewidth = 0.8) + labs(x = NULL, y = "growth (ppm/yr)")
p3 <- ggplot(gr, aes(time, seas)) + geom_line(colour = harbour[["moss"]], linewidth = 0.4) +
  labs(x = NULL, y = "seasonal (ppm)")
patchwork::wrap_plots(p1, p2, p3, ncol = 1, heights = c(1.3, 1, 0.8))
Three stacked panels sharing a time axisThree stacked panels sharing a time axis. Top: a rising, slightly curving line through dense points. Middle: a line rising steadily from about 0.9 to about 2.4 between 1960 and 2015, then nearly flat, with a narrow band. Bottom: a regular oscillation of about plus or minus 3 ppm. 350 400 1970 1990 2010 2030 CO2 (ppm) 1.0 1.5 2.0 2.5 1970 1990 2010 2030 growth (ppm/yr) -2 0 2 1970 1990 2010 2030 seasonal (ppm)
Structural decomposition of the Keeling curve, 1958–2026. Top: monthly concentrations (points) and the smoothed level (line). Middle: the growth rate of the level in ppm per year, with a 90% band; it rose from under 1 ppm per year around 1960 to about 2.4 by the mid-2010s and has changed little since. Bottom: the seasonal component, peaking in May and falling to its minimum in September or October, with an amplitude that has grown slightly.

The sampler explores just four parameters, and all four are well estimated (R^=1.00, effective sample sizes above 3,000). The growth rate tells a clear story. It rose from about 0.9 ppm per year around 1960 to 1.1 by 1970, 1.5 by 1980, 1.8 by 2000 and 2.2 by 2010, with a pause in the early 1990s (when the eruption of Mount Pinatubo cooled the climate and the economies of the former Soviet bloc collapsed). From 2015 to 2025 it barely changed, from about 2.4 to 2.5 ppm per year, with 90% intervals of about ±0.05 to 0.1. Carbon dioxide is still rising, by about 2.5 ppm a year, but on this model it is no longer rising faster each year, which matches the levelling-off of global fossil-fuel emissions over the same decade. Two cautions. The model has decided, through the estimated standard deviations, that year-to-year wobbles belong to the level rather than the growth rate, so the growth rate is smooth by construction. And single-year increases vary much more: the increase in 2024, boosted by El Niño and wildfires, was the largest on record. Whether that marks a renewed acceleration only further years will show.

What about 2020? Fossil fuel emissions fell by about 1.9 billion tonnes of carbon dioxide that year, roughly 5%. About 2.1 billion tonnes of carbon (7.8 billion tonnes of CO₂) raise the atmospheric concentration by 1 ppm, and only about half of each year's emissions stay in the air (the rest is taken up by oceans and plants), so the expected effect on the 2020 growth rate is about 1.9/7.8×0.5≈0.1 to 0.2 ppm. The year-to-year variation in the growth rate, driven by the response of tropical forests and oceans to El Niño and La Niña, is of the order of 0.5 ppm. A dip of 0.15 ppm in one year is buried in noise three times its size, and the model, which separates a slowly evolving growth rate from short-term variation, shows no break in 2020. Detecting it would have required years of reduced emissions. The 2020 non-detection was correctly predicted in advance by atmospheric scientists using exactly this signal-to-noise reasoning, and it is a good example of a power calculation for a time series.

Check Yourself

In the local level model, the maximum-likelihood estimate of σμ (how much the true level moves each year) turns out to be close to zero. What does that imply about the filtered and smoothed estimates?

8.8 Gaussian processes

Autoregression models a series through its own past; a state-space model builds it from evolving components. A Gaussian process (GP) takes a third view: it puts a prior directly on the whole unknown function f(t). The prior says that the values of f at any set of times are jointly normal, with correlations given by a kernel k(t,t′) that encodes what kind of function we expect. The most common is the squared exponential kernel,

k(t,t′)=α2exp⁡(−(t−t′)22ℓ2),

under which values at nearby times are strongly correlated and values more than a few length-scales ℓ apart are nearly independent; α is the typical size of the function's departures from its mean. Draws from this prior are smooth, wiggly curves, and conditioning on data gives a posterior over curves that pass near the data. Kernels can be added (a smooth trend plus a periodic seasonal component) and multiplied, which makes GPs a modular language for describing structure.

A Gaussian process, conditioned on temperature

Annual global temperature, 1880–2025, with a Gaussian process posterior: the mean (blue), a 90% band, and three draws (thin). Choose the kernel and set its length-scale ℓ, amplitude α and the noise standard deviation. The forecast runs to 2045. The readout gives the log marginal likelihood (higher is better supported by the data) and the forecast for 2035. Toggle "prior draws" to see what the kernel believes before seeing any data.

Three lessons come out of the widget. First, the marginal likelihood (the probability of the data with f integrated out) is a built-in Occam's razor: very short length-scales fit every wiggle but are penalised for being able to fit anything, very long ones cannot follow the data, and the likelihood peaks in between. Second, the band is narrow where there are data and widens beyond them. Third, and most important for forecasting: with a stationary kernel such as the squared exponential, the forecast reverts to the mean once you are more than a length-scale beyond the data, because the prior says that distant values are unrelated to the observed ones and centred on the overall mean. A GP knows only what its kernel tells it. If you believe warming will continue, the kernel must say so, for example by adding a linear trend, and the forecast then depends on that assumption rather than discovering it.

In brms, gp(x) adds a GP term with a squared exponential kernel and estimates ℓ and α with priors rather than fixing them. Exact GPs need a Cholesky factorisation of an n×n matrix at every step of the sampler, which becomes slow even for a few hundred points, so we use the Hilbert-space approximate GP of Riutort-Mayol et al. (2023): gp(x, k = 25, c = 5/4) represents the GP with 25 basis functions, which is accurate for functions that are smooth relative to the range of the data and fits in seconds.

R
annual <- annual |> mutate(x = (year - 2000) / 50)          # half-centuries from 2000
m_gp <- brm(temp ~ gp(x, k = 25, c = 5/4), data = annual,
            prior = c(prior(normal(0, 1), class = Intercept),
                      prior(exponential(1), class = sdgp),
                      prior(exponential(5), class = sigma)),
            seed = 8, refresh = 0, control = list(adapt_delta = 0.99), file = "fits/ch8_gp")
posterior_summary(m_gp, variable = c("sdgp_gpx", "lscale_gpx", "sigma")) |> round(3)
future <- tibble(year = c(2025, 2030, 2035, 2045)) |> mutate(x = (year - 2000) / 50)
cbind(future["year"], round(fitted(m_gp, newdata = future, probs = c(.05, .95)), 2))
output
           Estimate Est.Error  Q2.5 Q97.5
sdgp_gpx      0.665     0.271 0.339 1.359
lscale_gpx    0.173     0.026 0.125 0.228
sigma         0.101     0.006 0.090 0.114
  year Estimate Est.Error   Q5  Q95
1 2025     1.14      0.05 1.05 1.23
2 2030     1.28      0.11 1.10 1.46
3 2035     1.38      0.19 1.08 1.70
4 2045     1.41      0.40 0.79 2.10

The estimated length-scale is about 0.17 half-centuries, or roughly 9 years (95% interval 6 to 11), the amplitude about 0.7 °C and the residual standard deviation about 0.10 °C. The forecast shows the behaviour the widget warned about. The GP continues the recent rise for about a length-scale: 1.28 °C in 2030 and 1.38 °C in 2035. Then it flattens, and its interval widens rapidly: by 2045 the forecast is 1.41 °C with a 90% interval from 0.8 to 2.1 °C. The flattening is not a prediction that warming will stop. It is the prior reasserting itself where the data run out, because the squared exponential kernel knows nothing about greenhouse gases.

8.9 Forecasting honestly

The only convincing test of a forecast is to make it before the outcome is known. With a time series we can simulate that: fit each model to the data up to some year, forecast the following years, and compare with what happened. This is leave-future-out validation (Bürkner, Gabry and Vehtari, 2020). Ordinary cross-validation, which leaves out random observations and predicts them from both their past and their future, overstates forecasting skill for time series, because interpolating between known neighbours is much easier than extrapolating.

R
train <- filter(annual, year <= 2015); test <- filter(annual, year > 2015)
m_gp_15 <- update(m_gp, newdata = train, seed = 8, refresh = 0)
m_lin_15 <- brm(temp ~ x, data = filter(train, year >= 1970),
                prior = c(prior(normal(0.5, 1), class = Intercept), prior(normal(0, 2), class = b),
                          prior(exponential(5), class = sigma)), seed = 8, refresh = 0)
score <- function(m) { set.seed(8); p <- posterior_predict(m, newdata = test)
  c(rmse = sqrt(mean((colMeans(p) - test$temp)^2)), bias = mean(colMeans(p) - test$temp),
    coverage_90 = mean(test$temp > apply(p, 2, quantile, .05) & test$temp < apply(p, 2, quantile, .95))) }
rbind(`GP, 1880-2015` = score(m_gp_15), `linear, 1970-2015` = score(m_lin_15)) |> round(3)
output
                   rmse   bias coverage_90
GP, 1880-2015     0.283 -0.246         0.4
linear, 1970-2015 0.193 -0.152         0.6

Fitted to the data up to 2015, the GP forecasts 2016–2025 too low by about 0.25 °C on average, and only 4 of the 10 years fall inside its 90% predictive intervals. The straight line fitted to 1970–2015 does better (an average error of −0.15 °C, 6 of 10 years covered), but it too is too low and too confident. Both models were surprised by the warmth of the last decade, which is itself evidence for the acceleration of Section 8.4 (and for the El Niño of 2023–2024). Neither model is a climate projection; projections come from physical climate models driven by emission scenarios. The point is the method: a forecast evaluated on a future it did not see reveals overconfidence that no in-sample check would.

Research Practice: Reporting Forecasts

  • Evaluate forecasts by leave-future-out validation at the horizons that matter, never by in-sample fit or random cross-validation. The loo package implements approximate leave-future-out for Stan models.
  • Report predictive intervals and their coverage on held-out periods, and a proper scoring rule (the log predictive density or the continuous ranked probability score), not just the error of the point forecast.
  • Compare against simple baselines: last value, seasonal naive (same month last year), a linear trend. Sophisticated models often fail to beat them, and a paper that does not show the comparison will not convince a referee.
  • State what the model assumes about the future: a stationary GP reverts to its mean, a random walk extrapolates the last level, a linear trend continues forever. The forecast inherits these assumptions, and different defensible assumptions give different forecasts.

8.10 Time series in practice

The three approaches of this chapter are less rivals than tools for different questions, and a careful analysis often uses more than one. Regression with AR or ARMA errors is the natural choice when the question is about the effect of predictors (a trend, a policy, a covariate) and the time structure is a nuisance to be accounted for. State-space models are the choice when the evolving components are themselves the object of interest (a growth rate, a seasonal pattern, an underlying level), when the data arrive in real time and estimates must be updated, or when there are missing observations (the Kalman filter simply skips the update step). Gaussian processes are the choice for flexible smoothing and interpolation with honest uncertainty, and for irregularly spaced times. All three are Bayesian models fitted with the same machinery as every other model in this book, and many standard GP kernels have exact state-space representations, so the distinctions are partly a matter of computation and convenience.

Project Connections: Calls, Shifts and Semesters

Daily emergency medical service call volume in New York is a textbook structural time series: a weekly cycle (two harmonics, or seven day-of-week effects), an annual cycle, holiday effects, a slowly changing level, and occasional shocks (heat waves, the 2020 pandemic surge). A state-space model with these components gives real-time estimates of the underlying demand for staffing, and leave-future-out validation of one- and seven-day-ahead forecasts is the evidence a city agency will ask for. Response times, aggregated by day or week, are autocorrelated too, so any trend in them (or any before-and-after comparison around a policy change) must use autocorrelated errors. For AI tools in university mathematics, course outcomes by semester form a short time series per course: with few time points, a multilevel model with a varying slope per course (Chapter 6) and AR(1) errors over semesters is more realistic than any purely time-series method, and the "hiatus" lesson applies directly to any claim that outcomes changed when AI tools arrived.

Key Takeaways: Lecture 16

  • A state-space model has latent states that evolve over time and observations that are noisy views of them. The Kalman filter is Bayes' rule applied once per time step; the smoother revises estimates using the whole series; the filter's by-product is the likelihood with the states integrated out.
  • The growth rate of atmospheric CO₂ rose from about 0.9 ppm per year around 1960 to about 2.4 by the mid-2010s and has been roughly flat since; the 2020 emissions dip is far too small to detect against natural variability.
  • A Gaussian process is a prior over functions defined by a kernel. Its marginal likelihood balances fit against flexibility; stationary kernels make forecasts revert to the mean.
  • Judge forecasts by leave-future-out validation against simple baselines, and report predictive intervals and their coverage.

8.11 Exercises

Each exercise has a hint and an answer button.

Exercises

  1. AR(2) and ARMA(1,1). Refit the trend model m_ar1 with AR(2) errors and with ARMA(1,1) errors. Do the estimated warming rate and its uncertainty change? Compare the three error models with loo() (note the caveat about cross-validation for time series in Section 8.9).

    ar(time = t, p = 2) and arma(time = t, p = 1, q = 1).

    The warming rate is essentially unchanged: 0.200 °C per decade (95% interval 0.184 to 0.217) with AR(1) errors, 0.197 (0.175 to 0.218) with AR(2) and 0.196 (0.172 to 0.220) with ARMA(1,1), whose autoregressive coefficient is 0.86 and moving-average coefficient −0.35. The richer error models make the interval about 30–45% wider, because they capture longer memory than AR(1) does. loo_compare ranks AR(2) first, ARMA(1,1) close behind (difference −2.0, standard error 2.4) and AR(1) well behind (−22.5, standard error 6.4). Leave-one-out cross-validation lets the model see both neighbours of each left-out month, so it measures interpolation rather than forecasting; it is still a reasonable guide to which error structure describes the data. Foster and Rahmstorf (2011) used ARMA(1,1) for exactly this reason.

  2. Annual or monthly?. Fit the straight-line trend (with independent errors) to the annual anomalies for 1970–2025, and compare the slope's standard error with that from the monthly data with independent errors and with AR(1) errors. Explain the pattern.

    Averaging twelve autocorrelated months does not give twelve times the information.

    Fitted to the 56 annual means, the straight line gives 0.202 °C per decade with a standard error of 0.0087, almost exactly the AR(1) model's posterior standard deviation from the monthly data (0.0083) and more than twice the independent-errors monthly value (0.0036). Averaging twelve months removes most of the short-term autocorrelation (the annual residuals have lag-one autocorrelation 0.29, against 0.68 for months), and twelve correlated months contain little more information about a decades-long trend than their average does. Aggregating to a scale at which errors are roughly independent is a crude but often effective check on a time-series model.

  3. The hiatus, properly. Using monthly data from 1998 to 2012 only, estimate the warming rate with AR(1) errors. What is the posterior probability that it is below the 1970–2025 rate? What is the probability that it is below zero?

    Filter recent to the window and update(m_ar1, newdata = ...); compare the draws of b_t with the posterior mean from m_ar1.

    Using only 1998–2012, with AR(1) errors, the posterior mean warming rate is 0.12 °C per decade (95% interval 0.04 to 0.20). The probability that it was below the 1970–2025 rate (0.20) is 0.97, and the probability that it was below zero is 0.005. Taken in isolation, the window suggests a real slowdown, though not a stop. But the window was chosen because it looked slow, starting at the El Niño peak of 1998; a fair assessment must account for that selection and for El Niño, which is what Rahmstorf, Foster and Cahill (2017) did, finding no significant change in the underlying rate.

  4. Crossing 1.5 °C. Using the hinge model with AR(1) errors, in which year does the fitted trend line (not individual years) cross 1.2 °C above the 1951–1980 average, assuming the post-2010 rate continues? (The 1951–1980 baseline is roughly 0.3 °C above pre-industrial temperatures, so this is roughly 1.5 °C above pre-industrial.) Give a 90% interval.

    For each posterior draw, solve α+βt+γ(t−1)=1.2 for t (in decades since 2000).

    Solving the hinge model for each posterior draw, the fitted trend crosses 1.2 °C above the 1951–1980 average in about 2027 (posterior median 2026.9, 90% interval 2024.9 to 2029.6), roughly 1.5 °C above pre-industrial temperatures. Individual years have already exceeded that level (2024 was about 1.3 °C above the 1951–1980 average), but the Paris Agreement's 1.5 °C refers to a long-term average, so the trend crossing is the relevant quantity. The answer assumes the post-2010 rate continues and inherits the hinge model's uncertainty about it.

  5. The growing breath of the biosphere. Using the smoothed seasonal component of the Keeling curve, compute the peak-to-trough amplitude of the seasonal cycle in each year. Has it changed between the 1960s and the 2010s?

    Group the median seasonal component gr$seas by year and take max - min.

    The peak-to-trough amplitude of the smoothed seasonal component averages about 5.7 ppm in the 1960s, 6.0 in the 1970s, 6.3 in the 1980s to 2000s and 6.6 in the 2010s and 2020s: an increase of about 15% over sixty years. This growing seasonal amplitude at Mauna Loa (and much larger increases at high northern latitudes) is attributed to increased summer uptake by northern vegetation, a CO₂ fertilisation and warming effect (Graven et al., 2013). The state-space model's slowly evolving seasonal component detects it without any special modelling.

  6. Missing months. Delete the CO₂ observations for all of 2020 and run the Kalman smoother (at the posterior mean of the hyperparameters) on the series with gaps (NA values). How well are the missing months reconstructed? What does the filter do at a missing observation?

    dlmSmooth accepts NA. Compare the smoothed level plus seasonal component with the deleted values.

    At a missing observation, the Kalman filter performs the predict step and skips the update: the filtered state is just the prediction, and its variance grows. The smoother then uses the observations on both sides of the gap. With all twelve months of 2020 deleted, the reconstructed values (level plus seasonal component) are within −0.5 to +0.3 ppm of the deleted measurements, with a root mean squared error of 0.31 ppm, compared with an observation noise standard deviation of 0.22 ppm. A whole year can be interpolated almost as accurately as the monthly measurements themselves, because the level, growth rate and seasonal cycle are all well determined by the surrounding years. Missing data, as Chapter 7 said, are just unobserved states.

  7. A GP with a trend. Fit temp ~ x + gp(x, k = 25, c = 5/4) to the annual data up to 2015 and repeat the leave-future-out comparison of Section 8.9. Does adding a linear trend to the GP fix its forecast?

    Use the same priors as m_gp, plus prior(normal(0, 2), class = b).

    Adding a linear term helps only a little: fitted to 1880–2015, the GP with a linear trend forecasts 2016–2025 with a root mean squared error of 0.25 °C and an average error of −0.21 °C, covering 6 of the 10 years with its 90% intervals, compared with 0.28, −0.25 and 4 of 10 for the GP alone. Its forecasts rise from 0.76 °C in 2016 to only 0.84 °C in 2025. The linear term's slope is estimated from 1880–2015 as a whole, about half the recent rate, and the GP's departures from it revert to zero, so the forecast continues the long-run average rate rather than the recent one. A kernel encodes beliefs about the future; to forecast a trend that has steepened, the model must allow the trend itself to change.

  8. A results paragraph. Write a short results paragraph (four or five sentences) reporting the warming rate since 1970 and the evidence for acceleration, following the Research Practice box in Section 8.4.

    Give the rate and interval, the error model and why, the acceleration estimate, its sensitivity to the break year, and the role of El Niño.

    Example. We estimated the rate of global warming from NASA GISTEMP monthly surface temperature anomalies for 1970–2025 (672 months). Because residuals from a linear trend were strongly autocorrelated (lag-one autocorrelation 0.68), we modelled them as an AR(1) process; this doubled the posterior standard deviation of the trend relative to a model with independent errors. The warming rate was 0.20 °C per decade (95% CrI 0.18 to 0.22). A hinge model allowing the rate to change in 2010 estimated an increase of 0.15 °C per decade after 2010 (95% CrI 0.08 to 0.23; posterior probability of an increase above 0.99), from 0.17 °C per decade before. The estimated increase depended strongly on the assumed break year (0.09 °C per decade for a break in 2000, 0.22 for 2015), the break year was chosen after inspecting the data, and the recent record warmth includes the effect of the 2023–2024 El Niño, which we did not model; the evidence for acceleration should be read with these caveats. Models were fitted with brms 2.20 (all R^≤1.01, bulk ESS above 3,000).

Further reading and references

Further Reading

  • Hyndman and Athanasopoulos, Forecasting: Principles and Practice (3rd ed., 2021): the clearest practical introduction to forecasting, free online.
  • Durbin and Koopman, Time Series Analysis by State Space Methods (2nd ed., 2012): the standard reference on state-space models and the Kalman filter.
  • Rasmussen and Williams, Gaussian Processes for Machine Learning (2006): the standard GP text, free online; Chapter 5 fits the Mauna Loa CO₂ series with a sum of kernels.
  • McElreath, Statistical Rethinking (2nd ed., 2020), Chapter 14: Gaussian processes as continuous multilevel models.
  • Bürkner, Gabry and Vehtari, "Approximate leave-future-out cross-validation for Bayesian time series models" (2020).
  1. Beaulieu, C., Gallagher, C., Killick, R., Lund, R. and Shi, X. (2024). A recent surge in global warming is not detectable yet. Communications Earth & Environment, 5, 576.
  2. Bürkner, P.-C., Gabry, J. and Vehtari, A. (2020). Approximate leave-future-out cross-validation for Bayesian time series models. Journal of Statistical Computation and Simulation, 90(14), 2499–2523.
  3. Durbin, J. and Koopman, S. J. (2012). Time Series Analysis by State Space Methods (2nd ed.). Oxford University Press.
  4. Foster, G. and Rahmstorf, S. (2011). Global temperature evolution 1979–2010. Environmental Research Letters, 6(4), 044022.
  5. Granger, C. W. J. and Newbold, P. (1974). Spurious regressions in econometrics. Journal of Econometrics, 2(2), 111–120.
  6. Graven, H. D. et al. (2013). Enhanced seasonal exchange of CO₂ by northern ecosystems since 1960. Science, 341(6150), 1085–1089.
  7. Hansen, J. E. et al. (2025). Global warming has accelerated: are the United Nations and the public well-informed? Environment: Science and Policy for Sustainable Development, 67(1), 6–44.
  8. Harvey, A. C. (1989). Forecasting, Structural Time Series Models and the Kalman Filter. Cambridge University Press.
  9. Hyndman, R. J. and Athanasopoulos, G. (2021). Forecasting: Principles and Practice (3rd ed.). OTexts.
  10. Kalman, R. E. (1960). A new approach to linear filtering and prediction problems. Journal of Basic Engineering, 82(1), 35–45.
  11. Keeling, C. D. (1960). The concentration and isotopic abundances of carbon dioxide in the atmosphere. Tellus, 12(2), 200–203.
  12. Lenssen, N. et al. (2019). Improvements in the GISTEMP uncertainty model. Journal of Geophysical Research: Atmospheres, 124(12), 6307–6326. Data via github.com/datasets/global-temp.
  13. McElreath, R. (2020). Statistical Rethinking (2nd ed.). CRC Press.
  14. Rahmstorf, S., Foster, G. and Cahill, N. (2017). Global temperature evolution: recent trends and some pitfalls. Environmental Research Letters, 12(5), 054001.
  15. Rasmussen, C. E. and Williams, C. K. I. (2006). Gaussian Processes for Machine Learning. MIT Press.
  16. Riutort-Mayol, G., Bürkner, P.-C., Andersen, M. R., Solin, A. and Vehtari, A. (2023). Practical Hilbert space approximate Bayesian Gaussian processes for probabilistic programming. Statistics and Computing, 33, 17.
  17. Yule, G. U. (1926). Why do we sometimes get nonsense-correlations between time-series? Journal of the Royal Statistical Society, 89(1), 1–63.
  18. Yule, G. U. (1927). On a method of investigating periodicities in disturbed series, with special reference to Wolfer's sunspot numbers. Philosophical Transactions of the Royal Society A, 226, 267–298.