23  Additive Models and Basis Expansions in rtemis

rtemis fits generalized additive models with setup_GAM(), using mgcv::gam(); multivariate adaptive regression splines with setup_MARS(), using the earth package; the highly adaptive lasso with setup_HAL(), using hal9001; and isotonic regression with setup_Isotonic(). This chapter fits them to the diabetes and heart failure data and compares their cross-validated performance with that of the linear models of Section 15.5.

db <- read_diabetes()
hf <- read_heart_failure()
cv <- setup_KFold(n_resamples = 10L, seed = 2026L)

23.1 Generalized additive models

setup_GAM(k = 5L) fits a penalized spline with a basis of dimension 5 for each numeric feature and a linear term for each factor:

mod_gam <- train(
  db,
  hyperparameters = setup_GAM(k = 5L),
  execution_config = setup_SerialExecution(seed = 2026L),
  question = diabetes_question,
  verbosity = 0L
)
summary(mod_gam@model)

Family: gaussian 
Link function: identity 

Formula:
progression ~ s(age, k = 5) + s(bmi, k = 5) + s(bp, k = 5) + 
    s(s1, k = 5) + s(s2, k = 5) + s(s3, k = 5) + s(s4, k = 5) + 
    s(s5, k = 5) + s(s6, k = 5) + sex

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)   163.48       3.68   44.43  < 2e-16 ***
sex2          -24.23       5.75   -4.21  3.1e-05 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
        edf Ref.df     F p-value    
s(age) 2.68   3.21  2.38   0.064 .  
s(bmi) 1.76   2.21 29.22 < 2e-16 ***
s(bp)  1.00   1.00 26.54 8.8e-07 ***
s(s1)  1.00   1.00  0.86   0.354    
s(s2)  1.00   1.00  0.57   0.449    
s(s3)  1.00   1.00  0.27   0.601    
s(s4)  1.00   1.00  1.03   0.311    
s(s5)  2.39   2.83 12.35 2.8e-05 ***
s(s6)  2.10   2.61  2.67   0.050 .  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.533   Deviance explained = 54.9%
GCV = 2880.4  Scale est. = 2776.5    n = 442

The edf column gives the effective degrees of freedom of each smooth. A value of 1 means that the penalty reduced the smooth to a straight line: in this model, the smooths of bp, s1, s2, s3, s4 are linear, and those of age, bmi, s5, s6 are curved. The partial effect of each feature, its contribution \(f_j(x_j)\) to the prediction, is returned by predict(type = "terms") on the mgcv model:

terms_gam <- predict(mod_gam@model, type = "terms")
partial <- function(v) {
  o <- order(db[[v]])
  list(x = db[[v]][o], y = terms_gam[o, paste0("s(", v, ")")])
}
draw_panels(
  lapply(c("age", "bmi", "s5", "s6"), function(v) {
    pe <- partial(v)
    draw_line(
      pe[["x"]], list(`Partial effect` = pe[["y"]]),
      points = FALSE, xlab = v, ylab = "Contribution to progression"
    )
  }),
  ncol = 2L,
  height = 640
)

Each curve shows how the predicted progression changes with one feature, holding the others fixed, on the scale of the outcome. Cross-validation assesses the model:

mod_gam_cv <- train(
  db,
  hyperparameters = setup_GAM(k = 5L),
  outer_resampling_config = cv,
  execution_config = setup_SerialExecution(seed = 2026L),
  question = diabetes_question,
  verbosity = 0L
)

23.2 Multivariate adaptive regression splines

setup_MARS() fits an additive MARS model by default (degree = 1L), and allows interactions between pairs of hinge functions with degree = 2L:

mod_mars <- train(
  db,
  hyperparameters = setup_MARS(),
  execution_config = setup_SerialExecution(seed = 2026L),
  verbosity = 0L
)
summary(mod_mars@model)
Call: earth(x=matrix[442,11], y=c(151,75,141,20...), pmethod="backward",
            trace=0, degree=1, nfold=0, ncross=1, stratify=TRUE, thresh=0.001,
            minspan=0, endspan=0, newvar.penalty=0, fast.k=20, fast.beta=1)

             coefficients
(Intercept)        64.248
sex_2             -24.666
h(38-age)           1.927
h(age-38)           0.662
h(bmi-22.2)         5.925
h(99-bp)           -0.753
h(bp-99)            1.550
h(249-s1)           1.211
h(181.2-s2)        -0.998
h(s5-4.0254)       77.420
h(s6-94)            1.365

Selected 11 of 20 terms, and 8 of 11 predictors
Termination condition: Reached nk 23
Importance: bmi, s5, bp, s1, s2, sex_2, s6, age, sex_1-unused, s3-unused, ...
Number of terms at each degree of interaction: 1 10 (additive model)
GCV 2951    RSS 1183375    GRSq 0.5046    RSq 0.5485

Each term h(x - t) or h(t - x) is a hinge function with knot t. The summary reports how many of the terms generated by the forward pass were kept after pruning, and the generalized cross-validation estimate of \(R^2\) (GRSq). get_varimp() returns earth’s importance measures:

plot_varimp(mod_mars)
mod_mars_cv <- train(
  db,
  hyperparameters = setup_MARS(),
  outer_resampling_config = cv,
  execution_config = setup_SerialExecution(seed = 2026L),
  question = diabetes_question,
  verbosity = 0L
)
mod_mars2_cv <- train(
  db,
  hyperparameters = setup_MARS(degree = 2L),
  outer_resampling_config = cv,
  execution_config = setup_SerialExecution(seed = 2026L),
  question = diabetes_question,
  verbosity = 0L
)

