¿Qué funciona mejor para evaluar un modelo de clasificación en Python?

Los modelos de clasificación son unos de los algoritmos supervisados más utilizados debido a su utilidad y a su facilidad para ser comprendidos. A la hora de evaluar su rendimiento casi siempre se suele recurrir a la curva ROC. Descubriremos porque no siempre es tan buena idea y la curva precision-recall como alternativa.

Recordemos las tareas de clasificación…

Primero vamos a recordar cómo funcionan los modelos de clasificación. La manera más clásica es entrenar modelos cuya salida indica si un registro pertenece a una clase u a otra. Sin embargo, otra aproximación a la hora de implementar estos modelos es predecir las probabilidades de pertenencia a una clase como alternativa a las clases directamente. Esto nos permite una mayor flexibilidad ya que dichas probabilidades pueden ser interpretadas utilizando diferentes umbrales. Estos umbrales van a permitir al usuario del modelo encontrar la famosa «solución de compromiso» (trade-off) entre los errores de los modelos. Esta solución de compromiso será el equilibrio entre los falsos positivos vs los falsos negativos. Si queréis revisar la mayoría de métricas que existen para evaluar las tareas de clasificación dadle una lectura a este post.

Para interpretar correctamente las predicciones realizadas por modelos de clasificación binarios (dos clases) utilizaremos las curvas ROC y las curvas de precisión-sensibilidad (Precision-Recall).

En este artículo hablaremos dichas curvas y sobre cuándo utilizar cada una de ellas. El uso de estas curvas nos permitirá interpretar correctamente las predicciones de probabilidad de nuestros modelos de clasificación binaria.

Predecir probabilidades

Como se indicaba en la introducción, una aproximación distinta es predecir las probabilidades de cada clase. La clase final se determinará utilizando esta probabilidad y un umbral de decisión.

Es decir, podríamos obtener la clase utilizando un umbral de 0.5 y decidiendo que toda aquella predicción con una probabilidad inferior al umbral (<0.5) se considerará que pertenece a la clase 0 mientras que todo aquella predicción con un valor igual o superior al umbral (0.5) se considerará que pertenece a la clase 1. Este umbral puede ser ajustado para modificar el comportamiento de nuestro modelo para un problema específico. La modificación del umbral serviría si se quisiera inclinar el modelo hacia una de las dos clase. Esta orientación del modelo hacia una clase serviría para tratar de minimizar los fallos de predicción de una determinada clase.

Para entender esto con más claridad, es interesante hablar brevemente de los dos tipos errores de predicción que tenemos en un modelo de clasificación binario:

  • Falso Positivo. Predecir un evento cuando no hubo evento
  • Falso Negativo. Predecir que no hubo un evento cuando sí que hubo evento

Al calibrar el umbral de nuestro modelo de predicción, el balanceo entre estos dos errores es lo que nos otorgará el nivel de umbral óptimo. 

En nuestro modelo de detección de infección por COVID-19, deberíamos estar más preocupados de tener pocos falsos negativos que de tener pocos falsos positivos. Un falso negativo significaría que el modelo ha identificado que no hay infección cuando sé que la habrá con sus efectos derivados. Un falso positivo solo provocaría que se mandara a cuarentena a una persona sin infección. Entender el impacto de los falsos negativos y de los falsos positivos es clave para todo modelo de clasificación. La manera más común de comparar modelos que predicen probabilidades para clases binarias es utilizando la curva ROC.

¿Qué es la curva ROC?

ROC es un acrónimo que viene del inglés Receiver Operating Characteristic (Característica Operativa del Receptor). Es una gráfica que enfrenta el ratio de falsos positivos (eje x) con el ratio de falsos negativos (eje y). Estos ratios los va obteniendo en función de una serie de umbrales definidos entre 0 y 1. En palabras comunes y referenciando al ejemplo anterior, enfrenta la «falsa alarma» vs la tasa de éxito.

La tasa de falsos positivos se calcula como el número de positivos verdaderos divididos entre el número de positivos verdaderos y de falsos negativos. Describe cómo de bueno es nuestro modelo prediciendo las clases positivas cuando la salida real es positiva. También se conoce esta tasa como sensibilidad

