A limitation of the sail method is that the same basis expansion function \(f(\cdot)\) is applied to all columns of the predictor matrix \(\mathbf{X}\). Being able to automatically select linear vs. nonlinear components was not a focus of our paper, but is an active area of reserch for main effects only e.g. ref1 and ref2.

However, if the user has some prior knowledge on possible effect relationships, then they can supply their own design matrix. This can be useful for example, when one has a combination of categorical (e.g. gender, race) and continuous variables, but would only like to apply \(f(\cdot)\) on the continuous variables. We provide an example below to illustrate this functionality.

User-defined design matrix

We use the simulated dataset sailsim provided in our package. We first add a categorical variable race to the data:

set.seed(1234)
library(sail)
x_df <- as.data.frame(sailsim$x)
x_df$race <- factor(sample(1:5, nrow(x_df), replace = TRUE))
table(x_df$race)
#> 
#>  1  2  3  4  5 
#> 25 26 17 17 15

We then use the model.matrix function to create the design matrix. Note that the intercept should not be included, as this is added internally in the sail function. This is why we add 0 to the formula. Notice also the flexibility we can have by including different basis expansions to each predictor:

library(splines)
x <- stats::model.matrix(~ 0 +  bs(X1, degree = 5) + bs(X2, degree = 3) + ns(X3, df = 8) + 
                           bs(X4, degree = 6) + X5 + poly(X6,2) + race, data = x_df)
head(x)
#>   bs(X1, degree = 5)1 bs(X1, degree = 5)2 bs(X1, degree = 5)3
#> 1        0.0001654794         0.003945507        0.0470361237
#> 2        0.2470181057         0.345144379        0.2411253263
#> 3        0.1299195522         0.007832449        0.0002360971
#> 4        0.3808392973         0.121815907        0.0194821217
#> 5        0.1737663057         0.014898419        0.0006386822
#> 6        0.1184145931         0.281407715        0.3343772913
#>   bs(X1, degree = 5)4 bs(X1, degree = 5)5 bs(X2, degree = 3)1
#> 1        2.803692e-01        6.684809e-01           0.3272340
#> 2        8.422768e-02        1.176866e-02           0.3065738
#> 3        3.558391e-06        2.145244e-08           0.1896790
#> 4        1.557896e-03        4.983113e-05           0.4100900
#> 5        1.368987e-05        1.173746e-07           0.3946500
#> 6        1.986587e-01        4.721047e-02           0.3175164
#>   bs(X2, degree = 3)2 bs(X2, degree = 3)3 ns(X3, df = 8)1 ns(X3, df = 8)2
#> 1          0.41274967         0.173537682      0.06566652               0
#> 2          0.04879618         0.002588901      0.00000000               0
#> 3          0.01508834         0.000400076      0.00000000               0
#> 4          0.12345871         0.012389196      0.00000000               0
#> 5          0.35302552         0.105263760      0.00000000               0
#> 6          0.05370432         0.003027827      0.00000000               0
#>   ns(X3, df = 8)3 ns(X3, df = 8)4 ns(X3, df = 8)5 ns(X3, df = 8)6
#> 1     0.000000000    0.000000e+00       0.0000000   -1.589937e-01
#> 2     0.000000000    5.775107e-04       0.3179489    5.395130e-01
#> 3     0.000000000    4.989926e-03       0.4147696    4.830810e-01
#> 4     0.133404268    6.839146e-01       0.1826811    3.022366e-08
#> 5     0.000000000    8.944913e-05       0.2775548    5.564842e-01
#> 6     0.001578195    3.415384e-01       0.6070588    4.566909e-02
#>   ns(X3, df = 8)7 ns(X3, df = 8)8 bs(X4, degree = 6)1 bs(X4, degree = 6)2
#> 1    4.436233e-01   -2.846296e-01        0.1820918880        0.3088147022
#> 2    1.732713e-01   -3.131078e-02        0.0120101010        0.0000608354
#> 3    1.434410e-01   -4.628144e-02        0.0002900763        0.0044075535
#> 4    7.673343e-09   -4.923233e-09        0.2978877432        0.0579746877
#> 5    1.863219e-01   -2.045032e-02        0.0114895681        0.0645689076
#> 6    1.159471e-02   -7.439189e-03        0.0102152807        0.0595722132
#>   bs(X4, degree = 6)3 bs(X4, degree = 6)4 bs(X4, degree = 6)5
#> 1        2.793213e-01        1.421126e-01        3.856204e-02
#> 2        1.643482e-07        2.497444e-10        2.024070e-13
#> 3        3.571755e-02        1.628127e-01        3.958163e-01
#> 4        6.017595e-03        3.513419e-04        1.094046e-05
#> 5        1.935272e-01        3.262743e-01        2.933747e-01
#> 6        1.852831e-01        3.241534e-01        3.024572e-01
#>   bs(X4, degree = 6)6         X5 poly(X6, 2)1 poly(X6, 2)2 race1 race2
#> 1        4.359896e-03 0.51332996  -0.13705545   0.09851639     1     0
#> 2        6.835086e-17 0.02643863   0.18835303   0.22584415     0     0
#> 3        4.009478e-01 0.76746637  -0.15841216   0.16140597     0     0
#> 4        1.419483e-07 0.69077618  -0.03664279  -0.07954100     0     0
#> 5        1.099135e-01 0.27718210   0.13128945   0.05620199     0     0
#> 6        1.175889e-01 0.48384748   0.08486354  -0.03559388     0     0
#>   race3 race4 race5
#> 1     0     0     0
#> 2     0     1     0
#> 3     0     1     0
#> 4     0     1     0
#> 5     0     0     1
#> 6     0     1     0