23.3 The highly adaptive lasso

setup_HAL(max_degree = 1L) restricts the basis to functions of single features, an additive model; smoothness_orders = 1L, the default, uses hinge functions, and 0L would use indicator functions. The penalty is selected by internal cross-validation, whose fold assignment is fixed by seed:

mod_hal_cv <- train(
  db,
  hyperparameters = setup_HAL(max_degree = 1L, seed = 2026L),
  outer_resampling_config = cv,
  execution_config = setup_SerialExecution(seed = 2026L),
  question = diabetes_question,
  verbosity = 0L
)

HAL is the slowest of the methods in this chapter: its basis contains one function per observed value of each feature, and its fit is a lasso over all of them, cross-validated within each training fold.

23.4 Comparison

mod_glm_cv <- train(
  db,
  hyperparameters = setup_GLM(),
  outer_resampling_config = cv,
  execution_config = setup_SerialExecution(seed = 2026L),
  verbosity = 0L
)
compare_db <- rbindlist(lapply(
  list(
    GLM = mod_glm_cv,
    GAM = mod_gam_cv,
    `MARS, degree 1` = mod_mars_cv,
    `MARS, degree 2` = mod_mars2_cv,
    `HAL, degree 1` = mod_hal_cv
  ),
  function(m) {
    data.table(
      rsq = m@metrics_test@mean_metrics[["rsq"]],
      rsq_sd = m@metrics_test@sd_metrics[["rsq"]],
      rmse = m@metrics_test@mean_metrics[["rmse"]]
    )
  }
), idcol = "model")
compare_db
            model    rsq  rsq_sd  rmse
           <char>  <num>   <num> <num>
1:            GLM 0.4935 0.08864 54.43
2:            GAM 0.5112 0.07796 53.51
3: MARS, degree 1 0.4721 0.08536 55.58
4: MARS, degree 2 0.4654 0.07196 56.01
5:  HAL, degree 1 0.4660 0.08097 55.97

The GAM has the highest mean test \(R^2\), 0.511 against 0.493 for the linear model, a difference much smaller than the standard deviation across folds. Since all models were assessed on the same folds, their per-fold results can be compared directly:

fold_rsq <- function(m) {
  vapply(m@metrics_test@res_metrics, function(x) x@metrics[["rsq"]], numeric(1))
}
diff_gam_glm <- fold_rsq(mod_gam_cv) - fold_rsq(mod_glm_cv)
diff_gam_glm
   Fold_1    Fold_2    Fold_3    Fold_4    Fold_5    Fold_6    Fold_7    Fold_8 
 0.047113 -0.003861 -0.018685  0.032624  0.041075  0.011495 -0.001762  0.008602 
   Fold_9   Fold_10 
 0.042019  0.018976 

The GAM’s \(R^2\) is higher than the linear model’s in 7 of the 10 folds. The standard deviation of the paired differences, 0.022, is smaller than that of either model’s fold results, 0.078 for the GAM, because both models face the same test cases in each fold. The differences are still not independent across folds, for the reasons of Section 10.9. The interaction terms of the degree-2 MARS model do not improve on the additive model, consistent with Section 19.3.

23.5 Classification

MARS fits binary outcomes with a logistic link. For the heart failure data:

mod_mars_hf <- train(
  hf,
  hyperparameters = setup_MARS(),
  outer_resampling_config = cv,
  execution_config = setup_SerialExecution(seed = 2026L),
  question = hf_question
)
<Resampled Classification Model>
MARS (Multivariate Adaptive Regression Splines (earth))
⟳ Tested using 10 independent folds.

  <Resampled Classification Training Metrics>
  Aggregate Confusion Matrix across resamples.
                     Predicted
          Reference  Yes  No    
                Yes  146   340
                 No   68  2056

  Showing mean (sd) across resamples.
        Sensitivity: 0.300 (0.071)
        Specificity: 0.968 (0.009)
  Balanced Accuracy: 0.634 (0.035)
                PPV: 0.682 (0.073)
                NPV: 0.858 (0.012)
                 F1: 0.414 (0.075)
           Accuracy: 0.844 (0.014)
                AUC: 0.807 (0.021)
        Brier Score: 0.115 (0.007)

  <Resampled Classification Test Metrics>
  Aggregate Confusion Matrix across resamples.
                     Predicted
          Reference  Yes  No   
                Yes   11   43
                 No   11  225

  Showing mean (sd) across resamples.
        Sensitivity: 0.200 (0.132)
        Specificity: 0.953 (0.047)
  Balanced Accuracy: 0.577 (0.070)
                PPV: NA (NA)
                NPV: 0.840 (0.024)
                 F1: NA (NA)
           Accuracy: 0.813 (0.045)
                AUC: 0.714 (0.146)
        Brier Score: 0.144 (0.032)

The mean test AUC, 0.714, is similar to that of the logistic regression with the same folds.

23.6 Isotonic regression

setup_Isotonic() fits a non-decreasing function of a single feature. Its use for recalibrating probabilities is shown in Chapter 43. As a regression of progression on s5 alone:

mod_iso <- train(
  db[, .(s5, progression)],
  hyperparameters = setup_Isotonic(),
  execution_config = setup_SerialExecution(seed = 2026L),
  verbosity = 0L
)
s5_grid <- data.table(s5 = sort(unique(db[["s5"]])))
draw_fits(
  db[["s5"]], db[["progression"]],
  x_fit = s5_grid[["s5"]],
  fits = list(Isotonic = as.numeric(predict(mod_iso, s5_grid))),
  xlab = "s5", ylab = "Disease progression",
  point_alpha = 0.4
)