Saltar al contenido principal

Machine Learning con Kaggle: ingeniería de características

Descubre cómo la ingeniería de características puede ayudarte a subir el nivel al construir modelos de machine learning en Kaggle: crea nuevas columnas, transforma variables y mucho más.
· 14 min leer

Explora con IA

En los dos tutoriales anteriores de Kaggle, aprendiste a dejar tus datos listos para construir tu primer modelo de machine learning usando análisis exploratorio de datos y modelos base de machine learning. Después, conseguiste construir tu primer modelo, un clasificador de árbol de decisión. Enviaste todos estos modelos a Kaggle e interpretaste su accuracy.

En este tercer tutorial, verás en detalle la ingeniería de características, un proceso en el que usas el conocimiento del dominio de tus datos para crear características adicionales relevantes que aumenten el poder predictivo del algoritmo y hagan que tus modelos de machine learning rindan aún mejor.

En concreto,

  • Primero empezarás con las importaciones necesarias y cargarás los datos en tu espacio de trabajo;
  • Luego, verás por qué merece la pena hacer ingeniería de características y te pondrás manos a la obra creando nuevas variables para tu dataset. Crearás columnas nuevas, transformarás variables a numéricas, tratarás valores ausentes y mucho más.
  • Por último, construirás un nuevo modelo con tu dataset actualizado y lo enviarás a Kaggle.

¡Empezamos!

Antes de nada, harás todas las importaciones, igual que en el tutorial anterior, usarás algo de magia de IPython para asegurarte de que las figuras se generan inline en el Jupyter Notebook y establecerás el estilo de visualización. A continuación, importarás tus datos y te asegurarás de guardar la variable objetivo del conjunto de entrenamiento en un lugar seguro. Después, fusionarás los conjuntos de train y test (a excepción de la columna 'Survived' de df_train) y guardarás el resultado en data.

Recuerda que haces esto para que cualquier preprocesado que apliques se refleje tanto en train como en test.

Por último, usa el método .info() para echar un vistazo a tus datos:

# Imports
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import re
import numpy as np
from sklearn import tree
from sklearn.model_selection import GridSearchCV

# Figures inline and set visualization style
%matplotlib inline
sns.set()

# Import data
df_train = pd.read_csv('data/train.csv')
df_test = pd.read_csv('data/test.csv')

# Store target variable of training data in a safe place
survived_train = df_train.Survived

# Concatenate training and test sets
data = pd.concat([df_train.drop(['Survived'], axis=1), df_test])

# View head
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 11 columns):
PassengerId    1309 non-null int64
Pclass         1309 non-null int64
Name           1309 non-null object
Sex            1309 non-null object
Age            1046 non-null float64
SibSp          1309 non-null int64
Parch          1309 non-null int64
Ticket         1309 non-null object
Fare           1308 non-null float64
Cabin          295 non-null object
Embarked       1307 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 122.7+ KB

¿Por qué hacer ingeniería de características?

Haces ingeniería de características para extraer más información de tus datos y así llevar la delantera a la hora de construir modelos.

Títulos de los pasajeros del Titanic

Veámoslo con un ejemplo. Echa un ojo a la columna 'Name' con .tail(), que te muestra las cinco últimas filas de tus datos:

# View head of 'Name' column
data.Name.tail()
413              Spector, Mr. Woolf
414    Oliva y Ocana, Dona. Fermina
415    Saether, Mr. Simon Sivertsen
416             Ware, Mr. Frederick
417        Peter, Master. Michael J
Name: Name, dtype: object

De repente aparecen distintos títulos. Es decir, esta columna contiene textos con títulos como "Mr", "Master" o "Dona".

Estos títulos aportan información sobre el estatus social, la profesión, etc., que al final podrían decirte algo sobre la supervivencia.

A primera vista, podría parecer complicado separar nombres y títulos, pero no te preocupes. Puedes usar expresiones regulares para extraer el título y guardarlo en una nueva columna 'Title':

# Extract Title from Name, store in column and plot barplot
data['Title'] = data.Name.apply(lambda x: re.search(' ([A-Z][a-z]+)\.', x).group(1))
sns.countplot(x='Title', data=data);
plt.xticks(rotation=45);

gráfico de barras

Nota: esta nueva columna 'Title' es en realidad una nueva característica para tu dataset.

