12. Evaluación de modelos#

Introducción

  • Después de discutir los fundamentos del aprendizaje supervisado y sus algoritmos, abordaremos la evaluación de modelos y la selección de parámetros. Nos enfocaremos en modelos supervisados, específicamente en regresión y clasificación, ya que la evaluación en aprendizaje no supervisado es más cualitativa.

  • Para evaluar modelos supervisados, dividimos el conjunto de datos en entrenamiento y prueba usando train_test_split, construimos un modelo con fit, y lo evaluamos en el conjunto de prueba con score, que calcula la fracción de muestras correctamente clasificadas.

import warnings
warnings.filterwarnings('ignore')
from sklearn.datasets import make_blobs
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
  • Creamos un conjunto de datos sintético

X, y = make_blobs(random_state=0)
  • Dividimos los datos y las etiquetas en un conjunto de entrenamiento y otro de prueba

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
  • Instanciar el modelo y ajustarlo al conjunto de entrenamiento

logreg = LogisticRegression().fit(X_train, y_train)
  • Evaluar el modelo en el conjunto de prueba

print("Test set score: {:.2f}".format(logreg.score(X_test, y_test)))
Test set score: 0.88
  • Evaluar el modelo en el conjunto de entrenamiento

print("Train set score: {:.2f}".format(logreg.score(X_train, y_train)))
Train set score: 0.91

Observación

  • Recuerde que la razón por la que dividimos nuestros datos en conjuntos de entrenamiento y de prueba, es que estamos interesados en medir lo bien que nuestro modelo se generaliza a nuevos datos (no vistos anteriormente).

  • No nos interesa lo bien que nuestro modelo se ajusta al conjunto de entrenamiento, sino lo bien que puede hacer predicciones sobre datos no observados durante el entrenamiento.

  • En esta sección, ampliaremos dos aspectos de esta evaluación. En primer lugar, introduciremos la validación cruzada (cross-validation), una forma más sólida de evaluar el rendimiento de la generalización, y discutiremos los métodos para evaluar el rendimiento de la clasificación y la regresión que van más allá de las medidas por defecto accuracy y \(R^2\) proporcionadas por el método score.

  • También hablaremos del GridSearchCV, un método eficaz para ajustar los parámetros de los modelos supervisados y obtener el mejor rendimiento de la generalización.

12.1. Cross-Validation#

Introducción

  • La validación cruzada (cross-validation) es un método estadístico para evaluar el rendimiento de la generalización que es más estable y exhaustivo que el uso de una división en un conjunto de entrenamiento y otro de prueba.

  • En la validación cruzada, los datos se dividen repetidamente y se entrenan múltiples modelos. La versión más utilizada de la validación cruzada es k-fold cross-validation, donde k es un número especificado por el usuario, normalmente 5 o 10.

  • Cuando se realiza la validación cruzada five-fold, los datos se dividen primero en cinco partes de tamaño (aproximadamente) igual, llamadas pliegues (folds). A continuación, se entrena una secuencia de modelos. El primer modelo se entrena utilizando el primer pliegue como conjunto de prueba, y los pliegues restantes (2-5) se utilizan como conjunto de entrenamiento. El modelo se construye utilizando los datos de los pliegues 2-5, y luego se evalúa accuracy en el pliegue 1.

  • A continuación, luego se construye otro modelo, esta vez utilizando el pliegue 2 como conjunto de prueba y los datos de los pliegues 1, 3, 4 y 5 como conjunto de entrenamiento. Este proceso se repite utilizando los pliegues 3, 4 y 5 como conjuntos de prueba. Para cada una de estas cinco divisiones de los datos en conjuntos de entrenamiento y de prueba, calculamos accuracy. Al final, hemos recogido cinco valores de accuracy.

import mglearn
mglearn.plots.plot_cross_validation()
_images/464ba17b4c200310244bcd04b0b83810a15c15e388b3bdb0c2b90033a9130eb5.png
  • Normalmente, la primera quinta parte de los datos es conocida como el primer fold, la segunda quinta parte de los datos es el segundo fold, y así sucesivamente.

12.1.1. Validación cruzada en scikit-learn#

  • La validación cruzada se implementa en scikit-learn utilizando la función cross_val_score del módulo model_selection. Los parámetros de la función cross_val_score son, el modelo que queremos evaluar, los datos de entrenamiento y las etiquetas reales. Vamos a evaluar LogisticRegression en el conjunto de datos iris.

  • Utilizaremos los parámetros por defecto de este modelo, más adelante estudiaremos como conseguir los más óptimos por medio de grid-search, por ahora solo estamos interesados en evaluar el modelo por defecto usando cross_val_score. Para más información acerca de los argumentos del modelo (ver sklearn.linear_model.LogisticRegression).

Iris dataset

  • Objetivo Principal: Predecir la especie de una flor iris basándose en medidas de sus características morfológicas. El dataset contiene tres especies de iris: setosa, versicolor y virginica, y las características medidas son el largo y el ancho del sépalo y el pétalo.

  • Uso: Este problema de clasificación permite evaluar la capacidad de un modelo para diferenciar entre las especies basándose en características continuas.

_images/iris_dataset.png
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
import numpy as np
iris = load_iris()
iris.data.shape
(150, 4)
iris.target.shape
(150,)
for i, feature in enumerate(iris.feature_names):
    data = iris.data[:, i]
    print(f"{feature}:")
    print(f"  Mean: {np.mean(data)}")
    print(f"  Std: {np.std(data)}")
    print(f"  Min: {np.min(data)}")
    print(f"  Max: {np.max(data)}")
    print(f"  25th percentile (Q1): {np.percentile(data, 25)}")
    print(f"  Median (Q2): {np.median(data)}")
    print(f"  75th percentile (Q3): {np.percentile(data, 75)}")
    print()
sepal length (cm):
  Mean: 5.843333333333334
  Std: 0.8253012917851409
  Min: 4.3
  Max: 7.9
  25th percentile (Q1): 5.1
  Median (Q2): 5.8
  75th percentile (Q3): 6.4

sepal width (cm):
  Mean: 3.0573333333333337
  Std: 0.4344109677354946
  Min: 2.0
  Max: 4.4
  25th percentile (Q1): 2.8
  Median (Q2): 3.0
  75th percentile (Q3): 3.3

petal length (cm):
  Mean: 3.7580000000000005
  Std: 1.759404065775303
  Min: 1.0
  Max: 6.9
  25th percentile (Q1): 1.6
  Median (Q2): 4.35
  75th percentile (Q3): 5.1

petal width (cm):
  Mean: 1.1993333333333336
  Std: 0.7596926279021594
  Min: 0.1
  Max: 2.5
  25th percentile (Q1): 0.3
  Median (Q2): 1.3
  75th percentile (Q3): 1.8
print("Target (species):")
unique, counts = np.unique(iris.target, return_counts=True)
for i, count in zip(unique, counts):
    print(f"  {iris.target_names[i]}: {count} samples")
Target (species):
  setosa: 50 samples
  versicolor: 50 samples
  virginica: 50 samples
  • Queda como ejercicio para el estudiante, realizar un EDA exahustivo para el dataset, teniendo en cuenta cada una de las variables y sus categorías. Evaluemos el uso de la función cross_val_score

logreg = LogisticRegression()
scores = cross_val_score(logreg, iris.data, iris.target);
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [0.96666667 1.         0.93333333 0.96666667 1.        ]
  • Por defecto, cross_val_score realiza una five-fold cross validation, devolviendo cinco valores de accuracy. Podemos cambiar el número de pliegues (folds) utilizados, cambiando el parámetro cv:

scores = cross_val_score(logreg, iris.data, iris.target, cv=10);
print("Cross-validation scores: {}".format(scores))
Cross-validation scores: [1.         0.93333333 1.         1.         0.93333333 0.93333333
 0.93333333 1.         1.         1.        ]
  • Una forma habitual de resumir la precisión de la validación cruzada es calcular la media

print("Average cross-validation score: {:.2f}".format(scores.mean()))
Average cross-validation score: 0.97
  • Utilizando la validación cruzada media podemos concluir que, esperamos que el modelo sea de precisión en torno al 97% de media. Si observamos las cinco puntuaciones producidas por la validación cruzada de cinco pliegues five-fold cross validation, también podemos concluir que hay una varianza relativamente alta en la precisión entre pliegues, que va del 100% de precisión al 93.33% de precisión aproximadamente.

  • Esto podría implicar que el modelo es muy dependiente de los pliegues particulares utilizados para el entrenamiento, pero también podría ser simplemente una consecuencia del pequeño tamaño del conjunto de datos. Normalmente, si los resultados de accuracy varían considerablemente durante la validación cruzada de 5 pliegues, esto puede indicar problemas en el modelo, en los datos, o en el proceso de validación.

