Predicción de la Supervivencia de los Pasajeros del Titanic 🚢

Predicción de la Supervivencia de los Pasajeros del Titanic 🚢

Juan Gabriel Gomila Juan Gabriel Gomila
31 minutos

Leer el artículo
Audio generated by DropInBlog's Blog Voice AI™ may have slight pronunciation nuances. Learn more

En este artículo, repasaremos todo el proceso de creación de un modelo de aprendizaje automático utilizando el famoso conjunto de datos del Titanic, utilizado por personas de todo el mundo. Este conjunto de datos proporciona información sobre el destino de los pasajeros del Titanic, resumida según su estatus económico (clase), sexo, edad y supervivencia.

Inicialmente aprendí acerca de este problema en una competición de kaggle.com, titulada “Titanic: Machine Learning from Disaster”. En este desafío, se nos pedía predecir si un pasajero del Titanic habría sobrevivido o no. Aquí os voy a detallar un análisis completo de cómo lo hice.


RMS Titanic ⚓

El RMS Titanic fue un transatlántico británico que se hundió en el océano Atlántico norte en las primeras horas de la mañana del 15 de abril de 1912, después de chocar con un iceberg durante su viaje inaugural de Southampton a Nueva York. Se estima que había 2,224 pasajeros y tripulantes a bordo del Titanic, y más de 1,500 murieron, lo que lo convierte en uno de los desastres marítimos comerciales más mortales de la historia moderna. 

El Titanic fue el barco más grande en servicio en el momento en que entró en funcionamiento y fue el segundo de tres transatlánticos de la clase olímpica operados por la White Star Line. Fue construido en el astillero Harland and Wolff en Belfast. Thomas Andrews, su arquitecto, murió en el desastre.


Importación de las librerías 📚

# Álgebra lineal
import numpy as np # Procesamiento de datos import pandas as pd # Visualización de datos import seaborn as sns %matplotlib inline from matplotlib import pyplot as plt from matplotlib import style # Algoritmos from sklearn import linear_model from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import Perceptron from sklearn.linear_model import SGDClassifier from sklearn.tree import DecisionTreeClassifier from sklearn.neighbors import KNeighborsClassifier from sklearn.svm import SVC, LinearSVC from sklearn.naive_bayes import GaussianNB


Obtención de los Datos 📊

test_df = pd.read_csv("test.csv")
train_df = pd.read_csv("train.csv")


Exploración y Análisis de los Datos 🔍

train_df.info()

El conjunto de entrenamiento tiene 891 ejemplos y 11 características más la variable objetivo (survived). 2 de las características son flotantes, 5 son enteros y 5 son objetos. A continuación, se describen brevemente las características:

  • survival: Supervivencia

  • PassengerId: ID único de un pasajero.

  • pclass: Clase del billete

  • sex: Sexo

  • Age: Edad en años

  • sibsp: Número de hermanos/esposos a bordo

  • parch: Número de padres/hijos a bordo

  • ticket: Número de billete

  • fare: Tarifa del pasajero

  • cabin: Número de la cabina

  • embarked: Puerto de embarque

train_df.describe()

En la tabla anterior, podemos ver que el 38% del conjunto de entrenamiento, los pasajeros del Titanic sobrevivieron a la tragedia. También observamos que las edades de los pasajeros oscilan entre 0.4 y 80 años. Además, ya podemos detectar algunas características que contienen valores faltantes, como la característica 'Age'.

train_df.head(8)

En la tabla anterior, podemos observar que necesitamos convertir muchas características en variables numéricas para que los algoritmos de aprendizaje automático puedan procesarlas. También notamos que las características tienen rangos muy diferentes, lo que requerirá que las pongamos en una escala más uniforme. También se pueden detectar más características con valores faltantes (NaN = not a number), que necesitamos abordar.


Análisis de los Datos Faltantes ❌

total = train_df.isnull().sum().sort_values(ascending=False) percent_1 = train_df.isnull().sum()/train_df.isnull().count()*100 percent_2 = (round(percent_1, 1)).sort_values(ascending=False) missing_data = pd.concat([total, percent_2], axis=1, keys=['Total', '%']) missing_data.head(5)

La característica Embarked tiene solo 2 valores faltantes, lo cual es fácil de llenar. Será mucho más complicado tratar con la característica 'Age', que tiene 177 valores faltantes. La característica 'Cabin' necesita más investigación, pero parece que podríamos querer eliminarla del conjunto de datos, ya que el 77% de los valores están faltantes.

