Utilice casos y código para explorar la nueva clase que ayuda a ajustar los umbrales de decisión en scikit-learn
La versión 1.5 de scikit-learn incluye una nueva clase, TunedThresholdClassifierCV, facilitando la optimización de los umbrales de decisión de los clasificadores de scikit-learn. Un umbral de decisión es un punto de corte que convierte las probabilidades predichas generadas por un modelo de aprendizaje automático en clases discretas. El umbral de decisión por defecto del .predict() El método de los clasificadores scikit-learn en una configuración de clasificación binaria es 0,5. Aunque se trata de un valor predeterminado sensato, rara vez es la mejor opción para las tareas de clasificación.
Esta publicación presenta la clase TunedThresholdClassifierCV y demuestra cómo puede optimizar los umbrales de decisión para diversas tareas de clasificación binaria. Esta nueva clase ayudará a cerrar la brecha entre los científicos de datos que crean modelos y las partes interesadas del negocio que toman decisiones basadas en el resultado del modelo. Al ajustar los umbrales de decisión, los científicos de datos pueden mejorar el rendimiento del modelo y alinearse mejor con los objetivos comerciales.
Esta publicación cubrirá las siguientes situaciones en las que es beneficioso ajustar los umbrales de decisión:
- Maximizar una métrica: utilícelo al elegir un umbral que maximice una métrica de puntuación, como la puntuación F1.
- Aprendizaje sensible a los costos: Ajuste el umbral cuando el costo de clasificar erróneamente un falso positivo no sea igual al costo de clasificar erróneamente un falso negativo y tenga una estimación de los costos.
- Sintonización bajo restricciones: Optimice el punto de operación en la ROC o la curva de recuperación de precisión para cumplir con restricciones de rendimiento específicas.
El código utilizado en esta publicación y los enlaces a conjuntos de datos están disponibles en GitHub.
¡Empecemos! Primero, importe las bibliotecas necesarias, lea los datos y divida los datos de entrenamiento y prueba.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.compose import make_column_selector as selector
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
RocCurveDisplay,
f1_score,
make_scorer,
recall_score,
roc_curve,
confusion_matrix,
)
from sklearn.model_selection import TunedThresholdClassifierCV, train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScalerRANDOM_STATE = 26120
Maximizar una métrica
Antes de comenzar el proceso de creación de modelos en cualquier proyecto de aprendizaje automático, es fundamental trabajar con las partes interesadas para determinar qué métricas optimizar. Tomar esta decisión temprano garantiza que el proyecto se alinee con los objetivos previstos.
Usar una métrica de precisión en casos de uso de detección de fraude para evaluar el desempeño del modelo no es ideal porque los datos a menudo están desequilibrados y la mayoría de las transacciones no son fraudulentas. La puntuación F1 es la media armónica de precisión y recuperación y es una mejor métrica para conjuntos de datos desequilibrados como la detección de fraude. usemos el TunedThresholdClassifierCV clase para optimizar el umbral de decisión de un modelo de regresión logística para maximizar la puntuación F1.
Usaremos el Conjunto de datos de detección de fraude con tarjetas de crédito de Kaggle para introducir la primera situación en la que necesitamos ajustar un umbral de decisión. Primero, divida los datos en conjuntos de entrenamiento y prueba, luego cree una canalización de scikit-learn para escalar los datos y entrenar un modelo de regresión logística. Ajuste la canalización a los datos de entrenamiento para que podamos comparar el rendimiento del modelo original con el rendimiento del modelo ajustado.
creditcard = pd.read_csv("data/creditcard.csv")
y = creditcard["Class"]
X = creditcard.drop(columns=["Class"])X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y
)
# Only Time and Amount need to be scaled
original_fraud_model = make_pipeline(
ColumnTransformer(
[("scaler", StandardScaler(), ["Time", "Amount"])],
remainder="passthrough",
force_int_remainder_cols=False,
),
LogisticRegression(),
)
original_fraud_model.fit(X_train, y_train)
Aún no se ha realizado ningún ajuste, pero llegará en el siguiente bloque de código. Los argumentos a favor TunedThresholdClassifierCV son similares a otros CV clases en scikit-learn, como CuadrículaBuscarCV. Como mínimo, el usuario sólo necesita pasar el estimador original y TunedThresholdClassifierCV almacenará el umbral de decisión que maximiza la precisión equilibrada (predeterminado) utilizando una validación cruzada K-veces estratificada 5 veces (predeterminada). También utiliza este umbral al llamar. .predict(). Sin embargo, cualquier métrica de scikit-learn (o invocable) se puede utilizar como scoring métrico. Además, el usuario puede pasar el familiar cv argumento para personalizar la estrategia de validación cruzada.
Crea el TunedThresholdClassifierCV instancia y ajustar el modelo a los datos de entrenamiento. Pase el modelo original y establezca la puntuación en “f1”. También querremos establecer store_cv_results=True para acceder a los umbrales evaluados durante la validación cruzada para su visualización.
tuned_fraud_model = TunedThresholdClassifierCV(
original_fraud_model,
scoring="f1",
store_cv_results=True,
)tuned_fraud_model.fit(X_train, y_train)
# average F1 across folds
avg_f1_train = tuned_fraud_model.best_score_
# Compare F1 in the test set for the tuned model and the original model
f1_test = f1_score(y_test, tuned_fraud_model.predict(X_test))
f1_test_original = f1_score(y_test, original_fraud_model.predict(X_test))
print(f"Average F1 on the training set: {avg_f1_train:.3f}")
print(f"F1 on the test set: {f1_test:.3f}")
print(f"F1 on the test set (original model): {f1_test_original:.3f}")
print(f"Threshold: {tuned_fraud_model.best_threshold_: .3f}")
Average F1 on the training set: 0.784
F1 on the test set: 0.796
F1 on the test set (original model): 0.733
Threshold: 0.071
Ahora que hemos encontrado el umbral que maximiza la verificación de puntuación de F1 tuned_fraud_model.best_score_ para descubrir cuál fue el mejor puntaje promedio de F1 en todos los pliegues en la validación cruzada. También podemos ver qué umbral generó esos resultados usando tuned_fraud_model.best_threshold_. Puede visualizar las puntuaciones de las métricas en los umbrales de decisión durante la validación cruzada utilizando el objective_scores_ y decision_thresholds_ atributos:
fig, ax = plt.subplots(figsize=(5, 5))
ax.plot(
tuned_fraud_model.cv_results_["thresholds"],
tuned_fraud_model.cv_results_["scores"],
marker="o",
linewidth=1e-3,
markersize=4,
color="#c0c0c0",
)
ax.plot(
tuned_fraud_model.best_threshold_,
tuned_fraud_model.best_score_,
"^",
markersize=10,
color="#ff6700",
label=f"Optimal cut-off point = {tuned_fraud_model.best_threshold_:.2f}",
)
ax.plot(
0.5,
f1_test_original,
label="Default threshold: 0.5",
color="#004e98",
linestyle="--",
marker="X",
markersize=10,
)
ax.legend(fontsize=8, loc="lower center")
ax.set_xlabel("Decision threshold", fontsize=10)
ax.set_ylabel("F1 score", fontsize=10)
ax.set_title("F1 score vs. Decision threshold -- Cross-validation", fontsize=12)
# Check that the coefficients from the original model and the tuned model are the same
assert (tuned_fraud_model.estimator_[-1].coef_ ==
original_fraud_model[-1].coef_).all()
Hemos utilizado el mismo modelo de regresión logística subyacente para evaluar dos umbrales de decisión diferentes. Los modelos subyacentes son los mismos, como lo demuestra la igualdad de coeficientes en la afirmación anterior. Optimización en TunedThresholdClassifierCV se logra utilizando técnicas de posprocesamiento, que se aplican directamente a las probabilidades predichas generadas por el modelo. Sin embargo, es importante señalar que TunedThresholdClassifierCV utiliza validación cruzada de forma predeterminada para encontrar el umbral de decisión para evitar el sobreajuste de los datos de entrenamiento.
Aprendizaje sensible a los costos
El aprendizaje sensible a los costos es un tipo de aprendizaje automático que asigna un costo a cada tipo de clasificación errónea. Esto traduce el desempeño del modelo en unidades que las partes interesadas entienden, como los dólares ahorrados.
Usaremos el Conjunto de datos de abandono de clientes de TELCO, un conjunto de datos de clasificación binaria, para demostrar el valor del aprendizaje sensible a los costos. El objetivo es predecir si un cliente abandonará o no, dadas las características sobre la demografía del cliente, los detalles del contrato y otra información técnica sobre la cuenta del cliente. La motivación para utilizar este conjunto de datos (y parte del código) proviene de Curso de Dan Becker sobre optimización del umbral de decisión.
data = pd.read_excel("data/Telco_customer_churn.xlsx")
drop_cols = [
"Count", "Country", "State", "Lat Long", "Latitude", "Longitude",
"Zip Code", "Churn Value", "Churn Score", "CLTV", "Churn Reason"
]
data.drop(columns=drop_cols, inplace=True)# Preprocess the data
data["Churn Label"] = data["Churn Label"].map({"Yes": 1, "No": 0})
data.drop(columns=["Total Charges"], inplace=True)
X_train, X_test, y_train, y_test = train_test_split(
data.drop(columns=["Churn Label"]),
data["Churn Label"],
test_size=0.2,
random_state=RANDOM_STATE,
stratify=data["Churn Label"],
)
Configure una canalización básica para procesar los datos y generar probabilidades previstas con un modelo de bosque aleatorio. Esto servirá como punto de referencia para comparar con el TunedThresholdClassifierCV.
preprocessor = ColumnTransformer(
transformers=[("one_hot", OneHotEncoder(),
selector(dtype_include="object"))],
remainder="passthrough",
)original_churn_model = make_pipeline(
preprocessor, RandomForestClassifier(random_state=RANDOM_STATE)
)
original_churn_model.fit(X_train.drop(columns=["customerID"]), y_train);
La elección del preprocesamiento y el tipo de modelo no es importante para este tutorial. La empresa quiere ofrecer descuentos a los clientes que se prevé que abandonen. Durante la colaboración con las partes interesadas, aprende que ofrecer un descuento a un cliente que no abandonará (un falso positivo) costaría $80. También aprende que vale $200 ofrecer un descuento a un cliente que habría abandonado. Puede representar esta relación en una matriz de costos:
def cost_function(y, y_pred, neg_label, pos_label):
cm = confusion_matrix(y, y_pred, labels=[neg_label, pos_label])
cost_matrix = np.array([[0, -80], [0, 200]])
return np.sum(cm * cost_matrix)cost_scorer = make_scorer(cost_function, neg_label=0, pos_label=1)
También incluimos la función de costo en un marcador personalizado de scikit-learn. Este anotador será utilizado como scoring argumento en TunedThresholdClassifierCV y para evaluar las ganancias en el conjunto de prueba.
tuned_churn_model = TunedThresholdClassifierCV(
original_churn_model,
scoring=cost_scorer,
store_cv_results=True,
)tuned_churn_model.fit(X_train.drop(columns=["CustomerID"]), y_train)
# Calculate the profit on the test set
original_model_profit = cost_scorer(
original_churn_model, X_test.drop(columns=["CustomerID"]), y_test
)
tuned_model_profit = cost_scorer(
tuned_churn_model, X_test.drop(columns=["CustomerID"]), y_test
)
print(f"Original model profit: {original_model_profit}")
print(f"Tuned model profit: {tuned_model_profit}")
Original model profit: 29640
Tuned model profit: 35600
El beneficio es mayor en el modelo sintonizado que en el original. Nuevamente, podemos trazar la métrica objetiva frente a los umbrales de decisión para visualizar la selección del umbral de decisión en los datos de entrenamiento durante la validación cruzada:
fig, ax = plt.subplots(figsize=(5, 5))
ax.plot(
tuned_churn_model.cv_results_["thresholds"],
tuned_churn_model.cv_results_["scores"],
marker="o",
markersize=3,
linewidth=1e-3,
color="#c0c0c0",
label="Objective score (using cost-matrix)",
)
ax.plot(
tuned_churn_model.best_threshold_,
tuned_churn_model.best_score_,
"^",
markersize=10,
color="#ff6700",
label="Optimal cut-off point for the business metric",
)
ax.legend()
ax.set_xlabel("Decision threshold (probability)")
ax.set_ylabel("Objective score (using cost-matrix)")
ax.set_title("Objective score as a function of the decision threshold")
En realidad, asignar un costo estático a todas las instancias que están mal clasificadas de la misma manera no es realista desde una perspectiva empresarial. Existen métodos más avanzados para ajustar el umbral asignando un peso a cada instancia del conjunto de datos. Esto está cubierto en Ejemplo de aprendizaje sensible a los costos de scikit-learn.
Sintonización bajo restricciones
Este método no está cubierto actualmente en la documentación de scikit-learn, pero es un caso comercial común para casos de uso de clasificación binaria. El método de sintonización bajo restricción encuentra un umbral de decisión identificando un punto en la curva ROC o en la curva de recuperación de precisión. El punto de la curva es el valor máximo de un eje mientras restringe el otro eje. Para este tutorial, utilizaremos el conjunto de datos sobre diabetes de los indios Pima. Esta es una tarea de clasificación binaria para predecir si un individuo tiene diabetes.
Imagine que su modelo se utilizará como prueba de detección para una población de riesgo promedio aplicada a millones de personas. Se estima que hay 38 millones de personas con diabetes en Estados Unidos. Esto representa aproximadamente el 11,6% de la población, por lo que la especificidad del modelo debe ser alta para no diagnosticar erróneamente a millones de personas con diabetes y derivarlas a pruebas de confirmación innecesarias. Supongamos que su director ejecutivo imaginario le ha comunicado que no tolerará más del 2% de tasa de falsos positivos. Construyamos un modelo que logre esto usando TunedThresholdClassifierCV.
Para esta parte del tutorial, definiremos una función de restricción que se utilizará para encontrar la tasa máxima de verdaderos positivos con una tasa de falsos positivos del 2%.
def max_tpr_at_tnr_constraint_score(y_true, y_pred, max_tnr=0.5):
fpr, tpr, thresholds = roc_curve(y_true, y_pred, drop_intermediate=False)
tnr = 1 - fpr
tpr_at_tnr_constraint = tpr[tnr >= max_tnr].max()
return tpr_at_tnr_constraintmax_tpr_at_tnr_scorer = make_scorer(
max_tpr_at_tnr_constraint_score, max_tnr=0.98)
data = pd.read_csv("data/diabetes.csv")
X_train, X_test, y_train, y_test = train_test_split(
data.drop(columns=["Outcome"]),
data["Outcome"],
stratify=data["Outcome"],
test_size=0.2,
random_state=RANDOM_STATE,
)
Construya dos modelos, uno de regresión logística que sirva como modelo de referencia y el otro, TunedThresholdClassifierCV que envolverá el modelo de regresión logística de referencia para lograr el objetivo trazado por el CEO. En el modelo sintonizado, establezca scoring=max_tpr_at_tnr_scorer. Nuevamente, la elección del modelo y el preprocesamiento no es importante para este tutorial.
# A baseline model
original_model = make_pipeline(
StandardScaler(), LogisticRegression(random_state=RANDOM_STATE)
)
original_model.fit(X_train, y_train)# A tuned model
tuned_model = TunedThresholdClassifierCV(
original_model,
thresholds=np.linspace(0, 1, 150),
scoring=max_tpr_at_tnr_scorer,
store_cv_results=True,
cv=8,
random_state=RANDOM_STATE,
)
tuned_model.fit(X_train, y_train)
Compare la diferencia entre el umbral de decisión predeterminado de los estimadores de scikit-learn, 0,5, y uno encontrado utilizando el enfoque de ajuste bajo restricción en la curva ROC.
# Get the fpr and tpr of the original model
original_model_proba = original_model.predict_proba(X_test)[:, 1]
fpr, tpr, thresholds = roc_curve(y_test, original_model_proba)
closest_threshold_to_05 = (np.abs(thresholds - 0.5)).argmin()
fpr_orig = fpr[closest_threshold_to_05]
tpr_orig = tpr[closest_threshold_to_05]# Get the tnr and tpr of the tuned model
max_tpr = tuned_model.best_score_
constrained_tnr = 0.98
# Plot the ROC curve and compare the default threshold to the tuned threshold
fig, ax = plt.subplots(figsize=(5, 5))
# Note that this will be the same for both models
disp = RocCurveDisplay.from_estimator(
original_model,
X_test,
y_test,
name="Logistic Regression",
color="#c0c0c0",
linewidth=2,
ax=ax,
)
disp.ax_.plot(
1 - constrained_tnr,
max_tpr,
label=f"Tuned threshold: {tuned_model.best_threshold_:.2f}",
color="#ff6700",
linestyle="--",
marker="o",
markersize=11,
)
disp.ax_.plot(
fpr_orig,
tpr_orig,
label="Default threshold: 0.5",
color="#004e98",
linestyle="--",
marker="X",
markersize=11,
)
disp.ax_.set_ylabel("True Positive Rate", fontsize=8)
disp.ax_.set_xlabel("False Positive Rate", fontsize=8)
disp.ax_.tick_params(labelsize=8)
disp.ax_.legend(fontsize=7)
El método ajustado bajo restricción encontró un umbral de 0,80, lo que resultó en una sensibilidad promedio del 19,2% durante la validación cruzada de los datos de entrenamiento. Compare la sensibilidad y la especificidad para ver cómo se mantiene el umbral en el conjunto de prueba. ¿El modelo cumplió con el requisito de especificidad del CEO en el conjunto de pruebas?
# Average sensitivity and specificity on the training set
avg_sensitivity_train = tuned_model.best_score_# Call predict from tuned_model to calculate sensitivity and specificity on the test set
specificity_test = recall_score(
y_test, tuned_model.predict(X_test), pos_label=0)
sensitivity_test = recall_score(y_test, tuned_model.predict(X_test))
print(f"Average sensitivity on the training set: {avg_sensitivity_train:.3f}")
print(f"Sensitivity on the test set: {sensitivity_test:.3f}")
print(f"Specificity on the test set: {specificity_test:.3f}")
Average sensitivity on the training set: 0.192
Sensitivity on the test set: 0.148
Specificity on the test set: 0.990
Conclusión
El nuevo TunedThresholdClassifierCV La clase es una herramienta poderosa que puede ayudarlo a convertirse en un mejor científico de datos al compartir con los líderes empresariales cómo llegó a un umbral de decisión. Aprendiste a usar el nuevo scikit-learn. TunedThresholdClassifierCV clase para maximizar una métrica, realizar un aprendizaje sensible a los costos y ajustar una métrica bajo restricción. Este tutorial no pretende ser completo ni avanzado. Quería presentar la nueva característica y resaltar su poder y flexibilidad para resolver problemas de clasificación binaria. Consulte la documentación, la guía del usuario y los ejemplos de scikit-learn para obtener ejemplos de uso completos.
Un gran saludo a Guillaume Lemaitre por su trabajo en esta función.
Gracias por leer. Feliz sintonización.
Licencias de datos:
Fraude con tarjetas de crédito: DbCL
Diabetes de los indios pima: CC0
Rotación de telecomunicaciones: uso comercial correcto