---
title: "Evaluating and interpreting your model"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Evaluating and interpreting your model}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

In the first tutorial ([Get started](easy.glmnet.html)), you learned to build a
prediction model and to estimate its accuracy correctly with cross-validation.
In this tutorial, you will learn to:

1. Measure how well the model **discriminates** the patients who will relapse
   from those who will not, with the area under the ROC curve.
2. Measure whether the probabilities that the model estimates are correct,
   i.e., whether the model is well **calibrated**.
3. Find which variables are the most **relevant** for the predictions.
4. Obtain the model that you would use with future patients, and use it to
   estimate the probability of relapse of a **new patient**.

This tutorial takes about 20–30 minutes and, as in the first tutorial, all the
code runs in a few seconds.


## The data and the cross-validation

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:

```{r}
library(easy.glmnet)
x = as.matrix(patients[, 1:40])
y = patients$relapse
x_new = as.matrix(new_patients[, 1:40])
y_new = new_patients$relapse
```

We 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.

```{r}
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:

```{r}
res
```

Let's see what these numbers mean.


## Step 1: Discrimination

### Probabilities and thresholds

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:

```{r}
y.pred = round(res$predictions$y.pred)
simple.accuracy(res$predictions$y, y.pred, "bac")
```

However, 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

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`:

```{r}
simple.accuracy(res$predictions$y, res$predictions$y.pred, "auc")
```


## Step 2: Calibration

### Are the probabilities correct?

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**.

### Calibration-in-the-large and calibration slope

`simple.calibration` summarizes the calibration with two numbers, each with its
95% confidence interval:

```{r}
simple.calibration(res$predictions$y, res$predictions$y.pred, "binomial")
```

- The **calibration-in-the-large** (`calibr.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.
- The **calibration slope** (`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.


## Step 3: Which variables are relevant?

### How often is each variable selected?

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:

```{r}
count = glmnet_get.items.count(res$models)
count[which(count >= 0.9)]
```

Variables 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.

### How much does each variable weigh?

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:

```{r}
relevance = glmnet_get.items.relevance(res$models)
round(relevance[which(relevance >= 0.05)], 3)
```

The 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.


## Step 4: The model to use with new patients

### Two options

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:

```{r}
m_all = glmnet_fit(x, y, family = "binomial")
```

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):

```{r}
m_main = glmnet_get.main.model(res$models, verbose = FALSE)
```

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:

```{r}
y.pred_all = predict(m_all, x_new)
y.pred_main = predict(m_main, x_new)
simple.accuracy(y_new, y.pred_all, "auc")
simple.accuracy(y_new, y.pred_main, "auc")
simple.calibration(y_new, y.pred_all, "binomial")
simple.calibration(y_new, y.pred_main, "binomial")
```

Both 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.

### Estimating the probability of relapse of a new patient

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:

```{r}
new_patient = x_new[1, , drop = FALSE]
predict(m_main, 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.


## Summary

- Keep the probabilities (do not round them) to assess the model fully.
- The **AUC** measures discrimination without choosing a threshold.
- The **calibration-in-the-large** and the **calibration slope**
  (`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.
- The model to use with future patients can be built with `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](missing-data.html) (`vignette("missing-data")`),
you will learn what to do when some data are missing.


## References

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](https://doi.org/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](https://doi.org/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](https://doi.org/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](https://doi.org/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](https://doi.org/10.1016/j.jpsychores.2024.111656).
