🚢 Ciclo de Vida de Machine Learning Supervisado: Predicción de Supervivencia en el Titanic¶
Asignatura: NRC-8772 — Machine Learning Avanzado — Semana 5
Docente: Leonardo Valderrama García
Integrantes del Grupo 10:
- Alexander Oviedo Fadul
- María Fernanda Ruiz Paipilla
- Neheman Samir Jaller Cerchiaro
- William Andrés Martínez Paternina
Fecha: Junio 08 de 2026
Corporación Universitaria Minuto de Dios — UNIMINUTO
Especialización en Inteligencia Artificial
Objetivo¶
Desarrollar un modelo de clasificación binaria que prediga si un pasajero del Titanic sobrevivió o no al naufragio, siguiendo el ciclo de vida completo de un modelo de Machine Learning: obtención de datos → exploración (EDA) → limpieza y preparación → ingeniería de variables → entrenamiento → evaluación → análisis de resultados.
Preguntas Orientadoras¶
- ¿Qué son los conjuntos de datos de evaluación y entrenamiento y por qué son importantes en el aprendizaje automático?
- ¿Cómo se seleccionan y preparan los conjuntos de datos de evaluación y entrenamiento para garantizar una alta precisión en los modelos de aprendizaje automático?
1. Obtención de Datos¶
El primer paso en el ciclo de vida de cualquier modelo de ML consiste en definir de dónde obtenemos los datos y con qué instrumentos de recolección trabajamos (Rothman, 2018). En este caso, utilizamos el dataset clásico del Titanic, disponible públicamente en múltiples repositorios. Se carga directamente desde una URL pública para garantizar la reproducibilidad sin necesidad de archivos locales.
El dataset contiene información demográfica y de viaje de 891 pasajeros del RMS Titanic, junto con la variable objetivo Survived que indica si el pasajero sobrevivió (1) o falleció (0) en el naufragio de abril de 1912.
# ============================================================================
# 1.1 Importación de librerías necesarias
# ============================================================================
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
confusion_matrix, classification_report, roc_curve, auc,
ConfusionMatrixDisplay, RocCurveDisplay
)
from sklearn.preprocessing import LabelEncoder
import warnings
warnings.filterwarnings('ignore')
# Configuración visual
plt.style.use('seaborn-v0_8-whitegrid')
sns.set_palette('deep')
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['font.size'] = 12
print('✅ Librerías importadas correctamente')
print(f' pandas: {pd.__version__}')
print(f' numpy: {np.__version__}')
✅ Librerías importadas correctamente pandas: 2.2.2 numpy: 2.0.2
# ============================================================================
# 1.2 Carga del dataset desde URL pública
# ============================================================================
url = 'https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv'
df = pd.read_csv(url)
print(f'✅ Dataset cargado exitosamente')
print(f' Filas: {df.shape[0]}')
print(f' Columnas: {df.shape[1]}')
print(f'\nPrimeras 5 filas del dataset:')
df.head()
✅ Dataset cargado exitosamente Filas: 891 Columnas: 12 Primeras 5 filas del dataset:
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
| 1 | 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
| 2 | 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
| 3 | 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S |
| 4 | 5 | 0 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S |
2. Exploración de Datos (EDA)¶
La exploración de datos es la fase donde buscamos comprender la estructura, distribución y calidad de nuestro dataset. Como señala Bi et al. (2019), antes de entrenar cualquier modelo es fundamental conocer las características de los datos: tipos de variables, valores faltantes, distribuciones y relaciones entre variables.
2.1 Panorama General del Dataset¶
Utilizamos .info() para obtener el conteo de valores no nulos y tipos de datos por columna, y .describe() para las estadísticas descriptivas.
# ============================================================================
# 2.1 Información general del dataset
# ============================================================================
print('='*60)
print('INFORMACIÓN GENERAL DEL DATASET')
print('='*60)
df.info()
print('\n' + '='*60)
print('VALORES NULOS POR COLUMNA')
print('='*60)
nulos = df.isnull().sum()
nulos_pct = (nulos / len(df) * 100).round(2)
print(pd.DataFrame({'Nulos': nulos, '% del Total': nulos_pct})[nulos > 0])
============================================================
INFORMACIÓN GENERAL DEL DATASET
============================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 891 non-null int64
1 Survived 891 non-null int64
2 Pclass 891 non-null int64
3 Name 891 non-null object
4 Sex 891 non-null object
5 Age 714 non-null float64
6 SibSp 891 non-null int64
7 Parch 891 non-null int64
8 Ticket 891 non-null object
9 Fare 891 non-null float64
10 Cabin 204 non-null object
11 Embarked 889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
============================================================
VALORES NULOS POR COLUMNA
============================================================
Nulos % del Total
Age 177 19.87
Cabin 687 77.10
Embarked 2 0.22
Análisis: Observamos que Age tiene 177 valores nulos (19.9%), Cabin tiene 687 nulos (77.1%) y Embarked tiene 2 nulos (0.2%). La columna Cabin presenta una densidad de datos tan baja que, como discutió el docente en clase, no conviene imputarla porque no hay suficiente información para hacerlo de manera confiable.
# ============================================================================
# 2.2 Estadísticas descriptivas completas
# ============================================================================
print('ESTADÍSTICAS DESCRIPTIVAS (Variables numéricas y categóricas)')
print('='*60)
df.describe(include='all')
ESTADÍSTICAS DESCRIPTIVAS (Variables numéricas y categóricas) ============================================================
| PassengerId | Survived | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 891.000000 | 891.000000 | 891.000000 | 891 | 891 | 714.000000 | 891.000000 | 891.000000 | 891 | 891.000000 | 204 | 889 |
| unique | NaN | NaN | NaN | 891 | 2 | NaN | NaN | NaN | 681 | NaN | 147 | 3 |
| top | NaN | NaN | NaN | Dooley, Mr. Patrick | male | NaN | NaN | NaN | 347082 | NaN | G6 | S |
| freq | NaN | NaN | NaN | 1 | 577 | NaN | NaN | NaN | 7 | NaN | 4 | 644 |
| mean | 446.000000 | 0.383838 | 2.308642 | NaN | NaN | 29.699118 | 0.523008 | 0.381594 | NaN | 32.204208 | NaN | NaN |
| std | 257.353842 | 0.486592 | 0.836071 | NaN | NaN | 14.526497 | 1.102743 | 0.806057 | NaN | 49.693429 | NaN | NaN |
| min | 1.000000 | 0.000000 | 1.000000 | NaN | NaN | 0.420000 | 0.000000 | 0.000000 | NaN | 0.000000 | NaN | NaN |
| 25% | 223.500000 | 0.000000 | 2.000000 | NaN | NaN | 20.125000 | 0.000000 | 0.000000 | NaN | 7.910400 | NaN | NaN |
| 50% | 446.000000 | 0.000000 | 3.000000 | NaN | NaN | 28.000000 | 0.000000 | 0.000000 | NaN | 14.454200 | NaN | NaN |
| 75% | 668.500000 | 1.000000 | 3.000000 | NaN | NaN | 38.000000 | 1.000000 | 0.000000 | NaN | 31.000000 | NaN | NaN |
| max | 891.000000 | 1.000000 | 3.000000 | NaN | NaN | 80.000000 | 8.000000 | 6.000000 | NaN | 512.329200 | NaN | NaN |
2.2 Distribución de la Variable Objetivo¶
Antes de entrenar cualquier modelo, es crucial conocer la distribución de la variable que queremos predecir. Si existe un desbalance significativo entre las clases, esto afectará directamente la elección de métricas de evaluación. Como explicó el docente en clase, la accuracy puede dar una impresión engañosa en problemas desbalanceados: un modelo que prediga siempre "no sobrevivió" tendría ~61% de accuracy simplemente por la distribución de los datos.
# ============================================================================
# 2.3 Distribución de la variable objetivo (Survived)
# ============================================================================
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Gráfico de barras
survived_counts = df['Survived'].value_counts()
colors = ['#e74c3c', '#2ecc71']
bars = axes[0].bar(['No Sobrevivió (0)', 'Sobrevivió (1)'],
survived_counts.values, color=colors, edgecolor='white', linewidth=1.5)
axes[0].set_title('Distribución de Supervivencia', fontsize=14, fontweight='bold')
axes[0].set_ylabel('Cantidad de Pasajeros')
for bar, val in zip(bars, survived_counts.values):
axes[0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 10,
str(val), ha='center', fontweight='bold', fontsize=13)
# Gráfico de pie
survived_pct = df['Survived'].value_counts(normalize=True) * 100
axes[1].pie(survived_pct.values, labels=[f'No Sobrevivió\n({survived_pct[0]:.1f}%)',
f'Sobrevivió\n({survived_pct[1]:.1f}%)'],
colors=colors, autopct='%1.1f%%', startangle=90,
textprops={'fontsize': 12}, wedgeprops={'edgecolor': 'white', 'linewidth': 2})
axes[1].set_title('Proporción de Supervivencia', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
print(f'\n📊 Desbalance de clases:')
print(f' No sobrevivió: {survived_counts[0]} ({survived_pct[0]:.1f}%)')
print(f' Sobrevivió: {survived_counts[1]} ({survived_pct[1]:.1f}%)')
print(f' Ratio: {survived_counts[0]/survived_counts[1]:.2f}:1')
📊 Desbalance de clases: No sobrevivió: 549 (61.6%) Sobrevivió: 342 (38.4%) Ratio: 1.61:1
Análisis: El dataset presenta un desbalance moderado: aproximadamente el 61.6% de los pasajeros no sobrevivió frente al 38.4% que sí lo hizo. Este desbalance no es extremo, pero justifica que no nos basemos exclusivamente en accuracy como métrica de evaluación. Como mencionó el docente en clase, en un contexto médico este tipo de desbalance podría traducirse en pacientes enfermos enviados a casa como sanos (falsos negativos), lo cual hace que el recall sea una métrica crítica.
# ============================================================================
# 2.4 Distribución de variables numéricas
# ============================================================================
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# Histograma de Edad
axes[0].hist(df['Age'].dropna(), bins=30, color='#3498db', edgecolor='white', alpha=0.8)
axes[0].axvline(df['Age'].median(), color='red', linestyle='--', label=f'Mediana: {df["Age"].median():.0f}')
axes[0].set_title('Distribución de Edad', fontsize=13, fontweight='bold')
axes[0].set_xlabel('Edad')
axes[0].set_ylabel('Frecuencia')
axes[0].legend()
# Histograma de Tarifa
axes[1].hist(df['Fare'], bins=30, color='#e67e22', edgecolor='white', alpha=0.8)
axes[1].axvline(df['Fare'].median(), color='red', linestyle='--', label=f'Mediana: {df["Fare"].median():.1f}')
axes[1].set_title('Distribución de Tarifa', fontsize=13, fontweight='bold')
axes[1].set_xlabel('Tarifa (£)')
axes[1].set_ylabel('Frecuencia')
axes[1].legend()
# Distribución por Clase
class_counts = df['Pclass'].value_counts().sort_index()
bars = axes[2].bar(['1ª Clase', '2ª Clase', '3ª Clase'], class_counts.values,
color=['#f1c40f', '#95a5a6', '#cd7f32'], edgecolor='white')
axes[2].set_title('Distribución por Clase', fontsize=13, fontweight='bold')
axes[2].set_ylabel('Cantidad de Pasajeros')
for bar, val in zip(bars, class_counts.values):
axes[2].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5,
str(val), ha='center', fontweight='bold')
plt.tight_layout()
plt.show()
Análisis: La distribución de edades muestra una concentración entre los 20 y 30 años, con una mediana alrededor de 28 años. La tarifa presenta una distribución fuertemente sesgada a la derecha, con la mayoría de pasajeros pagando tarifas bajas y unos pocos con tarifas muy altas (correspondientes a primera clase). La tercera clase concentra la mayor cantidad de pasajeros (491), seguida de primera (216) y segunda clase (184).
# ============================================================================
# 2.5 Supervivencia por variables categóricas clave
# ============================================================================
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# Por Sexo
sns.countplot(data=df, x='Sex', hue='Survived', ax=axes[0], palette=colors)
axes[0].set_title('Supervivencia por Sexo', fontsize=13, fontweight='bold')
axes[0].set_xlabel('Sexo')
axes[0].legend(['No Sobrevivió', 'Sobrevivió'], loc='upper right')
# Por Clase
sns.countplot(data=df, x='Pclass', hue='Survived', ax=axes[1], palette=colors)
axes[1].set_title('Supervivencia por Clase', fontsize=13, fontweight='bold')
axes[1].set_xlabel('Clase del Pasajero')
axes[1].legend(['No Sobrevivió', 'Sobrevivió'], loc='upper right')
# Por Puerto de Embarque
sns.countplot(data=df, x='Embarked', hue='Survived', ax=axes[2], palette=colors)
axes[2].set_title('Supervivencia por Puerto de Embarque', fontsize=13, fontweight='bold')
axes[2].set_xlabel('Puerto (C=Cherbourg, Q=Queenstown, S=Southampton)')
axes[2].legend(['No Sobrevivió', 'Sobrevivió'], loc='upper right')
plt.tight_layout()
plt.show()
Análisis: Se observan patrones claros que validan el contexto histórico del Titanic:
- Sexo: Las mujeres tuvieron una tasa de supervivencia considerablemente mayor que los hombres, reflejando la política "mujeres y niños primero" durante la evacuación.
- Clase: La primera clase tuvo la mayor proporción de sobrevivientes, lo cual tiene sentido logístico: los camarotes de primera clase estaban ubicados más cerca de la cubierta y los botes salvavidas.
- Puerto de embarque: Cherbourg (C) muestra una mayor proporción de sobrevivientes, lo cual podría estar correlacionado con la clase socioeconómica de los pasajeros que embarcaron allí.
# ============================================================================
# 2.6 Boxplot de Edad por Clase y Supervivencia
# ============================================================================
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
sns.boxplot(data=df, x='Pclass', y='Age', hue='Survived', ax=axes[0], palette=colors)
axes[0].set_title('Distribución de Edad por Clase y Supervivencia', fontsize=13, fontweight='bold')
axes[0].set_xlabel('Clase del Pasajero')
axes[0].set_ylabel('Edad')
axes[0].legend(['No Sobrevivió', 'Sobrevivió'])
# Scatter: Edad vs Tarifa
scatter = axes[1].scatter(df['Age'], df['Fare'], c=df['Survived'],
cmap='RdYlGn', alpha=0.5, edgecolors='gray', linewidth=0.5)
axes[1].set_title('Edad vs Tarifa (coloreado por Supervivencia)', fontsize=13, fontweight='bold')
axes[1].set_xlabel('Edad')
axes[1].set_ylabel('Tarifa (£)')
axes[1].legend(*scatter.legend_elements(), title='Survived')
plt.tight_layout()
plt.show()
Análisis: El boxplot confirma que los pasajeros de primera clase tienden a ser mayores y que la primera clase tuvo mayor proporción de sobrevivientes en todos los rangos de edad. En el gráfico de dispersión edad vs. tarifa se observa una relación débil entre ambas variables: la tarifa alta se concentra en primera clase independientemente de la edad, y los puntos verdes (sobrevivientes) se concentran en las tarifas más altas. Esto sugiere que la tarifa no es un predictor independiente fuerte, sino que está mediada por la clase del pasajero.
# ============================================================================
# 2.7 Mapa de correlación
# ============================================================================
plt.figure(figsize=(10, 8))
numeric_cols = df.select_dtypes(include=[np.number]).columns
corr_matrix = df[numeric_cols].corr()
mask = np.triu(np.ones_like(corr_matrix, dtype=bool))
sns.heatmap(corr_matrix, mask=mask, annot=True, fmt='.2f', cmap='coolwarm',
center=0, linewidths=1, square=True, vmin=-1, vmax=1)
plt.title('Mapa de Correlación de Pearson (Variables Numéricas)', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
Análisis: El mapa de correlación muestra que Survived tiene correlación positiva con Fare (0.26) y negativa con Pclass (-0.34). Sin embargo, como advirtió el docente, las correlaciones de Pearson solo capturan relaciones lineales. Una variable puede no tener correlación lineal significativa con Survived y aun así ser un predictor importante en un modelo no lineal como Random Forest. Por eso, la eliminación de variables no debe basarse únicamente en este mapa.
3. Limpieza y Preparación de Datos¶
La preparación de datos es, según Janiesch et al. (2021), la fase que consume más tiempo pero que define el techo del modelo. Como enfatizó el docente en clase: "en unas pocas líneas ya hemos tomado decisiones bien fuertes de eliminar ciertas variables que no tienen ninguna correlación lógica con la variable objetivo".
3.1 Eliminación de Variables Ruidosas¶
Eliminamos las variables que, tras el análisis exploratorio, identificamos como generadoras de ruido:
# ============================================================================
# 3.1 Eliminación de variables ruidosas
# ============================================================================
print('Variables antes de la limpieza:', list(df.columns))
print(f'Dimensiones: {df.shape}')
# Justificación de cada eliminación:
# - PassengerId: ID secuencial sin relación con supervivencia
# - Name: Identificador único (podría extraerse el título, pero se omite por simplicidad)
# - Ticket: Alfanumérico sin relación causal con la variable objetivo
# - Cabin: 77.1% de valores nulos — densidad insuficiente para ser útil
df_clean = df.drop(columns=['PassengerId', 'Name', 'Ticket', 'Cabin'])
print(f'\nVariables después de la limpieza: {list(df_clean.columns)}')
print(f'Dimensiones: {df_clean.shape}')
print(f'\n✅ Se eliminaron 4 variables ruidosas')
Variables antes de la limpieza: ['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'] Dimensiones: (891, 12) Variables después de la limpieza: ['Survived', 'Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked'] Dimensiones: (891, 8) ✅ Se eliminaron 4 variables ruidosas
3.2 Imputación de Valores Nulos¶
Siguiendo la estrategia discutida en clase, imputamos Age con la mediana agrupada por Pclass y Sex (más representativa que la media global), Fare con la media (solo 1 nulo, impacto mínimo) y Embarked con la moda ('S' — Southampton, la más frecuente).
# ============================================================================
# 3.2 Imputación de valores nulos
# ============================================================================
print('Valores nulos ANTES de imputación:')
print(df_clean.isnull().sum())
# Age: imputar mediana por grupo (Pclass + Sex)
# Esta estrategia preserva la distribución de edad dentro de cada subgrupo
age_medians = df_clean.groupby(['Pclass', 'Sex'])['Age'].median()
print('\nMedianas de edad por grupo (Pclass, Sex):')
print(age_medians)
for pclass in [1, 2, 3]:
for sex in ['male', 'female']:
median_age = age_medians[pclass][sex]
mask = (df_clean['Pclass'] == pclass) & (df_clean['Sex'] == sex) & (df_clean['Age'].isnull())
df_clean.loc[mask, 'Age'] = median_age
# Fare: imputar media global (1 solo nulo)
df_clean['Fare'].fillna(df_clean['Fare'].mean(), inplace=True)
# Embarked: imputar moda ('S' = Southampton)
df_clean['Embarked'].fillna(df_clean['Embarked'].mode()[0], inplace=True)
print('\nValores nulos DESPUÉS de imputación:')
print(df_clean.isnull().sum())
print('\n✅ Todos los valores nulos han sido tratados')
Valores nulos ANTES de imputación:
Survived 0
Pclass 0
Sex 0
Age 177
SibSp 0
Parch 0
Fare 0
Embarked 2
dtype: int64
Medianas de edad por grupo (Pclass, Sex):
Pclass Sex
1 female 35.0
male 40.0
2 female 28.0
male 30.0
3 female 21.5
male 25.0
Name: Age, dtype: float64
Valores nulos DESPUÉS de imputación:
Survived 0
Pclass 0
Sex 0
Age 0
SibSp 0
Parch 0
Fare 0
Embarked 0
dtype: int64
✅ Todos los valores nulos han sido tratados
4. Ingeniería de Variables (Feature Engineering)¶
La ingeniería de variables es la creación de nuevas columnas derivadas que pueden capturar patrones no evidentes en las variables originales. Como presentó el docente en la clase, combinar SibSp y Parch en una sola variable family_size simplifica el dataset y agrega contexto: un pasajero que viajaba solo pudo haber tenido menos apoyo durante la evacuación.
# ============================================================================
# 4.1 Creación de variables derivadas
# ============================================================================
# family_size: SibSp + Parch + 1 (incluyendo al propio pasajero)
df_clean['FamilySize'] = df_clean['SibSp'] + df_clean['Parch'] + 1
# is_alone: 1 si el pasajero viajó solo (family_size == 1)
df_clean['IsAlone'] = (df_clean['FamilySize'] == 1).astype(int)
print('Distribución de FamilySize:')
print(df_clean['FamilySize'].value_counts().sort_index())
print(f'\nPasajeros solos: {df_clean["IsAlone"].sum()} ({df_clean["IsAlone"].mean()*100:.1f}%)')
print(f'Pasajeros acompañados: {(df_clean["IsAlone"]==0).sum()} ({(1-df_clean["IsAlone"].mean())*100:.1f}%)')
Distribución de FamilySize: FamilySize 1 537 2 161 3 102 4 29 5 15 6 22 7 12 8 6 11 7 Name: count, dtype: int64 Pasajeros solos: 537 (60.3%) Pasajeros acompañados: 354 (39.7%)
# ============================================================================
# 4.2 Codificación de variables categóricas
# ============================================================================
# Sex: codificación binaria (female=1, male=0)
df_clean['Sex'] = LabelEncoder().fit_transform(df_clean['Sex'])
# Embarked: codificación One-Hot
df_clean = pd.get_dummies(df_clean, columns=['Embarked'], drop_first=True, dtype=int)
print('Dataset preparado para entrenamiento:')
print(f'Dimensiones: {df_clean.shape}')
print(f'Columnas: {list(df_clean.columns)}')
df_clean.head()
Dataset preparado para entrenamiento: Dimensiones: (891, 11) Columnas: ['Survived', 'Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'FamilySize', 'IsAlone', 'Embarked_Q', 'Embarked_S']
| Survived | Pclass | Sex | Age | SibSp | Parch | Fare | FamilySize | IsAlone | Embarked_Q | Embarked_S | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 3 | 1 | 22.0 | 1 | 0 | 7.2500 | 2 | 0 | 0 | 1 |
| 1 | 1 | 1 | 0 | 38.0 | 1 | 0 | 71.2833 | 2 | 0 | 0 | 0 |
| 2 | 1 | 3 | 0 | 26.0 | 0 | 0 | 7.9250 | 1 | 1 | 0 | 1 |
| 3 | 1 | 1 | 0 | 35.0 | 1 | 0 | 53.1000 | 2 | 0 | 0 | 1 |
| 4 | 0 | 3 | 1 | 35.0 | 0 | 0 | 8.0500 | 1 | 1 | 0 | 1 |
5. División en Conjuntos de Entrenamiento y Prueba¶
Respuesta a la Pregunta Orientadora 1¶
Los conjuntos de entrenamiento y evaluación son particiones del dataset original que cumplen funciones complementarias:
- Training set (80%): Porción de los datos que el modelo utiliza para ajustar sus parámetros internos, aprendiendo las relaciones entre las features (variables independientes) y la variable objetivo (Bi et al., 2019).
- Test set (20%): Porción reservada que el modelo nunca ve durante el entrenamiento, y que sirve para medir su capacidad de generalización — es decir, qué tan bien puede predecir resultados en datos nuevos.
Respuesta a la Pregunta Orientadora 2¶
La selección utiliza estratificación (stratify=y) para mantener la misma proporción de clases (61.6% / 38.4%) en ambos subconjuntos. Además, se fija una semilla aleatoria (random_state=42) para garantizar la reproducibilidad del experimento (Janiesch et al., 2021).
# ============================================================================
# 5. División train/test estratificada
# ============================================================================
# Separar features (X) y variable objetivo (y)
X = df_clean.drop(columns=['Survived'])
y = df_clean['Survived']
# División 80/20 con estratificación
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
print('✅ División train/test realizada')
print(f'\n Training set: {X_train.shape[0]} filas ({X_train.shape[0]/len(X)*100:.0f}%)')
print(f' Test set: {X_test.shape[0]} filas ({X_test.shape[0]/len(X)*100:.0f}%)')
print(f'\n Distribución de clases en training:')
print(f' No sobrevivió: {(y_train==0).sum()} ({(y_train==0).mean()*100:.1f}%)')
print(f' Sobrevivió: {(y_train==1).sum()} ({(y_train==1).mean()*100:.1f}%)')
print(f'\n Distribución de clases en test:')
print(f' No sobrevivió: {(y_test==0).sum()} ({(y_test==0).mean()*100:.1f}%)')
print(f' Sobrevivió: {(y_test==1).sum()} ({(y_test==1).mean()*100:.1f}%)')
print(f'\n Features: {list(X.columns)}')
✅ División train/test realizada
Training set: 712 filas (80%)
Test set: 179 filas (20%)
Distribución de clases en training:
No sobrevivió: 439 (61.7%)
Sobrevivió: 273 (38.3%)
Distribución de clases en test:
No sobrevivió: 110 (61.5%)
Sobrevivió: 69 (38.5%)
Features: ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'FamilySize', 'IsAlone', 'Embarked_Q', 'Embarked_S']
6. Entrenamiento de Modelos¶
Entrenamos dos algoritmos de clasificación para poder comparar su rendimiento y justificar la elección del modelo final:
Random Forest Classifier: Algoritmo basado en ensamble de árboles de decisión. Combina múltiples árboles entrenados en subconjuntos aleatorios de datos y features, reduciendo el sobreajuste (Bi et al., 2019). Es robusto ante variables con diferentes escalas y maneja bien las interacciones no lineales.
Logistic Regression: Modelo lineal clásico para clasificación binaria. Calcula la probabilidad de pertenecer a cada clase mediante la función sigmoide. Es interpretable, rápido y sirve como baseline sólido (Janiesch et al., 2021).
# ============================================================================
# 6. Entrenamiento de dos modelos
# ============================================================================
# Modelo 1: Random Forest
rf_model = RandomForestClassifier(n_estimators=100, random_state=42, max_depth=5)
rf_model.fit(X_train, y_train)
y_pred_rf = rf_model.predict(X_test)
y_prob_rf = rf_model.predict_proba(X_test)[:, 1]
print('✅ Modelo 1: Random Forest entrenado')
# Modelo 2: Regresión Logística
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train, y_train)
y_pred_lr = lr_model.predict(X_test)
y_prob_lr = lr_model.predict_proba(X_test)[:, 1]
print('✅ Modelo 2: Regresión Logística entrenada')
✅ Modelo 1: Random Forest entrenado ✅ Modelo 2: Regresión Logística entrenada
7. Evaluación de Modelos¶
La evaluación es donde demostramos si el modelo ha aprendido patrones generalizables. Como explicó el docente, no basta con reportar una única métrica: debemos presentar un panorama completo que incluya accuracy, precision, recall, F1-score, matriz de confusión y curva ROC.
7.1 Métricas de Evaluación Comparativas¶
# ============================================================================
# 7.1 Tabla comparativa de métricas
# ============================================================================
def calcular_metricas(y_true, y_pred, y_prob):
"""Calcula todas las métricas de evaluación para un modelo."""
fpr, tpr, _ = roc_curve(y_true, y_prob)
return {
'Accuracy': accuracy_score(y_true, y_pred),
'Precision': precision_score(y_true, y_pred),
'Recall': recall_score(y_true, y_pred),
'F1-Score': f1_score(y_true, y_pred),
'AUC-ROC': auc(fpr, tpr)
}
metricas_rf = calcular_metricas(y_test, y_pred_rf, y_prob_rf)
metricas_lr = calcular_metricas(y_test, y_pred_lr, y_prob_lr)
# Tabla comparativa
comparativa = pd.DataFrame({
'Random Forest': metricas_rf,
'Regresión Logística': metricas_lr
}).T
print('='*70)
print('TABLA COMPARATIVA DE MÉTRICAS DE EVALUACIÓN')
print('='*70)
print(comparativa.round(4).to_string())
print('\n' + '='*70)
# Determinar el ganador por F1-Score
ganador = 'Random Forest' if metricas_rf['F1-Score'] > metricas_lr['F1-Score'] else 'Regresión Logística'
print(f'\n🏆 Modelo con mejor F1-Score: {ganador}')
======================================================================
TABLA COMPARATIVA DE MÉTRICAS DE EVALUACIÓN
======================================================================
Accuracy Precision Recall F1-Score AUC-ROC
Random Forest 0.8212 0.8491 0.6522 0.7377 0.8498
Regresión Logística 0.8101 0.7869 0.6957 0.7385 0.8484
======================================================================
🏆 Modelo con mejor F1-Score: Regresión Logística
7.2 Matrices de Confusión¶
La matriz de confusión desglosa las predicciones en cuatro categorías:
- Verdaderos Negativos (TN): Predijo "no sobrevivió" y acertó
- Falsos Positivos (FP): Predijo "sobrevivió" pero falleció
- Falsos Negativos (FN): Predijo "no sobrevivió" pero sobrevivió (error más grave en contexto médico)
- Verdaderos Positivos (TP): Predijo "sobrevivió" y acertó
# ============================================================================
# 7.2 Matrices de confusión
# ============================================================================
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Random Forest
ConfusionMatrixDisplay.from_predictions(
y_test, y_pred_rf, ax=axes[0],
display_labels=['No Sobrevivió', 'Sobrevivió'],
cmap='Blues', colorbar=False
)
axes[0].set_title('Matriz de Confusión — Random Forest', fontsize=13, fontweight='bold')
# Regresión Logística
ConfusionMatrixDisplay.from_predictions(
y_test, y_pred_lr, ax=axes[1],
display_labels=['No Sobrevivió', 'Sobrevivió'],
cmap='Oranges', colorbar=False
)
axes[1].set_title('Matriz de Confusión — Regresión Logística', fontsize=13, fontweight='bold')
plt.tight_layout()
plt.show()
7.3 Classification Report¶
# ============================================================================
# 7.3 Classification Reports detallados
# ============================================================================
print('='*60)
print('CLASSIFICATION REPORT — RANDOM FOREST')
print('='*60)
print(classification_report(y_test, y_pred_rf, target_names=['No Sobrevivió', 'Sobrevivió']))
print('\n' + '='*60)
print('CLASSIFICATION REPORT — REGRESIÓN LOGÍSTICA')
print('='*60)
print(classification_report(y_test, y_pred_lr, target_names=['No Sobrevivió', 'Sobrevivió']))
============================================================
CLASSIFICATION REPORT — RANDOM FOREST
============================================================
precision recall f1-score support
No Sobrevivió 0.81 0.93 0.86 110
Sobrevivió 0.85 0.65 0.74 69
accuracy 0.82 179
macro avg 0.83 0.79 0.80 179
weighted avg 0.82 0.82 0.82 179
============================================================
CLASSIFICATION REPORT — REGRESIÓN LOGÍSTICA
============================================================
precision recall f1-score support
No Sobrevivió 0.82 0.88 0.85 110
Sobrevivió 0.79 0.70 0.74 69
accuracy 0.81 179
macro avg 0.80 0.79 0.79 179
weighted avg 0.81 0.81 0.81 179
7.4 Curvas ROC (Receiver Operating Characteristic)¶
La curva ROC visualiza la capacidad discriminativa del modelo. El área bajo la curva (AUC) varía entre 0.5 (modelo aleatorio) y 1.0 (clasificador perfecto). Un AUC > 0.8 se considera bueno para problemas de clasificación binaria.
# ============================================================================
# 7.4 Curvas ROC comparativas
# ============================================================================
fig, ax = plt.subplots(figsize=(10, 7))
# Random Forest
fpr_rf, tpr_rf, _ = roc_curve(y_test, y_prob_rf)
auc_rf = auc(fpr_rf, tpr_rf)
ax.plot(fpr_rf, tpr_rf, color='#2ecc71', linewidth=2.5,
label=f'Random Forest (AUC = {auc_rf:.4f})')
# Regresión Logística
fpr_lr, tpr_lr, _ = roc_curve(y_test, y_prob_lr)
auc_lr = auc(fpr_lr, tpr_lr)
ax.plot(fpr_lr, tpr_lr, color='#e67e22', linewidth=2.5,
label=f'Regresión Logística (AUC = {auc_lr:.4f})')
# Línea de referencia (clasificador aleatorio)
ax.plot([0, 1], [0, 1], 'k--', linewidth=1, alpha=0.5, label='Clasificador Aleatorio (AUC = 0.5000)')
ax.set_xlabel('Tasa de Falsos Positivos (FPR)', fontsize=12)
ax.set_ylabel('Tasa de Verdaderos Positivos (TPR)', fontsize=12)
ax.set_title('Curvas ROC — Comparación de Modelos', fontsize=14, fontweight='bold')
ax.legend(loc='lower right', fontsize=11)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
7.5 Importancia de Variables (Random Forest)¶
# ============================================================================
# 7.5 Importancia de variables (Random Forest)
# ============================================================================
importances = pd.Series(rf_model.feature_importances_, index=X.columns).sort_values(ascending=True)
plt.figure(figsize=(10, 6))
importances.plot(kind='barh', color='#3498db', edgecolor='white')
plt.title('Importancia de Variables — Random Forest', fontsize=14, fontweight='bold')
plt.xlabel('Importancia Relativa')
plt.tight_layout()
plt.show()
print('\nRanking de importancia:')
for i, (feat, imp) in enumerate(importances.sort_values(ascending=False).items(), 1):
print(f' {i}. {feat}: {imp:.4f}')
Ranking de importancia: 1. Sex: 0.4112 2. Fare: 0.1701 3. Pclass: 0.1384 4. Age: 0.1205 5. FamilySize: 0.0613 6. SibSp: 0.0248 7. Embarked_S: 0.0236 8. IsAlone: 0.0194 9. Parch: 0.0183 10. Embarked_Q: 0.0123
Análisis de la importancia de variables: El ranking confirma las hipótesis derivadas de la exploración inicial y del contexto histórico del Titanic. Sex (sexo del pasajero) resulta la variable más determinante, seguida de Fare (tarifa pagada, proxy de la clase socioeconómica) y Age (edad). Esto es coherente con la política de evacuación "mujeres y niños primero" y con la ubicación de los camarotes de primera clase cerca de la cubierta.
8. Predicción con Nuevo Caso¶
Para validar el modelo en un escenario práctico, simulamos la predicción para un pasajero hipotético.
# ============================================================================
# 8. Predicción con un nuevo pasajero
# ============================================================================
# Pasajero hipotético: mujer de 28 años, 2ª clase, viajando sola, embarcó en Cherbourg
nuevo_pasajero = pd.DataFrame({
'Pclass': [2],
'Sex': [1], # 1 = male (LabelEncoder: female=0, male=1)
'Age': [28],
'SibSp': [0],
'Parch': [0],
'Fare': [13.0],
'FamilySize': [1],
'IsAlone': [1],
'Embarked_Q': [0],
'Embarked_S': [0]
})
# Predicción con ambos modelos
pred_rf = rf_model.predict(nuevo_pasajero)[0]
prob_rf = rf_model.predict_proba(nuevo_pasajero)[0]
pred_lr = lr_model.predict(nuevo_pasajero)[0]
prob_lr = lr_model.predict_proba(nuevo_pasajero)[0]
print('='*60)
print('PREDICCIÓN PARA NUEVO PASAJERO')
print('='*60)
print('\nDatos del pasajero:')
print(' Clase: 2ª | Sexo: Masculino | Edad: 28')
print(' Viaja solo | Tarifa: £13.0 | Embarcó en: Cherbourg')
print(f'\n Random Forest: {"Sobrevivió ✅" if pred_rf == 1 else "No sobrevivió ❌"}')
print(f' Probabilidad: No={prob_rf[0]:.2%} | Sí={prob_rf[1]:.2%}')
print(f'\n Regresión Logística: {"Sobrevivió ✅" if pred_lr == 1 else "No sobrevivió ❌"}')
print(f' Probabilidad: No={prob_lr[0]:.2%} | Sí={prob_lr[1]:.2%}')
============================================================
PREDICCIÓN PARA NUEVO PASAJERO
============================================================
Datos del pasajero:
Clase: 2ª | Sexo: Masculino | Edad: 28
Viaja solo | Tarifa: £13.0 | Embarcó en: Cherbourg
Random Forest: No sobrevivió ❌
Probabilidad: No=81.09% | Sí=18.91%
Regresión Logística: No sobrevivió ❌
Probabilidad: No=71.45% | Sí=28.55%
9. Conclusiones¶
9.1 Sobre el Ciclo de Vida de ML¶
El ejercicio del Titanic nos permitió recorrer completo el ciclo de vida de un modelo de Machine Learning, desde la obtención de datos brutos hasta la predicción con un caso nuevo. Cada fase requirió decisiones analíticas fundamentadas:
- Obtención: Se eligió un dataset público y reproducible, cargado desde URL para eliminar dependencias de archivos locales.
- Exploración: Las visualizaciones revelaron el desbalance de clases (61.6% vs 38.4%) y las variables más discriminativas (Sex, Pclass).
- Limpieza: Se eliminaron 4 variables ruidosas y se imputaron valores nulos con estrategias diferenciadas por contexto (mediana agrupada para edad, media para tarifa, moda para embarque).
- Feature Engineering: La creación de
FamilySizeeIsAlonesintetizó información dispersa en variables más informativas. - Entrenamiento: Se compararon dos algoritmos con filosofías distintas: uno basado en ensamble de árboles (Random Forest) y otro lineal (Regresión Logística).
- Evaluación: Se utilizaron múltiples métricas complementarias, no solo accuracy, para tener una visión integral del rendimiento.
9.2 Sobre los Conjuntos de Entrenamiento y Evaluación¶
La división estratificada 80/20 garantizó que las proporciones de clase se mantuvieran en ambos subconjuntos, permitiendo una evaluación justa. Sin esta estratificación, el test set podría tener una distribución diferente al training set, lo que distorsionaría las métricas de evaluación.
9.3 Sobre las Métricas¶
En un problema con desbalance moderado como el Titanic, la elección de la métrica adecuada es crucial. Si solo miráramos accuracy, ambos modelos parecen similares. Sin embargo, el classification report y la curva ROC revelan diferencias en la capacidad de detectar sobrevivientes (recall) y en la discriminación global (AUC). Como explicó el docente, en contextos donde el costo de un falso negativo es alto (ej. diagnóstico médico), el recall sería la métrica prioritaria.
9.4 Reflexión Personal¶
Desde mi experiencia trabajando con herramientas de análisis de datos en la Rama Judicial colombiana, he podido comprobar que el mayor desafío no está en el código sino en la interpretación de resultados. Un modelo con 80% de accuracy puede parecer excelente en el papel, pero si está fallando sistemáticamente en los casos que más importan (como tutelas urgentes o pacientes enfermos), entonces las métricas globales son engañosas. Este ejercicio refuerza la importancia de contextualizar siempre los resultados analíticos.
Referencias Bibliográficas¶
Bi, Q., Goodman, K. E., Kaminsky, J., & Lessler, J. (2019). What is Machine Learning? A Primer for the Epidemiologist. American Journal of Epidemiology, 188(12), 2222–2239. https://doi.org/10.1093/aje/kwz189
Janiesch, C., Zschech, P., & Heinrich, K. (2021). Machine learning and deep learning. Electronic Markets, 31, 685-695. https://doi.org/10.1007/s12525-021-00475-2
Rothman, D. (2018). Become an adaptive thinker. En Artificial intelligence by example: Develop machine intelligence from scratch using real artificial intelligence use cases (pp. 8-39). Packt Publishing.
Nota sobre el uso de IA: En la elaboración de este notebook se utilizó IA generativa como herramienta de apoyo para la corrección de estilo y revisión de redacción. Todos los análisis, interpretaciones y conclusiones son de autoría del equipo de trabajo.