train_df.columns.values


En la imagen anterior puedes ver las 11 características más la variable objetivo (survived). ¿Qué características podrían contribuir a una mayor tasa de supervivencia de los pasajeros del Titanic? A mí me parece lógico que todas, excepto ‘PassengerId’, ‘Ticket’ y ‘Name’, estén correlacionadas con una mayor probabilidad de supervivencia de los pasajeros del Titanic.

Edad y Sexo 👶👩👨

survived = 'survived'
not_survived = 'not survived' fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(10, 4)) women = train_df[train_df['Sex'] == 'female'] men = train_df[train_df['Sex'] == 'male'] ax = sns.distplot(women[women['Survived'] == 1].Age.dropna(), bins=18, label=survived, ax=axes[0], kde=False) ax = sns.distplot(women[women['Survived'] == 0].Age.dropna(), bins=40, label=not_survived, ax=axes[0], kde=False) ax.legend() ax.set_title('Mujeres') ax = sns.distplot(men[men['Survived'] == 1].Age.dropna(), bins=18, label=survived, ax=axes[1], kde=False) ax = sns.distplot(men[men['Survived'] == 0].Age.dropna(), bins=40, label=not_survived, ax=axes[1], kde=False) ax.legend() _ = ax.set_title('Hombres')

Como podemos ver, los hombres tienen una alta probabilidad de supervivencia cuando tienen entre 18 y 30 años, lo cual también es algo cierto para las mujeres, pero no completamente. Para las mujeres, las probabilidades de supervivencia son mayores entre los 14 y 40 años. Por otro lado, los hombres tienen una probabilidad de supervivencia muy baja entre 5 y 18 años, pero esto no es cierto para las mujeres. Además, los infantes también tienen algo más de probabilidad de sobrevivir.

Como parece que hay edades específicas que aumentan las probabilidades de supervivencia en los pasajeros del Titanic y debido a que quiero que todas las características estén en una escala similar, crearé grupos de edades más tarde.

Embarque, Clase y Sexo ⚓👩‍✈️

FacetGrid = sns.FacetGrid(train_df, row='Embarked', size=4.5, aspect=1.6) FacetGrid.map(sns.pointplot, 'Pclass', 'Survived', 'Sex', palette=None, order=None, hue_order=None) FacetGrid.add_legend()

El puerto de embarque de los pasajeros del Titanic parece estar correlacionado con la supervivencia, dependiendo del sexo. Las mujeres en el puerto Q y el puerto S tienen una mayor posibilidad de supervivencia, mientras que es inverso en el puerto C. Los hombres tienen una alta probabilidad de supervivencia si están en el puerto C, pero una baja probabilidad en los puertos Q y S.

4.Pclass (Clase del Pasajero del Titanic) 🏷️

sns.barplot(x='Pclass', y='Survived', data=train_df)

Aquí podemos ver claramente que la clase del pasajero (Pclass) del Titanic contribuye a las probabilidades de supervivencia, especialmente si la persona está en la clase 1. A continuación, crearemos otro gráfico que muestre cómo varía la supervivencia según la clase.

grid = sns.FacetGrid(train_df, col='Survived', row='Pclass', size=2.2, aspect=1.6) grid.map(plt.hist, 'Age', alpha=.5, bins=20) grid.add_legend()

El gráfico anterior confirma nuestra suposición sobre la Pclass 1, pero también podemos observar una alta probabilidad de que una persona en Pclass 3 no sobreviva.

SibSp y Parch (Hermanos y Cónyuges / Padres e Hijos) 👨‍👩‍👧‍👦

Las características SibSp (número de hermanos y cónyuges a bordo) y Parch (número de padres e hijos a bordo) tienen más sentido como una característica combinada que muestra el número total de familiares de una persona en el Titanic. A continuación, crearé esta nueva característica y también una variable que indica si alguien no estaba solo en el barco Titanic .

data = [train_df, test_df] for dataset in data: dataset['relatives'] = dataset['SibSp'] + dataset['Parch'] dataset.loc[dataset['relatives'] > 0, 'not_alone'] = 0 dataset.loc[dataset['relatives'] == 0, 'not_alone'] = 1 dataset['not_alone'] = dataset['not_alone'].astype(int) train_df['not_alone'].value_counts()

A continuación, se muestra el gráfico que refleja la relación entre el número de familiares y la supervivencia:

axes = sns.factorplot('relatives', 'Survived', data=train_df, aspect=2.5)

Aquí podemos ver que tenías una mayor probabilidad de sobrevivir con entre 1 y 3 familiares, pero una probabilidad menor si tenías menos de 1 o más de 3 (excepto en algunos casos con 6 familiares). Este análisis sugiere que los pasajeros del Titanic que tienen una familia a bordo puede influir en las probabilidades de supervivencia, aunque el impacto varía según la cantidad de familiares.


Preprocesamiento de los Datos 🛠️

Primero, eliminaré ‘PassengerId’ del conjunto de entrenamiento, ya que no contribuye a la probabilidad de supervivencia de una persona. No lo eliminaré del conjunto de prueba, ya que es necesario para la presentación de resultados.

train_df = train_df.drop(['PassengerId'], axis=1)

Tratamiento de los Valores Faltantes 🛠️

Cabina: Como recordatorio, debemos tratar los valores faltantes de Cabin (687), Embarked (2) y Age (177). Primero pensé que tendríamos que eliminar la variable ‘Cabin’, pero después encontré algo interesante. Un número de cabina parece como ‘C123’, y la letra se refiere al puente. Por lo tanto, vamos a extraer estos valores y crear una nueva característica que contenga el puente de una persona. Posteriormente, convertiremos esta variable en un valor numérico. Los valores faltantes se convertirán en 0.

import re
deck = {"A": 1, "B": 2, "C": 3, "D": 4, "E": 5, "F": 6, "G": 7, "U": 8} data = [train_df, test_df] for dataset in data: dataset['Cabin'] = dataset['Cabin'].fillna("U0") dataset['Deck'] = dataset['Cabin'].map(lambda x: re.compile("([a-zA-Z]+)").search(x).group()) dataset['Deck'] = dataset['Deck'].map(deck) dataset['Deck'] = dataset['Deck'].fillna(0) dataset['Deck'] = dataset['Deck'].astype(int) train_df = train_df.drop(['Cabin'], axis=1) test_df = test_df.drop(['Cabin'], axis=1)

Edad: Ahora podemos abordar el problema de los valores faltantes en la edad. Crearé una matriz que contenga valores aleatorios, calculados a partir de la media de la edad en función de la desviación estándar y los valores nulos.

for dataset in data:
mean = train_df["Age"].mean() std = test_df["Age"].std() is_null = dataset["Age"].isnull().sum() rand_age = np.random.randint(mean - std, mean + std, size=is_null) age_slice = dataset["Age"].copy() age_slice[np.isnan(age_slice)] = rand_age dataset["Age"] = age_slice dataset["Age"] = train_df["Age"].astype(int) train_df["Age"].isnull().sum() # Devuelve 0 ahora

Embarque: Como la característica Embarked solo tiene 2 valores faltantes, simplemente los llenaremos con el valor más común.

common_value = 'S'
for dataset in data: dataset['Embarked'] = dataset['Embarked'].fillna(common_value)

Conversión de Características 🔄

train_df.info()

Arriba se puede ver que «Tarifa» es un valor decimal (float) y que debemos lidiar con cuatro características categóricas: Nombre, Sexo, Ticket y Embarque. Investiguemos y transformemos uno tras otro.

Nombre y Títulos de los Pasajeros 🏷️

Vamos a utilizar la característica Nombre para extraer los títulos del nombre, de modo que podamos construir una nueva característica a partir de esta información. A continuación, definimos los títulos y asignamos un valor numérico a cada uno:

data = [train_df, test_df] titles = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}

Luego, para cada conjunto de datos, extraemos los títulos de los nombres, reemplazamos algunos títulos menos comunes por el valor "Rare", y convertimos los títulos en valores numéricos:

for dataset in data: # Extraer títulos dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False) # Reemplazar títulos raros por un valor común o "Rare" dataset['Title'] = dataset['Title'].replace(['Lady', 'Countess', 'Capt', 'Col', 'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare') dataset['Title'] = dataset['Title'].replace('Mlle', 'Miss') dataset['Title'] = dataset['Title'].replace('Ms', 'Miss') dataset['Title'] = dataset['Title'].replace('Mme', 'Mrs') # Convertir títulos en números dataset['Title'] = dataset['Title'].map(titles) # Llenar NaN con 0 para seguridad dataset['Title'] = dataset['Title'].fillna(0)

Finalmente, eliminamos la columna 'Name' del conjunto de datos, ya que ahora la información relevante sobre el título está representada en la nueva característica 'Title':

