# Fit a Monotone Increasing Smooth

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).


``` python
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.


``` python
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.


``` python
m_constrained.plot()
```


<style>
  #altair-viz-8547348105d04565ba5cb08df2b7f261.vega-embed {
    width: 100%;
    display: flex;
  }

  #altair-viz-8547348105d04565ba5cb08df2b7f261.vega-embed details,
  #altair-viz-8547348105d04565ba5cb08df2b7f261.vega-embed details summary {
    position: relative;
  }
</style>


``` python
m_unconstrained.plot()
```


<style>
  #altair-viz-96593f336c5e451bb96d60709db25269.vega-embed {
    width: 100%;
    display: flex;
  }

  #altair-viz-96593f336c5e451bb96d60709db25269.vega-embed details,
  #altair-viz-96593f336c5e451bb96d60709db25269.vega-embed details summary {
    position: relative;
  }
</style>


# 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.


``` python
wk.check(m_constrained)
```


<style>
  #altair-viz-bd424202b0d24d40919f1819309c6b8b.vega-embed {
    width: 100%;
    display: flex;
  }

  #altair-viz-bd424202b0d24d40919f1819309c6b8b.vega-embed details,
  #altair-viz-bd424202b0d24d40919f1819309c6b8b.vega-embed details summary {
    position: relative;
  }
</style>


# 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.
