Fit a Monotone Increasing Smooth

Constrain a smooth to be monotone increasing using bs=‘mpi’.

A standard penalized spline can bend in any direction, which is appropriate when the relationship’s shape is unknown. When economic or scientific reasoning guarantees that a predictor can only push the response upward (wages rising with experience, dose-response curves) imposing a monotone constraint protects against overfitting and produces an interpretable smooth. The bs="mpi" basis enforces monotone increase while otherwise behaving like any other GAM smooth.

Fit both models

Load the wages dataset and fit two models side by side: one with the monotone increasing basis and one with the default thin-plate spline. The experience predictor is a natural candidate for a monotone constraint (wages generally rise as a worker accumulates experience, not fall).

import whittaker as wk

# Load data and fit constrained and unconstrained models
data = wk.load_dataset("wages")

m_constrained   = wk.GAM("wage ~ s(experience, bs='mpi')", family=wk.Gamma()).fit(data)
m_unconstrained = wk.GAM("wage ~ s(experience, bs='tp')",  family=wk.Gamma()).fit(data)

Summarize the constrained model

The summary for the monotone smooth reports the same statistics as any other term: effective degrees of freedom, reference degrees of freedom, and a p-value. A lower EDF reflects the extra rigidity imposed by the shape constraint.

m_constrained.summary()
GAM fit summary
============================================================
Formula:    wage ~ s(experience, bs='mpi')
Family:     Gamma(link='log')
Inference:  GCV
Observations: 800
Coefficients: 20

Parametric coefficients:
  Term                       Estimate    Std.Err    t value    p-value
  ------------------------ ---------- ---------- ---------- ----------
  (Intercept)                  4.1535     0.0551     75.429    < 1e-16

Approximate significance of smooth terms:
  Term                        EDF Ref.df     Chi.sq    p-value
  ------------------------ ------ ------ ---------- ----------
  s(experience, bs='mpi')    2.33      3    139.457    < 1e-16

Total EDF:  3.33
Scale est:  0.217939
Deviance:   173.6248
Null dev:   205.9584
Dev. expl:  15.7%
GCV score:  0.218850
AIC:        6976.20
BIC:        6991.81

Compare the partial effects

Call model.plot() on each model to compare the estimated smooths. The constrained smooth will never reverse direction and the unconstrained smooth can dip or rise locally wherever the data pull it.

m_constrained.plot()
m_unconstrained.plot()

Check diagnostics

Run wk.check() on the constrained model to confirm the residuals look reasonable. Shape constraints do not change the residual structure when the constraint is consistent with the data.

wk.check(m_constrained)

When to use bs="mpi"

Situation Recommendation
Strong prior that effect is non-decreasing bs="mpi"
Effect direction uncertain bs="tp" (default)
Enforcing monotone decrease bs="mpd"

Use bs="mpi" when subject-matter knowledge justifies the constraint and you want the model to respect it even in data-sparse regions where an unconstrained smooth might wander.