Ir al contenido principal

Curva ROC

La curva ROC (Receiver Operating Characteristic) pasó de los campos de batalla de la Segunda Guerra Mundial, donde fue usada para analizar las señales de radar, a formar parte importante de la teoría de detección de señales, y a ser usada en campos tan diversos como psicología, epidemiología, radiología, investigaciones médicas, aprendizaje automático (machine learning) y otros. Básicamente sirve para ilustrar el desempeño de un modelo o sistema de clasificación a través de una secuencia de umbrales. El área bajo la curva (AUC) es también usada para seleccionar modelos.

En R (CRAN) existen muchos paquetes con los que se pueden obtener excelentes gráficos de la curva ROC; sin embargo, con la finalidad de indagar un poco acerca del procedimiento para su obtención, conviene intentar hacer los cálculos y el gráfico prescindiendo de paquetes.

Es preciso disponer de las secuencias de tasas de verdaderos positivos y falsos positivos a lo largo de los umbrales impuestos por los pronósticos:
tpr = tp/(tp + fn)
fpr = fp/(fp + tn) = 1 - especificidad
donde: fp : falsos positivos
tp: verdaderos positivos (true positive)
fn: falsos negativos
tn: verdaderos negativos (true negatives)


Generalmente se cuenta con una columna de pronósticos procedentes de un modelo y una columna de observaciones, como los siguientes:
set.seed(1227)

obs <- rbinom(100, 1, .5)

pronos <- rnorm(100, mean = obs, sd = 1)

Obs corresponde a los valores reales y 'pronos' los pronósticos provenientes de algún modelo, ficticio en este caso, pues estoy usando valores simulados.
Un primer paso será ordenar las observaciones en función de los pronósticos, estos son ordenados a su vez en modo decreciente.
# observaciones ordenadas como factor ordenado
obs <- ordered(obs)

levels(obs)

order_pron <- order(pronos, decreasing = TRUE)

obs_ord <- obs[order(pronos)]

# pronosticos ordenados

pronos_ord <- pronos[order_pron]

El total de casos positivos y negativos se obtiene rapidamente mediante el conteo de casos positivos y negativos simplemente:
# numero de positivos 

n_pos <- sum(obs == "1")

# numero de negativos

n_neg <- sum(obs == "0")

Los falsos negativos representan el número de valores falsos en cada punto del umbral (pronósticos ordenados), mediante la función cumsum se obtiene el acumulado de casos con la condición negativa.
# falsos-positivos

fp <- cumsum(obs[order_pron] == "0")
De modo similar se calculan los verdaderos positivos
# positivos-reales

tp <- cumsum(obs[order_pron] == "1")

# la curva parte de cero, añadiré un cero al inicio
tp <- c(0, tp)

fp <- c(0, fp)

Comparando con los valores producidos por el paquete ROCR
library(ROCR) # para comprobar los cálculos
library(WVPlots) # para comparar los gráficos

predROCR <- predictions(pronos, obs)

all.equal(predROCR@fp[[1]], fp)
# [1] TRUE

all.equal(predROCR@tp[[1]], tp)
# [1] TRUE
Los valores coinciden, para el cálculo de falsos negativos 'fn' y 'verdaderos negativos 'tn' procederé de forma análoga, pero en este caso serán resultado de la sustracción de los totales. Es decir, restando el vector de falsos positivos del total de negativos y al total de positivos se le sustrayendole el vector de verdaderos positivos
tn <- n_neg - fp # verdaderos positivos

fn <- n_pos - tp # falsos negativos

El siguiente cotejo muestra que coinciden:
all.equal(predROCR@fn[[1]], fn)
# [1] TRUE
all.equal(predROCR@tn[[1]], tn)
# [1] TRUE
La sensibilidad (tpr o recall) es el cociente de los verdaderos positivos entre el total de casos positivos.
sens <- tp/n_pos

fpr <- fp/n_neg # igual a 1 - especificidad

El área bajo la curva (AUC) resulta de multiplicar la altura (tpr) por cada una de las diferencias entre los valores de la tasa de falsos positivos. A la manera del cálculo integral, cada diferencia representa la pequeña base de un rectangulo cuya altura es el valor correspondiente fpr, al efectuar la suma se obtiene el área.
base <- diff(c(0, fp/n_neg))
areas <- sens * base

auc <- sum(areas)

auc

# [1] 0.7895158

Comprobando con ROCR:

prf <- performance(predROCR, "auc")

slot(prf, "y.values")

# [1] 0.7895158

all.equal(auc, unlist(slot(prf, "y.values")))