Varianza alta de accuracy entre pliegues

  • Varianza en los datos: Si los datos no están bien distribuidos o existen diferencias significativas entre los pliegues, los resultados de accuracy pueden variar mucho entre cada uno. Esto sucede comúnmente cuando los datos presentan sesgos o cuando ciertos pliegues contienen muestras que son más difíciles de clasificar.

  • Modelo inestable o de alta varianza: Algunos modelos, especialmente los que son complejos o sensibles a los datos de entrenamiento (como los árboles de decisión sin poda o redes neuronales profundas con pocos datos), pueden tener un rendimiento inconsistente en diferentes subconjuntos de los datos. En estos casos, el modelo se ajusta demasiado a los pliegues específicos, produciendo fluctuaciones en el accuracy.

  • Tamaño de la muestra: Si el conjunto de datos es pequeño, la variabilidad en los pliegues puede ser mayor, ya que cada subconjunto de datos tiene un peso proporcionalmente alto. En estos casos, la validación cruzada puede reflejar variaciones amplificadas.

  • Datos atípicos: La presencia de datos atípicos (outliers) en algunos de los pliegues puede afectar el accuracy y producir resultados más dispares. Es importante revisar si los pliegues contienen estos datos y considerar métodos para manejarlos.

12.1.2. Ventajas de la validación cruzada#

Observación

  • Son varios los beneficios de utilizar la validación cruzada en lugar de una única división en un conjunto de entrenamiento y otro de prueba. En primer lugar, recuerde que train_test_split realiza una división aleatoria de los datos. Imaginemos que tenemos “suerte” al dividir aleatoriamente los datos, y todos los ejemplos que son difíciles de clasificar acaban en el conjunto de entrenamiento.

  • En ese caso, el conjunto de prueba sólo contendrá ejemplos “fáciles”, y nuestra precisión en el conjunto de prueba será irrealmente alto. Por el contrario, si tenemos “mala suerte”, es posible que pongamos al azar todos los ejemplos difíciles de clasificar en el conjunto de prueba y en consecuencia, obtener una score irrealmente bajo.

  • La validación cruzada garantiza que cada ejemplo esté en el conjunto de prueba una vez, obligando al modelo a generalizar bien en todo el conjunto. Proporciona un rango de precisión que muestra el rendimiento en el mejor y peor caso. A diferencia de dividir una sola vez, la validación cruzada usa más datos para entrenamiento (por ejemplo, 80% en cinco pliegues o 90% en diez pliegues), lo que mejora la precisión. Sin embargo, su desventaja es el mayor coste computacional, ya que se entrenan varios modelos en lugar de uno solo.

Observación

  • Es importante tener en cuenta que la validación cruzada no es una forma de construir un modelo que pueda aplicarse a nuevos datos. La validación cruzada no devuelve un modelo.

  • Cuando se llama a cross_validation_score, se construyen internamente múltiples modelos, pero el propósito de la validación cruzada es evaluar lo bien que un algoritmo determinado generalizará cuando es entrenado en un conjunto de datos específico.

12.1.3. Validación cruzada estratificada \(k\)-fold y otras estrategias#

  • Dividir el conjunto de datos en k pliegues comenzando por la primera parte de los datos, como descrito en la sección anterior, puede no ser siempre una buena idea. Por ejemplo, veamos el conjunto de datos iris

from sklearn.datasets import load_iris
iris = load_iris()
print("Iris labels:\n{}".format(iris.target))
Iris labels:
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2
 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
 2 2]
  • En este conjunto de datos, el primer tercio corresponde a la clase 0, el segundo a la clase 1 y el último a la clase 2. Al realizar una validación cruzada de 3 pliegues, cada pliegue contendría solo una clase en el conjunto de prueba y las otras dos en el conjunto de entrenamiento. Esto causaría que la precisión fuera del 0%, ya que las clases en entrenamiento y prueba no coincidirían en ninguna división, lo cual es ineficaz para evaluar el modelo.

  • Como la estrategia simple de k-fold falla aquí, scikit-learn no la utiliza para clasificación, sino que utiliza la validación cruzada estratificada k-fold. En la validación cruzada estratificada, dividimos los datos de forma que las proporciones entre las clases sean las mismas en cada pliegue como en todo el conjunto de datos.

mglearn.plots.plot_stratified_cross_validation()
_images/16c4a06a1cbc231a522754d8efdd1a866821b29bdc053e9632c382dce6612dac.png
  • La validación cruzada estratificada de k pliegues distribuye las clases en cada pliegue según su proporción en el conjunto de datos, evitando que un pliegue carezca de muestras de alguna clase. Esto ofrece estimaciones más fiables del rendimiento del clasificador en comparación con la validación cruzada estándar de k pliegues, especialmente en conjuntos de datos desbalanceados.

  • Para la regresión, scikit-learn utiliza la validación cruzada k-fold estándar por defecto. Sería posible también tratar de hacer cada pliegue representativo de los diferentes valores objetivo de la regresión, pero esta no es una estrategia comúnmente utilizada.

from sklearn.model_selection import StratifiedKFold
stratified_kfold = StratifiedKFold(n_splits=3)

scores = cross_val_score(logreg, iris.data, iris.target, cv=stratified_kfold)
print("Cross-validation scores:\n{}".format(scores))
Cross-validation scores:
[0.98 0.96 0.98]

12.1.4. Más control sobre la validación cruzada#

  • Podemos ajustar el número de pliegues en cross_val_score con el parámetro cv. No obstante, scikit-learn permite mayor control al aceptar un divisor de validación cruzada como parámetro cv. Aunque los valores predeterminados suelen ser adecuados, en ciertos casos puede ser útil una estrategia diferente.

  • Por ejemplo, para replicar resultados con validación cruzada k-fold en clasificación, se puede importar la clase KFold de model_selection e instanciarla con el número deseado de pliegues.

from sklearn.model_selection import KFold
kfold = KFold(n_splits=3)
  • Entonces, podemos pasar el objeto kfold splitter como el parámetro cv a cross_val_score:

print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
Cross-validation scores:
[0. 0. 0.]
  • Usar validación cruzada triple (no estratificada) en el conjunto de datos iris es ineficaz, ya que cada pliegue corresponde a una clase, impidiendo el aprendizaje. Para evitarlo, se recomienda aleatorizar los datos en lugar de estratificarlos, configurando shuffle=True en KFold y fijando random_state para obtener resultados reproducibles. Esta aleatorización mejora significativamente los resultados.

kfold = KFold(n_splits=3, shuffle=True, random_state=0)
print("Cross-validation scores:\n{}".format(cross_val_score(logreg, iris.data, iris.target, cv=kfold)))
Cross-validation scores:
[0.98 0.96 0.96]

12.1.5. Validación cruzada con exclusión (leave-one-out)#

  • El método de validación cruzada leave-one-out es una variante de k pliegues donde cada pliegue de prueba contiene solo una muestra. Aunque es más lento en conjuntos de datos grandes, puede ofrecer mejores estimaciones en conjuntos de datos pequeños.

from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()
scores = cross_val_score(logreg, iris.data, iris.target, cv=loo)
print("Number of cv iterations: ", len(scores))
print("Mean accuracy: {:.2f}".format(scores.mean()))
Number of cv iterations:  150
Mean accuracy: 0.97

12.1.6. Validación cruzada aleatoria y dividida#

  • Otra estrategia muy flexible para la validación cruzada es la validación cruzada aleatoria (shuffle-split cross-validation). En la validación cruzada de división aleatoria, cada división (split) está compuesta de tantos train_size puntos (disyuntos) para el conjunto de entrenamiento y tantos test_size puntos (disjuntos) para el conjunto de prueba, se fijen inicialmente.

  • Esta división se repite n_iter veces, de forma aleatoria. A continuación se muestra la ejecución de cuatro iteraciones de división de un conjunto de datos que consta de 10 puntos, con un conjunto de entrenamiento de 5 puntos y conjuntos de prueba de 2 puntos cada uno

