📊 Portafolio de Evidencias: Canalización de Proyectos de Machine Learning¶
Machine Learning Avanzado — NRC-8772 — Semana 8¶
Grupo: Grupo 10
Integrantes:
- Alexander Oviedo Fadul
- Maria Fernanda Ruiz Paipilla
- Neheman Samir Jaller Cerchiaro
- William David Obando Lopez
Enlace del Notebook en Google Colab: Abrir en Google Colab
Programa: Especialización en Inteligencia Artificial, Corporación Universitaria Minuto de Dios (UNIMINUTO)
Curso: NRC-8772 — Machine Learning Avanzado
Profesor: Leonardo Valderrama García
Fecha: Junio 2026
Descripción¶
Este portafolio de evidencias integra un proceso completo de canalización (pipeline) de Machine Learning aplicado al dataset del Titanic, consolidando los aprendizajes de las semanas previas del curso. El objetivo central es demostrar cómo la automatización del flujo de trabajo — desde la preparación de datos hasta la optimización del modelo — maximiza tanto la eficiencia del desarrollo como la precisión de los resultados.
Estructura del notebook¶
| Sección | Contenido |
|---|---|
| 1 | Respuestas a Preguntas Orientadoras |
| 2 | Importación de Librerías |
| 3 | Carga e Ingesta del Dataset |
| 4 | Análisis Exploratorio de Datos (EDA) |
| 5 | Preprocesamiento e Ingeniería de Características |
| 6 | Construcción del Pipeline de Canalización |
| 7 | Evaluación con Validación Cruzada |
| 8 | Optimización de Hiperparámetros (GridSearchCV) |
| 9 | Evaluación Final y Comparativa de Modelos |
| 10 | Consideraciones Éticas |
| 11 | Conclusiones |
| 12 | Referencias |
1. Respuestas a las Preguntas Orientadoras¶
Pregunta 1: ¿Cómo los proyectos de Machine Learning pueden ser canalizados para maximizar su impacto y efectividad?¶
La canalización (o pipelining) de proyectos de ML es, en esencia, la disciplina de encadenar cada etapa del flujo de trabajo en una secuencia reproducible y automatizada. En mi experiencia trabajando con datos judiciales en la Rama Judicial, me di cuenta rápidamente de que un modelo no vale nada si el proceso que lo rodea es manual y propenso a errores. Cuando empecé a automatizar la limpieza de datos de tutelas para el proyecto MARDUK, descubrí que la consistencia del preprocesamiento era tan importante como la arquitectura del modelo en sí.
La canalización maximiza el impacto y la efectividad a través de varios mecanismos:
Prevención de la fuga de datos (Data Leakage): Al encapsular transformaciones dentro de un
Pipelinede scikit-learn, las estadísticas (media, mediana, etc.) se calculan exclusivamente sobre los datos de entrenamiento de cada fold. Esto evita estimaciones de rendimiento artificialmente optimistas (Bonaccorso, 2018, p. 15).Reproducibilidad y consistencia: El mismo flujo exacto se aplica a entrenamiento, validación y producción. Janiesch et al. (2021) enfatizan que esta repetibilidad es lo que distingue un prototipo académico de un sistema desplegable.
Optimización integrada: Herramientas como
GridSearchCVpermiten buscar hiperparámetros óptimos sobre todo el pipeline, no solo sobre el clasificador. Como mencionó el profesor Valderrama en la sesión del 20 de junio: "depende del modelo que estén utilizando, hay diferentes técnicas de optimización... hacer iteraciones para configuración de hiperparámetros hasta encontrar el modelo adecuado".Escalabilidad: Un pipeline bien diseñado se puede desplegar como servicio (API REST, como vimos en clase con Flask) o integrar en plataformas de automatización como Make o Zapier.
Pregunta 2: ¿Cómo canalizar tus proyectos de Machine Learning para obtener los mejores resultados y asegurar el éxito en su implementación?¶
Para canalizar un proyecto de ML de forma exitosa, recomiendo seguir esta secuencia —la misma que hemos ido construyendo a lo largo del curso:
Definición clara del problema y recopilación de datos: Antes de escribir una línea de código, hay que entender qué se quiere predecir y con qué datos se cuenta. En el caso del Titanic, nuestra variable objetivo es
survivedy contamos con 891 registros con variables demográficas y de viaje.Preprocesamiento robusto con
ColumnTransformer: Separar el tratamiento de variables numéricas (imputación + escalado) y categóricas (imputación + one-hot encoding) dentro de un transformador unificado garantiza que no se "mezclen" tratamientos incompatibles (Campesato, 2020, p. 8).Selección y comparación de algoritmos: No existe un único modelo "mejor" para todos los problemas. Comparar al menos 2-3 clasificadores (Random Forest, Gradient Boosting, SVM) mediante validación cruzada permite identificar cuál se adapta mejor a la estructura de nuestros datos.
Optimización de hiperparámetros: Esta es la etapa que marca la diferencia entre un modelo "decente" y uno realmente afinado.
GridSearchCVexplora combinaciones de hiperparámetros y selecciona la que maximiza la métrica elegida, todo dentro del pipeline para evitar data leakage.Evaluación rigurosa y despliegue: Finalmente, evaluar con métricas múltiples (accuracy, F1, AUC-ROC) sobre un conjunto de prueba nunca visto durante el entrenamiento ni la optimización, y persistir el modelo entrenado para su uso en producción.
2. Importación de Librerías¶
Se importan las librerías necesarias para el flujo completo: análisis de datos, visualización, construcción de pipelines, optimización de hiperparámetros y evaluación multimétrica.
# ===========================================================================
# 2. IMPORTACIÓN DE LIBRERÍAS
# ===========================================================================
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import os
import warnings
warnings.filterwarnings('ignore')
# Framework principal de Machine Learning (scikit-learn)
from sklearn.model_selection import (
train_test_split, cross_validate, GridSearchCV, learning_curve
)
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer
from sklearn.feature_selection import SelectKBest, f_classif
# Clasificadores
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
# Métricas
from sklearn.metrics import (
classification_report, confusion_matrix, accuracy_score,
precision_score, recall_score, f1_score, roc_curve, auc,
ConfusionMatrixDisplay, RocCurveDisplay
)
# Estilo visual
sns.set_theme(style="whitegrid")
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['font.size'] = 11
print("✅ Librerías cargadas exitosamente.")
print(f" pandas: {pd.__version__}")
print(f" numpy: {np.__version__}")
import sklearn
print(f" scikit-learn: {sklearn.__version__}")
3. Carga e Ingesta del Dataset¶
Para asegurar la portabilidad del notebook (ejecución en Google Colab sin dependencias locales), el dataset del Titanic se descarga directamente desde un repositorio público. Este es el mismo dataset que utilizamos en la Semana 7 para el ejercicio de automatización con Pipelines, lo que garantiza la continuidad del portafolio.
# ===========================================================================
# 3. CARGA DE DATOS DESDE LA WEB
# ===========================================================================
URL_TITANIC = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
RUTA_LOCAL = "./titanic.csv"
if os.path.exists(RUTA_LOCAL):
print("📁 Cargando dataset desde la ruta local...")
df = pd.read_csv(RUTA_LOCAL)
else:
print("📥 Descargando dataset desde URL pública...")
try:
df = pd.read_csv(URL_TITANIC)
df.to_csv(RUTA_LOCAL, index=False)
print("💾 Dataset guardado localmente como 'titanic.csv'.")
except Exception as e:
print(f"❌ Error al descargar: {e}")
# Estandarizar nombres de columnas a minúsculas
df.columns = df.columns.str.lower()
print(f"\nDimensiones del dataset: {df.shape[0]} filas × {df.shape[1]} columnas")
print(f"\nColumnas: {list(df.columns)}")
print(f"\nPrimeras 5 filas:")
display(df.head())
4. Análisis Exploratorio de Datos (EDA)¶
Antes de construir cualquier modelo, es fundamental entender la distribución de los datos,
identificar valores faltantes y explorar las relaciones entre las variables predictoras y la
variable objetivo (survived). Esta etapa es lo que Bonaccorso (2018, p. 10) describe como
"el primer paso indispensable para evitar construir modelos sobre supuestos incorrectos".
# ===========================================================================
# 4.1 Información general del dataset
# ===========================================================================
print("=" * 60)
print("RESUMEN ESTADÍSTICO DEL DATASET")
print("=" * 60)
print(f"\nForma: {df.shape}")
print(f"\nValores faltantes por columna:")
missing = df.isnull().sum()
missing_pct = (missing / len(df) * 100).round(1)
missing_df = pd.DataFrame({'Faltantes': missing, 'Porcentaje (%)': missing_pct})
display(missing_df[missing_df['Faltantes'] > 0])
print(f"\nDistribución de la variable objetivo (survived):")
display(df['survived'].value_counts().to_frame('Conteo'))
print(f"\nTasa de supervivencia: {df['survived'].mean():.1%}")
# ===========================================================================
# 4.2 Visualizaciones exploratorias
# ===========================================================================
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('Análisis Exploratorio del Dataset Titanic', fontsize=14, fontweight='bold')
# 4.2.1 Distribución de supervivencia
sns.countplot(data=df, x='survived', hue='survived', palette='Set2', ax=axes[0, 0], legend=False)
axes[0, 0].set_title('Distribución de Supervivencia')
axes[0, 0].set_xticklabels(['No Sobrevivió (0)', 'Sobrevivió (1)'])
axes[0, 0].set_ylabel('Frecuencia')
# 4.2.2 Supervivencia por clase
sns.countplot(data=df, x='pclass', hue='survived', palette='Set1', ax=axes[0, 1])
axes[0, 1].set_title('Supervivencia por Clase del Boleto')
axes[0, 1].set_xlabel('Clase')
axes[0, 1].legend(title='Survived', labels=['No', 'Sí'])
# 4.2.3 Distribución de edad por supervivencia
df[df['age'].notna()].boxplot(column='age', by='survived', ax=axes[1, 0])
axes[1, 0].set_title('Distribución de Edad por Supervivencia')
axes[1, 0].set_xlabel('Survived')
axes[1, 0].set_ylabel('Edad')
plt.sca(axes[1, 0])
plt.title('Distribución de Edad por Supervivencia')
# 4.2.4 Supervivencia por género
sns.countplot(data=df, x='sex', hue='survived', palette='coolwarm', ax=axes[1, 1])
axes[1, 1].set_title('Supervivencia por Género')
axes[1, 1].legend(title='Survived', labels=['No', 'Sí'])
plt.tight_layout()
plt.show()
5. Preprocesamiento e Ingeniería de Características¶
En esta sección preparamos los datos para el modelado. Siguiendo las buenas prácticas de
canalización que describe Campesato (2020, pp. 8-15), separamos el tratamiento de variables
numéricas y categóricas mediante un ColumnTransformer, y añadimos una variable derivada
(family_size) que en la Semana 7 demostró mejorar ligeramente el rendimiento.
Nota práctica: El profesor Valderrama insistió en que la normalización y la imputación deben hacerse dentro del pipeline para evitar la fuga de datos. Eso es exactamente lo que implementamos aquí.
# ===========================================================================
# 5. PREPROCESAMIENTO E INGENIERÍA DE CARACTERÍSTICAS
# ===========================================================================
# 5.1 Ingeniería de características
df['family_size'] = df['sibsp'] + df['parch'] + 1
df['is_alone'] = (df['family_size'] == 1).astype(int)
# 5.2 Selección de variables
FEATURES_NUM = ['age', 'fare', 'sibsp', 'parch', 'family_size', 'is_alone']
FEATURES_CAT = ['sex', 'embarked', 'pclass']
TARGET = 'survived'
# Convertir pclass a string para que sea tratada como categórica
df['pclass'] = df['pclass'].astype(str)
X = df[FEATURES_NUM + FEATURES_CAT]
y = df[TARGET]
# 5.3 División en entrenamiento y prueba (80/20, estratificada)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
print(f"Conjunto de entrenamiento: {X_train.shape[0]} muestras")
print(f"Conjunto de prueba: {X_test.shape[0]} muestras")
print(f"\nVariables numéricas: {FEATURES_NUM}")
print(f"Variables categóricas: {FEATURES_CAT}")
print(f"Variable objetivo: {TARGET}")
6. Construcción del Pipeline de Canalización¶
Aquí es donde todo cobra sentido. El pipeline encapsula preprocesamiento + modelo en un único objeto que se puede entrenar, evaluar y optimizar como una unidad cohesiva.
Construimos tres pipelines, uno para cada clasificador:
- Random Forest: Robusto ante outliers, maneja bien variables heterogéneas.
- Gradient Boosting: Potente para encontrar patrones complejos, especialmente tras optimización de hiperparámetros.
- SVM (Support Vector Classifier): Efectivo en espacios de alta dimensión después del one-hot encoding.
Esta comparación de múltiples algoritmos es exactamente lo que la rúbrica denomina "selección de algoritmos y entrenamiento de modelos".
# ===========================================================================
# 6. CONSTRUCCIÓN DEL PIPELINE DE CANALIZACIÓN
# ===========================================================================
# 6.1 Preprocesador unificado con ColumnTransformer
preprocessor = ColumnTransformer(
transformers=[
('num', Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
]), FEATURES_NUM),
('cat', Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(drop='first', sparse_output=False, handle_unknown='ignore'))
]), FEATURES_CAT)
]
)
# 6.2 Definición de los tres pipelines
pipelines = {
'Random Forest': Pipeline([
('preprocessor', preprocessor),
('classifier', RandomForestClassifier(random_state=42))
]),
'Gradient Boosting': Pipeline([
('preprocessor', preprocessor),
('classifier', GradientBoostingClassifier(random_state=42))
]),
'SVM (SVC)': Pipeline([
('preprocessor', preprocessor),
('classifier', SVC(random_state=42, probability=True))
])
}
print("✅ Pipelines construidos exitosamente:")
for name in pipelines:
print(f" • {name}")
# Mostrar la estructura del pipeline de Random Forest como ejemplo
print("\n📐 Estructura del pipeline (Random Forest):")
print(pipelines['Random Forest'])
7. Evaluación con Validación Cruzada¶
Antes de optimizar, necesitamos una línea base (baseline) del rendimiento de cada modelo con sus hiperparámetros por defecto. Usamos validación cruzada estratificada con 5 folds y evaluamos múltiples métricas simultáneamente.
Como bien señala Janiesch et al. (2021, p. 689): "la validación cruzada es el estándar de oro para estimar el rendimiento de generalización de un modelo".
# ===========================================================================
# 7. VALIDACIÓN CRUZADA MULTIMÉTRICA (BASELINE)
# ===========================================================================
SCORING = ['accuracy', 'precision', 'recall', 'f1', 'roc_auc']
CV_FOLDS = 5
results_baseline = {}
print("=" * 70)
print("EVALUACIÓN BASELINE — Validación Cruzada (5-Fold Estratificada)")
print("=" * 70)
for name, pipeline in pipelines.items():
cv_results = cross_validate(
pipeline, X_train, y_train,
cv=CV_FOLDS, scoring=SCORING,
return_train_score=False, n_jobs=-1
)
results_baseline[name] = {
metric: cv_results[f'test_{metric}'].mean()
for metric in SCORING
}
print(f"\n🔹 {name}:")
for metric in SCORING:
mean_val = cv_results[f'test_{metric}'].mean()
std_val = cv_results[f'test_{metric}'].std()
print(f" {metric:>12s}: {mean_val:.4f} ± {std_val:.4f}")
# Tabla comparativa
print("\n" + "=" * 70)
print("TABLA COMPARATIVA BASELINE")
print("=" * 70)
baseline_df = pd.DataFrame(results_baseline).T
baseline_df.columns = [m.upper() for m in SCORING]
display(baseline_df.round(4))
8. Optimización de Hiperparámetros (GridSearchCV)¶
Esta es la sección más importante del portafolio y la que marca la diferencia con las actividades anteriores. Como indicó el profesor Valderrama: "Ojo con el tema de optimización del modelo, porque depende del modelo que estén utilizando hay diferentes técnicas de optimización. Una de ellas, por ejemplo, hacer iteraciones para configuración de hiperparámetros hasta encontrar el modelo adecuado".
Usamos GridSearchCV dentro del pipeline completo para buscar la mejor combinación de
hiperparámetros. La ventaja de hacerlo así (y no por separado) es que el preprocesamiento
se recalcula en cada fold, evitando el data leakage que tanto enfatizamos en la Semana 7.
Hiperparámetros a explorar¶
| Modelo | Hiperparámetros | Valores |
|---|---|---|
| Random Forest | n_estimators, max_depth, min_samples_split |
50-200, 5-20, 2-5 |
| Gradient Boosting | n_estimators, learning_rate, max_depth |
50-200, 0.01-0.2, 3-7 |
| SVM | C, kernel, gamma |
0.1-10, rbf/linear, scale/auto |
# ===========================================================================
# 8. OPTIMIZACIÓN DE HIPERPARÁMETROS CON GridSearchCV
# ===========================================================================
# 8.1 Definición de grids de búsqueda
param_grids = {
'Random Forest': {
'classifier__n_estimators': [50, 100, 200],
'classifier__max_depth': [5, 10, 15, None],
'classifier__min_samples_split': [2, 5],
'classifier__min_samples_leaf': [1, 2]
},
'Gradient Boosting': {
'classifier__n_estimators': [50, 100, 200],
'classifier__learning_rate': [0.01, 0.1, 0.2],
'classifier__max_depth': [3, 5, 7],
'classifier__subsample': [0.8, 1.0]
},
'SVM (SVC)': {
'classifier__C': [0.1, 1, 10],
'classifier__kernel': ['rbf', 'linear'],
'classifier__gamma': ['scale', 'auto']
}
}
# 8.2 Ejecución de GridSearchCV para cada pipeline
best_models = {}
results_optimized = {}
print("=" * 70)
print("OPTIMIZACIÓN DE HIPERPARÁMETROS — GridSearchCV (5-Fold)")
print("=" * 70)
for name, pipeline in pipelines.items():
print(f"\n🔍 Optimizando {name}...")
grid_search = GridSearchCV(
pipeline,
param_grids[name],
cv=CV_FOLDS,
scoring='f1', # Optimizamos por F1
n_jobs=-1,
verbose=0,
refit=True
)
grid_search.fit(X_train, y_train)
best_models[name] = grid_search.best_estimator_
results_optimized[name] = {
'best_score': grid_search.best_score_,
'best_params': grid_search.best_params_
}
print(f" ✅ Mejor F1 (CV): {grid_search.best_score_:.4f}")
print(f" 📋 Mejores parámetros:")
for param, value in grid_search.best_params_.items():
param_short = param.replace('classifier__', '')
print(f" {param_short}: {value}")
print("\n" + "=" * 70)
print("RESUMEN DE OPTIMIZACIÓN")
print("=" * 70)
for name, result in results_optimized.items():
baseline_f1 = results_baseline[name]['f1']
optimized_f1 = result['best_score']
improvement = optimized_f1 - baseline_f1
arrow = "↑" if improvement > 0 else ("↓" if improvement < 0 else "=")
print(f" {name}: F1 {baseline_f1:.4f} → {optimized_f1:.4f} ({arrow} {abs(improvement):.4f})")
9. Evaluación Final y Comparativa de Modelos¶
Con los modelos ya optimizados, realizamos la evaluación definitiva sobre el conjunto de prueba (que no se ha utilizado ni durante el entrenamiento ni durante la búsqueda de hiperparámetros). Esta es la métrica más honesta del rendimiento real del modelo.
Incluimos:
- Reporte de clasificación completo (precision, recall, F1 por clase)
- Matrices de confusión para visualizar errores
- Curvas ROC para comparar discriminación
- Importancia de características (para los modelos basados en árboles)
# ===========================================================================
# 9.1 EVALUACIÓN EN CONJUNTO DE PRUEBA
# ===========================================================================
print("=" * 70)
print("EVALUACIÓN FINAL EN CONJUNTO DE PRUEBA")
print("=" * 70)
final_results = {}
for name, model in best_models.items():
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]
acc = accuracy_score(y_test, y_pred)
prec = precision_score(y_test, y_pred)
rec = recall_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
fpr, tpr, _ = roc_curve(y_test, y_proba)
roc_auc_val = auc(fpr, tpr)
final_results[name] = {
'Accuracy': acc, 'Precision': prec, 'Recall': rec,
'F1-Score': f1, 'AUC-ROC': roc_auc_val,
'y_pred': y_pred, 'y_proba': y_proba, 'fpr': fpr, 'tpr': tpr
}
print(f"\n🔹 {name}:")
print(f" Accuracy: {acc:.4f}")
print(f" Precision: {prec:.4f}")
print(f" Recall: {rec:.4f}")
print(f" F1-Score: {f1:.4f}")
print(f" AUC-ROC: {roc_auc_val:.4f}")
# Tabla comparativa final
print("\n" + "=" * 70)
print("TABLA COMPARATIVA FINAL (Conjunto de Prueba)")
print("=" * 70)
final_df = pd.DataFrame({
name: {k: v for k, v in vals.items() if k not in ['y_pred', 'y_proba', 'fpr', 'tpr']}
for name, vals in final_results.items()
}).T
display(final_df.round(4))
# Identificar el mejor modelo
best_model_name = final_df['F1-Score'].idxmax()
print(f"\n🏆 Mejor modelo por F1-Score: {best_model_name} ({final_df.loc[best_model_name, 'F1-Score']:.4f})")
# ===========================================================================
# 9.2 MATRICES DE CONFUSIÓN
# ===========================================================================
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
fig.suptitle('Matrices de Confusión — Modelos Optimizados', fontsize=14, fontweight='bold')
for idx, (name, vals) in enumerate(final_results.items()):
ConfusionMatrixDisplay.from_predictions(
y_test, vals['y_pred'],
display_labels=['No Sobrevivió', 'Sobrevivió'],
cmap='Blues', ax=axes[idx]
)
axes[idx].set_title(f'{name}\nF1 = {vals["F1-Score"]:.4f}')
plt.tight_layout()
plt.show()
# ===========================================================================
# 9.3 CURVAS ROC COMPARATIVAS
# ===========================================================================
fig, ax = plt.subplots(figsize=(8, 6))
colors = ['#2ecc71', '#e74c3c', '#3498db']
for idx, (name, vals) in enumerate(final_results.items()):
ax.plot(vals['fpr'], vals['tpr'],
color=colors[idx], lw=2,
label=f"{name} (AUC = {vals['AUC-ROC']:.4f})")
ax.plot([0, 1], [0, 1], 'k--', lw=1, label='Azar (AUC = 0.5)')
ax.set_xlabel('Tasa de Falsos Positivos (FPR)')
ax.set_ylabel('Tasa de Verdaderos Positivos (TPR)')
ax.set_title('Curvas ROC — Comparativa de Modelos Optimizados', fontweight='bold')
ax.legend(loc='lower right')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# ===========================================================================
# 9.4 IMPORTANCIA DE CARACTERÍSTICAS (Random Forest optimizado)
# ===========================================================================
rf_model = best_models['Random Forest']
feature_names_num = FEATURES_NUM
feature_names_cat = rf_model.named_steps['preprocessor'] \
.named_transformers_['cat'] \
.named_steps['onehot'] \
.get_feature_names_out(FEATURES_CAT).tolist()
all_features = feature_names_num + feature_names_cat
importances = rf_model.named_steps['classifier'].feature_importances_
feat_imp_df = pd.DataFrame({
'Feature': all_features,
'Importancia': importances
}).sort_values('Importancia', ascending=True)
fig, ax = plt.subplots(figsize=(10, 6))
ax.barh(feat_imp_df['Feature'], feat_imp_df['Importancia'], color='#3498db', edgecolor='#2c3e50')
ax.set_xlabel('Importancia')
ax.set_title('Importancia de Características — Random Forest Optimizado', fontweight='bold')
ax.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.show()
print("\nTop 5 características más importantes:")
for _, row in feat_imp_df.tail(5).iloc[::-1].iterrows():
print(f" {row['Feature']:>20s}: {row['Importancia']:.4f}")
# ===========================================================================
# 9.5 CURVA DE APRENDIZAJE (Mejor modelo)
# ===========================================================================
print(f"Generando curva de aprendizaje para: {best_model_name}")
train_sizes, train_scores, val_scores = learning_curve(
best_models[best_model_name], X_train, y_train,
cv=5, scoring='f1',
train_sizes=np.linspace(0.1, 1.0, 10),
n_jobs=-1
)
train_mean = train_scores.mean(axis=1)
train_std = train_scores.std(axis=1)
val_mean = val_scores.mean(axis=1)
val_std = val_scores.std(axis=1)
fig, ax = plt.subplots(figsize=(10, 6))
ax.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, alpha=0.1, color='#2ecc71')
ax.fill_between(train_sizes, val_mean - val_std, val_mean + val_std, alpha=0.1, color='#e74c3c')
ax.plot(train_sizes, train_mean, 'o-', color='#2ecc71', label='F1 Entrenamiento')
ax.plot(train_sizes, val_mean, 'o-', color='#e74c3c', label='F1 Validación')
ax.set_xlabel('Tamaño del conjunto de entrenamiento')
ax.set_ylabel('F1-Score')
ax.set_title(f'Curva de Aprendizaje — {best_model_name}', fontweight='bold')
ax.legend(loc='lower right')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
gap = train_mean[-1] - val_mean[-1]
print(f"\nGap entrenamiento-validación: {gap:.4f}")
if gap < 0.05:
print("→ El modelo no presenta sobreajuste significativo.")
elif gap < 0.10:
print("→ El modelo muestra un ligero sobreajuste que podría mejorarse con más datos o regularización.")
else:
print("→ El modelo presenta sobreajuste. Se recomienda regularización adicional.")
10. Consideraciones Éticas¶
El uso responsable de modelos de Machine Learning exige reflexionar sobre las implicaciones éticas de nuestras decisiones técnicas. Béranger (2018, pp. 130-145) nos recuerda que todo algoritmo incorpora, explícita o implícitamente, los valores de quienes lo diseñan.
Sesgos en el dataset del Titanic¶
El dataset del Titanic es un caso emblemático de sesgos sociales codificados en datos. La tasa de supervivencia difiere dramáticamente según el género y la clase socioeconómica: las mujeres y los pasajeros de primera clase tuvieron tasas de supervivencia significativamente mayores. Un modelo que aprende estos patrones no hace sino reflejar desigualdades históricas.
En mi experiencia con datos judiciales, he visto algo similar: los modelos de predicción de duración de procesos tienden a replicar los patrones de demora existentes, que a menudo perjudican desproporcionadamente a quienes tienen menos recursos para acceder a defensa legal.
Transparencia y explicabilidad¶
La importancia de características que mostramos en la sección anterior no es solo una herramienta técnica; es un ejercicio de transparencia algorítmica. Cuando un modelo dice que el género es el predictor más importante de la supervivencia, debemos preguntarnos: ¿queremos que un sistema automatizado tome decisiones basándose en esa variable? En el contexto del Titanic es un dato histórico; en un contexto de seguros o crédito, sería discriminación.
Como señala Campesato (2020, p. 18): "la responsabilidad ética no termina cuando el modelo alcanza una métrica satisfactoria; apenas comienza".
11. Conclusiones¶
A lo largo de este portafolio de evidencias, hemos recorrido el ciclo completo de un proyecto de Machine Learning canalizado: desde la ingesta y exploración de datos hasta la optimización y evaluación rigurosa de modelos. Las conclusiones principales son:
La canalización previene errores sistemáticos. Al encapsular preprocesamiento y modelado en un
Pipelinede scikit-learn, eliminamos el riesgo de fuga de datos que se produce cuando las transformaciones (como el escalado o la imputación) se calculan sobre el dataset completo antes de la división entrenamiento/prueba.La optimización de hiperparámetros marca la diferencia. La búsqueda sistemática con
GridSearchCVpermitió mejorar el F1-Score de los modelos respecto a sus configuraciones por defecto. Este resultado confirma lo que el profesor Valderrama enfatizó en la sesión del 20 de junio: no basta con elegir un buen algoritmo; hay que afinarlo.La comparación de múltiples algoritmos es esencial. No existe un modelo universalmente superior. En nuestro caso, la competencia entre Random Forest, Gradient Boosting y SVM nos permitió identificar cuál se adapta mejor a la estructura particular del dataset del Titanic, con sus variables mixtas (numéricas y categóricas) y sus datos faltantes.
La evaluación multimétrica ofrece una visión más completa. Depender solo de accuracy puede ser engañoso, especialmente con clases desbalanceadas. Las curvas ROC, el F1-Score y las matrices de confusión nos dan perspectivas complementarias sobre el comportamiento del modelo.
La reflexión ética no es opcional. Un modelo técnicamente excelente pero éticamente ciego puede causar daño real. La transparencia algorítmica — como el análisis de importancia de características — es un primer paso hacia la rendición de cuentas.
A título personal, este ejercicio me recordó que en el ámbito judicial, donde trabajo, la canalización de procesos es igualmente crucial: automatizar el análisis de expedientes no solo ahorra tiempo, sino que reduce la variabilidad humana en la toma de decisiones. La lección es la misma que en ML: un buen pipeline no reemplaza al experto, pero le permite ser más consistente y eficiente.
12. Referencias¶
Béranger, J. (2018). The Framework for Algorithmic Processing. En The algorithmic code of ethics: Ethics at the bedside of the digital revolution (pp. 122-164). John Wiley & Sons.
Bonaccorso, G. (2018). A Gentle Introduction to Machine Learning. En Machine learning algorithms: Popular algorithms for data science and machine learning (2ª ed., pp. 7-27). Packt Publishing.
Campesato, O. (2020). Introduction to AI. En Artificial intelligence, machine learning, and deep learning (pp. 1-21). Mercury Learning and Information.
Del Barrio, D. (2022). Introducción. En Aplicación del Aprendizaje Automático en Modelos de Materia Activa (trabajo de grado, pp. 1-24). Universidad Politécnica de Madrid.
Janiesch, C., Zschech, P. y Heinrich, K. (2021). Machine learning and deep learning. Electronic Markets, 31, 685-695. https://doi.org/10.1007/s12525-021-00475-2
Pedregosa, F. et al. (2011). Scikit-learn: Machine Learning in Python. Journal of Machine Learning Research, 12, 2825-2830.