Para este ejercicio seleccionamos variables de crédito, saldos y pagos. Excluimos el identificador y los atributos demográficos. Los códigos de situación de pago se tratan como categorías; las cantidades numéricas se estandarizan.
PYTHON · CLASIFICACIÓN DE INCUMPLIMIENTO
import numpy as np
import tensorflow as tf
from ucimlrepo import fetch_ucirepo
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.utils.class_weight import compute_class_weight
from sklearn.metrics import (
average_precision_score,
roc_auc_score,
confusion_matrix,
classification_report,
)
SEED = 42
tf.keras.utils.set_random_seed(SEED)
# 1. Descargar los datos originales de UCI.
dataset = fetch_ucirepo(id=350)
X_original = dataset.data.features.copy()
y = dataset.data.targets.iloc[:, 0].to_numpy(dtype=np.int32)
# Nombres de variables según la documentación de UCI:
# X1: límite de crédito.
# X6-X11: situación de pagos.
# X12-X17: importes de estados de cuenta.
# X18-X23: importes de pagos anteriores.
numericas = ["X1"] + [f"X{i}" for i in range(12, 24)]
categoricas = [f"X{i}" for i in range(6, 12)]
X = X_original[numericas + categoricas].copy()
X[categoricas] = X[categoricas].astype(str)
# 2. Separar: 60% entrenamiento, 20% validación, 20% prueba.
X_train, X_reserva, y_train, y_reserva = train_test_split(
X, y,
test_size=0.40,
random_state=SEED,
stratify=y,
)
X_val, X_test, y_val, y_test = train_test_split(
X_reserva, y_reserva,
test_size=0.50,
random_state=SEED,
stratify=y_reserva,
)
# 3. Ajustar el preprocesamiento SOLO con entrenamiento.
preparador = ColumnTransformer(
transformers=[
("numericas", StandardScaler(), numericas),
(
"categoricas",
OneHotEncoder(
handle_unknown="ignore",
sparse_output=False,
),
categoricas,
),
]
)
X_train_p = preparador.fit_transform(X_train).astype("float32")
X_val_p = preparador.transform(X_val).astype("float32")
X_test_p = preparador.transform(X_test).astype("float32")
# 4. Calcular pesos usando únicamente las etiquetas de entrenamiento.
clases = np.unique(y_train)
pesos = compute_class_weight(
class_weight="balanced",
classes=clases,
y=y_train,
)
pesos_clase = {
int(clase): float(peso)
for clase, peso in zip(clases, pesos)
}
# 5. Definir una red neuronal pequeña.
modelo = tf.keras.Sequential([
tf.keras.Input(shape=(X_train_p.shape[1],)),
tf.keras.layers.Dense(32, activation="relu"),
tf.keras.layers.Dropout(0.20),
tf.keras.layers.Dense(16, activation="relu"),
tf.keras.layers.Dense(1, activation="sigmoid"),
])
modelo.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
loss="binary_crossentropy",
)
# 6. Detener el entrenamiento si deja de mejorar la validación.
parada = tf.keras.callbacks.EarlyStopping(
monitor="val_loss",
patience=5,
restore_best_weights=True,
)
historial = modelo.fit(
X_train_p,
y_train,
validation_data=(X_val_p, y_val),
epochs=50,
batch_size=128,
class_weight=pesos_clase,
callbacks=[parada],
verbose=0,
)
# 7. Evaluación final sobre prueba.
probabilidades = modelo.predict(X_test_p, verbose=0).ravel()
# Umbral ilustrativo; no ha sido optimizado para el negocio.
umbral = 0.50
predicciones = (probabilidades >= umbral).astype(int)
print(f"Proporción de incumplimiento: {y_test.mean():.3f}")
print(
"Average precision:",
round(average_precision_score(y_test, probabilidades), 3),
)
print(
"ROC-AUC:",
round(roc_auc_score(y_test, probabilidades), 3),
)
print("Matriz de confusión:")
print(confusion_matrix(y_test, predicciones, labels=[0, 1]))
print(
classification_report(
y_test,
predicciones,
labels=[0, 1],
target_names=["No incumplimiento", "Incumplimiento"],
digits=3,
zero_division=0,
)
)