mglearn.plots.plot_shuffle_split()
_images/3da0128faf4cd0ca1231184ff81f1016555701727260db26519d838bb1c4c5a1.png
  • Puede usar enteros para train_size y test_size para asignarles sus tamaños absolutos, o números de tipo flotante para usar fracciones del conjunto de datos. El siguiente código divide el conjunto de datos en un 50% de entrenamiento y un 50% de prueba para 10 iteraciones

from sklearn.model_selection import ShuffleSplit
shuffle_split = ShuffleSplit(test_size=.5, train_size=.5, n_splits=10)
scores = cross_val_score(logreg, iris.data, iris.target, cv=shuffle_split)
print("Cross-validation scores:\n{}".format(scores))
Cross-validation scores:
[0.94666667 0.93333333 0.96       0.96       0.96       0.92
 0.98666667 0.94666667 0.97333333 0.97333333]
  • La validación cruzada aleatoria permite ajustar el número de iteraciones y usar solo una parte de los datos en cada iteración, lo cual es útil para grandes conjuntos de datos. La variante estratificada, StratifiedShuffleSplit, ofrece resultados más fiables en tareas de clasificación.

12.1.7. Validación cruzada con grupos#

GroupKFold

  • Digamos que quieres construir un sistema para reconocer emociones a partir de imágenes de rostros (o imágenes médicas), y se recopila un conjunto de datos con imágenes de 100 personas, donde cada persona es capturada varias veces, mostrando varias emociones.

  • El objetivo es construir un clasificador que pueda identificar correctamente las emociones de las personas que no están en el conjunto de datos. GroupKFold es una variación de k-fold que garantiza que el mismo grupo no esté representado en los conjuntos de prueba y de entrenamiento.

Observación

  • La validación cruzada estratificada puede medir el rendimiento de un clasificador, pero si hay imágenes de la misma persona en los conjuntos de entrenamiento y prueba, el modelo reconocerá más fácilmente emociones en rostros ya vistos. Para evaluar mejor la generalización a nuevas caras, es recomendable usar GroupKFold, que permite separar las imágenes por persona en los conjuntos de entrenamiento y prueba.

  • Este ejemplo ilustra el uso de la validación cruzada en un conjunto de datos sintético con agrupación definida por la matriz groups. El conjunto tiene 12 datos, organizados en cuatro grupos: los primeros tres puntos pertenecen al primer grupo, los siguientes cuatro al segundo, y así sucesivamente.

from sklearn.model_selection import GroupKFold
import numpy as np
from sklearn.datasets import make_blobs
  • Creamos nuestro conjunto de datos sintético, y la lista de grupos groups

X, y = make_blobs(n_samples=12, random_state=0)
y
array([1, 0, 2, 0, 0, 1, 1, 2, 0, 2, 2, 1])
groups = [0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 3]
gkf = GroupKFold(n_splits=3)
for train, test in gkf.split(X, y, groups=groups):
    print("GroupKFold: %s %s" % (train, test))
    Xtrain, Xtest = X[train], X[test]
    ytrain, ytest = y[train], y[test]
    logreg = LogisticRegression()
    logreg.fit(Xtrain, ytrain)
    print("LogisticRegression Score: ", logreg.score(Xtest, ytest))
GroupKFold: [ 0  1  2  7  8  9 10 11] [3 4 5 6]
LogisticRegression Score:  0.75
GroupKFold: [0 1 2 3 4 5 6] [ 7  8  9 10 11]
LogisticRegression Score:  0.6
GroupKFold: [ 3  4  5  6  7  8  9 10 11] [0 1 2]
LogisticRegression Score:  0.6666666666666666
  • No es necesario que las muestras estén ordenadas por grupos; sólo lo hemos hecho con fines ilustrativos. Como puede ver, para cada división, cada grupo está completamente en el conjunto de entrenamiento o completamente en el conjunto de prueba. Ademas, observe que los pliegues no tienen exactamente el mismo tamaño debido al desequilibrio de los datos.

  • Hay más estrategias de división para la validación cruzada en scikit-learn, que pueden utilizarse para una variedad aún mayor de casos (ver Cross-validation: evaluating estimator performance). Sin embargo, el KFold estándar, el StratifiedKFold y el GroupKFold son, como mucho, los más utilizados. En el siguiente link puede encontrar la documentición relacionada con el uso de cada parámetro de la función sklearn.model_selection.cross_val_score (ver Evaluate a score by cross-validation).

12.3. Métricas de evaluación y scoring#

  • Hasta ahora, hemos evaluado el rendimiento de la clasificación utilizando la precisión (accuracy) (la fracción de muestras correctamente clasificadas) y el rendimiento de la regresión mediante el \(R^2\). Sin embargo, éstas son sólo dos de las muchas formas posibles de resumir la eficacia de un modelo supervisado en un conjunto de datos determinado. En la práctica, estas métricas de evaluación pueden no ser apropiadas para su aplicación, y es importante elegir la métrica correcta cuando se selecciona entre modelos y se ajustan los parámetros.

12.3.1. Tenga en cuenta el objetivo final#

  • Al elegir una métrica en aprendizaje automático, debe alinearse con el objetivo final de la aplicación, conocido como métrica de negocio, ya que las predicciones influyen en la toma de decisiones y el impacto empresarial. Por ejemplo, podría buscar reducir accidentes, minimizar hospitalizaciones o aumentar ingresos.

  • La selección del modelo y sus parámetros debe maximizar el impacto positivo en la métrica de negocio, pero evaluarlo en producción puede ser riesgoso. Por ello, se emplean métricas sustitutas más fáciles de calcular, como la precision en la clasificación de imágenes. Sin embargo, estas métricas deben acercarse lo más posible al objetivo real.

  • El impacto empresarial puede no reducirse a un solo número (por ejemplo, más clientes pero menor gasto por cliente), pero debe reflejar la influencia del modelo. En esta sección, se abordarán métricas para clasificación binaria, multiclase y regresión.

12.3.2. Métricas para la clasificación binaria#

  • La clasificación binaria es probablemente la aplicación más común y conceptualmente simple de aprendizaje automático en la práctica. Sin embargo, todavía hay una serie de advertencias en evaluar incluso esta sencilla tarea. Antes de entrar en las métricas alternativas, echemos un vistazo a la forma en que se mide la precisión, la cual puede ser engañosa. Recordemos que para la clasificación binaria, a menudo hablamos de una clase positiva y una clase negativa, entendiendo que la clase positiva es la que estamos buscando.

12.3.3. Tipos de errores#

  • precisión (accuracy) no siempre es una buena medida del rendimiento, ya que los errores cometidos no reflejan toda la información relevante. Por ejemplo, en la detección temprana de cáncer, una prueba positiva implica más exámenes, mientras que una negativa considera al paciente sano. Dado que ningún modelo es perfecto, es crucial evaluar el impacto real de sus errores.

  • Un falso positivo ocurre cuando un paciente sano es clasificado como enfermo, causando pruebas innecesarias y posibles preocupaciones. En cambio, un falso negativo, donde un paciente enfermo es clasificado como sano, puede ser fatal al retrasar el tratamiento. Estos errores se conocen como tipo I y tipo II, pero los términos falso positivo y falso negativo son más claros. En este contexto, es crítico minimizar los falsos negativos (recall), aunque los falsos positivos sean una molestia.

  • Las consecuencias de estos errores varían según la aplicación. En el ámbito comercial, se pueden asignar costos monetarios a cada tipo de error, permitiendo evaluar modelos más allá de la precisión, optimizando la toma de decisiones.

  • Un ejemplo donde el equilibrio es clave pede ser la detección de spam en correos electrónicos.

    • Reducir falsos negativos (correos spam que se clasifican como no spam): Si un correo malicioso pasa desapercibido, el usuario podría ser víctima de estafas o phishing.

    • Reducir falsos positivos (correos legítimos que se clasifican como spam): Correos importantes, como facturas o mensajes de trabajo, podrían perderse en la carpeta de spam.

12.3.4. Conjuntos de datos desequilibrados#

  • Los errores son cruciales cuando una clase es mucho más frecuente que otra, como en la predicción de clicks, donde la mayoría de los elementos mostrados no reciben interacción. En estos casos, los datos están desequilibrados, con una gran mayoría perteneciente a la clase “no click”.

  • Dado que los eventos de interés suelen ser raros, un modelo con 99% para accuracy podría simplemente predecir siempre “no click” sin aportar valor real. Por ello, la precisión no distingue entre un modelo trivial y uno efectivo. Para ilustrarlo, se creará un conjunto de datos desequilibrado 9:1 con el conjunto digits, clasificando el dígito 9 frente a los demás.

