DEV Community

Cover image for ¿Fraude? Que no nos engañen. Detección de fraude en tarjetas de crédito con Random Forest Classifier.
Edgar Cajusol
Edgar Cajusol

Posted on • Edited on

¿Fraude? Que no nos engañen. Detección de fraude en tarjetas de crédito con Random Forest Classifier.

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.

Imagen de una tarjeta siendo robada

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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
Enter fullscreen mode Exit fullscreen mode
#Cargar el dataset y mostrarlo
df = pd.read_csv('archivo.csv')
df.head(10)
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode

Countplot de Clases

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()
Enter fullscreen mode Exit fullscreen mode

Scatterplot de Amount y Time

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)
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

Distribucion despues de SMOTE

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Matriz de confusion

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.

accuracy

  • Precision: La Presicion responde a la siguiente pregunta: De todos los positivos predichos por el modelo ¿Cuántos realmente eran positivos?.

precision

  • Recall: De todos los positivos (predichos y no predichos) ¿Cuántos logramos identificar?

recall

  • 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.

F1-score

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
Enter fullscreen mode Exit fullscreen mode
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')
Enter fullscreen mode Exit fullscreen mode

Curva-ROC

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)
Enter fullscreen mode Exit fullscreen mode
matriz_de_confusion(y_test,y_pred,"Balanced",'Fraude')
Enter fullscreen mode Exit fullscreen mode

Balanced

report = classification_report(y_test,y_pred,target_names={0:'Normal',1:'Fraude'},output_dict=True)
report_df = pd.DataFrame(report).transpose()
report_df
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# 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)
Enter fullscreen mode Exit fullscreen mode

mejores_parametros

print(f'Mejor score: {random_search.best_score_}')

Mejor score: 0.9995736448174799
Enter fullscreen mode Exit fullscreen mode

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

pipeline

Best_params

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')
Enter fullscreen mode Exit fullscreen mode

best_matriz_confusin

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
Enter fullscreen mode Exit fullscreen mode
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()
Enter fullscreen mode Exit fullscreen mode

bes_roc

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}")
Enter fullscreen mode Exit fullscreen mode

resultados_thresholds

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()
Enter fullscreen mode Exit fullscreen mode

precision-recall-threshold

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)