Proyecto Machine Learning 2: Evaluación de Métricas de Modelos¶
Información del Curso¶
| Campo | Información |
|---|---|
| Asignatura | Machine Learning |
| Semana | 8 |
| Actividad | Proyecto Colaborativo: Evaluación de Modelos de ML |
| Docente | Ing. Breyner Alexander Parra Rojas |
| Fecha | 28 de Febrero de 2026 |
Integrantes - Equipo de Trabajo¶
- Alexander Oviedo Fadul
- Josmar Peña Buitrago
- Maria Fernanda Ruiz Paipilla
- Neheman Samir Jaller Cerchiaro
- William David Obando Lopez
INTRODUCCIÓN¶
A la hora de aplicar algoritmos de Inteligencia Artificial para la toma de decisiones críticas (ya sea en salud, finanzas o robótica), no basta con que el algoritmo alcance un alto grado de "exactitud". Como bien señalan Bartneck et al. (2021), el ecosistema ético exige que comprendamos exactamente qué tipo de errores están cometiendo nuestras máquinas.
El propósito de este proyecto es profundizar en las métricas de evaluación para determinar qué modelo resulta más eficiente ante un desafío de clasificación. Aplicaremos una regresión logística (un modelo más clásico) frente a un árbol de decisión (que lidia mejor con asimetrías y reglas de decisión abruptas), evaluando métricas vitales como Matriz de Confusión, Exactitud (Accuracy), Precisión, Exhaustividad (Recall), Puntuación F1, Especificidad y la curva ROC transversal a las probabilidades.
PLANEACIÓN DEL PROYECTO¶
Para garantizar un análisis sistemático y confiable, estructuramos nuestro trabajo mediante las siguientes fases de desarrollo, apoyándonos en librerías estándar como Pandas, Scikit-Learn y Matplotlib:
- Cargar los datos clínicos: Retomaremos el dataset "Breast Cancer" (Cáncer de Mama) por su pertinencia en clasificación binaria e ideal para entender métricas complejas como falsos positivos o falsos negativos.
- División de Datos: Reservaremos un 25% de nuestro dataset exclusivamente para las pruebas de las métricas (
train_test_split). - Selección de Modelos: Entrenaremos un modelo lineal (Logistic Regression) y un modelo basado en reglas (Decision Tree Classifier).
- Cálculo de Métricas y Visualización Visual: Das y Mert (2018) mencionan que el despliegue analítico es primordial para construir IA. Visualizaremos todo mediante reportes de clasificación, matrices de calor de confusión y gráficos ROC.
DESARROLLO: CARGA Y PREPARACIÓN DE LOS DATOS¶
Empecemos cargando nuestro ecosistema de Scikit-Learn e importando los datos correspondientes.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Librerías para Dataset, Preprocesamiento y Métricas
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
confusion_matrix, roc_curve, auc
)
# Nuestros competidores del día de hoy
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
# Cargar los datos a un DataFrame
data = load_breast_cancer()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target # 0: maligno, 1: benigno
print("Datos cargados correctamente.")
print(f"Filas y columnas totales: {df.shape}")
# Separamos las variables y estandarizamos
X = df.drop('target', axis=1)
y = df['target']
# Train/Test split: 25% para nuestro conjunto de test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
# Estandarización de variables
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print("Los datos han sido escalados de forma independiente a la partición de prueba.")
4. ENTRENAMIENTO DE MODELOS BASE¶
Procedemos ahora a instanciar y enseñar a nuestra inteligencia artificial. Entrenaremos nuestros dos modelos con X_train_scaled y guardaremos sus predicciones en base a X_test_scaled.
# 1. MODELO: Regresión Logística
mod_logistic = LogisticRegression(random_state=42, max_iter=1000)
mod_logistic.fit(X_train_scaled, y_train)
pred_logistic = mod_logistic.predict(X_test_scaled)
prob_logistic = mod_logistic.predict_proba(X_test_scaled)[:, 1]
# 2. MODELO: Árbol de Decisión
mod_tree = DecisionTreeClassifier(random_state=42, max_depth=5)
mod_tree.fit(X_train_scaled, y_train)
pred_tree = mod_tree.predict(X_test_scaled)
prob_tree = mod_tree.predict_proba(X_test_scaled)[:, 1]
print("Modelos de IA entrenados exitosamente y predicciones calculadas.")
5. RESULTADOS Y EVALUACIÓN DE MÉTRICAS¶
A continuación vamos a evaluar cómo se comportaron estos dos enfoques bajo un extenso reporte estadístico. Crearemos una función en Python para que nos retorne exactamente todas las métricas solicitadas de una pasada.
def evaluar_modelo(y_real, y_pred, nombre_modelo):
# Extraer variables de confusión
# Recordatorio: sklearn matriz devuelve en orden TN, FP, FN, TP
matriz = confusion_matrix(y_real, y_pred)
TN = matriz[0][0]
FP = matriz[0][1]
FN = matriz[1][0]
TP = matriz[1][1]
# Cálculos estadísticos obligatorios
acc = accuracy_score(y_real, y_pred)
prec = precision_score(y_real, y_pred)
rec = recall_score(y_real, y_pred)
f1 = f1_score(y_real, y_pred)
# La Especificidad es la Tasa de Verdaderos Negativos (TN / (TN + FP))
especificidad = TN / (TN + FP) if (TN + FP) != 0 else 0
print(f"🔹 MÉTRICAS: {nombre_modelo.upper()}")
print("="*35)
print(f" - Exactitud (Accuracy): {acc*100:.2f}%")
print(f" - Precisión (Precision): {prec*100:.2f}%")
print(f" - Exhaustividad (Recall):{rec*100:.2f}%")
print(f" - Puntuación F1 (F-1): {f1*100:.2f}%")
print(f" - Especificidad: {especificidad*100:.2f}%")
print("\n")
return matriz
# Evaluación general
matriz_log_reg = evaluar_modelo(y_test, pred_logistic, "Regresión Logística")
matriz_arbol = evaluar_modelo(y_test, pred_tree, "Árbol de Decisión")
Gráfica 1: Matrices de Confusión¶
Para analizar los Verdaderos Positivos y detectar Dónde se equivocó el modelo:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
sns.heatmap(matriz_log_reg, annot=True, fmt='g', cmap='Blues', ax=ax1,
xticklabels=['Maligno (0)', 'Benigno (1)'], yticklabels=['Maligno (0)', 'Benigno (1)'])
ax1.set_title('Matriz de Confusión: Regresión Logística')
ax1.set_xlabel('Predicción del Algoritmo')
ax1.set_ylabel('Resultado Verdadero')
sns.heatmap(matriz_arbol, annot=True, fmt='g', cmap='Reds', ax=ax2,
xticklabels=['Maligno (0)', 'Benigno (1)'], yticklabels=['Maligno (0)', 'Benigno (1)'])
ax2.set_title('Matriz de Confusión: Árbol de Decisión')
ax2.set_xlabel('Predicción del Algoritmo')
ax2.set_ylabel('Resultado Verdadero')
plt.tight_layout()
plt.show()
Gráfica 2: Curva de Características Operativas del Receptor (ROC)¶
La curva ROC dibuja una línea comparando la sensibilidad de nuestro modelo contra la tasa de Falsos Positivos. Cuanto más cerca del vértice superior izquierdo (área calculada cercana a 1), más efectivo es el algoritmo diferenciando clases.
# Calculamos los valores falsos y verdaderos para la gráfica usando scikit-learn
fpr_log, tpr_log, _ = roc_curve(y_test, prob_logistic)
roc_auc_log = auc(fpr_log, tpr_log)
fpr_tree, tpr_tree, _ = roc_curve(y_test, prob_tree)
roc_auc_tree = auc(fpr_tree, tpr_tree)
plt.figure(figsize=(8, 6))
plt.plot(fpr_log, tpr_log, color='blue', lw=2, label=f'Regresión Logística (AUC = {roc_auc_log:.3f})')
plt.plot(fpr_tree, tpr_tree, color='red', lw=2, label=f'Árbol de Decisión (AUC = {roc_auc_tree:.3f})')
plt.plot([0, 1], [0, 1], color='gray', lw=2, linestyle='--') # Nuestro peor escenario (moneda al aire)
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('Tasa de Falsos Positivos (1 - Especificidad)')
plt.ylabel('Tasa de Verdaderos Positivos (Sensibilidad o Recall)')
plt.title('Gráfica de Curva ROC Comparativa')
plt.legend(loc="lower right")
plt.show()
6. EJERCICIO ADICIONAL NO SUPERVISADO (K-MEANS)¶
Dado que la presente actividad contempló el uso de datasets orientados al aprendizaje no supervisado (como se mencionaba en el título de la tarea), el equipo ha decidido ir más allá e incorporar un análisis exploratorio utilizando un modelo de Clustering K-Means. Para ello, rescataremos el dataset Adult Census Income previamente expuesto en la Semana 4.
Nuestra justificación radica en que, a pesar de que la rúbrica exigía métricas exclusivas de clasificación (ROC, Especificidad, Matriz de Confusión), el equipo deseaba demostrar el flujo completo aplicando también el componente no supervisado.
# ==============================================================================
# IMPORTACIÓN DE LIBRERÍAS Y CARGA DEL DATASET
# ==============================================================================
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
sns.set_style('whitegrid')
import warnings
warnings.filterwarnings('ignore')
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
columnas = [
'age', 'workclass', 'fnlwgt', 'education', 'education_num',
'marital_status', 'occupation', 'relationship', 'race', 'sex',
'capital_gain', 'capital_loss', 'hours_per_week', 'native_country', 'income'
]
print("Cargando dataset...")
datos_unsupervised = pd.read_csv(url, names=columnas, na_values=' ?', skipinitialspace=True)
datos_unsupervised = datos_unsupervised.dropna()
print(f"Dimensiones iniciales del dataset limpio: {datos_unsupervised.shape}")
6.1. Preprocesamiento para Clustering¶
A diferencia de los modelos de clasificación anteriores que podían manejar columnas categóricas luego del One-Hot, el K-Means calcula distancias euclidianas. Por este motivo, el grupo procedió a excluir las etiquetas numéricas de peso censal ('fnlwgt') y conservar únicamente variables puramente continuas para evitar la dominancia de dimensiones.
# Variables continuas pertinentes para clustering
vars_num = ['age', 'education_num', 'capital_gain', 'capital_loss', 'hours_per_week']
X_unsup = datos_unsupervised[vars_num].copy()
# Estandarización imprescindible para distancias de K-Means
scaler_u = StandardScaler()
X_unsup_scaled = scaler_u.fit_transform(X_unsup)
print("Variables estandarizadas con éxito.")
display(pd.DataFrame(X_unsup_scaled, columns=vars_num).head())
6.2. Determinación del Número de Clusters Óptimo¶
Validamos el número de grupos k aplicando el Método del Codo.
# ==============================================================================
# EJECUCIÓN DEL MÉTODO DEL CODO (SUBCONJUNTO PARA OPTIMIZAR TIEMPO)
# ==============================================================================
inercias = []
rango_k = range(2, 9)
# Por simplicidad y carga del notebook, probamos hasta k=8 en una muestra
np.random.seed(42)
sample_indices = np.random.choice(X_unsup_scaled.shape[0], size=10000, replace=False)
X_sample = X_unsup_scaled[sample_indices]
for k in rango_k:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(X_sample)
inercias.append(kmeans.inertia_)
plt.figure(figsize=(8, 4))
plt.plot(rango_k, inercias, marker='o', linestyle='--')
plt.xlabel('Número de Clusters (k)')
plt.ylabel('Inercia')
plt.title('Método del Codo para Selección de k')
plt.show()
6.3. Identificación de Agrupaciones (K=4)¶
Al observar que el codo flexiona paulatinamente, seleccionaremos k=4 como un punto razonable y calcularemos los promedios de cada clúster para interpretarlo, comprobando la cohesión general mediante el Silhouette Score.
# ==============================================================================
# ENTRENAMIENTO DEL MODELO DE K-MEANS
# ==============================================================================
kmeans_final = KMeans(n_clusters=4, random_state=42, n_init=10)
kmeans_final.fit(X_unsup_scaled)
X_unsup['Cluster'] = kmeans_final.labels_
# Muestra pequeña del Score por cuestiones de rendimiento en Jupyter
score_silueta = silhouette_score(X_sample, kmeans_final.predict(X_sample))
print(f"\nSilhouette Score (muestra de validación): {score_silueta:.3f}")
print("\n--- Promedios por Cluster ---")
display(X_unsup.groupby('Cluster').mean().round(2))
7. CONCLUSIONES¶
Como vimos a lo largo del proceso analítico, no todos los modelos de Machine Learning funcionan igual, a pesar de que la exactitud y las métricas simples lucían bien en términos generales:
- Comparación entre Modelos: La Regresión Logística nos arrojó un accuracy cerca del 98%, mientras que el Árbol de Decisión se quedó levemente por debajo (95%). Si bien esto no parece mucha diferencia, en campos sensibles es drástico. Particularmente, la Especificidad (capacidad para ver los casos negativos reales donde alguien SÍ tiene cáncer) de la regresión logística fue altísima en nuestra experimentación.
- Más Allá de la Precisión Total: Obtener la Matriz de Confusión y entender la Curva ROC (AUC de 0.99 frente a 0.94) expone que un modelo basado puramente en nodos de sí o no falló un par de veces más al identificar y separar correctamente a nuestros datos con tumores.
- Transparencia en el Proyecto: Como estudiantes universitarios apoyando la ética en la IA, comprendemos tras este flujo que siempre habrá áreas grises y falsos positivos/negativos. Por eso, es nuestra responsabilidad nunca presentar una "caja negra" como la realidad indiscutible, y este conjunto de métricas nos garantiza brindar el beneficio de la transparencia.
8. REFERENCIAS¶
- Bartneck, C., Lütge, C., Wagner, A., & Welsh, S. (2021). An Introduction to Ethics in Robotics and AI (pp. 10-25). Springer.
- Das, S., & Mert, C. U. (2018). Hands-on automated machine learning: A beginner's guide to building automated machine learning systems using automl and python (pp. 33-45). Packt Publishing.
- Carsten, B. (2021). Artificial Intelligence for a Better Future: An Ecosystem Perspective on the Ethics of AI and Emerging Digital Technologies (7-15). Springer.
- Géron, A. (2019). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow: Concepts, Tools, and Techniques to Build Intelligent Systems. O'Reilly Media.