Consejo: si quieres aprender más sobre expresiones regulares, echa un vistazo a mi resumen de nuestro último evento FB Live de code along o al tutorial de expresiones regulares en Python de DataCamp.

Como ves, hay varios títulos en el gráfico y muchos aparecen muy pocas veces. Tiene sentido agruparlos en menos categorías.

Por ejemplo, probablemente quieras sustituir 'Mlle' y 'Ms' por 'Miss' y 'Mme' por 'Mrs', ya que son títulos franceses y, en ideal, quieres tus datos en un solo idioma. Después, puedes agrupar un conjunto de títulos difíciles de categorizar en una categoría 'Special'.

Consejo: prueba distintas agrupaciones para ver cómo cambia el rendimiento del algoritmo.

Ahora, visualiza el resultado con .countplot():

data['Title'] = data['Title'].replace({'Mlle':'Miss', 'Mme':'Mrs', 'Ms':'Miss'})
data['Title'] = data['Title'].replace(['Don', 'Dona', 'Rev', 'Dr',
                                            'Major', 'Lady', 'Sir', 'Col', 'Capt', 'Countess', 'Jonkheer'],'Special')
sns.countplot(x='Title', data=data);
plt.xticks(rotation=45);

gráfico de barras

Así queda tu nueva característica 'Title'.

Ahora asegúrate de que tienes la columna 'Title' y vuelve a revisar tus datos con .tail():

# View head of data
data.tail()
 PassengerIdPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedTitle
41313053Spector, Mr. WoolfmaleNaN00A.5. 32368.0500NaNSMr
41413061Oliva y Ocana, Dona. Ferminafemale39.000PC 17758108.9000C105CSpecial
41513073Saether, Mr. Simon Sivertsenmale38.500SOTON/O.Q. 31012627.2500NaNSMr
41613083Ware, Mr. FrederickmaleNaN003593098.0500NaNSMr
41713093Peter, Master. Michael JmaleNaN11266822.3583NaNCMaster

Camarotes de los pasajeros

Cuando cargaste los datos y los inspeccionaste, viste que había varios NaN o valores ausentes en la columna 'Cabin'.

Es razonable suponer que esos NaN corresponden a personas sin camarote, lo cual podría decirte algo sobre 'Survival'. Así que vamos a crear una nueva columna 'Has_Cabin' que codifique si los pasajeros tenían camarote o no.

Nota: en el siguiente fragmento usas .isnull(), que devuelve True si el pasajero no tiene camarote y False en caso contrario. Pero como quieres guardar el resultado en 'Has_Cabin', te interesa invertirlo: quieres True si el pasajero tiene camarote. Por eso se utiliza la virgulilla ~.

# Did they have a Cabin?
data['Has_Cabin'] = ~data.Cabin.isnull()

# View head of data
data.head()
 PassengerIdPclassNameSexAgeSibSpParchTicketFareCabinEmbarkedTitleHas_Cabin
013Braund, Mr. Owen Harrismale22.010A/5 211717.2500NaNSMrFalse
121Cumings, Mrs. John Bradley (Florence Briggs Th...female38.010PC 1759971.2833C85CMrsTrue
233Heikkinen, Miss. Lainafemale26.000STON/O2. 31012827.9250NaNSMissFalse
341Futrelle, Mrs. Jacques Heath (Lily May Peel)female35.01011380353.1000C123SMrsTrue
453Allen, Mr. William Henrymale35.0003734508.0500NaNSMrFalse

Ahora quieres eliminar un conjunto de columnas que ya no aportan información útil (o que no sabes bien cómo aprovechar). En este caso, piensas en ['Cabin', 'Name', 'PassengerId', 'Ticket'], porque

  • Ya has extraído si el pasajero tenía camarote en la nueva columna 'Has_Cabin';
  • También has extraído los títulos de la columna 'Name';
  • Además, 'PassengerId' y 'Ticket' probablemente no aporten nada sobre la supervivencia de los pasajeros del Titanic.

Consejo: podría haber más información útil en 'Cabin', pero para este tutorial asumimos que no.

Para eliminar estas columnas del DataFrame data, usa el argumento inplace en .drop() y ponlo a True:

# Drop columns and view head
data.drop(['Cabin', 'Name', 'PassengerId', 'Ticket'], axis=1, inplace=True)
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_Cabin
03male22.0107.2500SMrFalse
11female38.01071.2833CMrsTrue
23female26.0007.9250SMissFalse
31female35.01053.1000SMrsTrue
43male35.0008.0500SMrFalse

¡Bien hecho! Has creado nuevas características como 'Title' y 'Has_Cabin' y te has deshecho de las que no aportan información útil para tu modelo.

Ahora toca tratar los valores ausentes, agrupar en rangos tus variables numéricas y transformar todas las características en variables numéricas con .get_dummies(). Por último, construirás el modelo final del tutorial. ¡Vamos a ello!

Tratamiento de valores ausentes

Con todos los cambios aplicados al DataFrame data, es buena idea comprobar si quedan valores ausentes con .info():

data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 9 columns):
Pclass       1309 non-null int64
Sex          1309 non-null object
Age          1046 non-null float64
SibSp        1309 non-null int64
Parch        1309 non-null int64
Fare         1308 non-null float64
Embarked     1307 non-null object
Title        1309 non-null object
Has_Cabin    1309 non-null bool
dtypes: bool(1), float64(2), int64(3), object(3)
memory usage: 133.3+ KB