from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
digits = load_digits()
print("Data shape: ", digits.data.shape, "; Target shape", digits.target.shape)
Data shape:  (1797, 64) ; Target shape (1797,)
np.unique(digits.target)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
np.unique(digits.data)
array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12.,
       13., 14., 15., 16.])
y = digits.target == 9 # Boolean
X_train, X_test, y_train, y_test = train_test_split(digits.data, y, random_state=0)
  • Podemos utilizar DummyClassifier para predecir siempre la clase mayoritaria (aquí “not nine”) para ver lo poco informativa que puede ser el (accuracy)

from sklearn.dummy import DummyClassifier
import numpy as np
dummy_majority = DummyClassifier(strategy='most_frequent').fit(X_train, y_train)
pred_most_frequent = dummy_majority.predict(X_test)
print("Unique predicted labels: {}".format(np.unique(pred_most_frequent)))
print("Test score: {:.2f}".format(dummy_majority.score(X_test, y_test)))
Unique predicted labels: [False]
Test score: 0.90
  • Obtuvimos una precisión cercana al 90% sin aprender nada. Esto puede parecer sorprendente, pero pero piénselo un momento. Imagine que alguien le dice que su modelo tiene un 90% de precisión. Podrías pensar que han hecho un buen trabajo. Pero dependiendo del problema, ¡eso podría ser posible con sólo predecir una clase! Comparemos esto con el uso de un clasificador real.

from sklearn.tree import DecisionTreeClassifier
tree = DecisionTreeClassifier(max_depth=2).fit(X_train, y_train)
pred_tree = tree.predict(X_test)
print("Test score: {:.2f}".format(tree.score(X_test, y_test)))
Test score: 0.92
  • Según la precisión, el DecisionTreeClassifier es sólo ligeramente mejor que el predictor constante. Esto podría indicar que algo está mal en la forma en que utilizamos DecisionTreeClassifier, o bien que accuracy no es una buena medida en este caso. Para comparar, evaluemos otro clasificador, LogisticRegression

from sklearn.linear_model import LogisticRegression
logreg = LogisticRegression(C=0.1).fit(X_train, y_train)
pred_logreg = logreg.predict(X_test)
print("logreg score: {:.2f}".format(logreg.score(X_test, y_test)))
logreg score: 0.98
  • El clasificador dummy con resultados aleatorios es el peor según accuracy, mientras que LogisticRegression muestra buenos resultados. Sin embargo, accuracy es inadecuada en entornos desequilibrados, dificultando la evaluación real del modelo. Exploraremos métricas alternativas que permitan seleccionar modelos de manera más efectiva, priorizando aquellas que indiquen cuánto mejor es un modelo en lugar de favorecer predicciones frecuentes o aleatorias. Una métrica adecuada debería descartar predicciones sin sentido.

12.3.5. Matrices de confusión#

  • Una de las formas más completas de representar el resultado de la evaluación de la clasificación binaria es el uso de matrices de confusión. Inspeccionemos las predicciones de LogisticRegres de la sección anterior utilizando la función confusion_matrix. Ya hemos almacenado las predicciones del conjunto de prueba en pred_logreg

from sklearn.metrics import confusion_matrix
confusion = confusion_matrix(y_test, pred_logreg)
print("Confusion matrix:\n{}".format(confusion))
Confusion matrix:
[[402   1]
 [  6  41]]
  • La salida de confusion_matrix es una matriz de dos por dos, donde las filas corresponden a las clases verdaderas y las columnas corresponden a las clases predichas. Cada entrada cuenta la frecuencia con la que una muestra que pertenece a la clase correspondiente a la fila (aquí “not nine” y “nine”) fue clasificada como la clase correspondiente a la columna.

mglearn.plots.plot_confusion_matrix_illustration()
_images/11103c5e86611f1ce3af8695501b801d5446ec857fc9741651a131b9c387683c.png
  • La diagonal principal de la matriz de confusión representa las clasificaciones correctas, mientras que las demás entradas indican errores. Si “nine” es la clase positiva, se pueden definir los términos falso positivo (FP) y falso negativo (FN). Además, las muestras correctamente clasificadas como positivas son verdaderos positivos (TP) y las negativas, verdaderos negativos (TN). Estos términos permiten interpretar la matriz de confusión.

mglearn.plots.plot_binary_confusion_matrix()
_images/919d33fd5494029644cef9f3a35f2ed7d5b259fc76d16cfada01ae13ea330e7a.png
  • Ahora utilicemos la matriz de confusión para comparar los modelos que hemos ajustado antes (los dos modelos dummy, árbol de decisión y regresión logística)

print("\nDummy model:")
print(confusion_matrix(y_test, pred_most_frequent))
Dummy model:
[[403   0]
 [ 47   0]]
print("\nDecision tree:")
print(confusion_matrix(y_test, pred_tree))
Decision tree:
[[390  13]
 [ 24  23]]
print("\nLogistic Regression")
print(confusion_matrix(y_test, pred_logreg))
Logistic Regression
[[402   1]
 [  6  41]]
  • La matriz de confusión revela que pred_most_frequent es ineficaz, ya que siempre predice la misma clase y tiene cero verdaderos y falsos positivos. Aunque el árbol de decisión y la regresión logística presentan predicciones más razonables, la regresión logística supera al árbol en todos los aspectos, con más verdaderos positivos y negativos y menos errores. Sin embargo, analizar manualmente la matriz es un proceso cualitativo y laborioso. A continuación, exploraremos métodos para resumir su información de manera más eficiente.

Accuracy

Ya vimos una forma de resumir el resultado en la matriz de confusión, calculando su accuracy, que puede expresarse como

\[ \text{Accuracy}=\frac{TP+TN}{TP+TN+FP+FN}. \]

En otras palabras, el accuracy es el número de predicciones correctas (TP y TN) dividido por el número de todas las muestras (todas las entradas de la matriz de confusión sumadas).

  • Precision, recall y f-score. Hay otras formas de resumir la matriz de confusión, siendo las más comunes: precision, recall y f-score.

Precision

Precision mide cuántas de las muestras predichas como positivas son realmente positivas, es decir, precision intenta responder a la siguiente pregunta: ¿qué proporción de identificaciones positivas fue correcta?

\[ \text{Precision} = \frac{TP}{TP+FP} \]

Precision se utiliza como métrica de rendimiento cuando el objetivo es limitar el número de falsos positivos.

  • Un modelo que predice la eficacia de un medicamento en ensayos clínicos debe minimizar falsos positivos, ya que estos experimentos son costosos y solo deben realizarse con alta certeza de éxito. Por ello, es crucial que el modelo tenga alta precision (o valor predictivo positivo, VPP). Nótese que cuando precision → 1, FP → 0, y cuando recall → 1, FN → 0.

Recall

El recall mide cuántas de las muestras de la clase positiva son realmente predichas positivas, es decir, recall intenta responder a la siguiente pregunta: ¿qué proporción de positivos reales se identificó en forma correcta?

\[ \text{Recall} = \frac{TP}{TP+FN} \]

Recall se utiliza como métrica de rendimiento cuando el objetivo es limitar el número de falsos negativos.

  • Existe un equilibrio entre recall y precision. Si se predicen todas las muestras como positivas, se elimina recall, pero con muchos falsos positivos, reduciendo precision. En cambio, si solo se predice como positiva la muestra más segura, precision será perfecta (si es realmente positiva), pero el recall será muy bajo.

Observación

Precision y recall son sólo dos de las muchas medidas de clasificación derivadas de TP, FP, TN y FN. Puede encontrar un gran resumen de todas las medidas en Sensitivity_and_specificity. En la comunidad del aprendizaje automático, precision y recall son las medidas más utilizadas para la clasificación binaria, aunque pueden utilizar otras métricas relacionadas.

\(f_{1}\)-score

Por lo tanto, aunque precision y recall sean medidas muy importantes, si sólo se tiene en cuenta una de ellas no se obtiene una visión completa. Una forma de resumirlas es usando el f-score o f-measure, que es la media armónica entre precision y recall:

\[ F=2\cdot\frac{\text{precision}\cdot\text{recall}}{\text{precision}+\text{recall}}. \]

Esta variante concreta también se conoce como \(f_{1}\)-score.

El F₁-score es mejor que la exactitud en conjuntos de datos desbalanceados, ya que considera precisión y recuperación. Lo aplicaremos al conjunto “nine vs. rest”, donde “nine” es la clase positiva y minoritaria (True), mientras que el resto es negativa (False).

from sklearn.metrics import f1_score
print("f1 score dummy: {:.2f}".format(f1_score(y_test, pred_most_frequent)))
f1 score dummy: 0.00
print("f1 score tree: {:.2f}".format(f1_score(y_test, pred_tree)))
f1 score tree: 0.55
print("f1 score logistic regression: {:.2f}".format(f1_score(y_test, pred_logreg)))
f1 score logistic regression: 0.92
  • El \(f_{1}\)-score distingue mejor las predicciones dummy de las del árbol que el accuracy, alineándose mejor con nuestra intuición sobre un buen modelo. Sin embargo, es menos interpretable. Para un resumen más completo de precision, recall y \(f_{1}\)-score, classification_report los calcula y muestra en un formato claro. Sus últimas filas incluyen macro avg, que pondera cada clase por igual, y weighted avg, que ajusta los pesos según la proporción de datos. Más detalles en sklearn.metrics.classification_report.

from sklearn.metrics import classification_report
print(classification_report(y_test, pred_most_frequent, target_names=["not nine", "nine"]))
              precision    recall  f1-score   support

    not nine       0.90      1.00      0.94       403
        nine       0.00      0.00      0.00        47

    accuracy                           0.90       450
   macro avg       0.45      0.50      0.47       450
weighted avg       0.80      0.90      0.85       450
  • La función classification_report produce una línea por clase (aquí, True y False) e informa precision, recall y \(f\)-score. Si consideramos la clase positiva por “not nine”, podemos ver en la salida de classification_report que obtenemos un \(f\)-score de 0.94 con el modelo dummy. Además, para la clase “not nine” tenemos un recall de 1, ya que clasificamos todas las muestras como “not nine”.

  • La última columna junto al \(f\)-score proporciona el soporte de cada clase, lo que significa simplemente el número de muestras en esta clase según la verdad básica. La última fila del informe de clasificación muestra una media ponderada (por el número de muestras en la clase) de los números de cada clase. Aquí hay dos informes más, uno para el clasificador arbol de decisión y otro para la regresión logística

print(classification_report(y_test, pred_tree, target_names=["not nine", "nine"]))
              precision    recall  f1-score   support

    not nine       0.94      0.97      0.95       403
        nine       0.64      0.49      0.55        47

    accuracy                           0.92       450
   macro avg       0.79      0.73      0.75       450
weighted avg       0.91      0.92      0.91       450
print(classification_report(y_test, pred_logreg, target_names=["not nine", "nine"]))
              precision    recall  f1-score   support

    not nine       0.99      1.00      0.99       403
        nine       0.98      0.87      0.92        47

    accuracy                           0.98       450
   macro avg       0.98      0.93      0.96       450
weighted avg       0.98      0.98      0.98       450
  • Como puede observar al mirar los informes, las diferencias entre los modelos dummy y un modelo muy bueno ya no son tan claras. La elección de la clase que se declarada como clase positiva, tiene un gran impacto en las métricas. Mientras que el \(f\)-score de la clasificación dummy es de 0.13 (frente a 0.89 para la regresión logística) en la clase “nine” es de 0.90 frente a 0.99, lo que parece un resultado razonable. Sin embargo, si se observan todas las cifras juntas, se obtiene una imagen bastante precisa, y podemos ver claramente la superioridad de la regresión logística.

12.3.6. Teniendo en cuenta la incertidumbre#

  • La matriz de confusión y el informe de clasificación analizan detalladamente un conjunto de predicciones, pero las predicciones en sí contienen información clave del modelo. La mayoría de los clasificadores incluyen decision_function o predict_proba para medir la certeza de las predicciones, utilizando umbrales fijos: 0 en decision_function y 0.5 en predict_proba para clasificación binaria.

  • Un ejemplo de clasificación binaria desequilibrada presenta 400 puntos negativos y 50 positivos. Se entrena un kernel SVM, y un mapa de calor ilustra la función de decisión. Un círculo negro en la gráfica central superior marca el umbral donde la función es cero: puntos dentro del círculo son positivos, los demás negativos.

from mglearn.datasets import make_blobs
X, y = make_blobs(n_samples=400, n_features=50, centers=2, cluster_std=[7.0, 2], random_state=22)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
svc = SVC(gamma=.05).fit(X_train, y_train)
mglearn.plots.plot_decision_threshold()
_images/92692614d9169b2871f2ac1fc0cfdd7a523727c772007bcc7ae208fe98b688df.png
  • Podemos utilizar la función classification_report para evaluar precision y recall de ambas clases

print(classification_report(y_test, svc.predict(X_test)))
              precision    recall  f1-score   support

           0       0.49      1.00      0.66        49
           1       0.00      0.00      0.00        51

    accuracy                           0.49       100
   macro avg       0.24      0.50      0.33       100
weighted avg       0.24      0.49      0.32       100
print(confusion_matrix(y_test, svc.predict(X_test)))
[[49  0]
 [51  0]]
  • Se obtienen las puntuaciones de decisión para medir la certeza del modelo, luego se calcula un umbral óptimo como la media de estas puntuaciones. A partir de este umbral, se generan predicciones ajustadas, considerando como positivos los valores superiores.

  • La función decision_function(X_test) de un clasificador SVM (SVC) devuelve un arreglo de valores numéricos que representan la distancia de cada muestra con respecto al hiperplano de decisión.

import numpy as np

decision_scores = svc.decision_function(X_test)
optimal_threshold = np.median(decision_scores)

print("Umbral óptimo:", optimal_threshold)
Umbral óptimo: -0.013244755286005602
y_pred_adjusted = decision_scores > optimal_threshold
print(classification_report(y_test, y_pred_adjusted))
              precision    recall  f1-score   support

           0       0.98      1.00      0.99        49
           1       1.00      0.98      0.99        51

    accuracy                           0.99       100
   macro avg       0.99      0.99      0.99       100
weighted avg       0.99      0.99      0.99       100
print(confusion_matrix(y_test, y_pred_adjusted))
[[49  0]
 [ 1 50]]
  • Como se esperaba, recall y precision para la clase 1 subió. Ahora estamos clasificando una región más grande del espacio como clase 1, como se ilustra en el panel superior derecho de la anterior figura. Si valora más precision que recall, o al revés, o sus datos están muy desequilibrados, cambiar el umbral de decisión es la forma más fácil de obtener mejores resultados. Como la función de decisión puede tener rangos arbitrarios, es difícil proporcionar una regla general sobre cómo elegir un umbral.

Observación

  • Si establece un umbral, debe tener cuidado de no hacerlo utilizando el conjunto de prueba. Como con cualquier otro parámetro, establecer un umbral de decisión en el conjunto de prueba es probable que produzca resultados demasiado optimistas. Utilice un conjunto de validación o aplique validación cruzada.

  • La media geométrica o G-mean es una métrica de clasificación desequilibrada que, si se optimiza, buscará un equilibrio entre la precision y recall.

\[ \text{G-mean}=\sqrt{\text{precision}\times\text{recall}}. \]
  • Un enfoque consistiría en probar el modelo con cada umbral devuelto por la llamada precision_recall_curve() y seleccionar el umbral con el mayor valor G-mean. Otras técnicas de oversampling, tales como SMOTE también pueden ser adecuadas, para datos de entrenamiento desbalanceados.

  • El umbral en modelos con predict_proba es más fácil de ajustar, ya que su salida va de 0 a 1. Por defecto, un umbral de 0.5 clasifica como positiva una instancia si la probabilidad supera el 50%. Aumentarlo exige mayor certeza para predecir positivo y menor para negativo.

  • Trabajar con probabilidades es intuitivo, pero no todos los modelos reflejan bien la incertidumbre (ej., un DecisionTree profundo siempre está 100% seguro, aunque se equivoque). Esto se relaciona con la calibración: un modelo calibrado mide correctamente su incertidumbre. Para más detalles, consulte “Predicting Good Probabilities with Supervised Learning” de Niculescu-Mizil y Caruana.

