GAM thin plate regression spline
Introduction
While we have already implemented GAM using smoothers with only one predictor, thin plate regression splines are used to implement smoothers with one or more predictors. We follow the implementation in [1] closely here. For more theoretical treatments on the subject, please refer to [1] and [2].
The following information is on regression. However, the translation to other families is straight-forward. For regression, we aim to approximate the response with . For the binomial family, we aim to approximate the log-odds with .
A simple linear GAM model built using smoothers from multiple predictors
Thin plate splines are used to estimate smooth functions of multiple predictor variables from noisy observations of the function at particular values of those predictors. Consider:
where:
- ,
- is the number of predictors in the smooth function .
Thin plate spline smoothing estimates by finding function by minimizing:
where:
- ,
- ,
- is a penalty measure of wiggliness of ,
- is the smoothing parameter (scale parameter) controlling the tradeoff between data fitting and smoothness of ,
- ,
- .
The function that minimizes Equation 1 has the following form:
which is subject to the constraint where each element of is and where:
- ;
- are the polynomial basis functions with order = 0,1,…,m-1;
- .
However, Equation 2 cannot be implemented due to the fact that all the rows of the dataset are used in generating the function .
Knot-based approximation of
Instead of using all the data points in the training set, only a subset of knots are used in the approximation of as follows:
where is the number of knots. The coefficients , can be obtained by minimizing the following objective function:
where:
- ;
- is a by matrix with zeros everywhere except in its upper left by blocks where and are knots.
Generation of
The data matrix consists of two parts: . First, we will generate , which consists of the distance measure part. is by in dimension, and the element is calculated as:

Generation of penalty matrix
Note that the penalty matrix . It is the distance measure calculated using only the knot points.
Generation of the polynomial basis
Let be the number of predictors included in the thin plate regression smoother, and let be the highest degree of the polynomial basis function used. We can calculate from by using the formula . The total number of polynomial basis function is determined by the formula . We will illustrate how this is done with two examples:
Polynomial basis for
In this case, and . The size of the polynomial basis is 3, and the polynomial basis consists of polynomials of degrees 0 and 1. When the two predictors are set as , the polynomial basis will consist of . consists of one column of ones, predictor , and predictor . The size of is by .
Polynomial basis for
In this case, and . The size of the polynomial basis is 15, and the polynomial basis consists of polynomials of degrees 0, 1, and 2. The four predictors are . consists of:
- one zero degree polynomial: one column of ones;
- four degree one polynomials: ;
- ten degree 2 polynomials: .
The size of is by . The size of the polynomial basis grows rapidly as the number of predictors increase in the thin plate regression smoother.
Generation of
Remember that is defined as . Therefore, is of size by . However, is only evaluated at the knots chosen by the user. Hence, by using the example of and letting the two predictors be , contains:

Absolving the constraint via matrix transformation
The constraint is equivalent to and is by . The following transformations are applied:
- Generate the QR decomposition of (which is equivalent to the QR decomposition of ). Therefore, rewrite where is by , and is by ;
- Next, generate an orthogonal basis which is by , and is orthogonal to . This will force the condition that in setting the number of knots.
- is easily generated by first generating the random vector. Next, use Gram-Schmidt to make the random vectors orthogonal to and to each other.
- Set and rewrite .
Let's also:
- decompose into two parts as where is by and is by ;
Let's rewrite the new objective with this decomposition:

