Fit a Monotone Decreasing Smooth

Constrain a smooth to be monotone decreasing using bs=‘mpd’.

Shape-constrained smoothing is most useful when domain knowledge dictates the direction of an effect. Heavy metals in river floodplains dilute with distance from the source so zinc concentration can only decrease (or stay flat) as you move further from the river, never increase. The bs="mpd" basis encodes this prior directly in the model, preventing the smooth from producing implausible upward turns in regions with sparse data.

Fit

Load the meuse dataset, which records zinc concentrations at locations across a Rhine floodplain. The predictor dist is the standardized distance from the river bank and zinc is the response.

import whittaker as wk

# Load data and fit monotone decreasing smooth
data = wk.load_dataset("meuse")

model = wk.GAM("zinc ~ s(dist, bs='mpd')").fit(data)

Summarize

The summary confirms the smooth’s complexity via its effective degrees of freedom. A monotone constraint restricts the function space, so EDF is often lower than an unconstrained equivalent.

model.summary()
GAM fit summary
============================================================
Formula:    zinc ~ s(dist, bs='mpd')
Family:     Gaussian(link='identity')
Inference:  GCV
Observations: 155
Coefficients: 20

Parametric coefficients:
  Term                       Estimate    Std.Err    t value    p-value
  ------------------------ ---------- ---------- ---------- ----------
  (Intercept)                504.6421    48.4040     10.426    < 1e-16

Approximate significance of smooth terms:
  Term                        EDF Ref.df     Chi.sq    p-value
  ------------------------ ------ ------ ---------- ----------
  s(dist, bs='mpd')          1.45      2     57.222  3.753e-13

Total EDF:  2.45
Scale est:  97965.885389
Deviance:   14944836.6468
Null dev:   20607458.7006
Dev. expl:  27.5%
GCV score:  99538.309696
AIC:        2223.64
BIC:        2231.09

Visualize the partial effect

model.plot() draws the estimated smooth against the predictor. The curve should decrease monotonically from left to right. Near the river bank, zinc is high but further away, it can only be equal or lower.

model.plot()

Check diagnostics

Inspect the four diagnostic panels to assess whether residuals are well-behaved. Systematic structure in the residuals vs fitted plot would suggest the smooth is too rigid and an unconstrained basis might fit better.

wk.check(model)

Choosing between constrained and unconstrained

Fit an unconstrained model to see whether the data actually support the monotone assumption, or whether the smooth would prefer a different shape when given the freedom to do so.

# Fit unconstrained model to verify monotone assumption
m_unconstrained = wk.GAM("zinc ~ s(dist, bs='tp')").fit(data)
m_unconstrained.plot()

If the unconstrained smooth is also monotonically decreasing, the constraint and the data agree. Use bs="mpd" to enforce the shape explicitly. If the unconstrained smooth turns upward somewhere, investigate the cause before committing to the constraint.