12.4. Curvas precision-recall y ROC#

  • Ajustar el umbral de clasificación permite equilibrar precision y recall. Por ejemplo, para un recall del 90%, el umbral debe adaptarse según los objetivos empresariales. Sin embargo, un umbral extremo, como clasificar todo como positivo, garantiza un recall del 100% pero hace inútil el modelo.

  • Fijar un punto operativo, como un recall del 90%, ayuda a garantizar el rendimiento en entornos empresariales. Al desarrollar un modelo, es clave explorar distintos umbrales y compromisos precision-recall para comprender mejor el problema.

  • La herramienta clave para esto es la curva precision-recall, calculable con precision_recall_curve() de sklearn.metrics, que usa etiquetas reales e incertidumbres de decision_function o predict_proba.

from sklearn.metrics import precision_recall_curve
precision, recall, thresholds = precision_recall_curve(y_test, svc.decision_function(X_test))
  • La función precision_recall_curve devuelve listas de precisión y recall para todos los umbrales posibles, permitiendo trazar la curva correspondiente. Más puntos generan una curva más suave. make_blobs crea datos gaussianos isotrópicos para agrupación.

X, y = make_blobs(n_samples=4000, n_features=500, centers=2, cluster_std=[7.0, 2], random_state=22)
  • También usaremos el conjunto de cáncer de mama con load_breast_cancer.

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.svm import SVC
from sklearn.metrics import precision_recall_curve
import numpy as np
import matplotlib.pyplot as plt
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
svc = SVC(gamma=.05).fit(X_train, y_train)
precision, recall, thresholds = precision_recall_curve(y_test, svc.decision_function(X_test))
closest_eq_idx = np.argmin(np.abs(precision - recall))
best_threshold = thresholds[closest_eq_idx]
best_precision = precision[closest_eq_idx]
best_recall = recall[closest_eq_idx]

best_threshold, best_precision, best_recall
(0.39103354055345696, 0.9529411764705882, 0.9)
close_zero = np.argmin(np.abs(thresholds))
plt.figure(figsize=(8, 6))
plt.plot(precision, recall, label="Precision-Recall Curve")
plt.plot(precision[closest_eq_idx], recall[closest_eq_idx], 'o', markersize=10, 
         label=f"Best Threshold ({best_threshold:.2f})", fillstyle="none", c='r', mew=2)
plt.plot(precision[close_zero], recall[close_zero], 'o', markersize=10, 
         label="Threshold ~ 0", fillstyle="none", c='k', mew=2)
plt.xlabel("Precision")
plt.ylabel("Recall")
plt.legend()
plt.title("Precision-Recall Curve with Best Threshold and Threshold ~ 0")
plt.show()
_images/b34f41fb7f11e354635ecbc9faec0b1b5d1729e7eb975876330ed4eed644703b.png
  • Cada punto de la curva representa un umbral de decision_function. Se puede lograr un recall de 0.99 con una precisión de 0.63. El círculo negro indica el umbral 0, que es el predeterminado de decision_function, mostrando la compensación usada en predict. Cuanto más cerca de la esquina superior derecha esté la curva, mejor es el clasificador, pues indica alto precision y recall simultáneamente.

  • La curva inicia en la esquina superior izquierda con un umbral bajo, clasificando todo como positivo. A medida que el umbral aumenta, la precisión mejora, pero el recall disminuye. Si el umbral es muy alto, solo los verdaderos positivos son clasificados correctamente, lo que maximiza precision pero reduce recall. Para precision mayor a 0.5, cada mejora en precisión cuesta una gran pérdida en recall.

  • Diferentes clasificadores rinden mejor en distintas partes de la curva. Comparando un SVM con un RandomForestClassifier, este último usa predict_proba en lugar de decision_function. La función precision_recall_curve requiere una medida de certeza, por lo que se usa rf.predict_proba(X_test)[:, 1]. El umbral predeterminado de predict_proba es 0.5 y está marcado en la curva.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_curve
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=42)

svc = SVC(gamma=0.05, probability=True).fit(X_train, y_train)
rf = RandomForestClassifier(n_estimators=100, random_state=0, max_features=2).fit(X_train, y_train)

y_scores_svc = svc.predict_proba(X_test)[:, 1]
precision_svc, recall_svc, thresholds_svc = precision_recall_curve(y_test, y_scores_svc)

y_scores_rf = rf.predict_proba(X_test)[:, 1]
precision_rf, recall_rf, thresholds_rf = precision_recall_curve(y_test, y_scores_rf)

closest_eq_idx_svc = np.argmin(np.abs(precision_svc - recall_svc))
best_threshold_svc = thresholds_svc[closest_eq_idx_svc]
close_zero_svc = np.argmin(np.abs(thresholds_svc))

closest_eq_idx_rf = np.argmin(np.abs(precision_rf - recall_rf))
best_threshold_rf = thresholds_rf[closest_eq_idx_rf]
close_default_rf = np.argmin(np.abs(thresholds_rf - 0.5))
plt.figure(figsize=(8, 6))
plt.plot(precision_svc, recall_svc, label="SVC Precision-Recall Curve")
plt.plot(precision_rf, recall_rf, label="RandomForest Precision-Recall Curve")

plt.plot(precision_svc[closest_eq_idx_svc], recall_svc[closest_eq_idx_svc], 'o', 
         markersize=10, label=f"Best Threshold SVC ({best_threshold_svc:.2f})", 
         fillstyle="none", c='r', mew=2)
plt.plot(precision_svc[close_zero_svc], recall_svc[close_zero_svc], 'o', 
         markersize=10, label="Threshold ~ 0 (SVC)", fillstyle="none", c='k', mew=2)
plt.plot(precision_rf[closest_eq_idx_rf], recall_rf[closest_eq_idx_rf], '^', 
         markersize=10, label=f"Best Threshold RF ({best_threshold_rf:.2f})", 
         fillstyle="none", c='g', mew=2)
plt.plot(precision_rf[close_default_rf], recall_rf[close_default_rf], '^', 
         markersize=10, label="Threshold 0.5 (RF)", fillstyle="none", c='b', mew=2)
plt.xlabel("Precision")
plt.ylabel("Recall")
plt.legend()
plt.title("Precision-Recall Curve for SVC and RandomForest")
plt.show()
_images/3f8a4f1d35759504585dca460cd17d09c724de780bfdca2d85cdc1c460f65223.png
  • En el gráfico de comparación podemos ver que el bosque aleatorio funciona mejor en los extremos, para requisitos de recall o de precision muy altos. Alrededor de cualquier nivel de precision, RF tiene un mejor rendimiento. Si sólo nos fijamos en el \(f_{1}\)-score para comparar el rendimiento general, habríamos pasado por alto estas sutilezas. El \(f_{1}\)-score sólo capta un punto de la curva precision-recall, el dado por el umbral por defecto.

print("f1_score of random forest: {:.3f}".format(f1_score(y_test, rf.predict(X_test))))
print("f1_score of svc: {:.3f}".format(f1_score(y_test, svc.predict(X_test))))
f1_score of random forest: 0.978
f1_score of svc: 0.767
  • La comparación de dos curvas precision-recall proporciona una visión muy detallada, pero es un proceso bastante manual. Para la comparación automática de modelos, es posible que queramos resumir la información contenida en la curva, sin limitarnos a un umbral o punto de operación. Una forma concreta de resumir la curva precisión-recall es calcular la integral o el área bajo la curva precision-recall, también conocida como precisión media. Para calcular la precisión media se puede utilizar la función average_precision_score. Como tenemos que calcular la curva ROC y considerar múltiples umbrales, el resultado de decision_function o predict_proba debe pasarse a average_precision_score, no el resultado de predict.

from sklearn.metrics import average_precision_score
ap_rf  = average_precision_score(y_test, rf.predict_proba(X_test)[:, 1])
ap_svc = average_precision_score(y_test, svc.decision_function(X_test))
print("Average precision of random forest: {:.3f}".format(ap_rf))
print("Average precision of svc: {:.3f}".format(ap_svc))
Average precision of random forest: 0.998
Average precision of svc: 0.948
  • Al promediar todos los umbrales posibles, vemos que el bosque aleatorio y el SVC tienen un rendimiento similar, con RF ligeramente por delante. Esto es bastante diferente del resultado que obtuvimos antes con \(f_{1}\)-score. Como la precisión media es el área área bajo una curva que va de 0 a 1, la precisión media siempre devuelve un valor entre 0 (worst) y 1 (best). La precisión media de un clasificador que asigna decision_function al azar es la fracción de muestras positivas en el conjunto de datos.