La tasa de falsos positivos se calcula como el número de falsos positivos dividido entre la suma de falsos positivos con los verdaderos negativos. Se considera como la tasa de «falsa alarma» y resumen como de común es que una clase negativa sea determinada por el modelo como positiva. 

La especificidad es la inversa de la tasa de falsos positivos. Se obtiene dividiendo el número total de verdaderos negativos entre la suma de los verdaderos negativos y los falsos positivos. 

¿Por qué es útil la curva ROC?

La curva ROC es útil por dos principales motivos:

  • Permite comparar diferentes modelos para identificar cual otorga mejor rendimiento como clasificador.
  • El área debajo de la curva (AUC) puede ser utilizado como resumen de la calidad del modelo.

En resumen:

  • Valores pequeños en el eje X indican pocos falsos positivos y muchos verdaderos negativos
  • Valores grandes en el eje Y indican elevados verdaderos positivos y pocos falsos negativos

En el dibujo a continuación podemos observar la curva ROC de diversos modelos (cada color es un modelo). Un clasificador aleatorio (que asignará 0 o 1 al azar) estaría representado en rojo. Es decir, por azar, el 50% de las predicciones serían acertadas.

Según se desplaza la curva hacia la esquina superior izquierda del gráfico, la calidad del modelo va aumentando. Esto se debe a que que mejora en su tasa de verdaderos positivos, minimizando también la tasa de falsos positivos. 

El valor AUC se utiliza como resumen del rendimiento del modelo. Cuanto más esté hacia la izquierda la curva, más área habrá contenida bajo ella y por ende, mejor será el clasificador. El clasificador aleatorio tendría una AUC de 0.5 mientras que el clasificador perfecto (en morado) tendría un AUC de 1. 

A la hora de desarrollar nuestro modelo, la curva ROC nos será de una gran utilidad. Nos permitirá elegir un umbral que optimice el comportamiento de nuestro modelo para resolver de la mejor manera nuestro problema de clasificación.

Curva ROC y el AUC en Python

Para pintar la curva ROC de un modelo en python podemos utilizar directamente la función roc_curve() de scikit-learn.

La función necesita dos argumentos. Por un lado las salidas reales (0,1)  del conjunto de test y por otro las predicciones de probabilidades obtenidas del modelo para la clase 1. Devuelve la tasa de falsos positivos y la tasa de verdaderos positivos para cada umbral así como la lista de valores utilizados como umbral.

Para obtener el valor de AUC, tenemos la función roc_auc_score() (mismo parámetros de entrada). En este caso devuelve el valor de AUC ,comprendido entre 0.5 (clasificador aleatorio) y 1.0 (clasificador perfecto).

A continuación se muestra un ejemplo completo utilizando un modelo de regresión logística en un pequeño conjunto de datos de prueba

#Importamos 
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve
from sklearn.metrics import roc_auc_score
from matplotlib import pyplot
# Generamos un dataset de dos clases
X, y = make_classification(n_samples=1000, n_classes=2, random_state=1)
# Dividimos en training y test
trainX, testX, trainy, testy = train_test_split(X, y, test_size=0.5, random_state=2)
#Generamos un clasificador sin entrenar , que asignará 0 a todo
ns_probs = [0 for _ in range(len(testy))]
# Entrenamos nuestro modelo de reg log
model = LogisticRegression(solver='lbfgs')
model.fit(trainX, trainy)
# Predecimos las probabilidades
lr_probs = model.predict_proba(testX)
#Nos quedamos con las probabilidades de la clase positiva (la probabilidad de 1)
lr_probs = lr_probs[:, 1]
# Calculamos el AUC
ns_auc = roc_auc_score(testy, ns_probs)
lr_auc = roc_auc_score(testy, lr_probs)
# Imprimimos en pantalla
print('Sin entrenar: ROC AUC=%.3f' % (ns_auc))
print('Regresión Logística: ROC AUC=%.3f' % (lr_auc))
# Calculamos las curvas ROC
ns_fpr, ns_tpr, _ = roc_curve(testy, ns_probs)
lr_fpr, lr_tpr, _ = roc_curve(testy, lr_probs)
# Pintamos las curvas ROC
pyplot.plot(ns_fpr, ns_tpr, linestyle='--', label='Sin entrenar')
pyplot.plot(lr_fpr, lr_tpr, marker='.', label='Regresión Logística')
# Etiquetas de los ejes
pyplot.xlabel('Tasa de Falsos Positivos')
pyplot.ylabel('Tasa de Verdaderos Positivos')
pyplot.legend()
pyplot.show()

