# Use Select Smoothing to Shrink Away a Term

Standard GAM smooths are penalised to control wiggliness, but they cannot shrink entirely to zero -- a term always contributes at least a linear effect. Select smoothing adds a second penalty that pushes the entire smooth toward zero when it has no explanatory power. This lets the fitting process perform automatic variable selection, analogous to LASSO in linear models.

Enable it globally with `select=True`, or per-term with the shrinkage cubic spline basis `bs='cs'`.


# Set up the data

Load the `wages` dataset and add a synthetic noise column -- a predictor that is genuinely unrelated to wages. The goal is to show that select smoothing identifies and discards it.


``` python
# Import libraries
import whittaker as wk
import numpy as np

# Load data and add synthetic noise predictor
data = wk.load_dataset("wages", as_frame=True)

rng = np.random.default_rng(23)
data["noise"] = rng.normal(0, 1, len(data))
```


# Fit without select smoothing

First, fit the model in the default way so you have a baseline for comparison. All three terms will receive some EDF regardless of whether they are truly useful.


``` python
# Fit standard GAM as baseline
m_standard = wk.GAM("wage ~ s(age) + s(experience) + s(noise)").fit(data)
m_standard.edf
```


    [4.027847821294607, 3.163582450809665, 1.0005439269452188]


Each smooth is allocated some EDF even if its contribution is negligible. The noise term will have a non-zero EDF here simply because the standard penalty cannot reach zero.


# Fit with select=True

Now refit using `select=True`. The extra penalty allows any smooth to be shrunk completely out of the model.


``` python
# Refit with select=True to enable extra shrinkage penalty
m_select = wk.GAM(
    "wage ~ s(age) + s(experience) + s(noise)",
).fit(data, select=True)
m_select.edf
```


    [3.994041131322576, 3.1619520817917213, 0.0036464502279951265]


The EDF values are returned in the order the terms appear in the formula: `age`, `experience`, `noise`. The noise term's EDF should be close to zero, indicating that the penalty has effectively removed it.


# Read the summary


``` python
m_select.summary()
```


    GAM fit summary
    ============================================================
    Formula:    wage ~ s(age) + s(experience) + s(noise)
    Family:     Gaussian(link='identity')
    Inference:  GCV
    Observations: 800
    Coefficients: 28

    Parametric coefficients:
      Term                       Estimate    Std.Err    t value    p-value
      ------------------------ ---------- ---------- ---------- ----------
      (Intercept)                 45.2167     0.4174    108.339    < 1e-16

    Approximate significance of smooth terms:
      Term                        EDF Ref.df     Chi.sq    p-value
      ------------------------ ------ ------ ---------- ----------
      s(age)                     3.99      4   1362.498    < 1e-16
      s(experience)              3.16      4    184.423    < 1e-16
      s(noise)                   0.00      1      0.000     0.9851

    Total EDF:  8.16
    Scale est:  139.354698
    Deviance:   110346.6741
    Null dev:   374733.5230
    Dev. expl:  70.6%
    GCV score:  140.790700
    AIC:        6228.08
    BIC:        6266.30


In the smooth terms table, look at the EDF column for `s(noise)`. An EDF near 0 means the term has been shrunk away and contributes almost nothing to the predictions. The real predictors -- `age` and `experience` -- retain meaningful EDF.


# Alternative: per-term shrinkage basis

Instead of the global flag, you can apply a shrinkage cubic spline (`bs='cs'`) to individual terms. This is useful when you want select behaviour on only some smooths.


``` python
# Fit with per-term shrinkage cubic spline basis
m_cs = wk.GAM(
    "wage ~ s(age, bs='cs') + s(experience, bs='cs') + s(noise, bs='cs')"
).fit(data)
m_cs.edf
```


    [3.985475625883009, 2.097999483251983, 8.306894703760463e-05]


The result should be comparable to `select=True`. Choose `bs='cs'` when you want fine-grained control over which terms are eligible for shrinkage.


# Compare EDF across approaches


``` python
# Compare total EDF across all three approaches
m_standard.edf_total, m_select.edf_total, m_cs.edf_total
```


    (9.19197419904949, 8.159639663342293, 7.08355817808203)


The select models should have lower total EDF because the noise term has been shrunk out. A lower total EDF with similar or better deviance explained is a sign the extra penalty is doing useful work.


# When to use select smoothing

| Situation | Recommendation |
|----|----|
| Many candidate predictors, unsure which matter | `select=True` for a single pass |
| Mixed confidence in predictors | `bs='cs'` on uncertain terms only |
| All predictors known to be relevant | Standard smooths; select adds overhead |

Select smoothing is most valuable in exploratory modeling where you have more candidate predictors than you expect to need. It does not replace domain knowledge, but it provides a principled way to let the data confirm or dismiss a term.