El resultado indica que hay valores ausentes en 'Age', 'Fare' y 'Embarked'.

Recuerda que puedes detectarlo fácilmente comparando el número total de filas (1309) con el número de valores no nulos en cada columna que muestra .info(). Aquí, 'Age' tiene 1046 valores no nulos, así que faltan 263. 'Fare' solo tiene un valor ausente y 'Embarked' tiene dos.

Como en el tutorial anterior, vas a imputar estos valores con .fillna():

Nota: de nuevo usas la mediana para rellenar 'Age' y 'Fare' porque es ideal cuando hay valores atípicos. Otras opciones serían la media o la moda.

Rellenas los dos valores ausentes de 'Embarked' con 'S' (Southampton), que es el valor más común en esa columna.

Consejo: puedes comprobarlo con algo más de análisis exploratorio.

# Impute missing values for Age, Fare, Embarked
data['Age'] = data.Age.fillna(data.Age.median())
data['Fare'] = data.Fare.fillna(data.Fare.median())
data['Embarked'] = data['Embarked'].fillna('S')
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 9 columns):
Pclass       1309 non-null int64
Sex          1309 non-null object
Age          1309 non-null float64
SibSp        1309 non-null int64
Parch        1309 non-null int64
Fare         1309 non-null float64
Embarked     1309 non-null object
Title        1309 non-null object
Has_Cabin    1309 non-null bool
dtypes: bool(1), float64(2), int64(3), object(3)
memory usage: 133.3+ KB
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_Cabin
03male22.0107.2500SMrFalse
11female38.01071.2833CMrsTrue
23female26.0007.9250SMissFalse
31female35.01053.1000SMrsTrue
43male35.0008.0500SMrFalse

Agrupar variables numéricas

Ahora quieres agrupar los valores numéricos, porque tienes rangos de edades y tarifas. Puede haber fluctuaciones que no reflejen patrones reales (ruido). Por eso vas a asignar a las personas dentro de ciertos rangos de edad o tarifa al mismo grupo. Puedes hacerlo con pandas y su función qcut():

# Binning numerical columns
data['CatAge'] = pd.qcut(data.Age, q=4, labels=False )
data['CatFare']= pd.qcut(data.Fare, q=4, labels=False)
data.head()
 PclassSexAgeSibSpParchFareEmbarkedTitleHas_CabinCatAgeCatFare
03male22.0107.2500SMrFalse00
11female38.01071.2833CMrsTrue33
23female26.0007.9250SMissFalse11
31female35.01053.1000SMrsTrue23
43male35.0008.0500SMrFalse21

Nota: pasas los datos como Series, data.Age y data.Fare, especificas el número de cuantiles q=4 y estableces labels=False para codificar los grupos como números.

Con esa información en grupos, ya puedes eliminar con seguridad las columnas 'Age' y 'Fare'. No olvides revisar las primeras filas.

data = data.drop(['Age', 'Fare'], axis=1)
data.head()
 PclassSexSibSpParchEmbarkedTitleHas_CabinCatAgeCatFare
03male10SMrFalse00
11female10CMrsTrue33
23female00SMissFalse11
31female10SMrsTrue23
43male00SMrFalse21

Número de familiares a bordo

Lo siguiente que puedes hacer es crear una nueva columna con el número de miembros de la familia que iban a bordo del Titanic. En este tutorial no la usaremos para ver cómo rinde el modelo sin ella. Si quieres probar cómo afecta esta columna adicional, ejecuta la siguiente línea:

# Create column of number of Family members onboard
data['Fam_Size'] = data.Parch + data.SibSp

Por ahora, simplemente elimina las columnas 'SibSp' y 'Parch' del DataFrame:

# Drop columns
data = data.drop(['SibSp','Parch'], axis=1)
data.head()
 PclassSexEmbarkedTitleHas_CabinCatAgeCatFare
03maleSMrFalse00
11femaleCMrsTrue33
23femaleSMissFalse11
31femaleSMrsTrue23
43maleSMrFalse21

Transformar variables a numéricas

Ahora que has creado más características (como 'Title' y 'Has_Cabin'), has tratado los valores ausentes y has agrupado las numéricas, toca transformar todas las variables a numéricas. Esto es importante porque, en general, los modelos de machine learning aceptan entradas numéricas.

Como antes, usarás .get_dummies():

# Transform into binary variables
data_dum = pd.get_dummies(data, drop_first=True)
data_dum.head()
 PclassHas_CabinCatAgeCatFareSex_maleEmbarked_QEmbarked_STitle_MissTitle_MrTitle_MrsTitle_Special
03False001010100
11True330000010
23False110011000
31True230010010
43False211010100

Con todo listo, es hora de construir el modelo final.

Construir modelos con tu nuevo dataset

Como antes, primero dividirás data de nuevo en train y test. Luego las transformarás en arrays:

# Split into test.train
data_train = data_dum.iloc[:891]
data_test = data_dum.iloc[891:]

# Transform into arrays for scikit-learn
X = data_train.values
test = data_test.values
y = survived_train.values

Ahora vas a entrenar un árbol de decisión sobre tu dataset con características ingenierizadas. Para elegir el hiperparámetro max_depth, usarás una variante de train/test split llamada «validación cruzada».

validación cruzada

Empiezas dividiendo el dataset en 5 grupos o folds. Mantienes el primero como conjunto de prueba, ajustas el modelo con los otros cuatro, predices sobre el de prueba y calculas la métrica de interés. Luego repites usando el segundo como prueba, y así con el tercero, cuarto y quinto.

Como resultado, obtienes cinco valores de accuracy, de los que puedes calcular estadísticas como la mediana y/o la media y los intervalos de confianza al 95%.

Repites esto para cada valor de cada hiperparámetro que estés ajustando y eliges la combinación que mejor rinde. Esto se llama búsqueda en rejilla (grid search).

Basta de teoría, ¡vamos a ello!

A continuación, usarás validación cruzada y grid search para elegir el mejor max_depth para tu dataset:

# Setup the hyperparameter grid
dep = np.arange(1,9)
param_grid = {'max_depth' : dep}

# Instantiate a decision tree classifier: clf
clf = tree.DecisionTreeClassifier()

# Instantiate the GridSearchCV object: clf_cv
clf_cv = GridSearchCV(clf, param_grid=param_grid, cv=5)

# Fit it to the data
clf_cv.fit(X, y)

# Print the tuned parameter and score
print("Tuned Decision Tree Parameters: {}".format(clf_cv.best_params_))
print("Best score is {}".format(clf_cv.best_score_))
Tuned Decision Tree Parameters: {'max_depth': 3}
Best score is 0.8103254769921436

Ahora puedes predecir sobre tu conjunto de test, crear una nueva columna 'Survived' y guardar ahí tus predicciones. No olvides exportar las columnas 'PassengerId' y 'Survived' de df_test a un .csv y subirlo a Kaggle.

Y_pred = clf_cv.predict(test)
df_test['Survived'] = Y_pred
df_test[['PassengerId', 'Survived']].to_csv('data/predictions/dec_tree_feat_eng.csv', index=False)

kaggle

La accuracy de tu envío es 78,9.

Próximos pasos

Prueba a hacer más ingeniería de características y a entrenar modelos nuevos para mejorar esta puntuación. Este notebook, junto con los dos anteriores, está publicado en GitHub y nos encantaría ver cómo mejoras estos modelos.

Hay mucho más preprocesado que puedes aprender, como el escalado de datos. También verás que las pipelines de scikit-learn son súper útiles. Échale un ojo a nuestro curso Supervised Learning with scikit-learn y a la documentación de scikit-learn para todo esto y más.

Temas

Relacionado