train_df = train_df.drop(['Name'], axis=1) test_df = test_df.drop(['Name'], axis=1)

Este proceso convierte los nombres en títulos útiles, que pueden ser una buena característica para predecir la supervivencia de los pasajeros. Usamos títulos como Mr, Miss, Mrs, etc., y les asignamos valores numéricos para su procesamiento posterior.

Fare: Convertiremos ‘Fare’ de flotante a entero.

for dataset in data:
dataset['Fare'] = dataset['Fare'].fillna(0) dataset['Fare'] = dataset['Fare'].astype(int)

Sexo: Convertimos ‘Sex’ a un valor numérico.

genders = {"male": 0, "female": 1}
for dataset in data: dataset['Sex'] = dataset['Sex'].map(genders)

Ticket: Dado que el atributo ‘Ticket’ tiene 681 tickets únicos, será complicado convertirlos en categorías útiles. Así que lo eliminaremos del conjunto de datos.

train_df = train_df.drop(['Ticket'], axis=1)
test_df = test_df.drop(['Ticket'], axis=1)

Embarked: Convertiremos el Puerto de ‘Embarked’ de los pasajeros del Titanic de dato categórico a numérico.

ports = {"S": 0, "C": 1, "Q": 2}
data = [train_df, test_df]

for dataset in data:
    dataset['Embarked'] = dataset['Embarked'].map(ports)

Creación de Categorías en las Características 🏷️

Edad (Age) 👶👴

Primero, vamos a convertir la característica ‘Age’. La convertiremos de flotante a entero y luego crearemos una nueva variable llamada ‘AgeGroup’, clasificando las edades en grupos. Es importante prestar atención a cómo formamos estos grupos, ya que no queremos que el 80% de los datos caigan en el primer grupo. Aquí te mostramos cómo hacerlo:

data = [train_df, test_df] for dataset in data: dataset['Age'] = dataset['Age'].astype(int) # Crear grupos de edad dataset.loc[dataset['Age'] <= 11, 'Age'] = 0 dataset.loc[(dataset['Age'] > 11) & (dataset['Age'] <= 18), 'Age'] = 1 dataset.loc[(dataset['Age'] > 18) & (dataset['Age'] <= 22), 'Age'] = 2 dataset.loc[(dataset['Age'] > 22) & (dataset['Age'] <= 27), 'Age'] = 3 dataset.loc[(dataset['Age'] > 27) & (dataset['Age'] <= 33), 'Age'] = 4 dataset.loc[(dataset['Age'] > 33) & (dataset['Age'] <= 40), 'Age'] = 5 dataset.loc[(dataset['Age'] > 40) & (dataset['Age'] <= 66), 'Age'] = 6 dataset.loc[dataset['Age'] > 66, 'Age'] = 6

Este proceso divide la variable ‘Age’ en grupos de edad, lo que facilita el análisis y el aprendizaje automático. Ahora podemos ver cómo se distribuyen los grupos de edad en el conjunto de entrenamiento:

train_df['Age'].value_counts()

Tarifa (Fare) 💵

Para la característica ‘Fare’ (tarifa), necesitamos hacer lo mismo que con ‘Age’. Sin embargo, esto no es tan sencillo, porque si dividimos el rango de valores de la tarifa en categorías grandes, el 80% de los valores caerían en la primera categoría.

Afortunadamente, podemos utilizar la función “qcut()” de sklearn para dividir las tarifas en categorías de tamaño más equilibrado. Así formamos las categorías de la siguiente manera:

data = [train_df, test_df] for dataset in data: dataset.loc[dataset['Fare'] <= 7.91, 'Fare'] = 0 dataset.loc[(dataset['Fare'] > 7.91) & (dataset['Fare'] <= 14.454), 'Fare'] = 1 dataset.loc[(dataset['Fare'] > 14.454) & (dataset['Fare'] <= 31), 'Fare'] = 2 dataset.loc[(dataset['Fare'] > 31) & (dataset['Fare'] <= 99), 'Fare'] = 3 dataset.loc[(dataset['Fare'] > 99) & (dataset['Fare'] <= 250), 'Fare'] = 4 dataset.loc[dataset['Fare'] > 250, 'Fare'] = 5 dataset['Fare'] = dataset['Fare'].astype(int)

Este proceso categoriza las tarifas de los pasajeros en diferentes grupos basados en rangos específicos, lo que ayuda a entender mejor las variaciones en la tarifa y cómo influyen en la supervivencia de los pasajeros.