Importante resaltar que los modelos tienen una naturaleza pseudoaleatoria. Puede que los valores obtenidos difieran de los observados en el ejemplo.

ROC AUC de regresion logistica
Curva ROC (Salida del modelo de ejemplo)

¿Qué es la curva de precisión-sensibilidad?

Existen numerosas maneras de evaluar el rendimiento de un modelo de predicción. Una bastante frecuente y sobre la que hablaremos más en detalle, se centra en la precisión y la sensibilidad como métricas fundamentales. 

La precisión se calcula como el número de verdaderos positivos entre la suma de verdaderos positivos y de falsos positivos. Describe cómo de bueno es el modelo a la hora de predecir las salidas de la clase positiva. Otra forma de llamar a la precisión también se le llama poder predictivo positivo.

Vuelve a aparecer la sensibilidad (recall), que ya ha sido comentada en apartados anteriores. Verdaderos positivos divididos entre la suma de verdaderos positivos y de falsos positivos.

Como resumen, la curva de precisión-sensibilidad enfrenta la precisión (eje y) con la sensibilidad (eje x) para diferentes umbrales.

En la imagen posterior se muestra dicha curva para una serie de modelos, cada uno con un poder predictivo distinto. El modelo (morado) en la esquina representaría a un clasificador perfecto. Las curvas más alejadas de esa esquina representarían a modelos peores. Un modelo aleatorio sin entrenar estaría representado como una línea horizontal a media altura (0.5).

Curva de precision vs recall
Ejemplo de curva de precisión – sensibilidadhttps://arxiv.org/pdf/1905.05441.pdf

¿Por qué es útil la curva de precisión-sensibilidad?

Prestar atención a la curva de precisión-sensibilidad es especialmente útil en aquellos casos en los que tenemos clases desbalanceadas. Suele ser bastante común que haya muchos registros negativos (clase 0) y muy pocos positivos (clase 1).

Esto ocurriría en el ejemplo comentado al principio del modelo de detección de infección por COVID-19. En este conjunto de datos, el número de registros negativos (clase 0) es mucho más alto que el de registros positivos (clase 1). Este desbalance desemboca en que nuestro interés principal se enfoque en la capacidad del modelo para predecir la clase minoritaria (clase 1).

Apenas nos interesará el rendimiento del modelo en la clase mayoritaria (clase 0) por lo que no le prestaremos demasiada atención a los verdaderos negativos. La clave del uso de la curva de precisión-sensibilidad es que no tiene en cuenta los falsos negativos. La curva de precisión-sensibilidad solo se preocupa de la clase positiva, es decir, de la clase minoritaria.

A la hora de resumir el rendimiento del modelo en un valor numérico, en este caso se utilizan dos valores:

  • Valor F (F-Score): Calcula la media armónica de la precisión y la sensibilidad. (Se utiliza la media armónica porque ambos valores son tasas)
  • Área bajo la curva: AUC. Al igual que en la curva ROC, el valor del área bajo la curva nos permite también determinar el rendimiento del modelo

En términos de elección del modelo, el valor F resume la habilidad del modelo para un valor específico del umbral. Como comparativa, el AUC contempla la habilidad del modelo a lo largo de los diversos umbrales.

Esto convierte a la curva precisión-sensibilidad en la manera más óptima de medir el rendimiento de modelos de clasificación binaria cuyas clases no estén balanceadas

Curvas de precisión-sensibilidad en Python

