The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.
In the first tutorial (Get started), you learned to build a prediction model and to estimate its accuracy correctly with cross-validation. In this tutorial, you will learn to:
This tutorial takes about 20–30 minutes and, as in the first tutorial, all the code runs in a few seconds.
We use the same data as in the first tutorial, i.e., 100 patients to build and cross-validate the model, and 900 new patients that we will use as a “crystal ball”. To keep things simple, we only use the 40 numeric variables:
library(easy.glmnet)
x = as.matrix(patients[, 1:40])
y = patients$relapse
x_new = as.matrix(new_patients[, 1:40])
y_new = new_patients$relapseWe cross-validate a lasso model as in the first tutorial, with one important difference: we will now use the probabilities of relapse, without rounding them to 0 or 1.
set.seed(1)
fit_fun = function (x_training, y_training) {
glmnet_fit(x_training, y_training, family = "binomial")
}
predict_fun = function (m, x_test) {
predict(m, x_test)
}
res = cv(x, y, family = "binomial", fit_fun, predict_fun, verbose = FALSE)(If you see a warning about type.measure, you can ignore
it, as explained in the first tutorial.)
Typing the name of the result of cv shows a summary of the accuracy and the calibration of the cross-validated predictions:
res
#> 10-fold CV of 100 binomial outcomes:
#> auc auc.lwr auc.upr calibr.large calibr.large.lwr calibr.large.upr
#> 1 0.801 0.714 0.888 -0.0691 -0.562 0.413
#> calibr.slope calibr.slope.lwr calibr.slope.upr
#> 1 0.813 0.481 1.22Let’s see what these numbers mean.
In the first tutorial, we converted the probabilities into
predictions of 0 or 1 with round, i.e., we predicted a
relapse if the probability was higher than 0.5. With these predictions,
we could estimate the balanced accuracy:
y.pred = round(res$predictions$y.pred)
simple.accuracy(res$predictions$y, y.pred, "bac")
#> bac bac.lwr bac.upr se se.lwr se.upr sp sp.lwr
#> 1 0.7581169 0.667 0.8315 0.6590909 0.5124696 0.7855432 0.8571429 0.7484684
#> sp.upr
#> 1 0.9300705However, 0.5 is an arbitrary threshold. For instance, if relapses are very harmful and the preventive treatment is cheap and safe, a clinician might prefer to treat all patients with a probability of relapse higher than 0.2. With another threshold, the sensitivity and the specificity would be different, and so would the balanced accuracy.
The area under the ROC curve (AUC) measures how well the model discriminates, without choosing any threshold. It is the probability that, if we take at random a patient who relapsed and a patient who did not, the model estimates a higher probability of relapse for the first one. Thus, an AUC of 0.5 means that the model discriminates no better than tossing a coin, and an AUC of 1 means that it discriminates perfectly.
The AUC was the first number shown by res, followed by
the lower (auc.lwr) and upper (auc.upr) limits
of its 95% confidence interval. You can also obtain them with
simple.accuracy:
A model may discriminate well but estimate wrong probabilities. For instance, it could correctly estimate that one patient has a higher risk than another, but estimate probabilities of 60% and 40% when the actual probabilities are 30% and 20%. If a clinician started a preventive treatment in all patients with a probability higher than 50%, they would unnecessarily treat the first patient. The agreement between the estimated probabilities and what actually happens is called calibration.
simple.calibration summarizes the calibration with two
numbers, each with its 95% confidence interval:
simple.calibration(res$predictions$y, res$predictions$y.pred, "binomial")
#> calibr.large calibr.large.lwr calibr.large.upr calibr.slope calibr.slope.lwr
#> 1 -0.06905186 -0.5616115 0.4129425 0.8131361 0.4807838
#> calibr.slope.upr
#> 1 1.215632calibr.large in the output) tells whether the estimated
probabilities are, on average, too high (negative values) or too low
(positive values). Its ideal value is 0.calibr.slope in
the output) tells whether the estimated probabilities are too extreme
(values lower than 1, e.g., probabilities too close to 0 or 1) or too
modest (values higher than 1, e.g., probabilities too close to the
mean). Its ideal value is 1.There are no universally accepted cutoffs, but a useful guide is to check whether the confidence intervals include the ideal values. Also, some authors consider a calibration slope of at least 0.9 acceptable, i.e., predictions no more than 10% too extreme (Riley et al., 2020).
These numbers were also shown by res. Here, the
calibration-in-the-large is close to zero and the slope is close to one,
i.e., the probabilities are, on average, about right, and only slightly
too extreme.
Two notes of caution. First, with small samples like this one, these estimates are quite imprecise, as their wide confidence intervals show. Second, the calibration-in-the-large depends on the proportion of patients who relapse in the sample. If this proportion differs from the proportion in the population where the model will be used, the model will be miscalibrated there, however well calibrated it seems in the cross-validation. We will come back to this below.
The lasso selects variables, but which ones it selects may change a
bit with the specific patients used to build the model. The
cross-validation built 10 models, each with a slightly different group
of patients, and they are stored in res$models.
glmnet_get.items.count tells in which proportion of these
models each variable was selected (Radua et al., submitted). We may look
at those selected in at least 90% of the folds:
count = glmnet_get.items.count(res$models)
count[which(count >= 0.9)]
#> X11 X14 X17 X21 X22 X28 X34 X38 X39 X5 X7 X33
#> 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 0.9Variables selected in all or almost all models are stable: the lasso finds them whatever the patients used. Variables selected in few models are unstable, and we should be careful interpreting them.
Being selected does not mean being important: a variable may be
selected in all models but have a tiny coefficient.
glmnet_get.items.relevance estimates the
relevance of each variable, i.e., its share of the
total weight of the model, from the size of its standardized coefficient
(standardized so that variables measured in different units can be
compared) (Solanes et al., 2022; Salazar de Pablo et al., 2025). It
averages the relevance across the 10 models, and the relevances of all
variables add up to 1. We may look at those with at least 5%
relevance:
relevance = glmnet_get.items.relevance(res$models)
round(relevance[which(relevance >= 0.05)], 3)
#> X21 X39 X22 X14 X5 X7 X11
#> 0.139 0.131 0.128 0.101 0.077 0.063 0.059The two functions answer different questions, so it is useful to look at both: a relevant variable selected in all models is much more convincing than a variable that is relevant in a few models only.
The cross-validation estimates how well the models work, but with the cv(), we built 10 models (one for each fold), so which model should you use with future patients? There are two reasonable options.
The first option is to build the model again, using the same
fit_fun with all the patients:
The second option is to choose, among the 10 models built during the
cross-validation, the most representative one.
glmnet_get.main.model selects the model whose variables are
most similar to the variables of the other models (according to the Dice
coefficient), and if there are several equally representative models, it
averages them (Sobregrau et al., 2024; Salazar de Pablo et al.,
2025):
The first option uses all the patients, so it may be slightly more accurate. The second option uses one of the models whose accuracy was actually estimated in the cross-validation, and it is useful when, as we will see in another tutorial, the cross-validation involves several models per fold (e.g., one for each imputation of the missing values). Let’s use our crystal ball to see how both work in the new patients:
y.pred_all = predict(m_all, x_new)
y.pred_main = predict(m_main, x_new)
simple.accuracy(y_new, y.pred_all, "auc")
#> auc auc.lwr auc.upr
#> 1 0.8273424 0.8011382 0.8535465
simple.accuracy(y_new, y.pred_main, "auc")
#> auc auc.lwr auc.upr
#> 1 0.8160386 0.788921 0.8431562
simple.calibration(y_new, y.pred_all, "binomial")
#> calibr.large calibr.large.lwr calibr.large.upr calibr.slope calibr.slope.lwr
#> 1 0.2572726 0.09140591 0.423091 0.8667944 0.7511321
#> calibr.slope.upr
#> 1 0.9902875
simple.calibration(y_new, y.pred_main, "binomial")
#> calibr.large calibr.large.lwr calibr.large.upr calibr.slope calibr.slope.lwr
#> 1 0.253726 0.09716647 0.4102683 1.003944 0.8670087
#> calibr.slope.upr
#> 1 1.14981Both models discriminate similarly in the new patients, and their AUCs are in line with the one estimated by the cross-validation.
However, the calibration-in-the-large is now a bit positive: the models slightly underestimate the probability of relapse. This might be because fewer of our 100 patients relapsed compared to the new patients. This is one reason why models should be validated in independent samples, and why sharing your model is so useful (Radua & Koutsouleris, 2024): others can test it, and recalibrate it if needed, in their own patients.
Finally, to estimate the probability of relapse of a single new
patient, use predict with the data of this patient. For
instance, for the first new patient:
The model estimates that this patient has a high probability of
relapse. Note the drop = FALSE: x_new[1, ]
would return a vector, whereas predict needs a matrix, even
if it only has one row.
simple.calibration)
measure whether the estimated probabilities are correct.glmnet_get.items.count tells how stable the selection
of each variable is, and glmnet_get.items.relevance how
much each variable weighs.fit_fun using all the patients, or chosen among the
cross-validation models with glmnet_get.main.model. Use
predict to apply it to new patients.In the next tutorial, Missing data
(vignette("missing-data")), you will learn what to do when
some data are missing.
Radua, J., Koutsouleris, N. (2024) Ten simple rules for using machine learning in mental health research. Biological Psychiatry, 96, 511–513, doi: 10.1016/j.biopsych.2023.11.012.
Radua, J., Rivas-Fernandez, M.A., Lindin, M., Diaz, F., Zurron, M., Pereiro, A.X., Lojo-Seoane, C., De Prisco, M., Oliva, V., Vilajosana, E., Vieta, E., Galdo-Alvarez, S. Submitted
Riley, R.D., Ensor, J., Snell, K.I.E., Harrell, F.E., Martin, G.P., Reitsma, J.B., Moons, K.G.M., Collins, G., van Smeden, M. (2020) Calculating the sample size required for developing a clinical prediction model. BMJ, 368, m441, doi: 10.1136/bmj.m441.
Salazar de Pablo, G., Radua, J., Frearson, G., Young, A.H., Arango, C., Kelleher, I., Sharma, A., Uhlhaas, P.J., Solmi, M., Fusar-Poli, P., Guinart, D., Correll, C.U. (2025) Development and validation of a prognostic model and risk calculator for the estimation of bipolar-spectrum disorder risk in hospitalised adolescents with non-psychotic/non-bipolar mental disorders. Molecular Psychiatry, 31, 1333–1342, doi: 10.1038/s41380-025-03244-1.
Solanes, A., Mezquida, G., Janssen, J., Amoretti, S., Lobo, A., Gonzalez-Pinto, A., Arango, C., Vieta, E., Castro-Fornieles, J., Berge, D., Albacete, A., Gine, E., Parellada, M., Bernardo, M.; PEPs group (collaborators); Pomarol-Clotet, E., Radua, J. (2022) Combining MRI and clinical data to detect high relapse risk after the first episode of psychosis. Schizophrenia, 8, 100, doi: 10.1038/s41537-022-00309-w.
Sobregrau, P., Bailles, E., Radua, J., Carreno, M., Donaire, A., Setoain, X., Bargallo, N., Rumia, J., Sanchez-Vives, M.V., Pintor, L. (2024) Design and validation of a diagnostic suspicion checklist to differentiate epileptic from psychogenic nonepileptic seizures (PNES-DSC). Journal of Psychosomatic Research, 180, 111656, doi: 10.1016/j.jpsychores.2024.111656.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.