Creación de Nuevas Características ✨

  1. Edad por Clase: Creamos una nueva característica combinando Edad y Pclass.

for dataset in data:
dataset['Age_Class'] = dataset['Age'] * dataset['Pclass']
  1. Tarifa por Persona: Creamos una nueva característica que muestra la tarifa pagada por persona.

for dataset in data:
dataset['Fare_Per_Person'] = dataset['Fare'] / (dataset['relatives'] + 1) dataset['Fare_Per_Person'] = dataset['Fare_Per_Person'].astype(int)


Entrenamiento de Modelos de Aprendizaje Automático 🧠

Ahora entrenaremos varios modelos de aprendizaje automático y compararemos sus resultados. Dado que el conjunto de datos no proporciona etiquetas para el conjunto de prueba, utilizaremos las predicciones sobre el conjunto de entrenamiento para comparar los algoritmos entre sí.

X_train = train_df.drop("Survived", axis=1)
Y_train = train_df["Survived"] X_test = test_df.drop("PassengerId", axis=1).copy()

Random Forest:

random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, Y_train)

Y_prediction = random_forest.predict(X_test)

random_forest.score(X_train, Y_train)
acc_random_forest = round(random_forest.score(X_train, Y_train) * 100, 2)

Logistic Regression:

logreg = LogisticRegression()
logreg.fit(X_train, Y_train)

Y_pred = logreg.predict(X_test)

acc_log = round(logreg.score(X_train, Y_train) * 100, 2)

K Nearest Neighbor:

# KNN knn = KNeighborsClassifier(n_neighbors = 3) knn.fit(X_train, Y_train)  Y_pred = knn.predict(X_test)  acc_knn = round(knn.score(X_train, Y_train) * 100, 2)

Gaussian Naive Bayes:

gaussian = GaussianNB() gaussian.fit(X_train, Y_train)  Y_pred = gaussian.predict(X_test)  acc_gaussian = round(gaussian.score(X_train, Y_train) * 100, 2)

Perceptron:

perceptron = Perceptron(max_iter=5)
perceptron.fit(X_train, Y_train)

Y_pred = perceptron.predict(X_test)

acc_perceptron = round(perceptron.score(X_train, Y_train) * 100, 2)

Linear Support Vector Machine:

linear_svc = LinearSVC()
linear_svc.fit(X_train, Y_train)

Y_pred = linear_svc.predict(X_test)

acc_linear_svc = round(linear_svc.score(X_train, Y_train) * 100, 2)

Decision Tree

decision_tree = DecisionTreeClassifier() decision_tree.fit(X_train, Y_train)  Y_pred = decision_tree.predict(X_test)  acc_decision_tree = round(decision_tree.score(X_train, Y_train) * 100, 2)


Resultados de los Modelos 💡

results = pd.DataFrame({
'Modelo': ['Máquinas de Vectores de Soporte', 'KNN', 'Regresión Logística', 'Bosque Aleatorio', 'Naive Bayes', 'Perceptrón', 'Descenso de Gradiente Estocástico', 'Árbol de Decisión'], 'Precisión': [acc_linear_svc, acc_knn, acc_log, acc_random_forest, acc_gaussian, acc_perceptron, acc_sgd, acc_decision_tree]}) result_df = results.sort_values(by='Precisión', ascending=False) result_df = result_df.set_index('Precisión') result_df.head(9)

Validación Cruzada K-Fold 🔄

Como podemos ver, el clasificador Random Forest ocupa el primer lugar. Pero antes, vamos a comprobar cómo se comporta Random Forest cuando utilizamos validación cruzada.

Validación Cruzada K-Fold

La validación cruzada K-Fold divide aleatoriamente los datos de entrenamiento en K subconjuntos llamados "folds". Imaginemos que dividimos los datos en 4 folds (K = 4). Nuestro modelo de Random Forest se entrenaría y evaluaría 4 veces, utilizando un fold diferente para la evaluación cada vez, mientras se entrena con los otros 3 folds.

El proceso de validación cruzada K-Fold con 4 folds se ve en la siguiente imagen. Cada fila representa un proceso de entrenamiento y evaluación. En la primera fila, el modelo se entrena con los primeros tres subconjuntos y se evalúa con el cuarto. En la segunda fila, el modelo se entrena con el segundo, tercero y cuarto subconjunto y se evalúa con el primero. El proceso se repite hasta que cada fold actúa como un fold de evaluación.