12.4.1. Características operativas del receptor (ROC) y AUC#

  • La curva ROC analiza el rendimiento de los clasificadores a distintos umbrales, graficando la tasa de verdaderos positivos (TPR) frente a la tasa de falsos positivos (FPR). Mientras que TPR equivale al recall, FPR mide la fracción de negativos reales clasificados incorrectamente. A diferencia de la curva precision-recall, la curva ROC evalúa el equilibrio entre la detección de positivos y la generación de falsos positivos.

\[ FPR=\frac{FP}{FP+TN}. \]
  • Nótese que \(FPR\) se utiliza como métrica de rendimiento cuando el objetivo es limitar el número de verdaderos negativos. La curva ROC puede calcularse mediante la función roc_curve. Nótese que si \(FPR\rightarrow 0\) entonces el número de \(TN\) crece.

from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_test, svc.decision_function(X_test))
plt.plot(fpr, tpr, label="ROC Curve")
plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
close_zero = np.argmin(np.abs(thresholds))
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10, label="threshold zero", fillstyle="none", c='k', mew=2)
plt.legend(loc=4);
_images/28aa93ff234e007850cf542255d75a59ffc1cd27466a0508bf140c95ee706e23.png
  • Nótese que, los valores más pequeños en el eje \(x\) indican menos falsos positivos (FP) y más verdaderos negativos (TN). Los valores más grandes en el eje \(y\) indican más verdaderos positivos (TP) y menos falsos negativos (FN). En cuanto a la curva precision-recall, a menudo queremos resumir la curva ROC utilizando un solo número, el área bajo la curva (comúnmente se denomina simplemente AUC, y se entiende que la curva en cuestión es la curva ROC). Podemos calcular el área bajo la curva ROC con la función roc_auc_score

from sklearn.metrics import roc_auc_score
rf_auc = roc_auc_score(y_test, rf.predict_proba(X_test)[:, 1])
svc_auc = roc_auc_score(y_test, svc.decision_function(X_test))
print("AUC for Random Forest: {:.3f}".format(rf_auc))
print("AUC for SVC: {:.3f}".format(svc_auc))
AUC for Random Forest: 0.997
AUC for SVC: 0.921
from sklearn.metrics import roc_curve
fpr_rf, tpr_rf, thresholds_rf = roc_curve(y_test, rf.predict_proba(X_test)[:, 1])
plt.plot(fpr, tpr, label="ROC Curve SVC")
plt.plot(fpr_rf, tpr_rf, label="ROC Curve RF")
plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10, label="threshold zero SVC", fillstyle="none", c='k', mew=2)
close_default_rf = np.argmin(np.abs(thresholds_rf - 0.5))
plt.plot(fpr_rf[close_default_rf], tpr_rf[close_default_rf], '^', markersize=10, label="threshold 0.5 RF", fillstyle="none", c='k', mew=2)
plt.legend(loc=4);
_images/cfc58c0450993597a1c5637f0f6b154c22a2a1183ba8a830bb086ae7dee8a655.png
  • El bosque aleatorio supera ligeramente a la SVM en AUC-score. La precisión media, representada por el área bajo la curva (AUC), varía entre 0 (peor) y 1 (mejor). Una predicción aleatoria siempre tiene un AUC de 0.5, sin importar el desequilibrio de clases, lo que hace que AUC sea una mejor métrica que el accuracy en estos casos.

  • El AUC mide qué tan bien el modelo clasifica muestras positivas en comparación con negativas. Un AUC de 1 indica que todas las muestras positivas tienen un puntaje mayor que las negativas. En problemas con clases desequilibradas, el AUC es más útil que el accuracy para la selección del modelo. Por ejemplo, al clasificar el dígito 9 frente a otros en un conjunto de datos, podemos evaluar el rendimiento de una SVM con diferentes valores de gamma.

plt.figure()
for gamma in [1, 0.07, 0.01]:
    svc = SVC(gamma=gamma).fit(X_train, y_train)
    accuracy = svc.score(X_test, y_test)
    auc = roc_auc_score(y_test, svc.decision_function(X_test))
    fpr, tpr, _ = roc_curve(y_test , svc.decision_function(X_test))
    print("gamma = {:.2f} accuracy = {:.2f} AUC = {:.2f}".format(gamma, accuracy, auc))
    plt.plot(fpr, tpr, label="gamma={:.3f}".format(gamma))
plt.xlabel("FPR")
plt.ylabel("TPR")
plt.xlim(-0.01, 1)
plt.ylim(0, 1.02)
plt.legend(loc="best");
gamma = 1.00 accuracy = 0.62 AUC = 0.53
gamma = 0.07 accuracy = 0.62 AUC = 0.91
gamma = 0.01 accuracy = 0.63 AUC = 0.93
_images/5f3160fe71dd1c9658c9c152fe075d73b700b467cc384ae65e78a486c04ac654.png
  • Los valores de accuracy para gamma de 1.0, 0.07 y 0.01 son 0.51, 0.94 y 0.95, respectivamente. Con gamma=1.0, el AUC es aleatorio, mientras que con gamma=0.07 mejora a 0.94, y con gamma=0.01, alcanza 0.95, indicando que los positivos están mejor clasificados que los negativos. Con un umbral adecuado, el modelo puede clasificar perfectamente.

  • Usar solo accuracy no revela esto, por lo que recomendamos el AUC, especialmente en datos desequilibrados. Sin embargo, dado que AUC no usa un umbral fijo, puede ser necesario ajustarlo para mejorar la clasificación.

12.5. Métricas para la clasificación multiclase#

  • La evaluación de la clasificación multiclase se basa en métricas de clasificación binaria, pero promediadas en todas las clases. El accuracy sigue siendo la fracción de ejemplos clasificados correctamente, pero no es ideal cuando las clases están desequilibradas.

  • Por ejemplo, en un problema con tres clases (A: 85%, B: 10%, C: 5%), un accuracy del 85% puede ser engañoso. La clasificación multiclase es más difícil de interpretar que la binaria, por lo que, además del accuracy, se utilizan la matriz de confusión y el informe de clasificación. Aplicaremos estos métodos al reconocimiento de dígitos escritos a mano en el conjunto de datos digits.

from sklearn.metrics import accuracy_score
X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target, random_state=0)
lr = LogisticRegression().fit(X_train, y_train)
pred = lr.predict(X_test)
print("Accuracy: {:.3f}".format(accuracy_score(y_test, pred)))
print("Confusion matrix:\n{}".format(confusion_matrix(y_test, pred)))
Accuracy: 0.951
Confusion matrix:
[[37  0  0  0  0  0  0  0  0  0]
 [ 0 40  0  0  0  0  0  0  2  1]
 [ 0  1 40  3  0  0  0  0  0  0]
 [ 0  0  0 43  0  0  0  0  1  1]
 [ 0  0  0  0 37  0  0  1  0  0]
 [ 0  0  0  0  0 46  0  0  0  2]
 [ 0  1  0  0  0  0 51  0  0  0]
 [ 0  0  0  1  1  0  0 46  0  0]
 [ 0  3  1  0  0  0  0  0 43  1]
 [ 0  0  0  0  0  1  0  0  1 45]]
  • El modelo tiene un accuracy del 95.1%, lo que ya nos dice que lo estamos haciendo bastante bien. La matriz de confusión nos proporciona algunos detalles más. Como en el caso binario, cada fila corresponde a una etiqueta verdadera y cada columna a una etiqueta predicha. En la siguiente figura se puede encontrar una mejor representación visual

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix

conf_matrix = confusion_matrix(y_test, pred).astype(int)

plt.figure(figsize=(8, 6))
sns.heatmap(conf_matrix, annot=True, fmt="d", cmap="gray_r", 
            xticklabels=digits.target_names, 
            yticklabels=digits.target_names)

plt.xlabel("Etiqueta Predicha")
plt.ylabel("Etiqueta Real")
plt.title("Matriz de Confusión")
plt.show()
_images/db8844e1ca026b2e1d1cbea6341b7b6fb8910af91c5f89f559445badfeb0fa9e.png
  • Para el dígito 0, las 37 muestras fueron clasificadas correctamente, sin falsos negativos ni falsos positivos. Sin embargo, hubo confusiones entre otros dígitos, como el 2, que se clasificó erróneamente como 3 en tres casos y como 1 en uno. También un dígito 8 se clasificó como 2. La función classification_report permite calcular precision, recall y \(f\)-score para cada clase.

