Quizá valga la pena añadir otro ejemplo, tal vez más directo, a la excelente respuesta de Stephen.
Consideremos una prueba médica, cuyo resultado se distribuye normalmente, tanto en enfermos como en sanos, por supuesto con parámetros diferentes (pero para simplificar, supongamos homocedasticidad, es decir, que la varianza es la misma): $$\begin{gather*}T \mid D \ominus \sim \mathcal{N}\left(\mu_{-},\sigma^2\right)\\T \mid D \oplus \sim \mathcal{N}\left(\mu_{+},\sigma^2\right)\end{gather*}.$$ Denotemos la prevalencia de la enfermedad con $p$ (es decir $D\oplus\sim Bern\left(p\right)$ ), por lo que ésta, junto con las anteriores, que son esencialmente distribuciones condicionales, especifica completamente la distribución conjunta.
Así pues, la matriz de confusión con umbral $b$ (es decir, aquellos con resultados superiores a $b$ se clasifican como enfermos) es $$\begin{pmatrix} & D\oplus & D\ominus\\ T\oplus & p\left(1-\Phi_{+}\left(b\right)\right) & \left(1-p\right)\left(1-\Phi_{-}\left(b\right)\right)\\ T\ominus & p\Phi_{+}\left(b\right) & \left(1-p\right)\Phi_{-}\left(b\right)\\ \end{pmatrix}.$$
Enfoque basado en la precisión
La precisión es $$p\left(1-\Phi_{+}\left(b\right)\right)+\left(1-p\right)\Phi_{-}\left(b\right),$$
tomamos su derivada respecto a $b$ igual a 0, multiplicar por $\sqrt{1\pi\sigma^2}$ y reorganizar un poco: $$\begin{gather*} -p\varphi_{+}\left(b\right)+\varphi_{-}\left(b\right)-p\varphi_{-}\left(b\right)=0\\ e^{-\frac{\left(b-\mu_{-}\right)^2}{2\sigma^2}}\left[\left(1-p\right)-pe^{-\frac{2b\left(\mu_{-}-\mu_{+}\right)+\left(\mu_{+}^2-\mu_{-}^2\right)}{2\sigma^2}}\right]=0\end{gather*}$$ El primer término no puede ser cero, por lo que la única forma de que el producto sea cero es que el segundo término sea cero: $$\begin{gather*}\left(1-p\right)-pe^{-\frac{2b\left(\mu_{-}-\mu_{+}\right)+\left(\mu_{+}^2-\mu_{-}^2\right)}{2\sigma^2}}=0\\-\frac{2b\left(\mu_{-}-\mu_{+}\right)+\left(\mu_{+}^2-\mu_{-}^2\right)}{2\sigma^2}=\log\frac{1-p}{p}\\ 2b\left(\mu_{+}-\mu_{-}\right)+\left(\mu_{-}^2-\mu_{+}^2\right)=2\sigma^2\log\frac{1-p}{p}\\ \end{gather*}$$ Así que la solución es $$b^{\ast}=\frac{\left(\mu_{+}^2-\mu_{-}^2\right)+2\sigma^2\log\frac{1-p}{p}}{2\left(\mu_{+}-\mu_{-}\right)}=\frac{\mu_{+}+\mu_{-}}{2}+\frac{\sigma^2}{\mu_{+}-\mu_{-}}\log\frac{1-p}{p}.$$
Tenga en cuenta que esto -por supuesto- no depende de los costes.
Si las clases están equilibradas, el óptimo es la media de los valores medios de las pruebas en enfermos y sanos; en caso contrario, se desplaza en función del desequilibrio.
Enfoque basado en los costes
Utilizando la notación de Stephen, el coste global esperado es $$c_{+}^{+}p\left(1-\Phi_{+}\left(b\right)\right) + c_{+}^{-}\left(1-p\right)\left(1-\Phi_{-}\left(b\right)\right) + c_{-}^{+} p\Phi_{+}\left(b\right) + c_{-}^{-} \left(1-p\right)\Phi_{-}\left(b\right).$$ Tomar su derivada respecto a $b$ y ponerlo a cero: $$\begin{gather*} -c_{+}^{+} p \varphi_{+}\left(b\right)-c_{+}^{-}\left(1-p\right)\varphi_{-}\left(b\right)+c_{-}^{+}p\varphi_{+}\left(b\right)+c_{-}^{-}\left(1-p\right)\varphi_{-}\left(b\right)=\\ =\varphi_{+}\left(b\right)p\left(c_{-}^{+}-c_{+}^{+}\right)+\varphi_{-}\left(b\right)\left(1-p\right)\left(c_{-}^{-}-c_{+}^{-}\right)=\\ = \varphi_{+}\left(b\right)pc_d^{+}-\varphi_{-}\left(b\right)\left(1-p\right)c_d^{-}= 0,\end{gather*}$$ utilizando la notación que introduje en mis comentarios debajo de la respuesta de Stephen, es decir $c_d^{+}=c_{-}^{+}-c_{+}^{+}$ y $c_d^{-}=c_{+}^{-}-c_{-}^{-}$ .
Por tanto, el umbral óptimo viene dado por la solución de la ecuación $$\boxed{\frac{\varphi_{+}\left(b\right)}{\varphi_{-}\left(b\right)}=\frac{\left(1-p\right)c_d^{-}}{pc_d^{+}}}.$$ Aquí hay que señalar dos cosas:
- Este resultado es totalmente genérico y trabaja para cualquier distribución de los resultados de las pruebas, no sólo normal. ( $\varphi$ en ese caso, por supuesto, significa la función de densidad de probabilidad de la distribución, no la densidad normal).
- Cualquiera que sea la solución para $b$ es, seguramente es una función de $\frac{\left(1-p\right)c_d^{-}}{pc_d^{+}}$ . (Es decir, vemos inmediatamente cómo importan los costes, además del desequilibrio de clases).
Me interesaría mucho saber si esta ecuación tiene una solución genérica para $b$ (parametrizado por el $\varphi$ s), pero me sorprendería.
No obstante, ¡podemos solucionarlo con normalidad! $\sqrt{2\pi\sigma^2}$ s se cancelan en el lado izquierdo, por lo que tenemos $$\begin{gather*} e^{-\frac{1}{2}\left(\frac{\left(b-\mu_{+}\right)^2}{\sigma^2}-\frac{\left(b-\mu_{-}\right)^2}{\sigma^2}\right)}=\frac{\left(1-p\right)c_d^{-}}{pc_d^{+}} \\ \left(b-\mu_{-}\right)^2-\left(b-\mu_{+}\right)^2 =2\sigma^2 \log \frac{\left(1-p\right)c_d^{-}}{pc_d^{+}} \\ 2b\left(\mu_{+}-\mu_{-}\right)+\left(\mu_{-}^2-\mu_{+}^2\right) =2\sigma^2 \log \frac{\left(1-p\right)c_d^{-}}{pc_d^{+}}\end{gather*}$$ por lo tanto la solución es $$b^{\ast}=\frac{\left(\mu_{+}^2-\mu_{-}^2\right)+2\sigma^2 \log \frac{\left(1-p\right)c_d^{-}}{pc_d^{+}}}{2\left(\mu_{+}-\mu_{-}\right)}=\frac{\mu_{+}+\mu_{-}}{2}+\frac{\sigma^2}{\mu_{+}-\mu_{-}}\log \frac{\left(1-p\right)c_d^{-}}{pc_d^{+}}.$$
(¡Compárelo con el resultado anterior! Vemos que son iguales si y sólo si $c_d^{-}=c_d^{+}$ es decir, las diferencias en el coste de la clasificación errónea en comparación con el coste de la clasificación correcta es el mismo en personas enfermas y sanas).
Breve demostración
Digamos que $c_{-}^{-}=0$ (es bastante natural desde el punto de vista médico), y que $c_{+}^{+}=1$ (siempre podemos obtenerlo dividiendo los costes por $c_{+}^{+}$ es decir, midiendo cada coste en $c_{+}^{+}$ unidades). Digamos que la prevalencia es $p=0.2$ . Además, digamos que $\mu_{-}=9.5$ , $\mu_{+}=10.5$ y $\sigma=1$ .
En este caso:
library( data.table )
library( lattice )
cminusminus <- 0
cplusplus <- 1
p <- 0.2
muminus <- 9.5
muplus <- 10.5
sigma <- 1
res <- data.table( expand.grid( b = seq( 6, 17, 0.1 ),
cplusminus = c( 1, 5, 10, 50, 100 ),
cminusplus = c( 2, 5, 10, 50, 100 ) ) )
res$cost <- cplusplus*p*( 1-pnorm( res$b, muplus, sigma ) ) +
res$cplusminus*(1-p)*(1-pnorm( res$b, muminus, sigma ) ) +
res$cminusplus*p*pnorm( res$b, muplus, sigma ) +
cminusminus*(1-p)*pnorm( res$b, muminus, sigma )
xyplot( cost ~ b | factor( cminusplus ), groups = cplusminus, ylim = c( -1, 22 ),
data = res, type = "l", xlab = "Threshold",
ylab = "Expected overall cost", as.table = TRUE,
abline = list( v = (muplus+muminus)/2+
sigma^2/(muplus-muminus)*log((1-p)/p) ),
strip = strip.custom( var.name = expression( {"c"^{"+"}}["-"] ),
strip.names = c( TRUE, TRUE ) ),
auto.key = list( space = "right", points = FALSE, lines = TRUE,
title = expression( {"c"^{"-"}}["+"] ) ),
panel = panel.superpose, panel.groups = function( x, y, col.line, ... ) {
panel.xyplot( x, y, col.line = col.line, ... )
panel.points( x[ which.min( y ) ], min( y ), pch = 19, col = col.line )
} )
El resultado es (los puntos representan el coste mínimo, y la línea vertical muestra el umbral óptimo con el enfoque basado en la precisión):
Podemos ver muy bien cómo el óptimo basado en los costes puede ser diferente del óptimo basado en la precisión. Es instructivo pensar por qué: si es más costoso clasificar a un enfermo erróneamente como sano que al revés ( $c_{-}^{+}$ es alta, $c_{+}^{-}$ es bajo) que el umbral baja, ya que preferimos clasificar más fácilmente en la categoría de enfermo, por otro lado, si es más costoso clasificar erróneamente como enfermo a una persona sana que al revés ( $c_{-}^{+}$ es baja, $c_{+}^{-}$ es alto) que el umbral sube, ya que preferimos clasificar más fácilmente en la categoría sano. (¡Compruébelo en la figura!)
Un ejemplo de la vida real
Veamos un ejemplo empírico, en lugar de una derivación teórica. Este ejemplo será diferente básicamente desde dos aspectos:
- En lugar de suponer la normalidad, utilizaremos simplemente los datos empíricos sin ninguna suposición de este tipo.
- En lugar de utilizar una única prueba, y sus resultados en sus propias unidades, utilizaremos varias pruebas (y las combinaremos con una regresión logística). El umbral se dará a la probabilidad predicha final. Este es en realidad el enfoque preferido, véase el capítulo 19 - Diagnóstico - en BBR de Frank Harrell .
En conjunto de datos ( acath
del paquete Hmisc
) procede del Banco de Datos de Enfermedades Cardiovasculares de la Universidad de Duke, y contiene si el paciente tenía una enfermedad coronaria significativa, evaluada mediante cateterismo cardíaco, éste será nuestro patrón oro, es decir, el verdadero estado de la enfermedad, y la "prueba" será la combinación de la edad, el sexo, el nivel de colesterol y la duración de los síntomas del sujeto:
library( rms )
library( lattice )
library( latticeExtra )
library( data.table )
getHdata( "acath" )
acath <- acath[ !is.na( acath$choleste ), ]
dd <- datadist( acath )
options( datadist = "dd" )
fit <- lrm( sigdz ~ rcs( age )*sex + rcs( choleste ) + cad.dur, data = acath )
Merece la pena trazar los riesgos predichos en escala logit, para ver hasta qué punto son normales (en esencia, eso es lo que suponíamos anteriormente, ¡con una sola prueba!):
densityplot( ~predict( fit ), groups = acath$sigdz, plot.points = FALSE, ref = TRUE,
auto.key = list( columns = 2 ) )
Bueno, no son normales...
Sigamos y calculemos el coste global previsto:
ExpectedOverallCost <- function( b, p, y, cplusminus, cminusplus,
cplusplus = 1, cminusminus = 0 ) {
sum( table( factor( p>b, levels = c( FALSE, TRUE ) ), y )*matrix(
c( cminusminus, cplusminus, cminusplus, cplusplus ), nc = 2 ) )
}
table( predict( fit, type = "fitted" )>0.5, acath$sigdz )
ExpectedOverallCost( 0.5, predict( fit, type = "fitted" ), acath$sigdz, 2, 4 )
Y vamos a trazarla para todos los costes posibles (una nota computacional: no necesitamos iterar sin sentido a través de números de 0 a 1, podemos reconstruir perfectamente la curva calculándola para todos los valores únicos de probabilidades previstas):
ps <- sort( unique( c( 0, 1, predict( fit, type = "fitted" ) ) ) )
xyplot( sapply( ps, ExpectedOverallCost,
p = predict( fit, type = "fitted" ), y = acath$sigdz,
cplusminus = 2, cminusplus = 4 ) ~ ps, type = "l", xlab = "Threshold",
ylab = "Expected overall cost", panel = function( x, y, ... ) {
panel.xyplot( x, y, ... )
panel.points( x[ which.min( y ) ], min( y ), pch = 19, cex = 1.1 )
panel.text( x[ which.min( y ) ], min( y ), round( x[ which.min( y ) ], 3 ),
pos = 3 )
} )
Podemos ver muy bien dónde debemos poner el umbral para optimizar el coste global previsto (¡sin utilizar la sensibilidad, la especificidad o los valores predictivos en ningún sitio!). Este es el enfoque correcto.
Resulta especialmente instructivo contrastar estas métricas:
ExpectedOverallCost2 <- function( b, p, y, cplusminus, cminusplus,
cplusplus = 1, cminusminus = 0 ) {
tab <- table( factor( p>b, levels = c( FALSE, TRUE ) ), y )
sens <- tab[ 2, 2 ] / sum( tab[ , 2 ] )
spec <- tab[ 1, 1 ] / sum( tab[ , 1 ] )
c( `Expected overall cost` = sum( tab*matrix( c( cminusminus, cplusminus, cminusplus,
cplusplus ), nc = 2 ) ),
Sensitivity = sens,
Specificity = spec,
PPV = tab[ 2, 2 ] / sum( tab[ 2, ] ),
NPV = tab[ 1, 1 ] / sum( tab[ 1, ] ),
Accuracy = 1 - ( tab[ 1, 1 ] + tab[ 2, 2 ] )/sum( tab ),
Youden = 1 - ( sens + spec - 1 ),
Topleft = ( 1-sens )^2 + ( 1-spec )^2
)
}
ExpectedOverallCost2( 0.5, predict( fit, type = "fitted" ), acath$sigdz, 2, 4 )
res <- melt( data.table( ps, t( sapply( ps, ExpectedOverallCost2,
p = predict( fit, type = "fitted" ),
y = acath$sigdz,
cplusminus = 2, cminusplus = 4 ) ) ),
id.vars = "ps" )
p1 <- xyplot( value ~ ps, data = res, subset = variable=="Expected overall cost",
type = "l", xlab = "Threshold", ylab = "Expected overall cost",
panel=function( x, y, ... ) {
panel.xyplot( x, y, ... )
panel.abline( v = x[ which.min( y ) ],
col = trellis.par.get()$plot.line$col )
panel.points( x[ which.min( y ) ], min( y ), pch = 19 )
} )
p2 <- xyplot( value ~ ps, groups = variable,
data = droplevels( res[ variable%in%c( "Expected overall cost",
"Sensitivity",
"Specificity", "PPV", "NPV" ) ] ),
subset = variable%in%c( "Sensitivity", "Specificity", "PPV", "NPV" ),
type = "l", xlab = "Threshold", ylab = "Sensitivity/Specificity/PPV/NPV",
auto.key = list( columns = 3, points = FALSE, lines = TRUE ) )
doubleYScale( p1, p2, use.style = FALSE, add.ylab2 = TRUE )
Ahora podemos analizar esas métricas que a veces se anuncian específicamente como capaces de llegar a un corte óptimo sin costes, ¡y contrastarlo con nuestro enfoque basado en los costes! Utilicemos las tres métricas más utilizadas:
- Precisión (maximizar la precisión)
- Regla de Youden (maximizar $Sens+Spec-1$ )
- Regla de Topleft (minimizar $\left(1-Sens\right)^2+\left(1-Spec\right)^2$ )
(Para simplificar, restaremos los valores anteriores de 1 para la regla de Youden y la de Precisión, de modo que tengamos un problema de minimización en todas partes).
Veamos los resultados:
p3 <- xyplot( value ~ ps, groups = variable,
data = droplevels( res[ variable%in%c( "Expected overall cost", "Accuracy",
"Youden", "Topleft" ) ] ),
subset = variable%in%c( "Accuracy", "Youden", "Topleft" ),
type = "l", xlab = "Threshold", ylab = "Accuracy/Youden/Topleft",
auto.key = list( columns = 3, points = FALSE, lines = TRUE ),
panel = panel.superpose, panel.groups = function( x, y, col.line, ... ) {
panel.xyplot( x, y, col.line = col.line, ... )
panel.abline( v = x[ which.min( y ) ], col = col.line )
panel.points( x[ which.min( y ) ], min( y ), pch = 19, col = col.line )
} )
doubleYScale( p1, p3, use.style = FALSE, add.ylab2 = TRUE )
Por supuesto, esto se refiere a una estructura de costes específica, $c_{-}^{-}=0$ , $c_{+}^{+}=1$ , $c_{+}^{-}=2$ , $c_{-}^{+}=4$ (obviamente, esto sólo importa para la decisión de coste óptimo). Para investigar el efecto de la estructura de costes, elijamos sólo el umbral óptimo (en lugar de trazar toda la curva) y representémoslo en función de los costes. Más concretamente, como ya hemos visto, el umbral óptimo depende de los cuatro costes sólo a través de la variable $c_d^{-}/c_d^{+}$ por lo que vamos a trazar el límite óptimo en función de esto, junto con las métricas típicamente utilizadas que no utilizan costes:
res2 <- data.frame( rat = 10^( seq( log10( 0.02 ), log10( 50 ), length.out = 500 ) ) )
res2$OptThreshold <- sapply( res2$rat,
function( rat ) ps[ which.min(
sapply( ps, Vectorize( ExpectedOverallCost, "b" ),
p = predict( fit, type = "fitted" ),
y = acath$sigdz,
cplusminus = rat,
cminusplus = 1,
cplusplus = 0 ) ) ] )
xyplot( OptThreshold ~ rat, data = res2, type = "l", ylim = c( -0.1, 1.1 ),
xlab = expression( {"c"^{"-"}}["d"]/{"c"^{"+"}}["d"] ), ylab = "Optimal threshold",
scales = list( x = list( log = 10, at = c( 0.02, 0.05, 0.1, 0.2, 0.5, 1,
2, 5, 10, 20, 50 ) ) ),
panel = function( x, y, resin = res[ ,.( ps[ which.min( value ) ] ),
.( variable ) ], ... ) {
panel.xyplot( x, y, ... )
panel.abline( h = resin[variable=="Youden"] )
panel.text( log10( 0.02 ), resin[variable=="Youden"], "Y", pos = 3 )
panel.abline( h = resin[variable=="Accuracy"] )
panel.text( log10( 0.02 ), resin[variable=="Accuracy"], "A", pos = 3 )
panel.abline( h = resin[variable=="Topleft"] )
panel.text( log10( 0.02 ), resin[variable=="Topleft"], "TL", pos = 1 )
} )
Las líneas horizontales indican los enfoques que no utilizan costes (y que, por tanto, son constantes).
Una vez más, vemos que a medida que aumenta el coste adicional de la clasificación errónea en el grupo sano en comparación con el grupo enfermo, aumenta el umbral óptimo: si realmente no queremos que las personas sanas se clasifiquen como enfermas, utilizaremos un umbral más alto (¡y al revés, por supuesto!).
Y, por último, volvemos a ver por qué los métodos que no utilizan costes no son ( ¡y no puedo! ) sea siempre óptima.