Stasinopoulos 2007 - Bayesian P-Splines and Distributional Regression in PyMC
Summary
GAMLSS’s penalized likelihood ℓp is the log-posterior of a model with Gaussian random-walk priors on spline coefficients. In PyMC you give each distribution parameter its own P-spline with a second-order random-walk (RW2) prior, a log link for positive scales, and a hyperprior on the smoothing variance. This note is a concept translation (not in the 2007 paper); the PyMC code is untested pseudo-real code.
Overview
Equation (6) of the paper, ℓp=ℓ−21∑λjkγjk⊤Gjkγjk, equals logp(y∣γ)+logp(γ∣λ) up to constants with prior γ∼N(0,(λK)−). GAMLSS therefore returns the posterior mode for fixed λ; a Bayesian analysis samples (γ,λ) jointly and propagates smoothing uncertainty into the centile curves.
Main Content
Bayesian P-spline (RW2)
For parameter k, with B-spline basis B(x)∈Rn×q and difference matrix D2 ((q−2)×q),
The smoothing parameter is λk=τk−2; the null space of D2′D2 (constant and linear) is handled by the intercept and a free linear slope (“centred RW2”). Equivalent GAMLSS ps(x, order=2).
Growth-chart model for E41 (PyMC 5 sketch, untested)
import numpy as np, pymc as pm, pytensor.tensor as ptfrom patsy import dmatrix# x = age (or age**xi), y = BMI, B = (n, q) cubic B-spline design matrixB = np.asarray(dmatrix("bs(x, df=20, degree=3, include_intercept=False) - 1", {"x": x}))q = B.shape[1]def rw2_spline(name, scale_sd=1.0): tau = pm.HalfNormal(f"tau_{name}", scale_sd) z = pm.Normal(f"z_{name}", 0, 1, shape=q) # non-centred increments g = pt.cumsum(pt.cumsum(z * tau)) / q # RW2 via double cumsum b0 = pm.Normal(f"b0_{name}", 0, 2) b1 = pm.Normal(f"b1_{name}", 0, 1) # linear part return b0 + b1 * x_std + pt.dot(B, g)with pm.Model() as m: mu = rw2_spline("mu") # identity link (centred on data scale) sigma = pt.exp(rw2_spline("sigma", 0.5)) # log link nu = rw2_spline("nu", 0.5) # Box-Cox power, identity # LMS: Z = ((y/M)**nu - 1)/(nu*S) ~ Normal(0,1), with Jacobian y**(nu-1)/(M**nu * S) M = pt.exp(mu) z = ((y / M) ** nu - 1) / (nu * sigma) pm.Potential("lms", pt.sum(-0.5 * z**2 + (nu - 1) * pt.log(y) - nu * pt.log(M) - pt.log(sigma))) idata = pm.sample(1000, tune=1000, target_accept=0.95)
Notes: (i) replace Normal by Student-t for BCT (add a tau = 2 + HalfNormal-type df); (ii) the Jacobian term is the Box-Cox change of variables; (iii) restrict ∣νσz∣<1 or the support is violated — use a centred parametrisation and tight priors on ν; (iv) E41 compares constant spread, smooth spread, skewed and heavy-tailed variants by LOO.
Comparing variants. Fit (a) σ constant, (b) smooth σ, (c) add ν (skew), (d) add τ (heavy tails) and compare by PSIS-LOO or by held-out log score; the Bayesian counterpart of GAIC/VGD (Stasinopoulos 2007 - Fitting Algorithms and Model Selection). Check calibration by PIT / normalised quantile residuals binned by age (worm-plot analogue; Posterior Predictive Checking).
Pitfalls. BCCG/BCT likelihood has a data-dependent support when ν=0; the paper warns about likelihood singularities for small samples (penalise σ→0). With many P-spline blocks use non-centred increments and target_accept≥0.9.
Examples
Alternative smoother: a Hilbert Space Gaussian Processes prior on each ηk(x) gives the same distributional structure with kernel-based smoothness and fewer knot-placement decisions.