El resultado de la validación cruzada K-Fold con 10 folds (K = 10) sería un array con 10 puntuaciones diferentes. A continuación, se muestra el código para realizar la validación cruzada:

from sklearn.model_selection import cross_val_score
rf = RandomForestClassifier(n_estimators=100) scores = cross_val_score(rf, X_train, Y_train, cv=10, scoring="accuracy") print("Scores:", scores) print("Mean:", scores.mean()) print("Standard Deviation:", scores.std())

Este resultado es mucho más realista que antes. Nuestro modelo tiene una precisión promedio del 82% con una desviación estándar de 4%. La desviación estándar nos muestra cuán precisas son las estimaciones. Esto significa que la precisión de nuestro modelo puede variar entre +/- 4%.

Creo que la precisión sigue siendo muy buena y, como Random Forest es un modelo fácil de usar, intentaremos mejorar su rendimiento aún más en la siguiente sección.


¿Qué es Random Forest? 🌲

Random Forest es un algoritmo de aprendizaje supervisado que analizamos en detalle en nuestro curso de Fundamentos Avanzados de Matemáticas para Machine Learning en Frogames Formación. Como su nombre indica, crea un "bosque" y lo hace de manera aleatoria. Este "bosque" está compuesto por varios árboles de decisión, generalmente entrenados con el método de bagging. La idea básica del método bagging es que una combinación de modelos de aprendizaje mejora el resultado general.

En pocas palabras, Random Forest construye múltiples árboles de decisión y los combina para obtener una predicción más precisa y estable.

Una gran ventaja de Random Forest es que puede ser utilizado tanto para problemas de clasificación como para regresión, que son los más comunes en los sistemas actuales de aprendizaje automático. Con algunas excepciones, un clasificador Random Forest tiene todos los hiperparámetros de un clasificador de árbol de decisión y también todos los hiperparámetros de un clasificador de bagging para controlar el conjunto de árboles.

El algoritmo de Random Forest introduce mayor aleatoriedad en el modelo cuando crece el árbol. En lugar de buscar la mejor característica al dividir un nodo, busca la mejor característica dentro de un subconjunto aleatorio de características. Este proceso genera una gran diversidad, lo que generalmente resulta en un mejor modelo.

Importancia de las Características 📊

Otra excelente cualidad de Random Forest es que facilita la medición de la importancia relativa de cada característica. Sklearn mide la importancia de una característica observando cuánto reducen la impureza los nodos del árbol que usan esa característica (en promedio, a través de todos los árboles del bosque). Este puntaje se calcula automáticamente para cada característica después del entrenamiento y los resultados se escalan de modo que la suma de todas las importancias sea igual a 1.

importances = pd.DataFrame({'feature': X_train.columns, 'importance': np.round(random_forest.feature_importances_, 3)})
importances = importances.sort_values('importance', ascending=False).set_index('feature') importances.head(15) importances.plot.bar()


Conclusión: Eliminación de Características Innecesarias 🚮

Las características not_alone y Parch no desempeñan un papel significativo en el proceso de predicción de nuestro clasificador Random Forest. Por eso, las eliminaré del conjunto de datos y entrenaré nuevamente el clasificador.

train_df = train_df.drop("not_alone", axis=1)
test_df = test_df.drop("not_alone", axis=1) train_df = train_df.drop("Parch", axis=1) test_df = test_df.drop("Parch", axis=1)

Entrenamiento de Random Forest nuevamente:

random_forest = RandomForestClassifier(n_estimators=100, oob_score=True)
random_forest.fit(X_train, Y_train) Y_prediction = random_forest.predict(X_test) random_forest.score(X_train, Y_train) acc_random_forest = round(random_forest.score(X_train, Y_train) * 100, 2) print(round(acc_random_forest, 2), "%")

Resultado: 92.82%

Nuestro modelo de Random Forest predice tan bien como antes. Una regla general es que, cuantas más características tengas, más probable será que tu modelo sufra de overfitting, y viceversa. Sin embargo, creo que nuestros datos están bien, ya que no contienen demasiadas características.

También existe otra forma de evaluar un clasificador Random Forest, que probablemente sea mucho más precisa que la puntuación que usamos antes. Me refiero a la estimación de error fuera de la bolsa (OOB) para estimar la precisión de generalización del modelo.

print("oob score:", round(random_forest.oob_score_, 4) * 100, "%")