One benefit of using stats::model.matrix is that it returns the group membership as an attribute:

attr(x, "assign")
#>  [1] 1 1 1 1 1 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 5 6 6 7 7 7 7 7

The group membership must be supplied to the sail function. This information is needed for the group lasso penalty, which will select the whole group as zero or non-zero.

Fit the sail model

We need to set the argument expand = FALSE and provide the group membership. The first element of the group membership corresponds to the first column of x, the second element to the second column of x, and so on.

fit_design <- sail(x = x, y = sailsim$y, e = sailsim$e, 
                   expand = FALSE,
                   group = attr(x, "assign"), verbose = 0)

We can plot the solution path for both main effects and interactions using the plot method for objects of class sail:

plot(fit_design)

In this instance, since we provided a user-defined design matrix and expand = FALSE, the numbers at the top of the plot represent the total number of non-zero coefficients.

Find the optimal value for lambda

We can use cross-validation to find the optimal value of lambda:

library(doMC)
#> Loading required package: foreach
#> Loading required package: iterators
#> Loading required package: parallel
registerDoMC(cores = 2)
cvfit_design <- cv.sail(x = x, y = sailsim$y, e = sailsim$e, 
                        expand = FALSE,
                        group = attr(x, "assign"), verbose = 0,
                        nfolds = 10, parallel = TRUE)

We can plot the cross-validated mean squared error as a function of lambda:

plot(cvfit_design)

The estimated coefficients at lambda.1se and lambda.min are:

cbind(coef(cvfit_design, s="lambda.1se"), # lambda.1se is the default
coef(cvfit_design, s = "lambda.min"))
#> 62 x 2 sparse Matrix of class "dgCMatrix"
#>                                 1            1
#> (Intercept)            5.42057935   5.45022411
#> bs(X1, degree = 5)1   -0.38913702  -0.99980981
#> bs(X1, degree = 5)2    0.00947607   0.84044532
#> bs(X1, degree = 5)3    0.17875174   1.11415914
#> bs(X1, degree = 5)4    0.40379077   1.16368947
#> bs(X1, degree = 5)5    0.98556730   2.63944592
#> bs(X2, degree = 3)1    .           -1.17689836
#> bs(X2, degree = 3)2    .           -1.03093803
#> bs(X2, degree = 3)3    .           -0.23466071
#> ns(X3, df = 8)1        2.17658372   3.43257956
#> ns(X3, df = 8)2        1.94334296   2.64241254
#> ns(X3, df = 8)3        0.65644441   1.07346393
#> ns(X3, df = 8)4       -1.16599084  -0.86948704
#> ns(X3, df = 8)5       -1.62789320  -1.61625869
#> ns(X3, df = 8)6       -1.29567515  -1.36272297
#> ns(X3, df = 8)7        0.46196993   0.51920037
#> ns(X3, df = 8)8       -1.12199469  -1.58839548
#> bs(X4, degree = 6)1    4.25730420   5.97839856
#> bs(X4, degree = 6)2   -0.21970890   0.09042593
#> bs(X4, degree = 6)3   -4.38745682  -7.10265598
#> bs(X4, degree = 6)4   -4.56639589  -6.62018513
#> bs(X4, degree = 6)5   -2.00248806  -1.84209587
#> bs(X4, degree = 6)6   -0.17598921  -0.35682238
#> X5                     .            .         
#> poly(X6, 2)1           .            .         
#> poly(X6, 2)2           .            .         
#> race1                  0.22601077   0.37075174
#> race2                 -0.13058802  -0.20968551
#> race3                  0.18074321   0.31408729
#> race4                 -0.09159055  -0.13947230
#> race5                 -0.18457540  -0.33568122
#> E                      1.93814553   2.12863361
#> bs(X1, degree = 5)1:E  .           -0.44742651
#> bs(X1, degree = 5)2:E  .            0.37610905
#> bs(X1, degree = 5)3:E  .            0.49859916
#> bs(X1, degree = 5)4:E  .            0.52076456
#> bs(X1, degree = 5)5:E  .            1.18118272
#> bs(X2, degree = 3)1:E  .            .         
#> bs(X2, degree = 3)2:E  .            .         
#> bs(X2, degree = 3)3:E  .            .         
#> ns(X3, df = 8)1:E      0.95642364   1.97522674
#> ns(X3, df = 8)2:E      0.85393413   1.52053690
#> ns(X3, df = 8)3:E      0.28845155   0.61770882
#> ns(X3, df = 8)4:E     -0.51235392  -0.50033335
#> ns(X3, df = 8)5:E     -0.71532077  -0.93005197
#> ns(X3, df = 8)6:E     -0.56933916  -0.78415862
#> ns(X3, df = 8)7:E      0.20299654   0.29876611
#> ns(X3, df = 8)8:E     -0.49302135  -0.91401850
#> bs(X4, degree = 6)1:E  8.46740068   9.02807791
#> bs(X4, degree = 6)2:E -0.43698152   0.13655369
#> bs(X4, degree = 6)3:E -8.72626271 -10.72583750
#> bs(X4, degree = 6)4:E -9.08215666  -9.99725035
#> bs(X4, degree = 6)5:E -3.98277126  -2.78177924
#> bs(X4, degree = 6)6:E -0.35002695  -0.53884334
#> X5:E                   .            .         
#> poly(X6, 2)1:E         .            .         
#> poly(X6, 2)2:E         .            .         
#> race1:E                .            .         
#> race2:E                .            .         
#> race3:E                .            .         
#> race4:E                .            .         
#> race5:E                .            .

The estimated non-zero coefficients at lambda.1se:

predict(cvfit_design, type = "nonzero")
#>                                 1
#> (Intercept)            5.42057935
#> bs(X1, degree = 5)1   -0.38913702
#> bs(X1, degree = 5)2    0.00947607
#> bs(X1, degree = 5)3    0.17875174
#> bs(X1, degree = 5)4    0.40379077
#> bs(X1, degree = 5)5    0.98556730
#> ns(X3, df = 8)1        2.17658372
#> ns(X3, df = 8)2        1.94334296
#> ns(X3, df = 8)3        0.65644441
#> ns(X3, df = 8)4       -1.16599084
#> ns(X3, df = 8)5       -1.62789320
#> ns(X3, df = 8)6       -1.29567515
#> ns(X3, df = 8)7        0.46196993
#> ns(X3, df = 8)8       -1.12199469
#> bs(X4, degree = 6)1    4.25730420
#> bs(X4, degree = 6)2   -0.21970890
#> bs(X4, degree = 6)3   -4.38745682
#> bs(X4, degree = 6)4   -4.56639589
#> bs(X4, degree = 6)5   -2.00248806
#> bs(X4, degree = 6)6   -0.17598921
#> race1                  0.22601077
#> race2                 -0.13058802
#> race3                  0.18074321
#> race4                 -0.09159055
#> race5                 -0.18457540
#> E                      1.93814553
#> ns(X3, df = 8)1:E      0.95642364
#> ns(X3, df = 8)2:E      0.85393413
#> ns(X3, df = 8)3:E      0.28845155
#> ns(X3, df = 8)4:E     -0.51235392
#> ns(X3, df = 8)5:E     -0.71532077
#> ns(X3, df = 8)6:E     -0.56933916
#> ns(X3, df = 8)7:E      0.20299654
#> ns(X3, df = 8)8:E     -0.49302135
#> bs(X4, degree = 6)1:E  8.46740068
#> bs(X4, degree = 6)2:E -0.43698152
#> bs(X4, degree = 6)3:E -8.72626271
#> bs(X4, degree = 6)4:E -9.08215666
#> bs(X4, degree = 6)5:E -3.98277126
#> bs(X4, degree = 6)6:E -0.35002695