Note that is by .
Sum-to-zero constraints implementation
This will follow the Identifiability constraints rules for GAM. Let be the model matrix that contains the basis functions of one predictor variable; the sum-to-zero constraints require that where contains the coefficients relating to the basis functions of that particular predictor column. The idea is to create a by matrix such that , then for any . is generated by using the Householder transform. Please refer to [3] for details. Therefore, we have . Rewrite the objective function again and we will have
and we will be solving for . Then, we will obtain . Last, we will obtain the original by multiplying the part of the coefficeints not corresponding to the polynomial basis with like .
Specifying GAM columns
Two ways to specify GAM columns for thin plate regression are available. Following the below example, gam_columns can be specified as:
For R:
gam_col1 <- list("C11", c("C12","C13"), c("C14", "C15", "C16"), "C17", "C18")orgam_col1 <- list(c("C11"), c("C12","C13"), c("C14", "C15", "C16"), c("C17"), c("C18"))
For Python:
gam_col1 = ["C11",["C12","C13"],["C14","C15","C16"],"C17","C18"]orgam_col1 = [["C11"],["C12","C13"],["C14","C15","C16"],["C17"],["C18"]]
When using a grid search, the GAM columns are specified inside of the subspaces hyperparameter. Otherwise, the gam_column parameter is entered on its own when building a GAM model.
Normal GAM model
- R
- Python
#Import the train and test datasets:
train <- h2o.importFile("https://s3.amazonaws.com/h2o-public-test-data/smalldata/glm_test/gaussian_20cols_10000Rows.csv")
test <- h2o.importFile("https://s3.amazonaws.com/h2o-public-test-data/smalldata/glm_test/gaussian_20cols_10000Rows.csv")
# Set the factors:
train$C1 <- h2o.asfactor(train$C1)
train$C2 <- h2o.asfactor(train$C2)
test$C1 <- h2o.asfactor(test$C1)
test$C2 <- h2o.asfactor(test$C2)
# Set the predictors, response, & GAM columns:
predictors <- c("C1", "C2")
response = "C21"
gam_col1 <- list("C11", c("C12","C13"), c("C14", "C15", "C16"), "C17", "C18")
# Build and train the model:
gam_model <- h2o.gam(x = predictors, y = response,
gam_columns = gam_col1, training_frame = train,
validation_frame = test, family = "gaussian",
lambda_search = TRUE)
# Retrieve the coefficients:
coefficients <- h2o.coef(gam_model)
from h2o.estimators import H2OGeneralizedAdditiveEstimator
# Import the train dataset and set the factors:
train = h2o.import_file("https://s3.amazonaws.com/h2o-public-test-data/smalldata/glm_test/multinomial_10_classes_10_cols_10000_Rows_train.csv")
train["C11"] = train["C11"].asfactor()
train["C1"] = train["C1"].asfactor()
train["C2"] = train["C2"].asfactor()
# Import the test dataset and set the factors:
test = h2o.import_file("https://s3.amazonaws.com/h2o-public-test-data/smalldata/glm_test/multinomial_10_classes_10_cols_10000_Rows_train.csv")
test["C11"] = test["C11"].asfactor()
test["C1"] = test["C1"].asfactor()
test["C2"] = test["C2"].asfactor()
# Set the predictors, response, and gam_cols:
x = ["C1", "C2"]
y = "C11"
gam_cols1 = ["C6", ["C7","C8"], "C9", "C10"]
gam_cols2 = [["C6"], ["C7", "C8"], ["C9"], ["C10"]]
# Build and train the two models:
h2o_model1 = H2OGeneralizedAdditiveEstimator(family='multinomial', gam_columns=gam_cols1, bs=[1,1,0,0], max_iterations=2)
h2o_model1.train(x=x, y=y, training_frame=train, validation_frame=test)
h2o_model2 = H2OGeneralizedAdditiveEstimator(family='multinomial', gam_columns=gam_cols2, bs=[1,1,0,0], max_iterations=2)
h2o_model2.train(x=x, y=y, training_frame=train, validation_frame=test)
# Retrieve the coefficients:
print(h2o_model1.coef())
print(h2o_model2.coef())
Grid search
- R
- Python
# Import the train dataset:
h2o_data <- h2o.importFile("https://s3.amazonaws.com/h2o-public-test-data/smalldata/gam_test/synthetic_20Cols_gaussian_20KRows.csv")
# Set the factors:
h2o_data$response <- h2o.asfactor(h2o_data$response)
h2o_data$C3 <- h2o.asfactor(h2o_data$C3)
h2o_data$C7 <- h2o.asfactor(h2o_data$C7)
h2o_data$C8 <- h2o.asfactor(h2o_data$C8)
h2o_data$C10 <- h2o.asfactor(h2o_data$C10)
# Set the predictors and response:
xL <- c("c_0", "c_1", "c_2", "c_3", "c_4", "c_5", "c_6", "c_7", "c_8",
"c_9", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10")
yR = "response"
# Set up the search criteria and hyperparameters:
search_criteria <- list()
search_criteria$strategy <- 'RandomDiscrete'
search_criteria$seed <- 1
hyper_parameters <- list()
hyper_parameters$lambda = c(1, 2)
subspace <- list()
subspace$scale <- list(c(0.001, 0.001, 0.001), c(0.002, 0.002, 0.002))
subspace$num_knots <- list(c(5, 10, 12), c(6, 11, 13))
subspace$bs <- list(c(1, 1, 1), c(0, 1, 1))
subspace$gam_columns <- list(list("c_0", c("c_1", "c_2"), c("c_3", "c_4", "c_5")), list("c_1", c("c_2", "c_3"), c("c_4", "c_5", "c_6")))
hyper_parameters$subspaces <- list(subspace)
# Build and train the grid:
gam_grid = h2o.grid("gam", grid_id="GAMModel1", x=xL, y=yR,
training_frame=h2o_data, family='binomial',
hyper_params=hyper_parameters, search_criteria=search_criteria)
# Retrieve the coefficients:
coefficients <- h2o.coef(gam_grid)
from h2o.estimators import H2OGeneralizedAdditiveEstimator
from h2o.grid.grid_search import H2OGridSearch
# Import the train dataset:
h2o_data = h2o.import_file("https://s3.amazonaws.com/h2o-public-test-data/smalldata/gam_test/synthetic_20Cols_gaussian_20KRows.csv")
# Set the factors:
h2o_data['response'] = h2o_data['response'].asfactor()
h2o_data['C3'] = h2o_data['C3'].asfactor()
h2o_data['C7'] = h2o_data['C7'].asfactor()
h2o_data['C8'] = h2o_data['C8'].asfactor()
h2o_data['C10'] = h2o_data['C10'].asfactor()
# Set the predictors and response:
names = h2o_data.names
myY = "response"
myX = names.remove(myY)
# Set the search criteria and hyperparameters:
search_criteria = {'strategy': 'RandomDiscrete', "seed": 1}
hyper_parameters = {'lambda': [1, 2],
'subspaces': [{'scale': [[0.001], [0.0002]], 'num_knots': [[5], [10]], 'bs':[[1], [0]], 'gam_columns': [[["c_0"]], [["c_1"]]]},
{'scale': [[0.001, 0.001, 0.001], [0.0002, 0.0002, 0.0002]],
'bs':[[1, 1, 1], [0, 1, 1]],
'num_knots': [[5, 10, 12], [6, 11, 13]],
'gam_columns': [[["c_0"], ["c_1", "c_2"], ["c_3", "c_4", "c_5"]],
[["c_1"], ["c_2", "c_3"], ["c_4", "c_5", "c_6"]]]}]}
# Build and train the grid:
gam_grid = H2OGridSearch(H2OGeneralizedAdditiveEstimator(family="binomial", keep_gam_cols=True),
hyper_params=hyper_parameters,
search_criteria=search_criteria)
gam_grid.train(x = myX, y = myY, training_frame = h2o_data)
# Check the coefficients:
coefficeints = gam_grid.coef()
References
- Simon N. Wood, Generalized Additive Models An Introduction with R, Texts in Statistical Science, CRC Press, Second Edition.
- T.J. Hastie, R.J. Tibshirani, Generalized Additive Models, Chapman and Hall, First Edition, 1990.
- Wendy C Wong, Gam.doc.
- Submit and view feedback for this page
- Send feedback about H2O-3 Secure to cloud-feedback@h2o.ai