Resultado OOB: 81.82%

Ahora podemos empezar a ajustar los hiperparámetros de Random Forest.


Ajuste de Hiperparámetros 🔧

A continuación, te muestro el código para el ajuste de hiperparámetros, donde se optimizan parámetros como criterion, min_samples_leaf, min_samples_split y n_estimators.

param_grid = { 
"criterion": ["gini", "entropy"], "min_samples_leaf": [1, 5, 10, 25, 50, 70], "min_samples_split": [2, 4, 10, 12, 16, 18, 25, 35], "n_estimators": [100, 400, 700, 1000, 1500] } from sklearn.model_selection import GridSearchCV, cross_val_score rf = RandomForestClassifier(n_estimators=100, max_features='auto', oob_score=True, random_state=1, n_jobs=-1) clf = GridSearchCV(estimator=rf, param_grid=param_grid, n_jobs=-1) clf.fit(X_train, Y_train) clf.best_params_


Evaluación del Modelo y Medidas Adicionales 📏

Ahora que tenemos un modelo adecuado, podemos empezar a evaluar su rendimiento de una manera más precisa. Hasta ahora solo hemos utilizado la precisión y el puntaje OOB, que son formas de medir la precisión. Sin embargo, es más complicado evaluar un modelo de clasificación que uno de regresión. Discutiremos más sobre esto a continuación.

Matriz de Confusión 🔍

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix predictions = cross_val_predict(random_forest, X_train, Y_train, cv=3) confusion_matrix(Y_train, predictions)

La primera fila se refiere a las predicciones de no sobrevivientes: 493 pasajeros fueron correctamente clasificados como no sobrevivientes (verdaderos negativos) y 56 fueron erróneamente clasificados como no sobrevivientes (falsos positivos). La segunda fila se refiere a las predicciones de sobrevivientes: 93 pasajeros fueron erróneamente clasificados como sobrevivientes (falsos negativos) y 249 fueron correctamente clasificados como sobrevivientes (verdaderos positivos).

Precisión y Recall

from sklearn.metrics import precision_score, recall_score
print("Precision:", precision_score(Y_train, predictions)) print("Recall:", recall_score(Y_train, predictions))
  • Precisión: 81% (porcentaje de veces que el modelo predijo correctamente la supervivencia de un pasajero)

  • Recall: 72% (porcentaje de personas que realmente sobrevivieron y fueron correctamente predichas)

F-Score

from sklearn.metrics import f1_score
f1_score(Y_train, predictions)

El F-Score es 0.76, lo que indica que el modelo tiene una combinación aceptable de precisión y recall, aunque no es perfecto. Este puntaje no es muy alto debido a que el recall es bajo (72%).


Curva de Precisión-Recall 📊

Para cada persona que el algoritmo de Random Forest clasifica, calcula una probabilida basada en una función y clasifica a la persona como sobreviviente (cuando la puntuación es mayor que el umbral) o como no sobreviviente(cuando la puntuación es menor que el umbral). Es por esto que el umbral juega un papel muy importante.

Vamos a graficar la precisión y el recall con el umbral utilizando matplotlib:

from sklearn.metrics import precision_recall_curve # Obtener las probabilidades de nuestras predicciones y_scores = random_forest.predict_proba(X_train) y_scores = y_scores[:, 1] precision, recall, threshold = precision_recall_curve(Y_train, y_scores) def plot_precision_and_recall(precision, recall, threshold): plt.plot(threshold, precision[:-1], "r-", label="precisión", linewidth=5) plt.plot(threshold, recall[:-1], "b", label="recall", linewidth=5) plt.xlabel("umbral", fontsize=19) plt.legend(loc="upper right", fontsize=19) plt.ylim([0, 1]) plt.figure(figsize=(14, 7)) plot_precision_and_recall(precision, recall, threshold) plt.show()

En el gráfico anterior, podemos ver claramente que el recall disminuye rápidamente cuando la precisión alcanza alrededor del 85%. Por lo tanto, es posible que desees seleccionar el punto de equilibrio entre precisión y recall antes de que la precisión alcance ese umbral, por ejemplo, alrededor del 75%.

Ahora puedes elegir un umbral que te proporcione el mejor trade-off entre precisión y recall para tu problema de aprendizaje automático. Si, por ejemplo, deseas una precisión del 80%, puedes observar en los gráficos que necesitarías un umbral de alrededor de 0.4. Luego, podrías entrenar un modelo con ese umbral y obtener la precisión deseada.