Con el módulo sklearn podemos calcular y obtener la curva de precisión-sensibilidad y sus métricas asociadas.

Utilizaremos la función precision_recall_curve() que toma como parámetros las salidas reales y las probabilidades obtenidas para la case positiva. Esta función devuelve vectores de precisión, sensibilidad y los umbrales para dichos valores.

La función f1_score() toma como parámetros los valores de salida reales y los valores de las clases obtenidas devolviendo el valor F. Recordemos que el valor F se calcula para un umbral determinado por lo que se usa la predicción de clases, no las probabilidades.

La función auc() de nuevo toma como entradas la sensibilidad y la precisión devolviendo el valor del área bajo la curva.

# Importamos
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_curve
from sklearn.metrics import f1_score
from sklearn.metrics import auc
from matplotlib import pyplot
#Generamos dataset
X, y = make_classification(n_samples=1000, n_classes=2, random_state=1)
#Dividimos en training y test
trainX, testX, trainy, testy = train_test_split(X, y, test_size=0.5, random_state=2)
#Entrenamos
model = LogisticRegression(solver='lbfgs')
model.fit(trainX, trainy)
# predecimos probabilidades
lr_probs = model.predict_proba(testX)
# Nos quedamos unicamente con las predicciones positicas
lr_probs = lr_probs[:, 1]
# Sacamos los valores
yhat = model.predict(testX)
lr_precision, lr_recall, _ = precision_recall_curve(testy, lr_probs)
lr_f1, lr_auc = f1_score(testy, yhat), auc(lr_recall, lr_precision)
# Resumimos s
print('Regresión Logística: f1=%.3f auc=%.3f' % (lr_f1, lr_auc))
# plot the precision-recall curves
no_skill = len(testy[testy==1]) / len(testy)
pyplot.plot([0, 1], [no_skill, no_skill], linestyle='--', label='Sin entrenar')
pyplot.plot(lr_recall, lr_precision, marker='.', label='Regresión Logística')
#Etiquetas de ejes
pyplot.xlabel('Sensibilidad')
pyplot.ylabel('Precisión')
pyplot.legend()
pyplot.show()
Ejemplo de curva prec vs recall en Python
Curva de precisión-sensibilidad (Salida del modelo de ejemplo)

¿Cuándo utilizar la curva ROC y cuándo utilizar la curva de precisión-sensibilidad?

Como norma general, el criterio a seguir a la hora de elegir es el nivel de desbalance entre las clases que presenta el modelo:

  • Las curvas ROC se deberían utilizar cuando más o menos existen las mismas observaciones para ambas clases
  • Las curvas de precisión sensibilidad se deberían utilizar cuando existe un notable desbalance entre el número de observaciones de cada clase

La razón reside en que las curvas ROC nos muestran una versión «optimista» de aquellos modelos con un desbalance de clases considerado. Esto puede llevar a interpretaciones erróneas sobre el rendimiento del modelo si nos ceñimos únicamente a la información obtenida de las curvas ROC. Esta versión optimista se debe a que la curva ROC utiliza la tasa de falsos positivos mientras que la curva de precisión-sensibilidad omite esta tasa.

Para terminar de aclarar esta diferencia y así asentar conceptos, vamos a realizar un ejemplo final. En este ejemplo trataremos de evaluar el rendimiento de un modelo para unos datos desbalanceados y veremos las diferencias entre ambas curvas.

Modelo de datos desbalanceados con Curva ROC

A continuación se muestra un código similar al utilizado en la sección de curva ROC. En este nuevo ejemplo los datos estarán desbalanceados en una proporción de unos 99 a 1. Es decir, por cada observación de la clase 1 hay aproximadamente 99 observaciones de la clase 0.

