Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Performance measure properness

suppressPackageStartupMessages({
library(tidyverse)
library(yardstick)
library(cowplot)
library(ggrepel)
library(ggbeeswarm)
})
theme_set(theme_cowplot())
options(repr.plot.width = 15, repr.plot.height = 9)
set.seed(42)

A performance measure is called proper if its expected value is optimal when using the correct model, which is the model that gives the correct probabilities based on the predictors or features in the model. Here, expected value refers to the average value obtained after repeating the validation study multiple times. In any given dataset, particularly when sample size is low, the correct model can be outperformed by an incorrect model due to random variation. The importance of properness is that a proper measure cannot be fooled: in expectation, the correct model cannot be outperformed by an incorrect one. A measure is strictly proper when its expected value is optimal only for the correct model. When the expected value is optimal for the correct model and for some incorrect models, a measure is called semi-proper. When an incorrect model can have a better expected value than the correct model, the measure is termed improper and cannot be trusted.

The above is a quote from the paper “Evaluation of performance measures in predictive artificial intelligence models to support medical decisions: overview and guidance”. In this notebook, I reproduced the results from the Supplementary Appendix 1 of this paper, which demonstrates the properness of some performance metrics.

Generate data

First we generate some synthetic data, the paper does not provide code for this section but describes:

To illustrate the property of properness, we consider a hypothetical situation with four continuous predictors that have a standard normal distribution and that are correlated with each other (Pearson correlation 0.4 between any pair of predictors). The correct model for outcome Y is a logistic model with intercept -1 and predictor coefficients of 0.74, 0.18, 0.18, and 0.18. This setup corresponds to a true AUROC of 0.746 and a prevalence of 0.304. The clinically relevant decision threshold is set to 0.1.

n_samples = 20000
intercept = -1.0
coefficients = c(0.74, 0.18, 0.18, 0.18)
n_features = length(coefficients)
corr_mat = matrix(0.4, n_features, n_features)
diag(corr_mat) = 1
corr_mat
Loading...
L = chol(corr_mat) # Cholesky decomposition
Z = matrix(rnorm(n_samples * n_features), n_samples, n_features)
X = Z %*% L
cor(X)
Loading...
cor(X) %>%
.[row(.)!=col(.)] |> # exclude diagonal
mean() # paper's target correlation is 0.4
Loading...

Generate models

12 different models are proposed, model 1 is the correct model, and the other eleven are variations that should perform worse.

# 1. true model
trueLP = as.vector(intercept + X %*% coefficients)
probs_1 = plogis(trueLP)
y_true = as.numeric(runif(n_samples) < probs_1)
mean(y_true) # paper's target prevalence is 0.304
Loading...
# 2. expit(trueLP + 0.75)
probs_2 = plogis(trueLP + 0.75)
# 3. expit(trueLP - 1)
probs_3 = plogis(trueLP - 1)
# 4. expit(trueLP/1.3)
probs_4 = plogis(trueLP / 1.3)
# 5. expit(trueLP*2)
probs_5 = plogis(trueLP * 2)
# 6. expit(trueLP*2 - 1)
probs_6 = plogis(trueLP * 2 - 1)
# 7. prob<0.1 are shrunk by a factor of 10 and prob>=0.1 are blown up with 1 - (0.1 * (1 - prob))
probs_7 = ifelse(probs_1 < 0.1, 0.1 * probs_1, 1 - (0.1 * (1 - probs_1)))
# 8. like 7 but but use prevalence as threshold
probs_8 = ifelse(probs_1 < mean(y_true), 0.1 * probs_1, 1 - (0.1 * (1 - probs_1)))
# 9. like 7 but use 0.5 as threshold
probs_9 = ifelse(probs_1 < 0.5, 0.1 * probs_1, 1 - (0.1 * (1 - probs_1)))
# 10. 0.04 is added or subtracted at random to prob range [0.051, 0.949]
probs_10 = ifelse(probs_1 >= 0.051 & probs_1 <= 0.949, probs_1+ifelse(rbinom(n_samples, 1, 0.5), 0.04, -0.04), probs_1)
# 11. expit(trueLP + U(-0.2,0.2)), a random value in the range [-0.2, 0.2]
probs_11 = plogis(trueLP + runif(n_samples, min=-0.2, max=0.2))
# 12. use coefficients 0.74, 0.74, 0.18, and 0.18
modLP = as.vector(intercept + X %*% c(0.74, 0.74, 0.18, 0.18))
probs_12 = plogis(modLP)
df <- tibble(
    y_true,
    probs_1, probs_2, probs_3, probs_4,
    probs_5, probs_6, probs_7, probs_8,
    probs_9, probs_10, probs_11, probs_12
) |>
pivot_longer(names_to = 'model', values_to = 'prob', contains('probs')) |>
mutate(
    gt=factor(ifelse(y_true, '1','0'), levels=c('1','0')),
    model=gsub('probs_', '',model),
    model=factor(model, levels=sort(as.numeric(unique(model)))),
)
filter(df, model==1) |>
roc_auc(gt, prob) # paper's target roc_auc is 0.746
Loading...
df |>
ggplot(aes(x=gt, y=prob)) +
geom_violin() + 
facet_wrap(~model)
plot without title