Otra forma de analizar el trade-off entre precisión y recall es graficando ambos valores contra cada uno:

def plot_precision_vs_recall(precision, recall): plt.plot(recall, precision, "g--", linewidth=2.5) plt.ylabel("recall", fontsize=19) plt.xlabel("precisión", fontsize=19) plt.axis([0, 1.5, 0, 1.5]) plt.figure(figsize=(14, 7)) plot_precision_vs_recall(precision, recall) plt.show()


Curva ROC AUC 🔍

Otra forma de evaluar y comparar tu clasificador binario es utilizando la Curva ROC AUC. Esta curva grafica la tasa de verdaderos positivos (también llamada recall) contra la tasa de falsos positivos (la proporción de instancias negativas clasificadas incorrectamente), en lugar de graficar la precisión contra el recall.

from sklearn.metrics import roc_curve
# Calcular tasa de verdaderos positivos y tasa de falsos positivos false_positive_rate, true_positive_rate, thresholds = roc_curve(Y_train, y_scores) # Graficar las tasas de falsos positivos y verdaderos positivos def plot_roc_curve(false_positive_rate, true_positive_rate, label=None): plt.plot(false_positive_rate, true_positive_rate, linewidth=2, label=label) plt.plot([0, 1], [0, 1], 'r', linewidth=4) plt.axis([0, 1, 0, 1]) plt.xlabel('Tasa de Falsos Positivos (FPR)', fontsize=16) plt.ylabel('Tasa de Verdaderos Positivos (TPR)', fontsize=16) plt.figure(figsize=(14, 7)) plot_roc_curve(false_positive_rate, true_positive_rate) plt.show()

La línea roja en el centro representa un clasificador aleatorio puro (como un lanzamiento de moneda). Por lo tanto, tu clasificador debería alejarse lo más posible de esta línea. En nuestro caso, el modelo Random Forest parece funcionar muy bien.

Por supuesto, también tenemos un trade-off aquí, ya que el clasificador produce más falsos positivos a medida que aumenta la tasa de verdaderos positivos.


Puntuación ROC AUC 🏅

La puntuación ROC AUC es la puntuación correspondiente a la Curva ROC AUC. Se calcula midiendo el área bajo la curva, lo que se conoce como AUC. Un clasificador 100% correcto tendría una puntuación ROC AUC de 1, mientras que un clasificador completamente aleatorio tendría una puntuación de 0.5.

python from sklearn.metrics import roc_auc_score
r_a_score = roc_auc_score(Y_train, y_scores) print("ROC-AUC-Score:", r_a_score)

Resultado ROC-AUC Score: 0.9450

¡Perfecto! Creo que esta puntuación es lo suficientemente buena como para enviar las predicciones al leaderboard de Kaggle.


Resumen 📝

Comenzamos con la exploración de datos, donde nos familiarizamos con el conjunto de datos, verificamos los datos faltantes y aprendimos qué características son importantes. Durante este proceso, utilizamos Seaborn y Matplotlib para realizar las visualizaciones.

En la parte de preprocesamiento de datos, tratamos los valores faltantes, convertimos características en valores numéricos, agrupamos los valores en categorías y creamos algunas nuevas características.

Después, entrenamos 8 modelos diferentes de aprendizaje automático para predecir la supervivencia de los pasajeros del Titanic, seleccionamos uno de ellos (Random Forest) y aplicamos validación cruzada. A continuación, discutimos cómo funciona Random Forest, analizamos la importancia que asigna a las diferentes características y optimizamos su rendimiento ajustando los hiperparámetros. Finalmente, revisamos la matriz de confusión del modelo y calculamos la precisión, el recall y el F-score.

A continuación, se muestra una imagen del antes y después del dataframe “train_df”:


Mejoras y Oportunidades 🔧

Por supuesto, aún hay margen para mejorar la predicción de la supervivencia de los pasajeros del Titanic, como realizar una ingeniería de características más extensa, comparando y graficando las características entre sí e identificando y eliminando las características ruidosas.

Otra forma de mejorar los resultados del dataset de los Pasajeros del Titanic en el leaderboard de Kaggle sería realizar un ajuste de hiperparámetros más exhaustivo en varios modelos de aprendizaje automático. Además, podrías probar con aprendizaje en conjunto(ensemble learning) para mejorar la precisión general del modelo.

« Volver al Blog

Obtener mi regalo ahora