set.seed(4209)
n <- 200; t <- 1:n
mu <- function(t) 3 * sin(pi * t / n) # gentle single-hump trend
e <- as.numeric(arima.sim(n = n, list(ar = 0.7), sd = 0.8))
y <- mu(t) + e
d <- data.frame(t = t, y = y)
naive <- gam(y ~ s(t, k = 25), data = d, method = "REML")
gmm <- gamm(y ~ s(t, k = 25), data = d,
correlation = corAR1(form = ~ t), method = "REML")
p9_edf_naive <- summary(naive)$edf
p9_edf_gamm <- summary(gmm$gam)$edf
p9_phi <- as.numeric(coef(gmm$lme$modelStruct$corStruct, unconstrained = FALSE))
p9_acf_naive <- as.numeric(acf(residuals(naive, type = "deviance"),
plot = FALSE, lag.max = 1)$acf[2])
p9_acf_gamm <- as.numeric(acf(residuals(gmm$lme, type = "normalized"),
plot = FALSE, lag.max = 1)$acf[2])
p9_ciw_naive <- 2 * 1.96 * predict(naive, data.frame(t = n/2), se.fit = TRUE)$se.fit
p9_ciw_gamm <- 2 * 1.96 * predict(gmm$gam, data.frame(t = n/2), se.fit = TRUE)$se.fitChecking a generalised additive model
A fitted smooth is easy to plot and easy to over-trust. The curve looks reasonable, the confidence band looks tight, and it is tempting to read the wiggles as findings. Three checks stand between a fitted GAM and a defensible conclusion, and each corresponds to a way the fit can mislead: correlated residuals that make the smooth chase noise, a basis or a second smooth that undermines the fit, and extrapolation that reports the shape of the basis rather than the behaviour of the process. This post works through all three on simulated data where the truth is known.
Check one: residual autocorrelation
A GAM assumes independent residuals. When the data are a time series or a transect and the residuals are correlated, the smooth mistakes that correlation for signal and bends to follow it, spending far more effective degrees of freedom than the trend warrants. The confidence band tightens at the same time, because the model believes it has more independent information than it does.
The naive fit, ignoring the correlation, spends 18.2 effective degrees of freedom on a trend that is a single smooth hump. Its residuals still carry a lag-one autocorrelation of 0.40: the model has fitted some of the correlated noise and left the rest in the residuals. Adding an AR(1) correlation structure through gamm changes the picture entirely. The trend now costs 3.2 degrees of freedom, close to the truth, the estimated autocorrelation is 0.71, recovering the value used to generate the data, and the normalised residuals are white, with a lag-one value of -0.05. The honest confidence band is wider: at the midpoint it spans 1.29 against the naive 0.94.
The residual autocorrelation function is the diagnostic. If it shows structure, the independence assumption has failed, the effective degrees of freedom are inflated, and the intervals are too narrow.
Check two: basis size and concurvity
Two structural checks come before reading any p-value. The first is whether the basis is large enough, tested with k.check; a too-small basis biases the fit, as covered in the companion post on choosing k. The second is whether smooth terms compete for the same signal, tested with concurvity; strong concurvity makes individual smooths and their intervals unreliable, as covered in the post on concurvity.
set.seed(21)
xk <- sort(runif(300))
yk <- sin(2 * pi * xk) + 0.6 * sin(6 * pi * xk) + rnorm(300, 0, 0.3)
m_small <- gam(yk ~ s(xk, k = 5), method = "REML")
m_big <- gam(yk ~ s(xk, k = 25), method = "REML")
kc_s <- k.check(m_small); kc_b <- k.check(m_big)
p9_ki5 <- kc_s[1, "k-index"]
p9_ki25 <- kc_b[1, "k-index"]; p9_p25 <- kc_b[1, "p-value"]Here a deliberately small basis fails the check with a k-index of 0.42 and a p-value below 0.001, while the generous basis passes with a k-index of 1.13 and a p-value of 0.98. On the concurvity side, the sibling post builds a case where two smooths have a worst-case concurvity of about 0.91 despite near-zero correlation. Run both checks on any multi-smooth model before interpreting the terms; a clean residual plot does not cover either failure.
Check three: extrapolation follows the basis
The last check is a caution about the edges. A smooth is supported only within the range of the data. Beyond it, mgcv extrapolates according to the boundary behaviour of the basis, which for thin-plate and cubic regression splines is roughly a straight line. The point estimate outside the data is therefore an assumption, not an inference, and the widening confidence band measures uncertainty within that assumption rather than about it.
set.seed(4219)
xc <- sort(runif(180)); yc <- sin(1.6 * pi * xc) + rnorm(180, 0, 0.4)
dc <- data.frame(x = xc, y = yc)
truef <- function(z) sin(1.6 * pi * z)
m_tp <- gam(y ~ s(x, bs = "tp", k = 15), data = dc, method = "REML")
m_cr <- gam(y ~ s(x, bs = "cr", k = 15), data = dc, method = "REML")
xin <- data.frame(x = seq(0.05, 0.95, length = 60))
p9_in_bases <- max(abs(predict(m_tp, xin) - predict(m_cr, xin)))
p9_in_truth <- sqrt(mean((predict(m_tp, xin) - truef(xin$x))^2))
pe <- predict(m_tp, data.frame(x = 1.30), se.fit = TRUE)
p9_tp13 <- pe$fit
p9_cr13 <- predict(m_cr, data.frame(x = 1.30))
p9_truth13 <- truef(1.30)
p9_lo13 <- pe$fit - 1.96 * pe$se.fit
p9_hi13 <- pe$fit + 1.96 * pe$se.fitInside the data the two bases are interchangeable: they agree with each other to 0.009 and track the truth to 0.103. Outside the data they still agree with each other, both extrapolating to about -1.11 at x of 1.30, so comparing bases would not reveal a problem. But the true curve, a continuing cycle, has turned back up to 0.25. Both bases are confidently wrong in shape, because both impose a straight-line tail. The interval, -2.64 to 0.41, happens to contain the truth here, but it is centred on the linear extrapolation the basis assumes; nothing in the observed range constrains the tail, and a periodic or saturating process would leave the band behind.
The wiggliness is only one sample deep
A closing caution ties the checks together. Even when the residuals are clean, the basis ample, and the range respected, the estimated smoothness is a quantity measured from one dataset, and it varies. Refitting the same process across simulated replicates gives a spread of effective degrees of freedom, not a single number.
set.seed(303); B <- 400; edfs <- numeric(B)
for (b in 1:B) {
xb <- sort(runif(180)); yb <- sin(1.6 * pi * xb) + rnorm(180, 0, 0.4)
edfs[b] <- summary(gam(yb ~ s(xb, k = 15), method = "REML"))$edf
}
p9_edf_med <- median(edfs)
p9_edf_lo <- quantile(edfs, 0.05); p9_edf_hi <- quantile(edfs, 0.95)The effective degrees of freedom for this fixed process range from 5.3 to 6.5 across replicates, around a median of 5.8. A specific effective degrees of freedom from one fit, and by extension a specific set of wiggles, is an estimate with sampling variability, not a fact about the process. Read a fitted smooth as one draw, check the residuals, the basis, the concurvity and the range, and keep the conclusions inside the data where the model has something to say.
References
Wood SN 2011. Journal of the Royal Statistical Society Series B 73(1):3-36 (10.1111/j.1467-9868.2010.00749.x).
Augustin NH, Sauleau EA, Wood SN 2012. Computational Statistics and Data Analysis 56(8):2404-2409 (10.1016/j.csda.2012.01.026).
Marra G, Wood SN 2012. Scandinavian Journal of Statistics 39(1):53-74 (10.1111/j.1467-9469.2011.00760.x).
Simpson GL 2018. Frontiers in Ecology and Evolution 6:149 (10.3389/fevo.2018.00149).
Zuur AF, Ieno EN, Walker NJ, Saveliev AA, Smith GM 2009. Springer. ISBN 978-0-387-87457-9.
Wood SN 2017. 2nd edn. CRC Press. ISBN 978-1-4987-2833-1.