Talvez valha a pena acrescentar outro exemplo, talvez mais direto, à excelente resposta de Stephen.
T| D ⊖ ~ N( μ-, σ2)T| D ⊕ ~ N( μ+, σ2).
pD ⊕ ~ B e r n ( p )
bb⎛⎝⎜T⊕T⊖D ⊕p ( 1 - Φ+( B ) )p Φ+( B )D ⊖( 1 - p ) ( 1 - Φ-( B ) )( 1 - p ) Φ-( B )⎞⎠⎟.
Abordagem baseada em precisão
p ( 1 - Φ+( b ) ) + ( 1 - p ) Φ-( B ) ,
b1 πσ2----√- p φ+( b ) + φ-( b ) - p φ-( b ) = 0e- ( b - μ-)22 σ2[ ( 1 - p ) - p e- 2 b ( μ-- μ+) + ( μ2+- μ2-)2 σ2] =0
( 1 - p ) - p e- 2 b ( μ-- μ+) + ( μ2+- μ2-)2 σ2= 0- 2 b ( μ-- μ+) + ( μ2+- μ2-)2 σ2= log1 - pp2 b ( μ+- μ-) + ( μ2-- μ2+) =2 σ2registro1 - pp
b∗= ( μ2+- μ2-) +2 σ2registro1 - pp2 ( μ+- μ-)= μ++ μ-2+ σ2μ+- μ-registro1 - pp.
Observe que isso - é claro - não depende dos custos.
Se as aulas são equilibradas, o ideal é a média dos valores médios dos testes em pessoas doentes e saudáveis, caso contrário, ela é deslocada com base no desequilíbrio.
Abordagem baseada em custos
Usando a notação de Stephen, o custo total esperado é c++p ( 1 - Φ+( b ) ) + c-+( 1 -p ) ( 1 - Φ-(b ) ) + c+-p Φ+( b) + c--( 1 - p) Φ-( B ) .
Tome seu derivado wrt b e defina-o como zero: - c++p φ+( B ) - C-+( 1 - p ) φ-( b ) + c+-p φ+( b ) + c--( 1 - p ) φ-( B ) == φ+( B ) p ( c+-- c++) + φ-(b)(1−p)(c−−−c−+)==φ+(b)pc+d−φ−(b)(1−p)c−d=0,
using the notation I introduced in my comments below Stephen's answer, i.e., c+d=c+−−c++ and c−d=c−+−c−−.
The optimal threshold is therefore given by the solution of the equation φ+(b)φ−(b)=(1−p)c−dp c+d.
Duas coisas devem ser observadas aqui:
- Esse resultado é totalmente genérico e funciona para qualquer distribuição dos resultados do teste, não apenas o normal. (φ nesse caso, é claro, significa a função de densidade de probabilidade da distribuição, não a densidade normal.)
- Qualquer que seja a solução para b é, certamente, uma função de ( 1 - p ) c-dp c+d. (Ou seja, vemos imediatamente como os custos são importantes - além do desequilíbrio de classe!)
Eu ficaria realmente interessado em ver se esta equação tem uma solução genérica para b (parametrizado pelo φs), mas eu ficaria surpreso.
No entanto, podemos resolver isso normalmente! 2 πσ2----√s cancelar no lado esquerdo, por isso temos e- 12( ( b - μ+)2σ2- ( b - μ-)2σ2)= ( 1 - p ) c-dp c+d( b - μ-)2- ( b - μ+)2= 2 σ2registro( 1 - p ) c-dp c+d2 b ( μ+- μ-) + ( μ2-- μ2+) =2 σ2registro( 1 - p ) c-dp c+d
portanto, a solução é b∗= ( μ2+- μ2-) +2 σ2registro( 1 - p ) c-dp c+d2 ( μ+- μ-)= μ++ μ-2+ σ2μ+- μ-registro( 1 - p ) c-dp c+d.
(Compare com o resultado anterior! Vemos que eles são iguais se e somente se c-d= c+d, ou seja, as diferenças no custo da classificação incorreta em comparação com o custo da classificação correta são as mesmas em pessoas doentes e saudáveis.)
Uma breve demonstração
Digamos c--= 0 (é bastante natural em termos médicos), e que c++= 1 (sempre podemos obtê-lo dividindo os custos com c++, medindo todos os custos em c++unidades). Digamos que a prevalência ép = 0,2. Além disso, digamos queμ-= 9,5, μ+= 10,5 e σ= 1.
Nesse 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 )
} )
O resultado é (os pontos representam o custo mínimo e a linha vertical mostra o limite ideal com a abordagem baseada na precisão):
Podemos ver muito bem como o ótimo baseado em custo pode ser diferente do ideal baseado em precisão. É instrutivo pensar no porquê: se é mais caro classificar pessoas doentes erroneamente saudáveis do que o contrário (c+- é alto, c-+ é baixo) do que o limiar diminui, pois preferimos classificar mais facilmente na categoria doente, por outro lado, se for mais caro classificar pessoas saudáveis erroneamente doentes do que o contrário (c+- é baixo, c-+é alto) do que o limite aumenta, pois preferimos classificar mais facilmente na categoria saudável. (Verifique estes na figura!)
Um exemplo da vida real
Vamos dar uma olhada em um exemplo empírico, em vez de uma derivação teórica. Este exemplo será diferente basicamente de dois aspectos:
- Em vez de assumir a normalidade, simplesmente usaremos os dados empíricos sem essa hipótese.
- Em vez de usar um único teste e seus resultados em suas próprias unidades, usaremos vários testes (e os combinaremos com uma regressão logística). O limite será dado à probabilidade final prevista. Esta é realmente a abordagem preferida, consulte o Capítulo 19 - Diagnóstico - no BBR de Frank Harrell .
O conjunto de dados ( acath
do pacote Hmisc
) é do Banco de Dados de Doenças Cardiovasculares da Universidade de Duke e contém se o paciente teve doença coronariana significativa, avaliada por cateterismo cardíaco, este será nosso padrão ouro, ou seja, o verdadeiro status da doença e o "teste "será a combinação da idade, sexo, nível de colesterol e duração dos sintomas do sujeito:
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 )
Vale a pena traçar os riscos previstos em escala logit, para ver como eles são normais (essencialmente, foi o que assumimos anteriormente, com um único teste!):
densityplot( ~predict( fit ), groups = acath$sigdz, plot.points = FALSE, ref = TRUE,
auto.key = list( columns = 2 ) )
Bem, eles não são normais ...
Vamos continuar e calcular o custo total esperado:
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 )
E vamos plotá-lo para todos os custos possíveis (uma observação computacional: não precisamos iterar irremediavelmente através de números de 0 a 1, podemos reconstruir perfeitamente a curva calculando-a para todos os valores exclusivos 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 muito bem onde devemos colocar o limite para otimizar o custo geral esperado (sem usar sensibilidade, especificidade ou valores preditivos em qualquer lugar!). Essa é a abordagem correta.
É especialmente instrutivo contrastar essas 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 )
Agora, podemos analisar as métricas que às vezes são anunciadas especificamente como capazes de criar um ponto de corte ideal sem custos, e contrastá-lo com nossa abordagem baseada em custos! Vamos usar as três métricas mais usadas:
- Precisão (maximizar precisão)
- Regra de Youden (maximizar Se n s + Sp e c - 1)
- Regra de Topleft (minimizar ( 1 - Se n s )2+ ( 1 - Sp e c )2)
(Por uma questão de simplicidade, subtrairemos os valores acima de 1 para a regra de Youden e de Precisão, para que tenhamos um problema de minimização em todos os lugares.)
Vamos ver os 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 )
Obviamente, isso se refere a uma estrutura de custos específica, c--= 0, c++= 1, c-+= 2, c+-= 4(isso obviamente é importante apenas para a decisão de custo ideal). Para investigar o efeito da estrutura de custos, escolha apenas o limite ideal (em vez de rastrear toda a curva), mas plote-o em função dos custos. Mais especificamente, como já vimos, o limite ideal depende dos quatro custos apenas através doc-d/ c+d , vamos traçar o ponto de corte ideal em função disso, juntamente com as métricas normalmente usadas que não usam custos:
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 )
} )
Linhas horizontais indicam as abordagens que não usam custos (e, portanto, são constantes).
Novamente, vemos bem que, à medida que o custo adicional da classificação incorreta no grupo saudável aumenta em comparação com o grupo doente, o limiar ideal aumenta: se realmente não queremos que as pessoas saudáveis sejam classificadas como doentes, usaremos pontos de corte mais altos (e o contrário, é claro!).
E, finalmente, mais uma vez vemos por que os métodos que não usam custos nem sempre são ( e não podem! ) Ser sempre ótimos.