print(classification_report(y_test, pred))
              precision    recall  f1-score   support

           0       1.00      1.00      1.00        37
           1       0.89      0.93      0.91        43
           2       0.98      0.91      0.94        44
           3       0.91      0.96      0.93        45
           4       0.97      0.97      0.97        38
           5       0.98      0.96      0.97        48
           6       1.00      0.98      0.99        52
           7       0.98      0.96      0.97        48
           8       0.91      0.90      0.91        48
           9       0.90      0.96      0.93        47

    accuracy                           0.95       450
   macro avg       0.95      0.95      0.95       450
weighted avg       0.95      0.95      0.95       450
  • No es de extrañar que los valores para precision y recall sean un 1 perfecto para la clase 0, ya que no hay confusiones con esta clase. Para la clase 6, en cambio, el precision es de 1 porque ninguna otra clase se clasificó erróneamente como 6. La métrica más utilizada para conjuntos de datos desequilibrados en el entorno multiclase es la versión multiclase del \(f\)-score.

  • La idea detrás del \(f\)-score multiclase es calcular un \(f\)-score binario por clase, siendo esa clase la positiva y las otras clases las negativas. Luego, estos \(f\)-scores por clase se promedian utilizando una de las siguientes estrategias:

    • El promedio “macro” calcula los \(f\)-scores no ponderadas por clase. De este modo, se da el mismo peso a todas las clases, independientemente de su tamaño.

    • El promedio “weighted” calcula la media de los \(f\)-scores por clase, ponderada por su soporte. Esto es lo que se indica en el informe de clasificación.

    • El promedio “micro” calcula el número total de falsos positivos, falsos negativos y verdaderos positivos en todas las clases, y luego calcula precision, recall y \(f\)-score utilizando estos recuentos.

  • Si se preocupa por igual de cada muestra, se recomienda utilizar el “micro” \(f\)-score; si le importa cada clase por igual, se recomienda utilizar la media macro \(f\)-score.

print("Macro average f1 score: {:.3f}".format(f1_score(y_test, pred, average="macro")))
print("Weighted average f1 score: {:.3f}".format(f1_score(y_test, pred, average="weighted")))
print("Micro average f1 score: {:.3f}".format(f1_score(y_test, pred, average="micro")))
Macro average f1 score: 0.952
Weighted average f1 score: 0.951
Micro average f1 score: 0.951

12.6. Métricas de regresión#

  • La evaluación en regresión puede ser tan detallada como en clasificación, analizando sobrepredicción y subpredicción. Sin embargo, en la mayoría de los casos, el \(R^2\) usado por defecto en los regresores es suficiente. En aplicaciones empresariales, métricas como MSE o MAE pueden influir en la optimización de modelos. No obstante, \(R^2\) suele ser más intuitivo. En el curso de Time Series Forecasting, se profundizará en métricas como MAE, MAPE, MSE y RMSE.

12.6.1. Uso de métricas de evaluación en la selección de modelos#

  • Para evaluar modelos con métricas como AUC en GridSearchCV o cross_val_score, scikit-learn permite usar el argumento scoring. Basta con proporcionar una cadena con la métrica deseada, como "roc_auc", para cambiar la evaluación predeterminada (accuracy) a AUC. Esto facilita la selección de modelos en tareas como clasificar “nueve vs. resto” en digits.

  • Scoring por defecto para la clasificación es accuracy

print("Default scoring: {}".format(cross_val_score(SVC(), digits.data, digits.target == 9)))
Default scoring: [0.975      0.99166667 1.         0.99442897 0.98050139]
  • Proporcionar scoring="accuracy" no cambia los resultados

explicit_accuracy = cross_val_score(SVC(), digits.data, digits.target == 9, scoring="accuracy")
print("Explicit accuracy scoring: {}".format(explicit_accuracy))
Explicit accuracy scoring: [0.975      0.99166667 1.         0.99442897 0.98050139]
  • Ahora asignemos scoring="roc_auc" para modificar la técnica de scoring utilizada

roc_auc = cross_val_score(SVC(), digits.data, digits.target == 9, scoring="roc_auc")
print("AUC scoring: {}".format(roc_auc))
AUC scoring: [0.99717078 0.99854252 1.         0.999828   0.98400413]
  • Del mismo modo, podemos cambiar la métrica utilizada para elegir los mejores parámetros en GridSearchCV

X_train, X_test, y_train, y_test = train_test_split(digits.data, digits.target == 9, random_state=0)
  • Proporcionamos una red a manera de ilustración del punto

param_grid = {'gamma': [0.0001, 0.01, 0.1, 1, 10]}
  • Utilizando el score accuracy por defecto

grid = GridSearchCV(SVC(), param_grid=param_grid)
grid.fit(X_train, y_train)
print("Grid-Search with accuracy")
print("Best parameters:", grid.best_params_)
print("Best cross-validation score (accuracy)): {:.3f}".format(grid.best_score_))
print("Test set AUC: {:.3f}".format(
roc_auc_score(y_test, grid.decision_function(X_test))))
print("Test set accuracy: {:.3f}".format(grid.score(X_test, y_test)))
Grid-Search with accuracy
Best parameters: {'gamma': 0.0001}
Best cross-validation score (accuracy)): 0.976
Test set AUC: 0.992
Test set accuracy: 0.973
  • Utilizando el scoring AUC en su lugar

grid = GridSearchCV(SVC(), param_grid=param_grid, scoring="roc_auc")
grid.fit(X_train, y_train)
print("\nGrid-Search with AUC")
print("Best parameters:", grid.best_params_)
print("Best cross-validation score (AUC): {:.3f}".format(grid.best_score_))
print("Test set AUC: {:.3f}".format(
roc_auc_score(y_test, grid.decision_function(X_test))))
print("Test set accuracy: {:.3f}".format(grid.score(X_test, y_test)))
Grid-Search with AUC
Best parameters: {'gamma': 0.01}
Best cross-validation score (AUC): 0.998
Test set AUC: 1.000
Test set accuracy: 1.000
  • Cuando se utiliza accuracy, se selecciona el parámetro gamma=0.0001, mientras que cuando se utiliza el AUC se selecciona gamma=0.01. Accuracy score para la validación cruzada es coherente con el accuracy del conjunto de prueba en ambos casos. Sin embargo, al utilizar AUC se encontró un mejor ajuste de los parámetros en en términos de AUC e incluso en términos de accuracy. Los valores más importantes del parámetro de scoring para la clasificación son accuracy (por defecto); roc_auc para el área bajo la curva ROC; average_precision para el área bajo la curva de precision-recall; f1, f1_macro, f1_micro y f1_weighted para \(f_{1}\)-score binario y las diferentes variantes ponderadas.

  • En cuanto a la regresión, los valores más utilizados son r2 para el score \(R^{2}\), mean_squared_error para el error medio al cuadrado y mean_absolute_error para el error medio absoluto. Puede encontrar una lista completa de argumentos admitidos en la documentación o consultando el diccionario SCORER definido en el módulo metrics.scorer.

Resumen y conclusiones

La validación cruzada, el grid-search y las métricas de evaluación son fundamentales para mejorar los modelos de aprendizaje automático. Hay dos puntos clave a considerar:

  1. Validación cruzada: Permite evaluar un modelo como se comportaría en el futuro. Sin embargo, si se usa para seleccionar modelos o hiperparámetros, los datos de prueba quedan “agotados”, lo que lleva a estimaciones optimistas. Para evitarlo, se deben separar los datos en entrenamiento, validación y prueba, usando validación cruzada en el conjunto de entrenamiento para la selección de modelos y parámetros.

  2. Métrica de evaluación: precision no siempre es el mejor criterio, especialmente en problemas con clases desbalanceadas, donde los falsos positivos y negativos tienen impactos diferentes. Es crucial elegir una métrica adecuada según el contexto.

Las herramientas descritas son esenciales para cualquier científico de datos. Sin embargo, grid-search y validación cruzada solo aplican a modelos supervisados individuales. En la siguiente sección, se introducirá Pipeline para optimizar procesos más complejos.