Evaluation

To calculate the performance metrics, I will use the yardstick package. See their doc for the definition of each metric used here.

Class Probability Metrics

Class probability metrics are measures that evaluate model performance based on the predicted probability assigned to the target classes.

These metrics do not depend on a definition of a prediction threshold.

ms = metric_set(roc_auc, pr_auc, mn_log_loss, average_precision, brier_class, gain_capture)
df |>
group_by(model) |>
ms(gt, prob) |>
mutate(
    best_metric = ifelse(.metric %in% c('brier_class','mn_log_loss'), .estimate == min(.estimate), .estimate == max(.estimate)),
    .by = .metric
) |>
ggplot(aes(y=model, x=.estimate, color=best_metric)) +
geom_point() +
geom_label_repel(aes(label=model), max.overlaps = Inf, min.segment.length = 0, box.padding = 0.3) +
facet_wrap(~.metric, scales='free')
plot without title

mn_log_loss and brier_class are strictly proper metrics, therefore, only the correct model has the best metric.

The other metrics are semi-proper, because we can’t distinguish between the correct model and the others.

Classification Metrics

Classification metrics are measures that evaluate model performance based on the predicted class labels.

These metrics require a prediction threshold to transform a predicted probabilities into discrete class predictions. For this experiment, the threshold was set at 0.1.

ms = metric_set(mcc, kap, j_index, f_meas, accuracy, bal_accuracy)
df |>
mutate(
    est=factor(ifelse(prob>0.1, '1','0'), levels=c('1','0'))
) |>
group_by(model) |>
ms(gt, estimate=est) |>
mutate(
    best_metric = .estimate == max(.estimate),
    .by = .metric
) |>
ggplot(aes(y=model, x=.estimate, color=best_metric)) +
geom_point() +
geom_label_repel(aes(label=model), max.overlaps = Inf, min.segment.length = 0, box.padding = 0.3) +
facet_wrap(~.metric, scales='free')
plot without title

All metrics tested were improper, because the correct model had a worse metric than some of the other models.

Regression metrics

Regression probability metrics are measures that evaluate model performance based on the predicted continuous output values rather than discrete categories.

ms = metric_set(rmse, rsq, rsq_trad, mae, ccc, huber_loss, iic, poisson_log_loss, smape, msd, )
df |>
group_by(model) |>
ms(y_true, prob) |>
mutate(
    best_metric = case_when(
        .metric == 'msd' ~ abs(.estimate) == min(abs(.estimate)),
        .metric %in% c('ccc','iic','rsq','rsq_trad') ~ .estimate == max(.estimate),
        TRUE ~ .estimate == min(.estimate)
    ),
    .by = .metric
) |>
ggplot(aes(y=model, x=.estimate, color=best_metric)) +
geom_point() +
geom_label_repel(aes(label=model),max.overlaps = Inf, min.segment.length = 0, box.padding = 0.3) +
facet_wrap(~.metric, scales='free')
plot without title

rmse, huber_loss, rsq, rsq_trad and poisson_log_loss are strictly proper.

The others are improper.

References
  1. Van Calster, B., Collins, G. S., Vickers, A. J., Wynants, L., Kerr, K. F., Barreñada, L., Varoquaux, G., Singh, K., Moons, K. G., Hernandez-Boussard, T., Timmerman, D., McLernon, D. J., van Smeden, M., & Steyerberg, E. W. (2025). Evaluation of performance measures in predictive artificial intelligence models to support medical decisions: overview and guidance. The Lancet Digital Health, 7(12), 100916. 10.1016/j.landig.2025.100916