#Importamos 
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_curve
from sklearn.metrics import roc_auc_score
from matplotlib import pyplot
# Generamos un dataset de dos clases (desbalanceadas en un 99:1)
X, y = make_classification(n_samples=1000, n_classes=2, weights=[0.99,0.01], random_state=1)
# Dividimos en training y test
trainX, testX, trainy, testy = train_test_split(X, y, test_size=0.5, random_state=2)
#Generamos un clasificador sin entrenar , que asignará 0 a todo
ns_probs = [0 for _ in range(len(testy))]
# Entrenamos nuestro modelo de reg log
model = LogisticRegression(solver='lbfgs')
model.fit(trainX, trainy)
# Predecimos las probabilidades
lr_probs = model.predict_proba(testX)
#Nos quedamos con las probabilidades de la clase positiva (la probabilidad de 1)
lr_probs = lr_probs[:, 1]
# Calculamos el AUC
ns_auc = roc_auc_score(testy, ns_probs)
lr_auc = roc_auc_score(testy, lr_probs)
# Imprimimos en pantalla
print('Sin entrenar: ROC AUC=%.3f' % (ns_auc))
print('Regresión Logística: ROC AUC=%.3f' % (lr_auc))
# Calculamos las curvas ROC
ns_fpr, ns_tpr, _ = roc_curve(testy, ns_probs)
lr_fpr, lr_tpr, _ = roc_curve(testy, lr_probs)
# Pintamos las curvas ROC
pyplot.plot(ns_fpr, ns_tpr, linestyle='--', label='Sin entrenar')
pyplot.plot(lr_fpr, lr_tpr, marker='.', label='Regresión Logística')
# Etiquetas de los ejes
pyplot.xlabel('Tasa de Falsos Positivos')
pyplot.ylabel('Tasa de Verdaderos Positivos')
pyplot.legend()
pyplot.show()

Si analizamos la salida del modelo observamos que, aunque no es el mejor modelo del mundo, sí que parece que está algo entrenado. El valor de de AUC de la curva ROC es 0.72.

Ejemplo de Curva ROC en Python
Curva ROC (Salida del modelo de ejemplo)

¿Nos podemos fiar de este valor? Si observamos con detalle las salidas, vemos que lo que está haciendo el modelo es predecir como salida la clase mayoritaria (0 en este caso). El modelo «no se la juega» y asigna el valor de la clase mayoritaria a todos los casos. El valor de AUC que se obtiene es a priori bueno pero en realidad está distorsionado.

Modelo de datos desbalanceados con curva de precisión-sensibilidad

¿Qué pasaría si analizamos los resultados de este modelo pero usando la curva de precisión-sensibilidad?

Si añadimos el siguiente trozo de código, observamos que la salida de modelo en esta ocasión es mucho más decepcionante:

yhat = model.predict(testX)
lr_precision, lr_recall, _ = precision_recall_curve(testy, lr_probs)
lr_f1, lr_auc = f1_score(testy, yhat), auc(lr_recall, lr_precision)
# Resumimos s
print('Regresión Logística: f1=%.3f auc=%.3f' % (lr_f1, lr_auc))
# Pintamos la curva de precision-sensibilidad curves
no_skill = len(testy[testy==1]) / len(testy)
pyplot.plot([0, 1], [no_skill, no_skill], linestyle='--', label='Sin entrenar')
pyplot.plot(lr_recall, lr_precision, marker='.', label='Regresión Logística')
#Etiquetas de ejes
pyplot.xlabel('Sensibilidad')
pyplot.ylabel('Precisión')
pyplot.legend()
Curva de precisión-sensibilidad (Salida del modelo)

El valor F es 0, mientras que el área bajo la curva es 0.05. Es un modelo que no mejora apenas al modelo sin entrenar por lo que no podemos considerar bueno. Se observa que el modelo es penalizado por predecir la clase mayoritaria en todos los casos. Parece un modelo válido si observamos la curva ROC pero en realidad es bastante pobre si utilizamos la curva de precisión-sensibilidad.

Como resumen se concluye que la mejor manera para elegir una curva u otra es tener en cuenta los desbalances en los datos. En el caso de que nuestros datos están más o menos balanceados se puede usar la curva ROC. Sin embargo, pero cuando existe un alto grado de desbalance en los datos, es necesario utilizar esta segunda curva.

Os dejamos otros posts por si queréis aprender mas cosas!