Los usuarios del sector bancario son especialmente vulnerables a los fraudes debido a factores como la delincuencia local, robo de datos por ciberataques, filtraciones de información o incluso la clonación de tarjetas. Cuando un delincuente obtiene nuestros datos, puede realizar compras o retiros de dinero que impactan directamente nuestras finanzas.
Afortunadamente, muchas de estas actividades fraudulentas son detectadas y bloqueadas a tiempo por los sistemas bancarios gracias a la identificación de patrones sospechosos o anomalías en el comportamiento del usuario. ¿Cómo logran hacer esto? La respuesta radica en el uso de algoritmos de machine learning. Desde técnicas clásicas como la regresión logística o los clasificadores basados en árboles de decisión, hasta avanzados modelos de deep learning, estos algoritmos son la clave para prevenir fraudes de manera eficiente.
Sin embargo, la implementación de un modelo de detección de fraudes no se reduce simplemente a aplicar un algoritmo sobre los datos. Hay una serie de pasos fundamentales que debemos considerar:
Identificación de factores relevantes: Es crucial analizar qué características podrían indicar actividades fraudulentas. Estas pueden incluir patrones en las transacciones, dispositivos utilizados para iniciar sesión, cambios frecuentes de contraseña o accesos desde ubicaciones inusuales, entre otros.
Construcción de un historial significativo: Si no contamos con datos históricos de fraudes, será necesario recopilar información suficiente para que el modelo pueda aprender patrones generales y no simplemente memorizar datos. Esto ayuda a evitar problemas como underfitting o overfitting. Además, entrenar un modelo basado en datos de otra empresa probablemente no funcionará debido a diferencias en los contextos y patrones de comportamiento.
Limpieza y procesamiento de datos: Entrenar un modelo sin un adecuado procesamiento de los datos es un error común. Dividir el dataset en conjuntos de entrenamiento y prueba no es suficiente si los datos contienen errores, valores atípicos o información incompleta. Recuerda que tu modelo será implementado en un entorno real; un modelo poco confiable no solo puede causar pérdidas económicas a la empresa, sino también afectar directamente a los usuarios.
Selección del algoritmo adecuado: La complejidad no siempre es sinónimo de calidad. Evalúa las diferentes opciones y considera que, en algunos casos, un algoritmo más simple puede ser más eficiente y efectivo para tu problema.
Evaluación con métricas variadas: No te quedes únicamente con la precisión del modelo. Dependiendo del caso, métricas como el recall, la F1-score o la matriz de confusión pueden ofrecer una mejor comprensión del rendimiento y ayudarte a evitar errores críticos. A veces, un modelo con alta precisión no es necesariamente el más adecuado.
Veamos el caso que trataremos aquí: un dataset obtenido de Kaggle. Este conjunto de datos incluye transacciones realizadas durante dos días y tiene las siguientes características clave:
Tamaño y balance de clases:
Total de transacciones: 284,807.
Transacciones clasificadas como fraude: 492.
La clase de fraude representa solo el 0.17% del total, lo que evidencia un conjunto de datos muy desequilibrado. Esto es común en problemas de detección de fraude, ya que los casos fraudulentos son poco frecuentes.
Columnas principales:
- V1 a V28: Estas columnas son el resultado de un análisis de componentes principales (PCA) para reducir la dimensionalidad. Además, el PCA se utilizó para ocultar información sensible, permitiendo que los científicos de datos las utilicen de manera segura para experimentar y mejorar sus habilidades.
- Time: Representa los segundos transcurridos entre cada transacción y la primera registrada en el dataset.
- Amount: Representa la cantidad de dinero que registró la transacción.
- Class: Es nuestra variable objetivo. Toma el valor 1 para transacciones fraudulentas y 0 en caso contrario. Como podemos ver, el desequilibrio en las clases será un desafío importante al entrenar modelos de machine learning, ya que puede llevar a que los algoritmos ignoren los casos minoritarios si no se manejan adecuadamente.
Link del dataset: https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud
1. Carga de datos y primer vistazo.
Importamos las liberías necesarias, cargamos nuestro dataset y visualizamos las 10 primeras filas.
#Importar liberías
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import make_pipeline
from sklearn.metrics import (classification_report, confusion_matrix,
precision_score, recall_score,
f1_score, precision_recall_curve)
from sklearn.model_selection import KFold, train_test_split, GridSearchCV, RandomizedSearchCV
from imblearn.over_sampling import SMOTE
from collections import Counter
#Cargar el dataset y mostrarlo
df = pd.read_csv('archivo.csv')
df.head(10)
Time | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 | V16 | V17 | V18 | V19 | V20 | V21 | V22 | V23 | V24 | V25 | V26 | V27 | V28 | Amount | Class | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0 | -1.359807 | -0.072781 | 2.536347 | 1.378155 | -0.338321 | 0.462388 | 0.239599 | 0.098698 | 0.363787 | 0.090794 | -0.551600 | -0.617801 | -0.991390 | -0.311169 | 1.468177 | -0.470401 | 0.207971 | 0.025791 | 0.403993 | 0.251412 | -0.018307 | 0.277838 | -0.110474 | 0.066928 | 0.128539 | -0.189115 | 0.133558 | -0.021053 | 149.62 | 0 |
1 | 0.0 | 1.191857 | 0.266151 | 0.166480 | 0.448154 | 0.060018 | -0.082361 | -0.078803 | 0.085102 | -0.255425 | -0.166974 | 1.612727 | 1.065235 | 0.489095 | -0.143772 | 0.635558 | 0.463917 | -0.114805 | -0.183361 | -0.145783 | -0.069083 | -0.225775 | -0.638672 | 0.101288 | -0.339846 | 0.167170 | 0.125895 | -0.008983 | 0.014724 | 2.69 | 0 |
2 | 1.0 | -1.358354 | -1.340163 | 1.773209 | 0.379780 | -0.503198 | 1.800499 | 0.791461 | 0.247676 | -1.514654 | 0.207643 | 0.624501 | 0.066084 | 0.717293 | -0.165946 | 2.345865 | -2.890083 | 1.109969 | -0.121359 | -2.261857 | 0.524980 | 0.247998 | 0.771679 | 0.909412 | -0.689281 | -0.327642 | -0.139097 | -0.055353 | -0.059752 | 378.66 | 0 |
3 | 1.0 | -0.966272 | -0.185226 | 1.792993 | -0.863291 | -0.010309 | 1.247203 | 0.237609 | 0.377436 | -1.387024 | -0.054952 | -0.226487 | 0.178228 | 0.507757 | -0.287924 | -0.631418 | -1.059647 | -0.684093 | 1.965775 | -1.232622 | -0.208038 | -0.108300 | 0.005274 | -0.190321 | -1.175575 | 0.647376 | -0.221929 | 0.062723 | 0.061458 | 123.50 | 0 |
4 | 2.0 | -1.158233 | 0.877737 | 1.548718 | 0.403034 | -0.407193 | 0.095921 | 0.592941 | -0.270533 | 0.817739 | 0.753074 | -0.822843 | 0.538196 | 1.345852 | -1.119670 | 0.175121 | -0.451449 | -0.237033 | -0.038195 | 0.803487 | 0.408542 | -0.009431 | 0.798278 | -0.137458 | 0.141267 | -0.206010 | 0.502292 | 0.219422 | 0.215153 | 69.99 | 0 |
5 | 2.0 | -0.425966 | 0.960523 | 1.141109 | -0.168252 | 0.420987 | -0.029728 | 0.476201 | 0.260314 | -0.568671 | -0.371407 | 1.341262 | 0.359894 | -0.358091 | -0.137134 | 0.517617 | 0.401726 | -0.058133 | 0.068653 | -0.033194 | 0.084968 | -0.208254 | -0.559825 | -0.026398 | -0.371427 | -0.232794 | 0.105915 | 0.253844 | 0.081080 | 3.67 | 0 |
6 | 4.0 | 1.229658 | 0.141004 | 0.045371 | 1.202613 | 0.191881 | 0.272708 | -0.005159 | 0.081213 | 0.464960 | -0.099254 | -1.416907 | -0.153826 | -0.751063 | 0.167372 | 0.050144 | -0.443587 | 0.002821 | -0.611987 | -0.045575 | -0.219633 | -0.167716 | -0.270710 | -0.154104 | -0.780055 | 0.750137 | -0.257237 | 0.034507 | 0.005168 | 4.99 | 0 |
7 | 7.0 | -0.644269 | 1.417964 | 1.074380 | -0.492199 | 0.948934 | 0.428118 | 1.120631 | -3.807864 | 0.615375 | 1.249376 | -0.619468 | 0.291474 | 1.757964 | -1.323865 | 0.686133 | -0.076127 | -1.222127 | -0.358222 | 0.324505 | -0.156742 | 1.943465 | -1.015455 | 0.057504 | -0.649709 | -0.415267 | -0.051634 | -1.206921 | -1.085339 | 40.80 | 0 |
8 | 7.0 | -0.894286 | 0.286157 | -0.113192 | -0.271526 | 2.669599 | 3.721818 | 0.370145 | 0.851084 | -0.392048 | -0.410430 | -0.705117 | -0.110452 | -0.286254 | 0.074355 | -0.328783 | -0.210077 | -0.499768 | 0.118765 | 0.570328 | 0.052736 | -0.073425 | -0.268092 | -0.204233 | 1.011592 | 0.373205 | -0.384157 | 0.011747 | 0.142404 | 93.20 | 0 |
9 | 9.0 | -0.338262 | 1.119593 | 1.044367 | -0.222187 | 0.499361 | -0.246761 | 0.651583 | 0.069539 | -0.736727 | -0.366846 | 1.017614 | 0.836390 | 1.006844 | -0.443523 | 0.150219 | 0.739453 | -0.540980 | 0.476677 | 0.451773 | 0.203711 | -0.246914 | -0.633753 | -0.120794 | -0.385050 | -0.069733 | 0.094199 | 0.246219 | 0.083076 | 3.68 | 0 |
Corroboramos que no tenga algún dato nulo.
df.isna().sum()
Time | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 | V12 | V13 | V14 | V15 | V16 | V17 | V18 | V19 | V20 | V21 | V22 | V23 | V24 | V25 | V26 | V27 | V28 | Amount | Class | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
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 |
Al no tener datos nulos, continuaremos con visualizar el tipo de elementos que tiene cada columna.
RangeIndex: 284807 entries, 0 to 284806 Data columns (total 31 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Time 284807 non-null float64 1 V1 284807 non-null float64 2 V2 284807 non-null float64 3 V3 284807 non-null float64 4 V4 284807 non-null float64 5 V5 284807 non-null float64 6 V6 284807 non-null float64 7 V7 284807 non-null float64 8 V8 284807 non-null float64 9 V9 284807 non-null float64 10 V10 284807 non-null float64 11 V11 284807 non-null float64 12 V12 284807 non-null float64 13 V13 284807 non-null float64 14 V14 284807 non-null float64 15 V15 284807 non-null float64 16 V16 284807 non-null float64 17 V17 284807 non-null float64 18 V18 284807 non-null float64 19 V19 284807 non-null float64 20 V20 284807 non-null float64 21 V21 284807 non-null float64 22 V22 284807 non-null float64 23 V23 284807 non-null float64 24 V24 284807 non-null float64 25 V25 284807 non-null float64 26 V26 284807 non-null float64 27 V27 284807 non-null float64 28 V28 284807 non-null float64 29 Amount 284807 non-null float64 30 Class 284807 non-null int64 dtypes: float64(30), int64(1) memory usage: 67.4 MB
Observando el resultado anterior podemos notar que todos tipos de datos son los que les corresponden, así que no habría que corregir nada aquí.
2. Análisis Exploratorio de Datos (EDA)
Aquí principalmente implementaremos visualizaciones para poder analizar y entender el comportamiento y distribución de nuestros datos.
plt.figure(figsize=(8,6))
ax = sns.countplot(x='Class',data=df,palette={0:'gray',1:'r'})
plt.xticks(ticks=[0,1],labels=['No Fraude','Fraude'])
# Añadir porcentajes
total = len(df)
for p in ax.patches:
altura = p.get_height()
porcentaje = altura / total * 100
ax.annotate(f"{porcentaje:.2f}%", (p.get_x() + p.get_width()/2, altura),
ha='center', va='center', xytext=(0,10), textcoords='offset points')
plt.title("Cantidad de Fraude y No Fraude en el Dataset",color='blue',size=16)
plt.show()
Podemos notar que tenemos una dataset desbalancedo la clase objetivo Fraude representa apenas un 0.17% de todo el conjunto de datos, esto será un reto importante para nuestro modelo.
# Graficar puntos para cada clase con diferente.
sns.scatterplot(x='Amount', y='Time', data=df[df['Class'] == 0], color='gray', alpha=0.5, label='No Fraude')
sns.scatterplot(x='Amount', y='Time', data=df[df['Class'] == 1], color='red', alpha=1, label='Fraude')
plt.title("Tiempo vs Cantidad de Dinero",color='blue',size=16)
plt.show()
Con este gráfico de dispersión, podemos observar con más claridad la diferencia entre las dos clases. Además, notamos que la clase 'Fraude' tiende a tener montos relativamente bajos, generalmente entre 0$ y 3000$. Esto se debe a que transacciones con montos excesivamente altos activarían un aviso inmediato de fraude.
A partir de aquí podemos incluir los gráficos que creamos convenientes para mejorar nuestro entendimiento de los datos, como un boxplot de Amount para ver como oscila, entre otros.
3. Aplicando algoritmos de clasificación
Utilizaré principalmente dos algoritmos con el objetivo de obtener una visión más amplia de los resultados y realizar una comparativa entre ambos modelos. Además, aplicaremos SMOTE (Synthetic Minority Over-Sampling Technique), una técnica de sobremuestreo que genera nuevas instancias sintéticas para la clase minoritaria, en lugar de simplemente duplicarlas, lo cual podría introducir sesgos o errores.
¿Cómo funciona SMOTE?
- Encuentra los vecinos cercanos de una instancia de la clase minoritaria.
- Genera nuevas instancias sintéticas ubicadas en el punto medio de la línea que conecta la instancia original con uno de sus vecinos más cercanos.
- Crea un nuevo punto tomando una proporción aleatoria de las características de la instancia original y su vecino más cercano.
- Repite el proceso hasta lograr un balance entre las clases.
3.1. SMOTE y Logistic Regression
- Dividiremos el conjunto de datos en entrenamiento y testing para evitar el overfitting y tener un conjunto de datos con que validar nuestro modelo.
X = np.array(df.drop(columns=['Class']))
y = np.array(df['Class'])
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=123,stratify=y)
Si te has fijado en el código, he incluido la línea de código stratify=y. Esto hace que la proporción de la clase 0 (No Fraude) y la clase 1 (Fraude) en el conjunto de datos original se preserve tanto en el conjunto de entrenamiento como en el de prueba. Es decir, al dividir el conjunto de datos en estos dos subconjuntos, nos aseguramos de que ambos mantengan las proporciones de cada clase, evitando que alguno de los conjuntos carezca de ejemplos de alguna clase, conservando la proporción de cómo se distribuyen los datos, es decir si nuestro conjunto de datos contiene una distribución de 99% para clase 0
y 1% para la clase 1
esta misma proporción se mantendrá en train y test.
#from imblearn import SMOTE
smote = SMOTE(random_state=123)
X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)
#from collections import Counter
# Mostrar el balance de clases antes y después de aplicar SMOTE
print('Distribución original de clases:', Counter(y_train))
print('Distribución después de SMOTE:', Counter(y_train_resampled))
Ahora que tenemos nuestra clase balanceada veamos como se comporta un modelo de Regresión Logística.
model = LogisticRegression(max_iter=1000)
model.fit(X_train_resampled,y_train_resampled)
y_pred = model.predict(X_test)
Ya que entrenamos el modelo con el dataset resampleado, gráfiquemos una matriz de confunsion para poder visualizar mejor los resultados.
def matriz_de_confusion(y_true, y_pred, title, detection):
from sklearn.metrics import accuracy_score
from sklearn.metrics import ConfusionMatrixDisplay
""" Visualiza la matriz de confusión """
matriz = confusion_matrix(y_true, y_pred)
accuracy = accuracy_score(y_true, y_pred)
#Código de matplotlib para graficar
plt.figure(figsize=(4, 4))
cm_display = ConfusionMatrixDisplay(matriz).plot()
matriz = pd.DataFrame(matriz,
columns=[f"No (0): {detection}", f"Si (1): {detection}"])
#plt.matshow(matriz, cmap="Blues", vmin=0, vmax=20, fignum=1)
plt.xticks(range(len(matriz.columns)), matriz.columns, rotation=45)
plt.yticks(range(len(matriz.columns)), matriz.columns)
etiquetas = (("Verdaderos\nnegativos", "Falsos\nnegativos"),
("Falsos\npositivos", "Verdaderos\npositivos"))
plt.text(2.45, -0.2, title, fontsize=25, c="red")
plt.text(2.25, 0.10, "Accuracy: %0.2f" % accuracy, fontsize=20)
for i in range(len(matriz.columns)):
for j in range(len(matriz.columns)):
#plt.text(i, j + 0.14, str(matriz.iloc[j, i]),
#fontsize=20, ha="center", va="center")
plt.text(i, j - 0.25, etiquetas[i][j],
fontsize=11.5, ha="center", va="center")
plt.show()
Al parecer nuestro modelo ha conseguido un Accuracy (Exactitud) muy buena de 99% pero ¿El modelo estará correcto?
Podemos entrar más a profundidad revisando las métricas de precision, recall y f1-score.
Antes de pasar al detalle de la tabla de reporte, me gustaría dar unos tips de como entender las metricas de accuracy, presicion, recall y el F1-Score.
(TN - True negative) (TP - True Positive)
- Accuracy: Accuracy o Exactitud es la capacidad de nuestro modelo para identificar los valores positivos o negativos, es decir mide la proporción total de predicciones correctas.
- Precision: La Presicion responde a la siguiente pregunta: De todos los positivos predichos por el modelo ¿Cuántos realmente eran positivos?.
- Recall: De todos los positivos (predichos y no predichos) ¿Cuántos logramos identificar?
- F1-Score: Es la media armónica entre la precisión y el recall. Se utiliza cuando se necesita un equilibrio entre estas dos métricas y es especialmente útil cuando las clases están desbalanceadas.
Una vez aclarado esto, veamos el resultado del reporte de clasificación de nuestro de modelo de regresión logistica con SMOTE.
report = classification_report(y_test,y_pred,target_names={0:'Normal',1:'Fraude'},output_dict=True)
report_df = pd.DataFrame(report).transpose()
report_df
precision | recall | f1-score | support | |
---|---|---|---|---|
0 | 0.999727 | 0.985990 | 0.992811 | 85295.000000 |
1 | 0.094697 | 0.844595 | 0.170300 | 148.000000 |
accuracy | 0.985745 | 0.985745 | 0.985745 | 0.985745 |
macro avg | 0.547212 | 0.915292 | 0.581555 | 85443.000000 |
weighted avg | 0.998159 | 0.985745 | 0.991386 | 85443.000000 |
Precisión de la clase 1 (Fraude) baja: La precisión mide la proporción de predicciones positivas correctas (fraudes detectados) sobre el total de predicciones positivas. Tengo 1,195 falsos positivos y solo 125 verdaderos positivos, lo que hace que la precisión sea baja: 9%.
Recall de la clase 1 (Fraude) alto: El recall mide la proporción de fraudes correctamente detectados sobre el total de fraudes reales: Esto significa que nuestro modelo está logrando capturar la mayoría de los fraudes, pero a costa de predecir muchos falsos positivos.
Aunque el modelo tenga un accuracy alto y un recall elevado (lo que indica una buena capacidad para identificar los fraudes reales), también está generando una gran cantidad de falsos positivos, es decir, está catalogando transacciones que no son fraudulentas como si lo fueran.
¿Cómo podría perjudicar este resultado?
Este tipo de errores puede tener un impacto directo tanto en el usuario como en la entidad bancaria. Cuando el modelo clasifica incorrectamente una transacción legítima como fraude, el banco podría bloquear la tarjeta del usuario. Esto interrumpe su capacidad para realizar compras hasta que se comunique con el banco, aclare la situación y, en muchos casos, responda a preguntas de seguridad. Este proceso es costoso en términos de tiempo, recursos humanos y logísticos. Además, genera incomodidad en los usuarios, lo que puede llevar a pérdidas de fidelización y de ingresos para el banco. Por estos motivos, sería recomendable descartar el modelo de regresión logística.
Antes de pasar a lo siguiente veamos un gráfico más.
#from sklearn.metrics import roc_auc_score, roc_curve
y_proba = model.predict_proba(X_test)[:,1]
fpr, tpr, _ = roc_curve(y_test,y_proba)
auc = roc_auc_score(y_test,y_proba)
plt.plot([0,1],[0,1],'k--')
plt.plot(fpr,tpr,label=f"ROC Curve (AUC = {auc:.2f})")
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Curva ROC',color='blue',size=16)
plt.legend(loc='best')
AUC-SCORE: La puntuación AUC oscila entre 0 y 1, donde una puntuación cercana a 1 indica un rendimiento excelente del modelo, 0,5 sugiere una adivinación aleatoria, y una puntuación cercana a 0 significa un rendimiento deficiente.
El modelo tiene un AUC muy bueno (0.96), lo que significa que es excelente a la hora de separar las dos clases (fraude y no fraude) basándose en las probabilidades. Sin embargo, el F1-Score para la clase 1 (fraude) es muy bajo (0.17), mientras que para la clase 0 (no fraude) es casi perfecto (0.99). Esto sugiere que, aunque el modelo puede identificar bien la clase mayoritaria (no fraude), no está funcionando bien para detectar fraudes.
3.2. Logistic Regression Balanced
El algoritmo de regresión logística también ofrece un parámetro llamado class_weight
, que nos permite asignar pesos a las clases de manera que la clase minoritaria no pierda protagonismo durante el proceso de entrenamiento. Esto es especialmente útil cuando estamos trabajando con conjuntos de datos desbalanceados, como en el caso de la detección de fraude, donde la clase fraude (minoritaria) podría ser ignorada por el modelo si no se ajusta adecuadamente. Al dar un peso mayor a la clase minoritaria, ayudamos a que el modelo preste más atención a esos casos, lo que mejora la capacidad de identificar fraudes sin que el modelo esté sesgado hacia la clase mayoritaria.
A continuación aplicaremos dicho parámetro además de estandarizar nuestras variables, esto no es estrictamente necesario sin embargo es altamente recomendable cuando tenemos variables muy diferentes de escala entre sí, mejorando la estabilidad del modelo.
#from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
#Aqui ya no usamos el dataset balanceado con SMOTE
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
model = LogisticRegression(max_iter=1000, class_weight='balanced')
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
matriz_de_confusion(y_test,y_pred,"Balanced",'Fraude')
report = classification_report(y_test,y_pred,target_names={0:'Normal',1:'Fraude'},output_dict=True)
report_df = pd.DataFrame(report).transpose()
report_df
precision | recall | f1-score | support | |
---|---|---|---|---|
0 | 0.999749 | 0.979741 | 0.989644 | 85295.00000 |
1 | 0.068464 | 0.858108 | 0.126810 | 148.00000 |
accuracy | 0.979530 | 0.979530 | 0.979530 | 0.97953 |
macro avg | 0.534106 | 0.918925 | 0.558227 | 85443.00000 |
weighted avg | 0.998136 | 0.979530 | 0.988149 | 85443.00000 |
Como podemos notar en el reporte y en la matriz de confusión nuestro modelo no ha mejorado mucho, de hecho a pesar de aumentar nuestro verdaderos positivos, las datos clasificados como verdaderos negativos han aumentado y el F1-Score de la clase Fraude ha disminuido.
3.3. Random Forest Classifier
El Random Forest Classifier es un algoritmo de aprendizaje supervisado que utiliza múltiples árboles de decisión para hacer predicciones. Cada árbol es entrenado con un subconjunto aleatorio de los datos y las características, y luego sus predicciones se combinan mediante una votación mayoritaria para determinar la clase final.
¿Cómo funciona?
Entrenamiento: Se crean varios árboles de decisión con subconjuntos aleatorios de datos y características.
Predicción: Cada árbol emite una predicción, y la clase final se elige por mayoría de votos.
Votación: El resultado es más robusto y preciso que el de un solo árbol de decisión.
Ventajas:
- Menos propenso a sobreajuste (overfitting).
- Robusto frente a datos ruidosos.
- Manejo eficiente de datos desbalanceados.
No solo me limitaré a entrenar el modelo, sino que lo haremos con RandomizedSearchCV, que es una técnica de optimización de hiperparámetros que realiza una búsqueda aleatoria dentro de un espacio de posibles valores para encontrar la mejor combinación de parámetros. A diferencia de GridSearchCV, que prueba todas las combinaciones posibles, RandomizedSearchCV selecciona aleatoriamente un número de combinaciones, lo que puede ser más eficiente cuando el espacio de búsqueda es muy grande.
Este método recibe parámetros como:
- estimator: El modelo que se desea optimizar (en este caso, el clasificador RandomForest).
- param_distributions: Un diccionario que define los rangos de los hiperparámetros que se desean explorar.
- n_iter: El número de combinaciones aleatorias que se probarán.
- scoring: La métrica que se usará para evaluar el rendimiento del modelo.
- cv: Número de particiones para la validación cruzada.
El uso de RandomizedSearchCV nos permite encontrar rápidamente una combinación óptima de parámetros, mejorando el rendimiento del modelo sin la necesidad de hacer una búsqueda exhaustiva.
Por si no recuerdas como funciona Cross Validation (CV):
Cross-Validation (CV) es una técnica de validación en aprendizaje automático que evalúa el rendimiento de un modelo dividiendo el conjunto de datos en varios subconjuntos o folds. En k-fold cross-validation, los datos se dividen en k grupos, y el modelo se entrena con k-1 de estos grupos, evaluándolo en el grupo restante. Este proceso se repite k veces, utilizando cada grupo como conjunto de prueba una vez. Al final, se promedian los resultados de todas las iteraciones para obtener una evaluación más precisa y generalizada del modelo.
Veamos el código de nuestro modelo.
import time
pipeline = make_pipeline(RandomForestClassifier())
param_grid = {
'randomforestclassifier__n_estimators': [100, 200, 300],
'randomforestclassifier__max_depth': [None, 10, 20, 30],
'randomforestclassifier__min_samples_split': [2, 5, 10],
'randomforestclassifier__min_samples_leaf': [1, 2, 4],
'randomforestclassifier__class_weight': ['balanced'],
'randomforestclassifier__criterion': ['gini', 'entropy']
}
start_time = time.time()
random_search = RandomizedSearchCV(estimator=pipeline, param_distributions=param_grid, n_iter=10, cv=5, n_jobs=-1, verbose=3, random_state=123)
random_search.fit(X_train,y_train)
# Calcular el tiempo total
total_time = time.time() - start_time
Explicación de los parámetros:
- n_estimators: Número de árboles en el bosque.
- max_depth: Profundidad máxima de los árboles. None indica sin límite.
- min_samples_split: Número mínimo de muestras necesarias para dividir un nodo.
- min_samples_leaf: Número mínimo de muestras que debe tener una hoja.
- class_weight: Ajusta el peso de las clases. 'balanced' ayuda a manejar datos desbalanceados.
- criterion: Función para medir la calidad de la división (Gini o Entropía).
He incluido una línea de código start_time, para poder calcular el tiempo que tarda RandomSearchCV en aplicar todas las combinaciones de hiperparámetros dados.
print(f"Tiempo total: {total_time:.2f} segundos")
Tiempo total: 1710.29 segundos = 28.5 minutos
# Obtener el mejor modelo y los mejores parámetros
best_model = random_search.best_estimator_
best_params = random_search.best_params_
print("Mejores parámetros:", best_params)
print(f'Mejor score: {random_search.best_score_}')
Mejor score: 0.9995736448174799
Ahora que tenemos un modelo más robusto aplicando RandomSearchCV y los hiperparámetros porque no entrenamos un modelo con los mejores parámetros encontrados.
4. Entrenando con los mejores parámetros
Aquí serviría de mucho dejar en claro la diferencia entre usar gini o entropía como criterion.
-
Índice de Gini: Mide la impureza de un nodo en un árbol de decisión. Penaliza las mezclas de clases, ya que un valor de Gini más alto indica que las instancias en el nodo están más mezcladas entre diferentes clases. Un Gini de 0 significa que el nodo es puro (todas las instancias son de la misma clase).
- Ventajas: Es computacionalmente más simple que la entropía. Tiende a funcionar bien en la práctica para muchos problemas de clasificación
-
Entropía: Mide la incertidumbre o impureza de un nodo. Penaliza la mezcla de clases en función de la cantidad de información necesaria para describir la clase. Un valor de entropía más alto indica mayor mezcla de clases, y un valor de 0 significa que el nodo es puro.
- Ventajas: Tiene una interpretación más directa en términos de teoría de la información. Puede ofrecer mejores resultados en algunos problemas, especialmente cuando se busca una separación más fina entre clases.
y_pred = best_model.predict(X_test)
matriz_de_confusion(y_test,y_pred,'Best Params','Fraude')
Con la visualización de nuestra matriz de confusion podemos notar una clara mejora en clasificación de nuestra clase Fraude y No Fraude.
Veamos el reporte para más detalle.
report = classification_report(y_test,y_pred,target_names=['No Fraude','Fraude'],output_dict=True)
report_df = pd.DataFrame(report).transpose()
report_df
precision | recall | f1-score | support | |
---|---|---|---|---|
No Fraude | 0.999508 | 0.999953 | 0.999730 | 85295.000000 |
Fraude | 0.963636 | 0.716216 | 0.821705 | 148.000000 |
accuracy | 0.999462 | 0.999462 | 0.999462 | 0.999462 |
macro avg | 0.981572 | 0.858085 | 0.910718 | 85443.000000 |
weighted avg | 0.999446 | 0.999462 | 0.999422 | 85443.000000 |
CLASE NO FRAUDE
- Precision: 0.9995 → Muy alta, lo que significa que casi todas las predicciones como "No Fraude" fueron correctas.
- Recall: 0.9996 → También muy alto, indicando que el modelo captura casi todos los casos verdaderos de "No Fraude".
CLASE FRAUDE
- Precision: 0.9636 → Muy buena para una clase minoritaria, lo que significa que la mayoría de las predicciones etiquetadas como "Fraude" son correctas, pasamos de valores como 0.09 a 0.9636.
- Recall: 0.7162 → No es tan alto como la precisión, lo que indica que el modelo pierde algunos casos de fraude (falsos negativos).
- F1-score: 0.8217 → Es un buen equilibrio entre precisión y recall, pero esto sugiere que aún hay margen de mejora en la captura de fraudes (recall).
Veamos como se comporta nuestra curva ROC.
y_pred_proba = best_model.predict_proba(X_test)[:,1]
fpr, tpr, _ = roc_curve(y_test,y_pred_proba)
auc = roc_auc_score(y_test,y_pred_proba)
plt.plot([0,1],[0,1],'k--')
plt.plot(fpr,tpr,label=f"ROC CURVE (AUC): {auc:.2f}")
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Curva ROC')
plt.legend(loc='best')
plt.show()
Tenemos un alto AUC Score 0.93 lo cual indica una muy buena capacidad para separar clases.
4.1. Ajustar el Umbral de clasificación (Clase 1:Fraude)
Para finaliza realizare un ajuste en cuanto a los umbrales para clasificar los datos como fraude, como "estandar" siempre colocamos un 50% o 0.5, pero podemos ir variandolo para ver como cambian las metricas de evaluación del modelo.
#from sklearn.metrics import precision_score, recall_score, f1_score
thresholds = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] # Diferentes umbrales para probar
for threshold in thresholds:
y_pred_adjusted = (y_pred_proba >= threshold).astype(int) # Ajustar el umbral
precision = precision_score(y_test, y_pred_adjusted)
recall = recall_score(y_test, y_pred_adjusted)
f1 = f1_score(y_test, y_pred_adjusted)
print(f"Threshold: {threshold}, Precision: {precision:.4f}, Recall: {recall:.4f}, F1: {f1:.4f}")
Si observamos los datos a medida que cambia el threshold, precision, recall y f1-score tambien lo hacen. Veamoslo con un gráfico para tenerlo más claro.
#from sklearn.metrics import precision_recall_curve
precision, recall, thresholds = precision_recall_curve(y_test, y_pred_proba)
plt.plot(thresholds, precision[:-1], 'b--', label='Precision')
plt.plot(thresholds, recall[:-1], 'g-', label='Recall')
plt.xlabel('Threshold')
plt.legend(loc='best')
plt.title('Precision-Recall vs Threshold')
plt.show()
Como vemos tenemos dos líneas que van tomando diferentes valores según el threshold dado, estás líneas son de las métricas de nuestro mayor interés recall y precision, para elegir un threshold podriamos simplemente elegir el punto de intersección, sin embargo hay algunas cosas que debemos de considerar las cual veremos a detalle en las conclusiones.
Sin embargo me gustaría decir que la elección de un modelo que equilibre precisión y recall es crucial en un entorno bancario, ya que un modelo que clasifica correctamente las transacciones fraudulentas (alto recall) reduce las pérdidas económicas por fraude y protege al cliente. Al mismo tiempo, minimizar los falsos positivos es esencial para evitar la incomodidad de bloquear transacciones legítimas, lo que puede llevar a la insatisfacción del cliente y pérdidas de fidelización. Random Forest, al ofrecer una buena combinación de estas métricas, se presenta como una herramienta eficaz para este tipo de problemas, alineándose con los objetivos del negocio: proteger a los clientes y optimizar los recursos operativos.
5. Conclusiones finales
El objetivo principal es maximizar la detección de fraudes (priorizando el recall), un umbral alrededor de 0.1 podría ser una buena elección. Aunque la precisión es relativamente baja (80%), el recall es el más alto (81.08%), capturando la mayoría de los fraudes a costa de aumentar los falsos positivos, teniendo el cuenta que es "mejor" clasificar una transacción como fraudulenta cuando no lo es, que clasificar una transacción como no fraudulenta cuando realmente lo es.
Para un balance entre precisión y recall, el umbral de 0.2 o 0.3 parece proporcionar un buen equilibrio. Con un F1-score de aproximadamente 0.83 y una precisión por encima del 89%, estos umbrales podrían ser más apropiados si se desea mantener una buena precisión sin sacrificar demasiado el recall.
Deberíamos de evaluar el impacto operativo de los falsos positivos en nuestra aplicación. Si los costos asociados con los falsos positivos son manejables, se podría optar por un umbral más bajo para asegurar una detección más exhaustiva de fraudes.
Como paso final se puede probar estos umbrales en una fase piloto para observar el impacto práctico de los ajustes en el entorno de producción. También sería útil visualizar la curva Precision-Recall para estos umbrales y observar gráficamente el trade-off.
Si crees que se puedan incluir mejoras o alguna parte del código no te funciona, hazmelo saber, te ayudaré con gusto :).
"Debugging es como ser un detective en una novela en la que tú mismo escribiste el guion... pero olvidaste el final. Sigue adelante, ¡cada error es un paso hacia el éxito!"
Edgar Cajusol - Data Scientist - Creando impacto un modelo a la vez.
https://www.linkedin.com/in/edgarcajusol/
Top comments (0)