Standard confidence intervals from a GAM rely on distributional assumptions – Gaussian errors, correctly specified variance functions, and a well-calibrated Bayesian posterior covariance. When those assumptions are suspect, or when you need a hard coverage guarantee regardless of the true data-generating process, conformal prediction offers an alternative: distribution-free prediction intervals with finite-sample coverage guarantees.
This page covers the three conformal methods available in Whittaker, shows how to fit and visualize them, and explains when to use conformal intervals instead of (or alongside) the classical Bayesian intervals from predict().
Comparing methods
The three conformal methods trade off computation, interval width, and theoretical coverage guarantees:
| Split |
1 |
\geq 1 - \alpha |
Widest |
Constant width |
| CV+ |
K (default 5) |
\geq 1 - 2\alpha |
Moderate |
Partially adaptive |
| Jackknife+ |
n |
\geq 1 - 2\alpha |
Tightest |
Most adaptive |
In practice, all three methods typically achieve coverage close to the nominal 1 - \alpha level. The theoretical worst-case bounds for CV+ and jackknife+ (1 - 2\alpha) are conservative and rarely observed.
# Side-by-side comparison of interval widths
comparison_data = []
for i in range(len(x_new)):
comparison_data.append({
"x": float(x_new[i]),
"method": "Split",
"width": float(result_split.upper[i] - result_split.lower[i]),
})
comparison_data.append({
"x": float(x_new[i]),
"method": "CV+",
"width": float(result_cv.upper[i] - result_cv.lower[i]),
})
width_chart = alt.Chart({"values": comparison_data}).mark_line(strokeWidth=2).encode(
x=alt.X("x:Q", title="x"),
y=alt.Y("width:Q", title="Interval width"),
color=alt.Color("method:N", title="Method"),
).properties(
width="container", height=280,
title="Conformal interval width by method"
)
width_chart
Coverage verification
After constructing conformal intervals, you can verify the empirical coverage on held-out data using wk.conformal_coverage(). This function computes the fraction of test observations that fall within the predicted intervals:
# Generate a fresh test set from the same process
x_test = rng.uniform(0, 2 * np.pi, 500)
y_test = np.sin(x_test) + 0.3 * (1 + 0.5 * np.abs(np.sin(x_test))) * rng.normal(0, 1, 500)
test_data = {"x": x_test, "y": y_test}
# Check coverage for each method
cov_split = wk.conformal_coverage(predictor_split, test_data, response="y")
cov_cv = wk.conformal_coverage(predictor_cv, test_data, response="y")
print(f"Nominal level: 0.95")
print(f"Split coverage: {cov_split:.4f}")
print(f"CV+ coverage: {cov_cv:.4f}")
Nominal level: 0.95
Split coverage: 0.9160
CV+ coverage: 0.9860
Empirical coverage on any single test set will fluctuate around the nominal level due to sampling variability. The conformal guarantee is that coverage is at least 1 - \alpha in expectation over the randomness in the calibration data. A single test set may show coverage slightly below the nominal level (averaging over many random splits would confirm the guarantee).
Using with non-Gaussian families
Conformal prediction works with any response family supported by Whittaker. For non-Gaussian models, conformal intervals are particularly valuable because the parametric assumptions underlying standard confidence intervals are harder to verify.
Here is an example with Poisson count data:
# Generate Poisson count data with a smooth rate function
rng = np.random.default_rng(99)
n = 400
x_pois = np.linspace(0, 2 * np.pi, n)
true_rate = np.exp(1.0 + 0.8 * np.sin(x_pois))
y_pois = rng.poisson(true_rate).astype(float)
pois_data = {"x": x_pois, "y": y_pois}
# Fit conformal predictor with Poisson family
predictor_pois = wk.conformal_fit(
"y ~ s(x)",
pois_data,
method="cv+",
level=0.95,
family=wk.Poisson(),
n_folds=5,
seed=23,
)
# Predict on a grid
x_pois_new = np.linspace(0, 2 * np.pi, 200)
result_pois = predictor_pois.predict({"x": x_pois_new})
print(f"Predicted rate range: [{result_pois.values.min():.2f}, {result_pois.values.max():.2f}]")
print(f"Lower bound range: [{result_pois.lower.min():.2f}, {result_pois.lower.max():.2f}]")
print(f"Upper bound range: [{result_pois.upper.min():.2f}, {result_pois.upper.max():.2f}]")
Predicted rate range: [1.36, 6.77]
Lower bound range: [-3.23, 2.14]
Upper bound range: [5.95, 11.63]
# Visualize Poisson conformal intervals
obs_pois = [{"x": float(x_pois[i]), "y": float(y_pois[i])} for i in range(n)]
points_pois = alt.Chart({"values": obs_pois}).mark_circle(
size=12, opacity=0.2, color="steelblue"
).encode(
x=alt.X("x:Q", title="x"),
y=alt.Y("y:Q", title="Count"),
)
fit_pois = [
{
"x": float(x_pois_new[i]),
"fit": float(result_pois.values[i]),
"lower": float(result_pois.lower[i]),
"upper": float(result_pois.upper[i]),
}
for i in range(len(x_pois_new))
]
line_pois = alt.Chart({"values": fit_pois}).mark_line(
color="firebrick", strokeWidth=2
).encode(x="x:Q", y="fit:Q")
band_pois = alt.Chart({"values": fit_pois}).mark_area(
opacity=0.15, color="firebrick"
).encode(x="x:Q", y="lower:Q", y2="upper:Q")
true_pois = [
{"x": float(x_pois_new[i]), "true": float(np.exp(1.0 + 0.8 * np.sin(x_pois_new[i])))}
for i in range(len(x_pois_new))
]
true_pois_line = alt.Chart({"values": true_pois}).mark_line(
color="gray", strokeDash=[4, 4], strokeWidth=1.5
).encode(x="x:Q", y="true:Q")
(band_pois + points_pois + line_pois + true_pois_line).properties(
width="container", height=320,
title="CV+ conformal prediction for Poisson counts"
)
For Poisson and binomial models, conformal intervals on the response scale can extend below zero. This is a consequence of the additive residual-based construction. If you need intervals that respect the natural constraints of the response, clip the lower bound: np.maximum(result.lower, 0).
Coverage verification for Poisson
# Test set for Poisson data
x_pois_test = rng.uniform(0, 2 * np.pi, 500)
y_pois_test = rng.poisson(np.exp(1.0 + 0.8 * np.sin(x_pois_test))).astype(float)
cov_pois = wk.conformal_coverage(
predictor_pois,
{"x": x_pois_test, "y": y_pois_test},
response="y",
)
print(f"Poisson CV+ coverage: {cov_pois:.4f} (nominal: 0.95)")
Poisson CV+ coverage: 0.9860 (nominal: 0.95)
Where to go next
- Prediction and inference: Bayesian confidence intervals, standard errors, and term-level decomposition.
- Diagnostics: residual plots and model checking to assess whether parametric intervals are trustworthy.
- Response families: all supported distributions and their link functions.