En artículos anteriores hemos visto cómo usar la matriz de confusión o las métricas precision, recall y F-score para caracterizar el desempeño de un clasificador.
Sin embargo, en ocasiones el clasificador hace uso de un umbral (es decir una cantidad numérica) que permite asignar una u otra categoría a un dato. Y el desempeño de dicho clasificador dependerá precisamente del umbral seleccionado.
Lo que sucede en estos casos es que tanto la matriz de confusión, el «precision», el «recall» o el F-score obtenidos dependerán de dicho umbral. Así que no resulta práctico calcular múltiples valores de estas métricas para determinar cuál es el umbral adecuado.
Entonces, en este tutorial veremos qué son y cómo interpretar la curva ROC (Receiver Operating Characteristic) y el AUC (Area Under the Curve) que permiten evaluar el desempeño de clasificadores binarios cuando el umbral de decisión puede variar.
Al final del tutorial se encuentra el enlace de descarga del set de datos y del notebook.
Motivación
En ocasiones, al realizar tareas de clasificación binaria, el modelo implementado requiere la definición de un umbral que en últimas se usa para determinar si un dato pertenece a una u otra categoría.
Para entender esto hagamos uso del set de datos «dataset_rojos_azules.csv», un set de datos muy simple que contiene 50 datos pertenecientes a la categoría «0» y 50 datos pertenecientes a la categoría «1». Cada dato contiene sólo una característica.
Comencemos leyendo este set de datos como un DataFrame de Pandas:
import pandas as pd
datos = pd.read_csv('dataset_rojos_azules.csv')
Este dataset contiene las columnas «coordenada» (que corresponde al valor numérico de la característica) y «categoría» (que contiene la categoría a la que pertenece cada dato).
Subdividamos ahora este dataset según su categoría: el arreglo «rojos» contendrá los datos pertenecientes a la categoría 1, mientras que el arreglo «azules» contendrá los datos pertenecientes a la categoría 0:
rojos = datos['coordenada'][datos['categoría']==1].values
azules = datos['coordenada'][datos['categoría']==0].values
Y con esto, podemos ahora dibujar estas dos agrupaciones con ayuda de la librería Matplotlib:
fig, ax = plt.subplots(figsize=(16,4))
plt.scatter(rojos,np.zeros(rojos.shape),color='red',s=200, alpha=0.4, edgecolors="k", linewidths=0.5)
plt.scatter(azules,np.zeros(azules.shape),color='blue',s=200, alpha=0.4, edgecolors="k", linewidths=0.5)
plt.xlabel('x')
plt.ylabel('y')
plt.ylim(-0.02,0.02)
plt.axvline(0.5,color='k',linestyle='--',linewidth=1, label='umbral ideal: x=0.5')
plt.legend()
plt.grid();
donde, en el bloque de código anterior, hemos usado la función «scatter» (líneas 2 y 3) para dibujar los puntos de cada agrupación con colores rojo y azul, respectivamente.
Además, hemos usado la función «axvline» (línea 8) para dibujar una línea vertical en x=0.5 que corresponde al umbral ideal que permite clasificar adecuadamente estas dos agrupaciones.
Al ejecutar el código anterior vemos una imagen similar a esta:

Como vemos en este gráfico, el umbral de x=0.5 permite:
- Clasificar como datos «azules» (categoría 0) los puntos que estén a la izquierda de dicho umbral
- Clasificar como datos «rojos» (categoría 1) los puntos que estén a la derecha de dicho umbral
Sin embargo, vemos que la separación entre las dos agrupaciones no es perfecta pues en esta ubicación algunos puntos rojos quedarán a la izquierda del umbral mientras que otros puntos azules quedarán a la derecha. A pesar de esto podemos decir que este umbral es ideal pues es el mejor compromiso entre la cantidad de puntos de cada categoría que quedarán de uno u otro lado del umbral.
Pero, ¿qué ocurriría si movemos este umbral ligeramente a la izquierda o a la derecha de este valor ideal?
Analicemos cada situación en detalle:
- Supongamos que movemos el umbral a la izquierda del valor ideal, digamos a x = 0.4. En este caso podemos ver que prácticamente ningún punto de color rojo queda a la izquierda de este umbral. Sin embargo, no todos los puntos azules quedarán dentro de esta región.
- Ahora supongamos que el umbral se mueve a la derecha del valor ideal, digamos a x=0.6. En este caso tenemos una situación similar al caso anterior: prácticamente ningún punto azul quedará a la derecha del umbral, pero no lograremos clasificar correctamente la totalidad de los puntos rojos.
El anterior ejemplo nos permitió verificar que efectivamente la elección del umbral generará inevitablemente un compromiso entre la cantidad de puntos rojos y de puntos azules detectados correctamente.
La idea es entonces poder cuantificar este desempeño, para lo cual resultarán precisamente útiles los conceptos de ROC y AUC.
Pero antes de revisar en detalle estos conceptos, debemos entender algunas definiciones básicas.
Definiciones básicas
«Positivos» y «negativos»
Para cuantificar el desempeño de un clasificador binario generalmente debemos definir los conceptos de «positivo» y «negativo», que es simplemente un adjetivo que asignaremos a cada una de las categorías.
Y la definición de lo que es «positivo» y «negativo» dependerá del problema que estemos resolviendo. Por ejemplo:
- Si estamos implementando un sistema de diagnóstico, el término «positivo» podría ser asignado a las personas enfermas mientras que «negativo» correspondería a las personas sanas.
- En un sistema de detección de fraudes, un resultado «positivo» sería la presencia de un fraude y «negativo» la ausencia del mismo
Para nuestro ejemplo hipotético de los puntos azules y rojos asignaremos, arbitrariamente, el término «positivo» a los puntos rojos y «negativo» a los puntos azules.
Así que en este sencillo clasificador el objetivo es encontrar un umbral que nos permita separar los «positivos» de los «negativos» de la mejor manera posible.
TP, FP, TN, FN
Como lo vimos hace un momento en el ejemplo inicial, no existirá un umbral que separe perfectamente los datos «positivos» de los «negativos».
Así que independientemente del umbral seleccionado tendremos una cierta cantidad de «positivos» clasificados correcta o incorrectamente y la misma idea se aplicará al momento de clasificar los «negativos».
Y es aquí donde aparecen los conceptos de verdaderos positivos, falsos positivos, verdaderos negativos y falsos negativos. Veamos estas definiciones:
- Verdaderos positivos (True Positives, TP): son los datos «positivos» que fueron clasificados correctamente como «positivos». En este caso se trata de los puntos rojos clasificados correctamente como rojos.
- Falsos positivos (False Positives, FP): son los datos «negativos» que fueron clasificados incorrectamente como «positivos». En este caso se trata de los puntos azules clasificados incorrectamente como rojos.
- Verdaderos negativos (True Negatives, TN): son los datos «negativos» que fueron clasificados correctamente como «negativos». En este caso serán los puntos azules clasificados correctamente como azules.
- Falsos negativos (False Negatives, FN): son los datos «positivos» que fueron clasificados incorrectamente como «negativos». En este caso serán los puntos rojos clasificados incorrectamente como azules.
Veamos un ejemplo de cálculo de estas cantidades con ayuda de Python.
Comencemos creando la función «calcular_TP» que nos permitirá obtener el número de TP suponiendo que conocemos las categorías reales y las categorías predichas:
# TP: positivos (1s) clasificados como positivos (1s)
def calcular_TP(y_true,y_pred):
multip = y_true*y_pred
return np.sum(multip) # Sólo los positivos (1s) en ambos arreglos se sumarán
En esta función:
- «y_true» será una lista que contendrá las categorías reales (1 para «positivos» y 0 para «negativos»
- «y_pred» será una lista con el resultado de la clasificación (de nuevo 1 será «positivos» y 0 será «negativos»
Así que si multiplicamos término a término «y_true» con «y_pred» (línea 3) sólo aquellas posiciones donde ambos valores sean iguales a 1 (es decir los TP) arrojarán resultados iguales a 1. Y por tanto, si sumamos estos valores (línea 4) tendremos precisamente los TP.
Asumamos que estos son los arreglos «y_true» y «y_pred»:
y_true = np.array([1,1,1,0,0,0]) # positivos = 3, negativos = 3
y_pred = np.array([0,1,1,0,1,0])
Y usemos la función que acabamos de crear para calcular los TP:
print(f'TP: {calcular_TP(y_true,y_pred)}')
Al ejecutar el código anterior obtenemos un TP igual a 2 lo cual nos indica que de los 3 datos «positivos», 2 fueron realmente clasificados como «positivos».
Siguiendo una lógica similar podemos implementar la función «calcular_FN» que nos permitirá obtener el conteo de falsos negativos. En este caso, el número de falsos negativos será simplemente el resultado de calcular los «positivos» en «y_pred» y luego restar los TP (obtenidos con la función anterior):
# FN: positivos (1s) clasificados como negativos (0s)
def calcular_FN(y_true,y_pred):
# Los FN son simplemente todos los positivos - TP
positivos = np.sum(y_true) # Todos los positivos
tp = calcular_TP(y_true,y_pred) # Verdaderos positivos
return positivos - tp # Falsos negativos
print(f'FN: {calcular_FN(y_true,y_pred)}')
Al ejecutar el código anterior obtenemos un conteo de FN igual a 1, lo cual quiere decir que de los 3 datos «positivos» uno de ellos fue clasificado incorrectamente como negativo.
Observemos además que al sumar TP con FN obtenemos un valor igual a 3 que es exactamente el mismo número de datos con categoría «positivo».
Veamos ahora cómo obtener los verdaderos negativos, para lo cual crearemos la función «calcular_TN». En este caso, la función será muy similar a «calcular_TP» con la diferencia de que en lugar de sumar los valores donde tanto «y_true» como «y_pred» sean iguales a 1 («positivos») debemos sumar los valores donde ambos arreglos sean iguales a 0 (es decir «negativos»).
Así que simplemente podemos realizar el cálculo no sobre «y_true» y «y_pred» sino sobre «1-y_true» y «1-y_pred» (de esta manera intercambiamos ceros por unos y viceversa):
# TN: negativos (0s) clasificados como negativos (0s)
def calcular_TN(y_true,y_pred):
# Multiplicar los arreglos invertidos(1->0, 0->1) pues en este caso nos
# interesan los 0s
multip = (1-y_true)*(1-y_pred)
return np.sum(multip) # Sólo los negativos (0s) en ambos arreglos se sumarán
print(f'TN: {calcular_TN(y_true,y_pred)}')
Y al ejecutar el código anterior tendremos un TN = 2, lo cual quiere decir que de los tres datos «negativos», 2 fueron clasificados correctamente como «negativos».
Finalmente implementemos la función «calcular_FP» que nos permitirá realizar el conteo de falsos positivos. En este caso simplemente calculamos el número total de negativos (ceros en «y_true») y a este le restamos los TN obtenidos con la función que acabamos de implementar.
Este sería el código para «calcular_FP»:
# FP: negativos (0s) clasificados como positivos (1s)
def calcular_FP(y_true,y_pred):
# Los FP son simplemente todos los negativos - TN
negativos = np.sum(1-y_true) # Todos los negativos
tn = calcular_TN(y_true,y_pred) # Verdaderos negativos
return negativos - tn # Falsos positivos
print(f'FP: {calcular_FP(y_true,y_pred)}')
Y tras ejecutar este código tendremos un FP = 1, lo cual quiere decir que de los tres datos «negativos» uno será incorrectamente clasificado como «positivo».
De igual forma podemos observar que al sumar los TN (2) con los FP (1) obtendremos un valor de 3, que es precisamente el número real de datos negativos.
TPR
Teniendo claro qué son los TP, FN, TN y FP ya podemos introducir dos conceptos que resultarán claves para entender qué son y para qué sirven el ROC y el AUC: la tasa de verdaderos positivos (o TPR: True Positive Rate) y la tasa de falsos positivos (o FPR: False Positive Rate).
Comencemos hablando de la TPR. Esta métrica responde a la pregunta: «de todo lo que es realmente POSITIVO, ¿qué proporción fue clasificada como POSITIVO?».
Y el cálculo de esta TPR se puede obtener precisamente usando los TP y los FN. Desglosemos la anterior definición en términos de estas cantidades:
- «de todo lo que realmente es POSITIVO»: esta cantidad se obtiene simplemente sumando los TP y los FN
- «¿qué proporción fue clasificada como POSITIVO?»: estos son simplemente los TP
Así que para calcular la TPR podemos usar esta ecuación:
$$TPR = \frac{TP}{TP+FN}$$
En esencia la TPR refleja cuántos positivos se detectan correctamente. Y por tanto tendrá un valor ideal igual a 1, pues en este caso tendríamos un 100% de los datos positivos clasificados correctamente como positivos.
Volvamos a Python e implementemos la función calcular TPR usando las funciones «calcular_TP» y «calcular_FN» previamente implementadas. Además, realicemos el cálculo de la TPR sobre los arreglos «y_true» y «y_pred» creados anteriormente:
def calcular_TPR(y_true,y_pred):
tp = calcular_TP(y_true,y_pred)
fn = calcular_FN(y_true,y_pred)
return tp/(tp+fn)
print(f'TPR: {100*calcular_TPR(y_true,y_pred):.1f}% (Porcentaje de positivos clasificados como positivos)')
Tras ejecutar la celda anterior obtenemos un TPR igual a 0.667 (o 66.7%). Esto quiere decir que del total de datos positivos, tan sólo el 66.7% fue clasificado correctamente como positivo.
Nota: vemos además que este TPR es exactamente el mismo «recall» que vimos en detalle en el artículo anterior.
FPR
Ahora veamos la tasa de falsos positivos (o FPR: False Positive Rate). En este caso esta cantidad responde a la pregunta: «de todo lo que realmente es NEGATIVO ¿qué fue clasificado incorrectamente como POSITIVO?».
Y esta cantidad la podemos calcular usando los FP y los TN. Veamos:
- «de todo lo que realmente es NEGATIVO» es simplemente la suma de los TN y los FP
- «¿qué fue clasificado incorrectamente como POSITIVO?» es simplemente FP
Así que la FPR se puede calcular usando esta ecuación:
$$FPR = \frac{FP}{TN+FP}$$
En esencia esta FPR refleja cuántos negativos clasifica incorrectamente el modelo como positivos. Idealmente este valor debería ser 0, lo cual quiere decir que ningún dato negativo es detectado como positivo.
Veamos cómo calcular esta FPR con ayuda de Python:
def calcular_FPR(y_true,y_pred):
fp = calcular_FP(y_true,y_pred)
tn = calcular_TN(y_true,y_pred)
return fp/(fp+tn)
print(f'FPR: {100*calcular_FPR(y_true,y_pred):.1f}% (Porcentaje de negativos clasificados incorrectamente como positivos)')
Y tras ejecutar las líneas de código anteriores obtenemos una FPR de 0.333, lo cual quiere decir que del total de datos negativos tan sólo el 33.3% fueron clasificados incorrectamente como positivo.
Variación de TPR y FPR con el umbral seleccionado
Muy bien, con lo visto hasta el momento aún nos falta responder a la pregunta planteada al inicio de este tutorial: ¿cómo escogemos el umbral más adecuado para nuestro clasificador?
Y acá es donde entran en juego estos tres elementos: el TPR, el FPR y el umbral.
De hecho, lo que ocurre en nuestro ejemplo particular de los puntos azules y los puntos rojos es que tanto TPR como FPR cambiarán con el umbral escogido.
Por ejemplo, supongamos que seleccionamos un umbral de 0.5. Hagamos uso de las funciones creadas anteriormente para calcular la TPR y la FPR:
# Categorías reales
y_true = datos['categoría'].values # Categorías reales: 1->rojo, 0->azul
# Caso 1: umbral=0.5
umbral = 0.5
# Predicción
coords = datos['coordenada'].values
y_pred = np.zeros(y_true.shape)
y_pred[coords>=umbral] = 1 # Si la coordenada >= umbral -> punto rojo
# Calcular TPR y FPR
print(f'Umbral: {umbral}')
print(f'TPR: {100*calcular_TPR(y_true,y_pred):.1f}%')
print(f'FPR: {100*calcular_FPR(y_true,y_pred):.1f}%')
donde:
- En la línea 1 hemos creado nuestro arreglo de categorías reales «y_true» (1 para puntos «rojos» y 0 para puntos «azules»)
- Con el código de las líneas 8 a 10 obtenemos las predicciones de nuestro clasificador usando el umbral de 0.5
- Y finalmente en las líneas 14 a 15 calculamos e imprimimos en pantalla la TPR y la FPR
Tras ejecutar el código anterior obtenemos una TPR de 82% y una FPR del 18%.
Sin embargo, si tomamos el código anterior y modificamos el umbral por 0.6, obtendremos una TPR de 66% y una FPR del 0%.
Y si por ejemplo repetimos el procedimiento para un umbral de 0.35, llegaremos a una TPR del 100% pero una FPR del 38%.
Así que acá podemos concluir algo muy importante: la selección del umbral de clasificación genera un compromiso entre la TPR y la FPR.
Y por tanto la idea es ver cómo cambia este compromiso entre la TPR y la FPR a medida que cambia el umbral. Y acá es donde aparece el concepto de la curva ROC.
La curva ROC (Receiver Operating Characteristic)
La curva ROC es una gráfica que nos permite ver cómo cambia la TPR con respecto a la FPR a medida que cambiamos el umbral de nuestro clasificador.
Para construir esta gráfica simplemente repetimos el procedimiento anterior de cálculo de la TPR y la FPR para varios umbrales, tabulamos los datos y luego representamos cada par de puntos de forma gráfica: en el eje vertical pondremos los diferentes valores de TPR y en el eje horizontal los FPR correspondientes.
Y esto nos permite determinar de forma gráfica qué tan bien o mal lo hace el clasificador en términos de las tasas de verdaderos y falsos positivos.
Tras construir esta gráfica podremos tener varios tipos de comportamientos que podemos ver en la siguiente figura:

Analicemos cada una de estas posibles situaciones en detalle:
- Clasificador ideal (esquina superior izquierda): un clasificador ideal tendría una TPR del 100% y una FPR del 0%. Es decir: detecta correctamente todos los positivos y no detecta incorrectamente ningún negativo. En este caso la curva ROC no sería una curva sino simplemente un punto
- Clasificador aleatorio (esquina superior derecha): un clasificador aleatorio es aquel que no es capaz de diferenciar entre categorías positivas y negativas y, por tanto, predice positivo o negativo al azar. Por tanto, TPR y FPR serán iguales independientemente del umbral, lo cual se traduce en una gráfica ROC con una línea diagonal que va desde el punto (FPR=0, TPR=0) hasta el punto (FPR=1, TPR=1)
- Un «buen clasificador» (esquina inferior izquierda): un buen clasificador debería tener una ROC cercana a la del «clasificador ideal». Es decir que debería tener un sesgo hacia el punto (FPR=0, TPR=1) (pues en este caso se tendría una TPR cercana al 100% y una FPR cercana al 0%)
- Un «mal clasificador» (esquina inferior derecha): un mal clasificador sería lo opuesto a un «buen clasificador». Es decir que tendría un sesgo hacia el punto (FPR=1, TPR=0) (pues en este caso prácticamente tendría una TPR cercana al 0% y una FPR cercana al 100%, lo cual no es ideal)
Así que teniendo en cuenta qué es y cuáles son las principales características de la curva ROC, veamos cómo construir esta gráfica para nuestro clasificador. Para ello crearemos la función «curva_ROC»:
def curva_ROC(data, plot_auc=False):
# Definir rango de umbrales
umbrales = np.linspace(0,1,50)
# Definir y_true
y_true = data['categoría'].values # Categorías reales: 1->rojo, 0->azul
# Para cada umbral calcular TPR y FPR a partir de la predicción
# y almacenar los resultados
tprs = []
fprs = []
for umbral in umbrales:
# Predicciones
coords = data['coordenada'].values
y_pred = np.zeros(y_true.shape)
y_pred[coords>=umbral] = 1 # Si la coordenada >= umbral -> punto rojo
# Calcular TPR y FPR para el umbral
tpr = calcular_TPR(y_true,y_pred)
fpr = calcular_FPR(y_true,y_pred)
# Añadir a los listados
tprs.append(tpr)
fprs.append(fpr)
# Y construir la gráfica
fig, ax = plt.subplots(figsize=(6,6))
plt.plot(fprs,tprs)
plt.xlabel('FPR')
plt.ylabel('TPR')
# Y dibujar AUC si plot_auc=True
if plot_auc:
plt.fill_between(fprs,tprs,alpha=0.4)
title = 'Curva ROC y AUC'
else:
title = 'Curva ROC'
plt.title(title)
plt.grid();
return tprs, fprs
TPRS, FPRS = curva_ROC(datos)
En esta función:
- Primero creamos un arreglo con 50 diferentes umbrales en el rango de 0 a 1 (línea 3)
- Luego definimos nuestro arreglo de categorías reales «y_pred» (línea 6)
- Posteriormente creamos dos listas vacías que contendrán los valores de TPR y FPR calculados para cada umbral (líneas 10 y 11)
- Y a continuación iteramos por cada umbral (línea 12) y en cada caso:
- Obtenemos el arreglo de predicciones con base en dicho umbral (líneas 14 a 16)
- Calculamos y almacenamos las TPR y FPR correspondientes (líneas 19 a 24)
- Una vez terminadas las iteraciones dibujamos la curva ROC (líneas 27 a 30)
En la función anterior también se incluye una porción (líneas 33 a 35) para dibujar el área bajo la curva (AUC) pero de esta parte hablaremos en un momento.
Finalmente, en la línea 43 hacemos el llamado a esta función, lo que genera una curva ROC como esta:

Y en este caso vemos que la curva se acerca al comportamiento ideal de un clasificador (pues tiene un sesgo hacia el punto FPR=0, TPR=1).
AUC: área bajo la curva ROC (Area Under the ROC Curve)
La AUC es una manera equivalente de ver el desempeño del clasificador para diferentes umbrales.
En esencia esta AUC es simplemente un número que corresponde, como su nombre lo indica, al área bajo la curva de la ROC.
Y al igual que ocurría con la ROC, en el caso de la AUC podemos tener cuatro situaciones:

Veamos en detalle cada una de estas situaciones:
- Clasificador ideal (esquina superior izquierda): un clasificador ideal tendrá una AUC exactamente igual a 1. Esto nos indica que la TPR será precisamente igual a 1 mientras que la FPR será exactamente igual a 0
- Clasificador aleatorio (esquina superior derecha): en este caso la AUC es igual a 0.5 lo cual nos indica que el modelo no tiene la capacidad de diferenciar entre categorías positivas y negativas y, por tanto, predice positivo o negativo al azar (de allí el valor de 0.5)
- Un «buen clasificador» (esquina inferior izquierda): un buen clasificador debería tener una AUC cercana al valor ideal de 1 (correspondiente con el sesgo de la ROC hacia el punto ideal FPR=0, TPR=1)
- Un «mal clasificador» (esquina inferior derecha): un mal clasificador tendrá una AUC cercana a 0 (e inferior a 0.5) lo cual indica que su desempeño es peor que el de un clasificador aleatorio
En el caso de la función «curva_ROC» hemos precisamente incluido una porción para dibujar esta AUC superpuesta a la curva ROC (líneas 35 a 37). Así que si ejecutamos este código:
TPRS, FPRS = curva_ROC(datos,plot_auc=True)
obtendremos una gráfica similar a esta (la cual contiene la curva ROC y la AUC sombreada en color azul):

De hecho, podemos usar la librería Scikit-Learn y la función «auc» para calcular la AUC de nuestro clasificador:
from sklearn.metrics import auc
print(f'AUC del clasificador: {auc(FPRS,TPRS)}')
donde los argumentos de entrada de esta función «auc» son precisamente la TPR y la FPR (línea 3).
Tras ejecutar el código anterior encontramos que la AUC de nuestro clasificador es de 0.9412.
Y en este punto es importante resaltar un aspecto importante sobre la ROC y la AUC:
- La ROC se construye usando diferentes umbrales, lo cual quiere decir que cada punto de la ROC es un nivel de desempeño (TPR vs. FPR) obtenido para un umbral en particular
- Sin embargo, la AUC es una medida general del desempeño del modelo pues es calculada con base en la ROC que a su vez se construye considerando todos los posibles umbrales.
Así que una AUC de 0.9412 (muy cercana a 1.0) nos indica que realmente tenemos un muy buen clasificador.
Elección del mejor umbral
Muy bien, en este punto ya tenemos claro qué son la ROC y la AUC y cómo estas dos herramientas nos permiten medir el desempeño general de nuestro clasificador (a través de la AUC) y para diferentes umbrales (a través de la ROC).
Así que sólo nos queda por responder una pregunta: ¿y entonces cómo elegimos el mejor umbral para un problema en particular?
Pues la respuesta inicial es: !depende!. Depende del uso final que queramos darle al clasificador y si queremos priorizar los «positivos» o los «negativos» (o ambos).
Sin embargo, en el contexto de lo que hemos visto hasta este punto, una manera de elegir este mejor umbral es eligiendo aquel umbral que nos arroje el punto más cercano a (0,1) en la curva ROC.
¿Por qué? Recordemos que un clasificador ideal tendrá una ROC que realmente es un punto ubicado en (FPR=0, TPR=1). Y si usamos como criterio la curva ROC ese sería precisamente el punto ideal.
La forma de hacer esto es sencilla:
- Definimos un rango de umbrales a considerar dependiendo de las características de nuestro clasificador
- Por cada umbral:
- Clasificamos los datos en una u otra categoría
- Almacenamos el valor del umbral y las tasas FPR y TPR correspondientes
- Calculamos la distancia de cada par de puntos (FPR, TPR) al punto ideal (0,1):
$$distancia = \sqrt{(1-TPR)^2+(0-FPR)^2}$$
- Y luego simplemente calculamos la distancia mínima y extraemos los FPR y TPR correspondientes
Veamos cómo implementar este pequeño algoritmo para nuestro ejemplo en particular:
# Definir rango de umbrales
umbrales = np.linspace(0.2,0.8,20)
# Iterar por los umbrales y en cada caso calcular y almacenar FPR y TPR
tprs = []
fprs = []
coords = datos['coordenada'].values
y_true = datos['categoría'].values # Categorías reales: 1->rojo, 0->azul
for umbral in umbrales:
# Predicciones
y_pred = np.zeros(y_true.shape)
y_pred[coords>=umbral] = 1 # Si la coordenada >= umbral -> punto rojo
# Calcular TPR y FPR para el umbral
tpr = calcular_TPR(y_true,y_pred)
fpr = calcular_FPR(y_true,y_pred)
# Añadir a los listados
tprs.append(tpr)
fprs.append(fpr)
# Calcular distancias
tprs = np.array(tprs)
fprs = np.array(fprs)
distancias = np.sqrt(np.array(1.-tprs)**2 + np.array(fprs)**2)
# Buscar la posición para la menor distancia obtenida
idx = np.argmin(distancias)
# Y extraer el mejor umbral correspondiente
mejor_umbral = umbrales[idx]
print(f'Mejor umbral basado en la ROC: {mejor_umbral}')
donde:
- En la línea 2 hemos definido un total de 20 posibles umbrales en el rango de 0.2 a 0.8
- En las líneas 5 a 20 hemos iterado por cada umbral y en cada caso hemos calculado y almacenado las TPR y FPR correspondientes
- En las líneas 23 a 25 calculamos las distancias de cada par de puntos (FPR, TPR) al punto ideal (0,1)
- Y finalmente en las líneas 28 a 31 calculamos el mejor umbral, es decir aquel cuya FPR y TPR están más cerca del punto ideal
Y tras ejecutar el código anterior obtenemos un mejor umbral de 0.48.
Ahora podemos usar este umbral para clasificar nuestros datos y medir el desempeño obtenido usando por ejemplo el F1-score:
# Con este mejor umbral calcular por ejemplo el F1-score
from sklearn.metrics import f1_score
y_pred = np.zeros(y_true.shape)
y_pred[coords>=mejor_umbral] = 1 # Si la coordenada >= umbral -> punto rojo
print(f'F1-score: {f1_score(y_true,y_pred)}')
Lo que nos arroja un puntaje F1 de 0.82.
Enlaces de descarga set de datos y código fuente
Conclusión
Muy bien, como acabamos de ver en este tutorial, el ROC y la AUC son otras dos herramientas que nos permiten cuantificar el desempeño de un clasificador binario cuando dicha clasificación depende de un umbral.
Y es importante tener en cuenta que el umbral seleccionado puede afectar las tasas de verdaderos positivos (TPR) y falsos positivos (FPR), así que la ROC nos permite analizar de forma gráfica el compromiso entre estos dos elementos a medida que cambia el umbral.
Por otra parte, la AUC es una medida del desempeño general del modelo y es, por tanto, independiente del umbral seleccionado.
