Introduction to pcLasso

Kenneth Tay and Rob Tibshirani

2020-09-01

Introduction

pcLasso is a package that fits the principal components lasso, a new method for obtaining sparse models for supervised learning problems. pcLasso shrinks predictions toward the top principal components of the feature matrix. It combines the power of Principal Components Regression with the sparsity of the lasso. The method is also able to handle grouped features, where the features can belong to one of many groups. In that case, pcLasso shrinks the component of the parameter vector towards the top principal componets of the corresponding feature matrix.

We introduce some notation that we will use throughout this vignette. Let there be \(n\) observations, each with feature vector \(x_i \in \mathbb{R}^p\) and response \(y_i\). Let \(X \in \mathbb{R}^{n \times p}\) denote the overall feature matrix, and let \(y \in \mathbb{R}^n\) denote the vector of responses. Assume our data features come in \(K\) groups, and let \(X_k \in \mathbb{R}^{n \times p_k}\) denote the feature matrix for group \(k\). In the simplest case with \(K=1\), there is no feature grouping.

For each feature matrix \(X_k\), let \(X_k = U_k D_k V_k^T\) be its singular value decomposition (SVD). Let \(D_k\) have diagonal entries \(d_{k1}, d_{k2}, \dots\), and let \(D_{d_{k1}^2 - d_{kj}^2}\) denote the diagonal matrix such that the \(j\)th diagonal entry is \(d_{k1} - d_{kj}^2\).

pcLasso solves the optimization problem

\(\underset{\beta_0,\beta}{\text{minimize}} \quad \dfrac{1}{n} \displaystyle\sum_{i=1}^N w_i \ell (y_i, \beta_0 + \beta^T x_i) + \lambda \|\beta\|_1 + \dfrac{\theta}{2} \sum_{k = 1}^K \beta_k^T V_k D_{d_{k1}^2 - d_{kj}^2} V_k^T \beta_k.\)

In the above, \(\ell(y, \eta)\) is the negative log-likelihood contribution for observation \(i\); e.g. in the Gaussian case \(\ell(y, \eta) = \frac{1}{2}(y - \eta)^2\). \(w_i\) is the observation weight given to observation \(i\) (by default \(w_i = 1\) for all \(i\)). \(\beta_k\) is the subvector of \(\beta\) which corresponds to group \(k\). \(\lambda\) and \(\theta\) are non-negative hyperparameters. pcLasso solves the optimization problem for a grid of \(\lambda\) values; \(\theta\) must be specified in advance by the user.

pcLasso uses cyclical coordinate descent, which successively optimizes the objective function over each parameter with all other parameters fixed, cycling repeatedly until convergence.

The package also includes methods for prediction and plotting, and a function which performs \(k\)-fold cross-validation.

For more details, please see our paper on arXiv.

Installation

The simplest way to obtain pcLasso is to install it directly from CRAN. Type the following command in R console:

install.packages("pcLasso")

This command downloads the R package and installs it to the default directories. Users may change add a repos option to the function call to specify which repository to download from, depending on their locations and preferences.

Alternatively, users can download the package source at CRAN and type Unix commands to install it to the desired location.

Quick Start

The purpose of this section is to give users a general sense of the package. We will briefly go over the main functions of the package, as well as some key options. More details are given in later sections.

First, we load the pcLasso package:

library(pcLasso)

Let’s generate some data:

set.seed(944)
n <- 100
p <- 20
x <- matrix(rnorm(n*p), n, p)
beta <- matrix(c(rep(2, 5), rep(0, 15)), ncol = 1)
y <- x %*% beta + rnorm(n)

We fit the model using the most basic call to pcLasso:

fit <- pcLasso(x, y, ratio = 0.8)

In addition to the feature matrix x and the response y, the user must specify either theta or ratio (but not both). theta is the hyperparameter value for the last term in the objective function. The scale for theta depends on x and y and hence can be hard to determine a priori. Instead of specifying theta, we recommend that the user specify ratio, a hyperparameter whose value lies in the interval \((0,1]\). Roughly speaking, smaller values of ratio represent greater shrinkage to the top principal components. (ratio = 1 gives the usual lasso.)

If the argument verbose is set to TRUE, while the function is running, pcLasso informs the user which lambda in the lambda sequence it is currently processing by printing it to the console.

If ratio is passed to pcLasso, it internally computes the corresponding value of theta and uses that in minimizing the objective function. This value can be retrieved as the value of theta in the output list.

The function pcLasso returns a pcLasso object. At the moment, the only way to extract model coefficients is to use list syntax on the returned object. For example, the code below extracts the intercept and coefficients for the model at the 20th lambda value:

# intercept
fit$a0[20]
#> [1] -0.05855159

# coefficients
fit$beta[, 20]
#>  [1]  0.26955362  0.34871182  0.31431625  0.44873422  0.17694987  0.21373963
#>  [7]  0.00000000 -0.05236147  0.00000000  0.00000000 -0.17767456  0.11606285
#> [13]  0.01645924  0.11182802  0.00000000 -0.10867816  0.06198964  0.12127325
#> [19] -0.04744480 -0.07523572

A pcLasso object has a nzero key which tells us the number of non-zero model coefficients at each value of lambda:

