import whittaker as wk
# Load data and fit candidate models
data = wk.load_dataset("fish")
m_simple = wk.GAM("count ~ s(temperature)", family=wk.Poisson()).fit(data)
m_full = wk.GAM("count ~ s(temperature) + s(depth)", family=wk.Poisson()).fit(data)Cross-Validate a GAM
Cross-validation (CV) splits the data into folds, trains on all but one fold, and evaluates predictions on the held-out fold. Repeating this across all folds gives an honest estimate of how well the model will perform on new data.
Use CV when predictive accuracy on unseen data is the primary goal, or when you want a model-selection criterion that does not rely on asymptotic approximations. wk.cross_validate() handles the fold splitting and returns a summary score.
Fit the candidate models
Load the fish dataset, which records fish counts by temperature and depth — a natural Poisson response.
Cross-validate the simple model
Run 5-fold CV on the simpler model. cv_score is the mean held-out deviance (lower is better); cv_se is its standard error across folds.
# Run 5-fold CV on the simple model
cv_simple = wk.cross_validate(
"count ~ s(temperature)",
data,
family=wk.Poisson(),
n_folds=5,
)
cv_simple.cv_score, cv_simple.cv_se(1.7706263709710162, 0.09754791292669326)
Cross-validate the full model
Repeat for the model that also includes depth.
# Run 5-fold CV on the full model
cv_full = wk.cross_validate(
"count ~ s(temperature) + s(depth)",
data,
family=wk.Poisson(),
n_folds=5,
)
cv_full.cv_score, cv_full.cv_se(1.13030500590506, 0.08696847398477088)
Compare the scores
A lower cv_score indicates better out-of-sample fit. However, because CV is a random estimate, a difference smaller than one standard error is not meaningful — this is the “one-SE rule”: the simplest model whose CV score falls within best_score + best_se is considered equivalent to the best model.
# Apply one-SE rule to compare models
threshold = cv_full.cv_score + cv_full.cv_se
cv_simple_within_1se = cv_simple.cv_score <= threshold
cv_full.cv_score, cv_simple.cv_score, threshold, cv_simple_within_1se(1.13030500590506, 1.7706263709710162, 1.2172734798898308, False)
If cv_simple_within_1se is True, the simpler model is not meaningfully worse and can be preferred for parsimony. If it is False, the extra term earns its place.
Inspect the full model summary
When the fuller model wins, examine its summary to confirm both smooths are contributing.
m_full.summary()GAM fit summary
============================================================
Formula: count ~ s(temperature) + s(depth)
Family: Poisson(link='log')
Inference: GCV
Observations: 300
Coefficients: 19
Parametric coefficients:
Term Estimate Std.Err z value p-value
------------------------ ---------- ---------- ---------- ----------
(Intercept) 1.5349 0.0289 53.128 < 1e-16
Approximate significance of smooth terms:
Term EDF Ref.df Chi.sq p-value
------------------------ ------ ------ ---------- ----------
s(temperature) 4.24 5 294.642 < 1e-16
s(depth) 1.74 2 194.880 < 1e-16
Total EDF: 6.98
Scale est: 1.000000
Deviance: 320.5707
Null dev: 843.4578
Dev. expl: 62.0%
GCV score: 1.120093
AIC: 1305.79
BIC: 1331.65
Look at the EDF for each smooth. A term with EDF close to 1.0 is nearly linear; a term with very low EDF is barely contributing to predictions.
CV vs AIC
| Criterion | Best for | Notes |
|---|---|---|
| AIC | Model parsimony | Fast; relies on asymptotic theory |
| CV | Predictive accuracy | Slower; honest estimate of generalisation |
Prefer CV when the deployment distribution matches your data and you care primarily about prediction. Prefer AIC when interpretability and parsimony matter most, or when data are too small for reliable fold estimation.