In this task, we are going to use various supervised learning methods to try to predict the antibody response to influenza vaccination using early gene expression measurements. The main goal of the practical is to understand the concepts behind cross-validation : why we need it, how it works, and how it is used to tune the hyperparameters of a model.
To save us some time, I have preprocessed the data for you. The data is an extract of open-access data from the Human Immune Project Consortium, with detail of the study found here. The relevant information for this exercise is as follows :
- Antibodies were measured at days 0 and 28
- Gene expression of around 10,000 genes was measured at days 0 and 7. To reduce the computation time, I kept only the 1,000 most variable genes
Flu is an interesting virus in this context, since it is a common disease, and many people may have already been infected with a given flu strain prior to vaccination. There is evidence in the literature that prior infection can modify the response to vaccination. Therefore, to take into account baseline (day 0) information, I have transformed both the response and the predictors into (log2) fold-change values. That is, the ratio between the post-vaccination and pre-vaccination measurements.
Our task will be to see if the day 7 fold-change of expression in some genes is predictive of the day 28 fold-change in antibody levels. That is, we seek to find an early gene expression signature which informs us of the later antibody response.
When you analyse data in R, you need to tell R where files are. For example, you might load data into your environment by defining a path
"C:/Users/me/Documents/data/file.xlsx". However, this is problematic, since it is specific to your own computer setup. The code will then break as soon as you move the files, share them with a colleague, or re-run the analysis on a different machine.
To avoid this, we will introduce some basic organisational skills for an R project. Firstly, we will contain the work in a single R project folder (the root), and refer to every file relative to that folder. This makes specifying paths for loading data and saving outputs more robust.
Using a project folder also helps to organise files in a clean way.
- raw data : the original data files, which you should never modify;
- analysis : the code (such as this
.Rmdfile) that reads the raw data and produces results;- output : everything the code creates (figures, tables, processed data), which should be able to be perfectly regenerated by re-running the code.
If you already created a project for a previous practical, you can reuse it : open it and go to “Add the files” below. Otherwise :
- In RStudio, click
File > New Project... > New Directory > New Project.- Give the project a name, for example
omics_practicals, and choose where to save it on your computer. ClickCreate Project.- RStudio restarts inside your new project. You can check this in the top-right corner of RStudio, which now shows the project name, and in the
Filespane, which shows a single file called e.g.omics_practicals.Rproj.
Next, create three sub-folders inside the project. In the
Filespane of RStudio, clickNew Folderand createdata-raw,analysis, andoutput. Alternatively, run the following code once in the R console.
dir.create("data-raw")
dir.create("analysis")
dir.create("output")
Your project should now look like this:
omics_practicals/
├── omics_practicals.Rproj <- marks the "root" of the project
├── data-raw/ <- the raw data files
├── analysis/ <- the practicals (.Rmd files)
└── output/ <- figures and results that you save
- Copy the data file I sent you,
SDY1119_day7_GE_foldChange_top1000.RData, into thedata-rawfolder.- Copy this practical (the
.Rmdfile) into theanalysisfolder.- In the
Filespane of RStudio, open theanalysisfolder and click on the.Rmdfile to open it.
Important: always start your work by opening the project (double-click the
.Rprojfile, or useFile > Open Project), and open the.Rmdfile from within RStudio’sFilespane. If you open the.Rmdfile directly from your Downloads folder, it will not be part of the project and the paths below will not work.
By default, R Markdown runs code from the folder where the
.Rmdfile is saved (here,analysis/), which would make paths confusing. Instead, we will use theherepackage. The functionhere()always returns the path to the root of your project (the folder containing the.Rprojfile), wherever the code is run from. We then build paths from this root. For example,here("data-raw", "my_file.RData")points tomy_file.RDatainside thedata-rawfolder, andhere("output", "my_plot.pdf")points to a file in theoutputfolder. The same code works on any computer and operating system, and you never have to type a full path.
If you ran the installation script I sent you, all the packages are already installed. Otherwise, the following chunk checks which of the packages needed for this practical you are missing and installs them :
dplyr,ggplot2,caret,glmnet,pls,rangerandhere.
cran_packages <- c(
"dplyr",
"ggplot2",
"caret",
"glmnet",
"pls",
"ranger",
"here"
)
# Install missing CRAN packages
install.packages(setdiff(cran_packages, installed.packages()[, "Package"]))
Now load the packages.
Run the following chunk to check that everything is in the right place. The first line should print the path to your project folder (the one containing the
.Rprojfile), and the data file should be found.
# Path to the root of the project
here()
## [1] "/home/ah3/Desktop/Work/Teaching/PHDS_omics_data_course"
# Check that the data file exists
file.exists(here("data-raw", "SDY1119_day7_GE_foldChange_top1000.RData"))
## [1] TRUE
If the result is
FALSE, or if the path printed above is not your project folder, go back to the previous steps : check that the file is indata-raw, and that you opened the project and then this.Rmdfile from theFilespane.
Load the data into the global environment, using
here()to build the path to the file. The functionload()reads the object saved in the file, andget()retrieves it.
df_all <- get(load(here("data-raw", "SDY1119_day7_GE_foldChange_top1000.RData")))
We can explore the data first to see what we’re dealing with. Let’s print the first few columns and rows.
df_all[, 1:8] %>%
head()
## participant_id sex age response a2m abca1 abcb8
## 1 SUB179655.1119 Female 66 0.5 0.07524736 -0.54463090 0.41377154
## 2 SUB179657.1119 Male 75 -0.5 0.67610584 1.32433852 -0.26258410
## 3 SUB179659.1119 Male 70 1.0 -0.57017270 -3.46176587 0.09333269
## 4 SUB179662.1119 Male 65 1.0 0.84066094 -0.07048454 0.52547832
## 5 SUB179665.1119 Female 66 4.0 0.43971367 -1.55241659 -0.03439272
## 6 SUB179673.1119 Male 70 0.5 0.86480922 -2.32612497 0.05644670
## abcc8
## 1 0.39287681
## 2 -0.31099800
## 3 0.15474707
## 4 0.04068224
## 5 0.51448642
## 6 0.58639101
We see that the first few columns give us some clinical information (participant id, sex, age), then we have the response value to be predicted, then the columns represent each gene, sorted alphabetically. Let’s plot the response as a histogram, and save the figure in the
outputfolder.
response_hist <- ggplot(df_all, aes(x = response)) +
geom_histogram(bins = 20) +
labs(x = "Response (log2 fold-change in antibodies)", y = "Number of participants")
ggsave(here("output", "response_histogram.pdf"), response_hist, width = 8, height = 6)
response_hist
We have a distribution with a relatively long tail. Some participants have negative values. These are participants whose antibody levels decreased after vaccination! When we log transform a fold-change value between 0 and 1 (i.e. less antibodies at day 28 compared to day 0), we get a negative.
Many models are sensitive to the scale of the variables. Standardise the response and every gene, so that each has mean 0 and standard deviation 1, using the
scale()function. Store the response as a numeric vector calledy_vec, and the genes as a numeric matrix calledX_mat(columns are genes and rows are participants; the genes are all the columns afterresponse). Before proceeding, check explicitly that the column means are 0 and the standard deviations are 1 across your dataset.
# Your code here
For the rest of the tasks in this practical, you should use the standardised predictors and response as your inputs.
Now that we have finished with the preprocessing of the data, we can remove the unnecessary objects from the global environment with the
rm()function. This is good practice with high-dimensional data, as memory can quickly be filled by large, unnecessary objects.
# List the objects to keep
objects_to_keep <- c("X_mat", "y_vec")
# Remove everything else from the global environment
rm(list = setdiff(ls(), objects_to_keep))
# Garbage collection to free unused memory
invisible(gc())
In this section, we are going to demonstrate how to fit a predictive model to a dataset and use it to make predictions. We will demonstrate how fitting a model to a dataset, and making predictions on the same samples used to fit the model, results in over-optimistic evaluation of the prediction error.
As a first example, we are going to fit a ridge regression model to the entire dataset. Ridge is a regularised linear regression with a penalty term controlling the size of the coefficients. It is implemented in the
glmnet()function of theglmnetpackage, with the argumentalpha = 0. The strength of the penalty is controlled by the argumentlambda. For now, we will fixlambda = 1. Theglmnet()function takes the predictors as a matrixxand the response as a vectory, and predictions can be made with thepredict()function, giving the predictors as the argumentnewx.
Fit a ridge regression with
lambda = 1to all of the observations.
# Your code here
Now use the
predict()function to make predictions of the response, using the same data (X_mat) as an input to thenewxargument.
# Your code here
Now we’ve made these predictions, we can evaluate them with performance metrics. For the purposes of this exercise, we will be using two metrics. The first is the root-mean-squared-error (RMSE) between the true values \(y_i\) and the predictions \(\hat{y}_i\) :
\[ \text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2} \]
The lower this is, the better. Since we standardised the response, it has a standard deviation of 1, and the RMSE can be read as follows : if RMSE = 1, your predictions are as good on average as a model which simply predicts the average response (i.e. one which does not use any of the predictor information). Anything below 1 indicates a model which predicts better than this naïve model.
The second metric is the R2 statistic, which is the squared Pearson correlation between the response and the predictions :
\[ R^{2} = cor_{\text{pearson}}(y, \hat{y})^{2} \]
The higher this is, the better. It can be interpreted as “the proportion of variability in the true response which is explained by the predictions”.
Below is a helper function which you can use to compute these metrics, by inputting your true and predicted response values.
calculate_metrics <- function(true, predicted) {
sRMSE = sqrt(mean((true - predicted) ^ 2))/sd(true)
R2 = cor(true, predicted, method = "pearson") ^ 2
metrics = list("sRMSE" = sRMSE, "R2" = R2)
return(metrics)
}
Compute and report the two metrics for your first ridge regression fit on the entire dataset.
# Your code here
We could also interpret our predictions by visualising the true versus predicted values. The below helper function allows you to do this, with the arguments being the true values, predicted values, and a string to use as a descriptive title for the figure.
plot_predictions <- function(true, predicted, title = "Insert a descriptive plot title here") {
# The same limits for both axes : the range of the true and predicted values, with some padding
results_range <- range(c(true, predicted))
padding <- diff(results_range) * 0.02
lims <- c(results_range[1] - padding, results_range[2] + padding)
ggplot(data.frame(true, predicted), aes(x = true, y = predicted)) +
geom_point(alpha = 0.7, size = 2) +
geom_abline(slope = 1, intercept = 0, color = "red", linetype = "dashed", linewidth = 0.8) +
coord_equal(xlim = lims, ylim = lims) +
labs(title = title, x = "True (observed)", y = "Predicted") +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
}
Plot your results using the above helper function, and save the resulting image with a descriptive title, using the
ggsave(), withhere()to build the path (you should do this for all the figures in the practical). What do you conclude about the quality of the predictions?
# Your code here
The predictions above are made on the data which was used to fit the model, so they say nothing about how the model would perform on new participants. The simplest way to evaluate the model on unseen data is a train-test split. Observations are randomly assigned to either the training set or the testing set. A model is trained on the training set, and used to predict on the unseen testing set. The predictions can then be compared to their true values to obtain an unbiased estimate of the model’s performance.
Randomly select 70% of the participants as the training set (hint : the
sample()function), and keep the remaining 30% as the testing set. Fit the same ridge regression model (lambda = 1) on the training set only, and predict the response of the testing set. Compute the same two metrics on the testing set, and plot the predictions.
How do the results compare with those of 1a? What does this tell us?
# Your code here
The train-test split uses the data inefficiently : only 30% of the participants are predicted, and the result depends on the random split. K-fold cross-validation is a better alternative. Here, we first split the data into K distinct folds (partitions of the data). Cross-validation is a loop, where in each stage, a different fold is chosen to act as the testing set, and a model is trained on all the other folds. Therefore, K-fold cross-validation results in a series of K models, and in a prediction on unseen data for every participant. In this section, we are going to improve on our previous train-test split by implementing 10-fold CV.
First, create the folds with the
createFolds()function (use the argumentsk = 10andlist = TRUE, which gives a list containing the indices of the participants in each fold).
# Your code here
Now, you will write a basic implementation of K-fold CV using a
forloop over the 10 folds. In theith iteration of the loop, form the training data by removing the observations belonging to theith fold from the data. Then, fit a ridge regression model to this training data and use it to make predictions, as we did in question 1. Hint: you will need to store the predictions after every iteration. This can be done by creating an empty vector withrep(NA_real_, n)prior to initiating theforloop, and filling it with the predictions using the indices of the test participants within every iteration.
# Your code here
Compute the same two metrics and plot the predictions. How do the results compare with those of 1b?
caretSome models have hyperparameters to specify, which are parameters which control certain aspects of the learning process. Ridge regression, for example, has the hyperparameter lambda, which controls the strength of the penalisation term. Since we are unlikely to have much reason to prefer any particular values of these hyperparameters, we may prefer to let the data decide for us which are the best values. We can also do this with cross-validation : we try different values of the hyperparameters, and we keep those which give the best predictions (here, the lowest RMSE).
However, we must be careful. If we choose the hyperparameters using the same predictions that we use to evaluate the model, the performance will be over-optimistic (the same problem we evoked in question 1). Instead, we typically do something called nested cross-validation in order to first build a model with optimal hyperparameters, and then use that model to predict on unseen data. A nested cross-validation has two loops. The first, called the outer loop, separates the training data from the testing data (which is what you implemented above). The second, called the inner loop, uses cross-validation on the training data only to select the best values of the model hyperparameters. The testing data is never used to choose the hyperparameters.
Coding the inner loop from scratch is tedious, especially when the model has several hyperparameters. Luckily, the
caretpackage exists to automate hyperparameter tuning. Caret standardises syntax across packages to allow for many predictive models to be easily implemented. To train models and find the best hyperparameter(s), we use thetrain()function incaret. This function takes the following main arguments :
x: the matrix or data frame of predictors (columns) by samples (rows)y: the vector of responsesmethod: the name of the regression method to be used. A full list of methods implemented in caret can be found here. Ridge regression is implemented inglmnet, so the method name is “glmnet”.tuneGrid: a data frame where each column is a named hyperparameter from the method, and each row is a unique combination of these hyperparameters to be used to fit a model to the training data. Theexpand.grid()function creates such a data frame from the values to try for each hyperparameter. For the “glmnet” method, the hyperparameters arealpha(0 for ridge regression) andlambda.trControl: an object created using thetrainControl()function which defines how the tuning process should be performed. For example,trainControl(method = "cv", number = 5)implements 5-fold (inner) CV for deciding the hyperparameters.metric: the metric to be optimised when choosing the best model. We will use the root mean squared error (metric= “RMSE”).The result of
train()is a model which has been fitted on all the training data with the best hyperparameters. It can be used withpredict()as before (with the testing predictors as the argumentnewdata), and the best hyperparameters are stored in its element$bestTune.
You are going to add 5-fold hyperparameter tuning in the ridge regression CV you implemented above. All this will require is to replace the call to
glmnet()above with a call tocaret::train(), using the argumentsmethod = "glmnet",tuneGrid = ridge_grid, andtrControl = trainControl(method = "cv", number = 5). As before, you may make predictions in the test set with thepredict()function. Also, store the tuned values of the hyperparameter lambda, extracted from thebestTune$lambdaof thecaret::train()object. You may use the grid of hyperparameter options suggested below as inputs (its values are spread over several orders of magnitude, since lambda acts on a multiplicative scale).
lambda_grid <- c(0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 3, 4, 5, 10, 20, 50, 100)
ridge_grid = expand.grid(alpha = 0, lambda = lambda_grid)
As before, compute the sRMSE and R2 of your predictions. Do these differ much from the non-tuned approach in question 2, where we simply fixed the value of lambda at 1?
Is the value of lambda selected the same in every iteration of the outer loop? What does this tell us?
# Your code here
Now that you have a nested cross-validation framework with
caret, it is easy to try other models : you only need to change themethodand thetuneGrid. For each of the following models, repeat your code from the previous section (with the same outer folds), and choose the hyperparameter values to try yourselves. Compute the metrics, plot the predictions, and save the figure. To compare the models later, store the predictions and metrics of each model in objects with distinct names (e.g.pred_lassoandmetrics_lasso).
How should you choose a hyperparameter grid? There is no rule, and a grid is always somewhat arbitrary, but here is some practical advice. (1) Start with a small, coarse grid : every extra value multiplies the computation time (the number of models fitted is the number of rows of the grid, times 5 inner folds, times 10 outer folds). (2) For penalty parameters such as lambda, which act on a multiplicative scale, use values spread over several orders of magnitude (e.g. 0.001, 0.01, 0.1, 1, 10) rather than evenly spaced values. (3) After tuning, look at the selected values (the
$bestTuneof thetrain()object) : if a selected value is at the edge of your grid, the best value may lie beyond it, so extend the grid in that direction. Each model below comes with an example grid to start from.
Lasso regression is another regularised method which has the advantage over ridge regression that it can reduce model coefficients fully to 0. It is implemented in the
"glmnet"method withalpha = 1. The hyperparameter to tune islambda. For example, you could use the gridexpand.grid(alpha = 1, lambda = c(0.001, 0.01, 0.1, 1, 10)).
# Your code here
Elastic net regression combines the ridge and lasso penalties. It is also implemented in the
"glmnet"method, but this time the hyperparameteralphacan take values between 0 (ridge) and 1 (lasso), and is tuned together withlambda. For example, you could use the gridexpand.grid(alpha = c(0, 0.5, 1), lambda = c(0.01, 0.1, 1, 10)), which contains all the combinations of the values ofalphaandlambda.
# Your code here
Principal component regression (PCR) first computes the principal components of the predictors, and then regresses the response on the first few components. It is implemented in the
"pcr"method (packagepls). The hyperparameter to tune is the number of components to keep,ncomp. Note that it cannot be larger than the number of participants in the training set. For example, you could use the griddata.frame(ncomp = c(1, 5, 10, 20)).
# Your code here
Partial least squares (PLS) regression is similar to PCR, but the components are chosen to explain the covariance between the predictors and the response, rather than the variance of the predictors only. It is implemented in the
"pls"method. Again, the hyperparameter to tune is the number of components to keep,ncomp. You could use the same example grid as for PCR :data.frame(ncomp = c(1, 5, 10, 20)).
# Your code here
A random forest is an ensemble of decision trees, each grown on a random subset of the participants, where only a random subset of the genes is considered at each split. The predictions of the trees are averaged. It is implemented in the
"ranger"method (packageranger), which has three hyperparameters. You must give a value for each of them in your tuning grid :
mtry: the number of genes randomly considered at each split. This is the main hyperparameter to tune; it must be at most the number of genes.splitrule: the criterion to split the nodes. For a regression, use"variance".min.node.size: the minimum number of participants in a terminal node of a tree (e.g. 5 or 10).For example, you could use the grid
expand.grid(mtry = c(50, 200), splitrule = "variance", min.node.size = c(5, 10)).
# Your code here
We have now tried multiple regression approaches in a nested cross-validation framework in order to make predictions of the day 28-post-vaccination fold-change in antibody values using day-7 fold-change in gene expression levels.
Collect the metrics of all your models in a single table, and save it in your
outputfolder.
# Your code here
In a few sentences summarise your conclusions on the predictive performance of these models. Did any model, or class of models, perform noticeably better than the rest?
Let’s say we choose the best model by that which produced the best predictions in our cross-validation framework. Comment on how this strategy could introduce bias into our results.
In the previous exercises, we have focused on code implementation and on maximising the predictive accuracy of the models. However, an important part of predictive analysis is the interpretation of any predictive signatures. Different regression methods may have different natural ways to interpret the importance of the input variables for the response. For example, in multiple linear regression, we can interpret the effect size estimates, confidence intervals, and p-values for a test of non-zero effect size. Random forest has a metric called variable importance, which tells us the loss in predictive accuracy when a given variable’s values are randomly permuted.
In this section, we are going to use the example of a lasso regression. When using L1-regularised methods like lasso (and elastic net), we may choose to interpret the predictors which have non-zero coefficients (i.e. the selected variables). We will do this in two setups : a model fit on the entire dataset, as in question 1, and cross-validation, as in questions 2 to 4.
Fit a tuned lasso regression model on the entire dataset (i.e. ignore cross-validation for now), using
caret::train()withmethod = "glmnet",alpha = 1and a 5-fold cross-validation to tune lambda. The final model, which corresponds to the best value of lambda, can be accessed from the$finalModelelement of thetrain()object, and the best value of lambda from its$bestTune$lambdaelement.
Extract the coefficients of the final model at the best value of lambda with the
coef()function, using the argumentsfor the value of lambda. The output ofcoef()is not particularly easy to use : convert it to a matrix withas.matrix(), and then to a data frame with one row per gene and its coefficient. Keep only the genes with a non-zero coefficient (excluding the intercept), sorted by decreasing absolute value of the coefficient. How many genes have non-zero coefficients?
# Your code here
Since we standardised the predictors, the coefficients of the genes are on the same scale, and their absolute values can be compared. Make a bar plot of the coefficients of the (up to) 30 genes with the largest absolute coefficients, sorted by coefficient, and save the figure. What does the sign of a coefficient mean for the relationship between the gene and the antibody response?
# Your code here
Can you think of a way to interpret the names of the selected genes, to see whether particular biological processes are over-represented?
# Your code here
There is a limitation to the interpretation above : the selected genes depend on the particular data used to fit the model. If we had a slightly different sample of participants, would the same genes be selected?
Cross-validation gives us a way to assess this. In each iteration of the cross-validation, a model is fit on a different part of the data, and may select different genes. With L1-regularisation methods like lasso and elastic net, we can therefore interpret the proportion of folds in which each gene is selected. These approaches can broadly be called stability selection methods, as genes which are more frequently selected are more stable predictors.
Modify your lasso cross-validation from question 4a (with the same outer folds), such that in each iteration you store the names of the genes with non-zero coefficients (as in 6a, using the model fitted by
train()on the training data of the iteration). Then, compute the proportion of the 10 folds in which each gene was selected, and sort the genes from the most to the least frequently selected.
# Your code here
Make a plot showing the proportion of selection on one axis and the gene names on the other axis, sorted from the most to the least frequently selected, for the genes selected in at least half of the folds. Save the figure.
# Your code here
mixOmics
(optional)Sparse partial least squares (sPLS) is an approach which uses L1 (lasso) regularisation on the weighting vectors in a partial least squares approach. That is, it finds sparse components (made from a few genes only) which are maximally covarying with the response variable. This gives a way to select variables which is different from the lasso : the selected genes are those which contribute to the components.
We are going to implement this model using the
mixOmicspackage, from Bioconductor. Unfortunately, this implementation is not available incaret, so we will have to usemixOmics’s own functions to tune the model. If you have not installed it yet, run the following chunk.
if (!requireNamespace("BiocManager", quietly = TRUE))
install.packages("BiocManager")
BiocManager::install("mixOmics")
Note : we do not load
mixOmicswithlibrary(): it replaces some functions of other packages (such asselect()fromdplyr). We will call its functions with themixOmics::prefix.
The sPLS model has two hyperparameters. The first is called
keepX, which is the number of genes to keep for each component. This is roughly equivalent to tuning the strength of the penalty term, like we have done previously in this practical. The second is calledncomp, which is the number of components to keep.
Read the sPLS (regression) example on the mixOmics site, and use the
mixOmics::tune.spls()function on the entire dataset to determine the best values ofncompandkeepX(again, ignore the outer cross-validation for now : the function does its own cross-validation to tune the hyperparameters). Use the standardised genesX_matas the predictors and the responsey_vec. You could try up to 3 components, and test the numbers of genes to keep with, for example,test.keepX = c(5, 10, 20, 50, 100). Use the argumentsmode = "regression",validation = "Mfold",folds = 5,measure = "MSE"andnrepeat = 3, and plot the tuning results. Save the plot.
Note : there is a known bug in the sPLS implementation in the regression setting, which may give you an error
Unmapped Y contains samples with no associated class. This seems to be a problem when some values of the response are 0 : a workaround is to give the response as a matrix (as.matrix()) and to add a small value like 0.000001 to the response values, and the error should go away.
# Your code here
In 7a, we tuned the hyperparameters of the sPLS model on the entire dataset. To evaluate sPLS fairly, and to compare it with the other models, we need to implement a nested cross-validation, as we did for the other models. Since sPLS is not available in
caret, you have to code the steps yourself with themixOmicsfunctions.
Use the same 10 outer folds as before (
folds), and write aforloop over the folds. In each iteration :
- Tune
ncompandkeepXon the training participants only, withmixOmics::tune.spls()as in 7a, and extract the best values.- Fit a sPLS model with these values on the training participants with
mixOmics::spls().- Predict the response of the testing participants with the
predict()function, giving the testing predictors as the argumentnewdata. The predictions are in the element$predictof the result, which is an array with three dimensions : participants, responses (here, just one) and components. Take the predictions for your response and for the number of components of the model.Store the predictions of all the participants in a vector, as before. Then compute the metrics, plot the predictions, and save the figure.
# Your code here
Finally, add the metrics of the sPLS model to the table of the metrics of the other models (
model_metrics, from section 5), and save the new table in youroutputfolder. How does the sPLS model compare with the other models?
# Your code here