fit$nzero
#>   [1]  0  1  1  4  4  4  5  5  5  6  7  7 11 12 13 14 15 16 16 16 17 17 17 18 18
#>  [26] 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20
#>  [51] 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
#>  [76] 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20

We can make predictions using a pcLasso object by calling the predict method. Each column gives the predictions for a value of lambda.

# get predictions for 20th model
predict(fit, x[1:5, ])[, 20]
#> [1]  1.0830384 -0.3009271  1.8452090  1.3588647 -1.1403960

Grouped features

By default, pcLasso assumes that all the features in x belong to one group. If the features come in groups, pcLasso can make use of this information in the model-fitting procedure.

Assume our features come in 4 (non-overlapping) groups of size 5:

groups <- vector("list", 4)
for (k in 1:4) {
    groups[[k]] <- 5 * (k-1) + 1:5
}
groups
#> [[1]]
#> [1] 1 2 3 4 5
#> 
#> [[2]]
#> [1]  6  7  8  9 10
#> 
#> [[3]]
#> [1] 11 12 13 14 15
#> 
#> [[4]]
#> [1] 16 17 18 19 20

We can use this information in our fit by specifying the groups option. groups must be a list of length \(K\) (the number of groups). groups[[k]] is a vector of column indices representing the features which belong to group \(k\). (For example, groups[[1]] <- c(3, 4, 6) means that columns 3, 4 and 6 of x belong to group 1.) Every feature must belong to at least one group.

fit <- pcLasso(x, y, ratio = 0.8, groups = groups)

One advantage of pcLasso is that the algorithm works with overlapping groups as well. For example, we modify the groups list such that features 6 and 7 also belong to group 1. We can make the same call to pcLasso:

groups[[1]] <- 1:7
groups
#> [[1]]
#> [1] 1 2 3 4 5 6 7
#> 
#> [[2]]
#> [1]  6  7  8  9 10
#> 
#> [[3]]
#> [1] 11 12 13 14 15
#> 
#> [[4]]
#> [1] 16 17 18 19 20
fit <- pcLasso(x, y, ratio = 0.8, groups = groups)

One thing to note with overlapping groups is that the model coefficients and the number of non-zero coefficients are stored differently in the output object. To get the coefficients in the original feature space, look at the origbeta key:

# intercept at 20th model: same as before
fit$a0[20]
#> [1] -0.03609611

# coefficients at 20th model: look at origbeta instead
fit$origbeta[, 20]
#>  [1] 0.9453523 1.0498390 1.0494266 1.3574616 0.7357579 0.3295442 0.0000000
#>  [8] 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000
#> [15] 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000 0.0000000

To get the number of non-zero coefficients in the original feature space, look at orignzero:

fit$orignzero
#>   [1]  0  1  1  3  4  4  5  5  5  5  5  6  6  6  6  6  6  6  6  6  7  8  8  8 10
#>  [26] 10 12 12 12 13 13 13 15 15 16 17 17 17 17 17 17 18 19 19 19 19 19 19 19 19
#>  [51] 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19
#>  [76] 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20

For more information on what the algorithm does in the case of overlapping groups, see Section 3.2 of our paper.

Cross-validation (CV)

We can perform \(k\)-fold cross-validation (CV) with cv.pcLasso. It does 10-fold cross-validation by default:

cvfit <- cv.pcLasso(x, y, groups = groups, ratio = 0.8)

We can change the number of folds using the nfolds option:

cvfit <- cv.pcLasso(x, y, groups = groups, ratio = 0.8, nfolds = 5)

If we want to specify which observation belongs to which fold, we can do that by specifying the foldid option, which is a vector of length \(n\), with the \(i\)th element being the fold number for observation \(i\).

foldid <- sample(rep(seq(10), length = n))
cvfit <- cv.pcLasso(x, y, groups = groups, ratio = 0.8, foldid = foldid)

A cv.pcLasso call returns a cv.pcLasso object. We can plot this object to get the CV curve with error bars (one standard error in each direction). The left vertical dotted line represents lambda.min, the lambda value which attains minimum CV error, while the right vertical dotted line represents lambda.1se, the largest lambda value with CV error within one standard error of the minimum CV error.

plot(cvfit)

The numbers at the top represent the number of non-zero coefficients for each model in the original feature space. If the groups are overlapping, we can plot the number of non-zero coefficients for each model in the expanded feature space instead by setting orignz = FALSE:

plot(cvfit, orignz = FALSE)

The two special lambda values can be extracted directly from the cv.pcLasso object as well:

cvfit$lambda.min
#> [1] 0.0002570626
cvfit$lambda.1se
#> [1] 0.1899878

Predictions can be made from the fitted cv.pcLasso object. By default, predictions are given for lambda being equal to lambda.1se. To get predictions are lambda.min, set s = "lambda.min".

predict(cvfit, x[1:5, ])   # s = lambda.1se
#> [1]  4.850616 -2.192256  5.120815  3.127318 -4.748510
predict(cvfit, x[1:5, ], s = "lambda.min")
#> [1]  5.059693 -2.553863  7.064400  4.051717 -4.870323

Other options

Here are some other options that one may specify for the pcLasso and cv.pcLasso functions:

For more information, type ?pcLasso or ?cv.pcLasso.