[1] TRUE
De seguidas, una posible forma de reunir estos pasos en una función:
curvaROC <- function(pronosticos, observaciones){
  
  obs <- ordered(observaciones)
  
  order_pron <- order(pronosticos, decreasing = TRUE)
  # numero de negativos
  
  n_neg <- sum(obs == levels(obs)[1])
  
  # numero de positivos 
  n_pos <- sum(obs == levels(obs)[2])
  
  # reales-positivos 
  tp <- cumsum(obs[order_pron] == levels(obs)[2])
  
  # falsos-positivos
  
  fp <- cumsum(obs[order_pron] == levels(obs)[1])
  
  # ajustando la longitud de vectores tp, fp
  tp <- c(0, tp)
  
  fp <- c(0, fp)
  
  # reales negativos
  tn <- n_neg
  sensit <- tp/n_pos
  auc <- sum(sensit*diff(c(0, fp/n_neg)))
  par(bg = "#F8F8FF")
  plot(fp/n_neg, sensit,
       type = "l",
       col = "#FFA500",
       lwd = 2,
       las = 1,
       ylab = "Sensibilidad",
       xlab = "1 - Especificidad",
       main = "Curva ROC ")
  grid(NULL, col =  "#CD8500")
  abline(c(0, 0), c(1, 1))
  text(.8, .2, labels = paste("AUC", round(auc, 
                                           digits = 3), sep = "="))
  return(list(tp = tp, fp = fp))
  
}

li <- curvaROC(pronos, obs)

<

Por supuesto, lo conveniente y apropiado es usar las functiones contenidas en los paquetes; ya que es un código eficaz, probado y libre de bugs; una función elaborada al vuelo, puede servir bien de modo interactivo durante una sesión, pero seguramente no será robusta. No obstante, para tener la noción del cálculo subyacente, conviene hacer el ejercicio.Finalmente, el gráfico producido con el paquete WVPlots

ROCPlot(d, xvar = "pronos", truthVar = "obs", 
        truthTarget = 1,
        title = "ROC") 

Comentarios

Entradas populares de este blog

R: Valores Faltantes en un Data Frame (Missing Values)

Son muy pocas las ocasiones en que las variables de un conjunto de datos están libres de observaciones faltantes ( NAs o missing values ). Es usual que al abordar una data nos interese saber la cantidad de ausencias, y también su caracterización, es decir, si esa ( no respuesta ) obedece a un patrón específico o es atribuible a causas aleatorías. El conteo de valores faltantes por variable, en un data frame, puede realizarse con pocas líneas de código como en el siguiente ejemplo, hecho con una data ficticia y funciones de la familia apply : # datos ficticios set.seed(4363) datos <- replicate(100, sample(c(rchisq(5, runif(1, 1, 100)), NA), 10, replace = TRUE), simplify = FALSE) datos <- do.call(rbind, datos) Luego el total de no respuesta por variable sería: datos <- data.frame(datos) unlist(lapply(datos, function(x) sum(is.na(x)))) # V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 # 18 18 18 15 15 19 14 14 15 14 El paquete magr

R: Simulacion de Variables Correlacionadas

En muchas situaciones suele ser conveniente generar un conjunto de variables con una correlación deseada. Algunos paquetes ofrecen medios para este fin de producir fake data ; pero también es perfectamente posible obtenerlas a través de métodos como la factorización (descomposicion) de Cholesky o la Descomposicion del Valor Singular (SVD: Singular Value Decomposition ). En el paquete de base de R existen funciones para hacer estos cálculos. La factorización de Cholesky, es un método con el que una matriz definida positiva y simetrica, es descompuesta en el producto de dos matrices triangulares (triangular inferior o superior) A = LL' (L es una matriz triangular inferior) A = U'U (U es una matriz triangular superior) siendo U' la traspuesta de U Mientras que la SVD (descomposición de valor singular) es una factorización de la forma: A = UΣV , la cuál generaliza la descomposición de autovalores. La implementación consiste simplemente en obtener el producto entre un vector

Optimizadores y Máximo Verosimil en R.

El proceso mediante el cual se obtienen estimaciones a partir de un conjunto de datos, frecuentemente involucra también un proceso de optimización. En lo más básico, por ejemplo, estimadores como la media o la mediana minimizan la suma de desviaciones al cuadrado y la suma de las desviaciones absolutas respectivamente Generalmente, se admite como un esquema rutinario del trabajo estadístico al momento de indagar sobre algún aspecto atinente a una población, asumir un modelo probabilístico, cuyos parámetros, siendo desconocidos, deben estimarse mediante la obtención de datos y posterior cálculo de los valores que mejor representen la data previamente recolectada. En ese último punto se halla frecuentemente implicada la optimización. La estimación por Máximo Verosimil, es generalmente obtenida mediante la aplicación de optimizadores no lineales, que son algoritmos que, por lo general, minimizan la función que se les pasa como argumento, debido a esto, para maximizar la función de verosi