Python
Machine Learning
Data Analysis
Data Feature Engineering
El análisis exploratorio de datos, también conocido como EDA (Exploratory Data Analysis) es el primer paso para resolver cualquier problema de Machine Learning. Consiste en un proceso que busca analizar e investigar los conjuntos de datos de los que disponen y resumir sus principales características, empleando a menudo técnicas de visualización de datos. Este análisis se lleva a cabo a través de una serie de pasos que se detallan a continuación.
En esta sección ahondaremos en el concepto trabajando con el conjunto de datos del Titanic.
Antes de comenzar a analizar el conjunto de datos, debemos entender, por un lado, el problema o desafío que estamos intentando resolver con esta información y lo idóneo o útil que puede resultarnos.
En este caso, queremos analizar qué personas sobrevivieron o no en el naufragio del Titanic y, en sucesivas fases, poder entrenar un modelo de Machine Learning para responder a la pregunta: "¿Qué tipo de personas tenían más probabilidades de sobrevivir?". Por lo tanto, encontramos que el dataset del que disponemos puede ayudarnos a resolver la cuestión planteada y aplicamos un proceso de EDA para conocerlo más en detalle.
Importaremos el conjunto de datos para comenzar a trabajar con él:
import pandas as pd
train_data = pd.read_csv("https://raw.githubusercontent.com/4GeeksAcademy/machine-learning-content/master/assets/titanic_train.csv")
test_data = pd.read_csv("https://raw.githubusercontent.com/4GeeksAcademy/machine-learning-content/master/assets/titanic_test.csv")
test_survived_data = pd.read_csv("https://raw.githubusercontent.com/4GeeksAcademy/machine-learning-content/master/assets/gender_submission.csv")
test_data["Survived"] = test_survived_data["Survived"]
total_data = pd.concat([train_data, test_data]).reset_index(inplace = False)
total_data.drop(columns = ["index"], inplace = True)
total_data.head()
Una vez que hemos cargado el conjunto de datos, debemos analizarlo en su totalidad, sin distinción de train y test, para obtener conclusiones conjuntas. Una vez que tenemos la información cargada en una estructura de datos manejable como es un DataFrame de Pandas, podemos arrancar con el proceso.
Conocer las dimensiones y tipologías de datos del objeto con el que estamos trabajando es vital. Para ello necesitamos el atributo shape
para obtener las dimensiones del objeto y la función info()
para conocer la tipología y la cantidad de valores no nulos:
# Obtener las dimensiones
total_data.shape
# Obtener información sobre tipos de datos y valores no nulos
total_data.info()
Una vez hemos obtenido esta información, es importante que seamos capaces de extraer conclusiones, como las siguientes:
Survived
.Cabin
solo tiene 295 instancias con valores, por lo que contendría más de 1000 valores nulos. La variable Age
también cuenta con valores nulos, pero en un número mucho más reducido que el anterior. El resto de variables cuentan siempre con valor.Un punto muy importante a tener en cuenta en este paso es eliminar aquellas instancias que pudieran estar duplicadas en el conjunto de datos. Esto es crucial, ya que, de dejarlo, el mismo punto tendría varias representaciones, lo cual es matemáticamente incoherente e incorrecto. Para ello, hemos de ser inteligentes buscando duplicados y conocer previamente si los hay y dónde, antes de eliminarlos. Además, tenemos que tener en cuenta que una instancia puede estar repetida independientemente del identificador que pueda tener, así que en este caso nos interesa eliminar del análisis la variable PassengerId
, ya que podría estar mal generada.
total_data.drop("PassengerId", axis = 1).duplicated().sum()
En este caso, no encontramos ningún valor duplicado. En el caso de que lo hubiésemos encontrado, el siguiente paso sería aplicar la función de drop_duplicates()
.
total_data = total_data.drop_duplicates(subset = total_data.columns.difference(['PassengerId']))
print(total_data.shape)
total_data.head()
Excluiríamos de nuevo la columna identificativa, aunque podríamos repetir el análisis incluyéndola para enriquecer el análisis:
if total_data.duplicated().sum():
total_data = total_data.drop_duplicates()
print(total_data.shape)
total_data.head()
Cuando queremos preparar los datos para entrenar un modelo predictivo, debemos responder a la siguiente pregunta: ¿son todas las características imprescindibles para realizar una predicción? Normalmente, esa pregunta es un rotundo no. Tenemos que tratar de ser lo más objetivos posible y llevar a cabo este proceso previo antes de la fase de selección de características. Por lo tanto, aquí lo que trataremos de hacer es una eliminación controlada de aquellas variables que podemos estar seguros de que el algoritmo no va a utilizar en el proceso predictivo, estas son PassengerId
, Name
, Ticket
y Cabin
.
total_data.drop(["PassengerId", "Name", "Ticket", "Cabin"], axis = 1, inplace = True)
total_data.head()
Una variable univariante es un término estadístico que se emplea para referenciar un conjunto de observaciones de un atributo. Esto es, el análisis columna a columna del DataFrame. Para ello, debemos distinguir si una variable es categórica o numérica, ya que el cuerpo del análisis y las conclusiones que se pueden obtener serán distintas.
Una variable categórica es un tipo de variable que puede tomar uno de un número limitado de categorías o grupos. Estos grupos son a menudo nominales (por ejemplo, el color de un coche: rojo, azul, negro, etc, pero ninguno de estos colores es inherentemente "mayor" o "mejor" que los demás) pero pueden también representarse mediante números finitos.
Para representar este tipo de variables utilizaremos histogramas. Antes de comenzar a graficar, debemos identificar cuáles son categóricas, y se puede comprobar fácilmente analizando el rango de valores. En este caso, las variables categóricas son Survived
, Sex
, Pclass
, Embarked
, SibSp
y Parch
:
import matplotlib.pyplot as plt
import seaborn as sns
fig, axis = plt.subplots(2, 3, figsize = (10, 7))
# Crear un histograma múltiple
sns.histplot(ax = axis[0, 0], data = total_data, x = "Survived").set_xlim(-0.1, 1.1)
sns.histplot(ax = axis[0, 1], data = total_data, x = "Sex").set(ylabel = None)
sns.histplot(ax = axis[0, 2], data = total_data, x = "Pclass").set(ylabel = None)
sns.histplot(ax = axis[1, 0], data = total_data, x = "Embarked")
sns.histplot(ax = axis[1, 1], data = total_data, x = "SibSp").set(ylabel = None)
sns.histplot(ax = axis[1, 2], data = total_data, x = "Parch").set(ylabel = None)
# Ajustar el layout
plt.tight_layout()
# Mostrar el plot
plt.show()
Un histograma es una representación gráfica de la distribución de un conjunto de datos. Se utiliza además para comprender la frecuencia de los datos. Al observar un histograma, podemos entender si los datos están sesgados hacia un extremo, si son simétricos, si tienen muchos valores atípicos, etcétera. Con la representación de cada variable podemos determinar que:
S
).Una variable numérica es un tipo de variable que puede tomar valores numéricos (enteros, fracciones, decimales, negativos, etc.) en un rango infinito. Una variable categórica numérica puede ser también una variable numérica (por ejemplo, para los sucesivos análisis, podemos tomar la clase Survived
como numérica también para estudiar relaciones). Normalmente se representan utilizando un histograma y diagramas de caja, expuestos juntos. Antes de comenzar a graficar, debemos también identificar cuáles son las numéricas, que son Fare
, Age
y PassengerId
. Sin embargo, esta última carece de sentido, así que graficaremos las dos primeras:
fig, axis = plt.subplots(2, 2, figsize = (10, 7), gridspec_kw={'height_ratios': [6, 1]})
# Crear una figura múltiple con histogramas y diagramas de caja
sns.histplot(ax = axis[0, 0], data = total_data, x = "Fare").set(xlabel = None)
sns.boxplot(ax = axis[1, 0], data = total_data, x = "Fare")
sns.histplot(ax = axis[0, 1], data = total_data, x = "Age").set(xlabel = None, ylabel = None)
sns.boxplot(ax = axis[1, 1], data = total_data, x = "Age")
# Ajustar el layout
plt.tight_layout()
# Mostrar el plot
plt.show()
La combinación de los dos gráficos anteriores nos permite conocer la distribución y sus características estadísticas. De la visualización resultante podemos ver que ambas variables tienen valores atípicos que están lejos de la distribución estándar y que sus distribuciones son ligeramente asimétricas pero cercanas a una distribución normal; la primera totalmente sesgada hacia la izquierda, donde la media es inferior a la moda y la otra con menor tendencia.
Tras analizar las características una a una, es momento de analizarlas en relación con la predictora y con ellas mismas, para sacar conclusiones más claras acerca de sus relaciones y poder tomar decisiones sobre su procesamiento.
Así, si quisiéramos eliminar una variable debido a una alta cantidad de valores nulos o ciertos outliers, es necesario antes aplicar este proceso para asegurar que la eliminación de ciertos valores no son críticos para la supervivencia de un pasajero. Por ejemplo, la variable Cabin
tiene muchos valores nulos, y tendríamos que asegurar que no hay relación entre ella y la supervivencia antes de eliminarla, ya que quizá pudiera ser muy significativa e importante para el modelo y su presencia podría decantar la predicción.
Cuando las dos variables que se comparan tienen datos numéricos, se dice que el análisis es numérico-numérico. Para comparar dos columnas numéricas se utilizan diagramas de dispersión y análisis de correlaciones.
Utilizaremos la variable Survived
para comenzar con el análisis bivariante porque al tratarse de una variable categórica, pero codificada en números, puede considerarse como numérica también. Primero analizamos la clase frente a las características numéricas:
fig, axis = plt.subplots(2, 2, figsize = (10, 7))
# Crear un diagrama de dispersión múltiple
sns.regplot(ax = axis[0, 0], data = total_data, x = "Fare", y = "Survived")
sns.heatmap(total_data[["Survived", "Fare"]].corr(), annot = True, fmt = ".2f", ax = axis[1, 0], cbar = False)
sns.regplot(ax = axis[0, 1], data = total_data, x = "Age", y = "Survived").set(ylabel=None)
sns.heatmap(total_data[["Survived", "Age"]].corr(), annot = True, fmt = ".2f", ax = axis[1, 1])
# Ajustar el layout
plt.tight_layout()
# Mostrar el plot
plt.show()
Existe una relación directa (aunque no muy fuerte) entre el precio del billete (Fare
) y la supervivencia del pasajero. Así, algunos pasajeros con un importe bajo de billete tuvieron menos probabilidad de supervivencia frente a los que adquirieron un billete con un precio mayor. También existe una relación lineal negativa, más débil que la anterior, entre la edad (Age
) y la variable objetivo. Esto tiene sentido considerando que los niños eran uno de los grupos que tenían preferencia en usar los botes para sobrevivir.
En resumen, a pesar de existir cierta relación con estas características frente a la predictora, la significancia no es muy elevada, no siendo factores decisivos sobre si un pasajero sobrevivía o no.
A continuación también podemos relacionar ambas variables para determinar su grado de afinidad o correlación:
fig, axis = plt.subplots(2, 1, figsize = (5, 7))
# Crear un diagrama de dispersión múltiple
sns.regplot(ax = axis[0], data = total_data, x = "Age", y = "Fare")
sns.heatmap(total_data[["Fare", "Age"]].corr(), annot = True, fmt = ".2f", ax = axis[1])
# Ajustar el layout
plt.tight_layout()
# Mostrar el plot
plt.show()
Se puede determinar que no existe una relación muy fuerte entre ambas variables y que la edad no impacta sobre que el precio del billete sea mayor o no.
Cuando las dos variables que se comparan tienen datos categóricos, se dice que el análisis es categórico-categórico. Para comparar dos columnas categóricas se utilizan histogramas y combinaciones.
Primero analizamos la clase frente a las características categóricas, una a una. Aquí no habrá combinaciones de varias predictoras y la clase:
fig, axis = plt.subplots(2, 3, figsize = (15, 7))
sns.countplot(ax = axis[0, 0], data = total_data, x = "Sex", hue = "Survived")
sns.countplot(ax = axis[0, 1], data = total_data, x = "Pclass", hue = "Survived").set(ylabel = None)
sns.countplot(ax = axis[0, 2], data = total_data, x = "Embarked", hue = "Survived").set(ylabel = None)
sns.countplot(ax = axis[1, 0], data = total_data, x = "SibSp", hue = "Survived")
sns.countplot(ax = axis[1, 1], data = total_data, x = "Parch", hue = "Survived").set(ylabel = None)
plt.tight_layout()
fig.delaxes(axis[1, 2])
plt.show()
Del gráfico anterior podemos obtener las siguientes conclusiones:
El análisis multivariante también permite combinar la clase con varias predictoras al mismo tiempo para enriquecer el análisis. Este tipo de operaciones deben ser subjetivas y deben combinar características relacionadas entre sí. Por ejemplo, no tendría sentido hacer un análisis entre la clase, el sexo del pasajero y la estación en la que accedió al Titanic, ya que no guarda relación el sexo del pasajero con la estación. Sin embargo, la clase y el sexo del pasajero frente a su supervivencia podría ser un análisis digno de estudio, entre otras casuísticas que se presentan a continuación:
fig, axis = plt.subplots(figsize = (10, 5), ncols = 2)
sns.barplot(ax = axis[0], data = total_data, x = "Sex", y = "Survived", hue = "Pclass")
sns.barplot(ax = axis[1], data = total_data, x = "Embarked", y = "Survived", hue = "Pclass").set(ylabel = None)
plt.tight_layout()
plt.show()
De esos análisis se observa claramente que, independientemente del puerto de embarque, las mujeres tuvieron más posibilidades de supervivencia independientemente de la clase en la que viajaron, lo que refuerza el conocimiento obtenido anteriormente. Además, de media, las personas que viajaron en clases más altas sobrevivieron más que aquellos que no lo hicieron.
El objetivo del análisis de correlaciónes con datos categóricos-categóricos es descubrir patrones y dependencias entre variables, lo que ayuda a entender cómo interactúan dentro de un conjunto de datos. Este análisis es fundamental en diversos campos como ciencias sociales, investigación de mercado y epidemiología, donde los datos categóricos a menudo representan atributos clave de interés.
Este análisis tiene como objetivo determinar si y cómo las categorías de una variable están relacionadas con las categorías de otra.
total_data["Sex_n"] = pd.factorize(total_data["Sex"])[0]
total_data["Embarked_n"] = pd.factorize(total_data["Embarked"])[0]
fig, axis = plt.subplots(figsize = (10, 6))
sns.heatmap(total_data[["Sex_n", "Pclass", "Embarked_n", "SibSp", "Parch", "Survived"]].corr(), annot = True, fmt = ".2f")
plt.tight_layout()
plt.show()
El análisis de correlaciones refleja una fuerte relación directa entre el sexo (Sex
) del pasajero y su supervivencia, como hemos visto en apartados anteriores. Además, se aprecia una relación entre el número de acompañantes de los pasajeros (variables SibSp
y Parch
). El resto de las correlaciones son débiles y no son tan significativas como para contemplarlas en el análisis.
Finalmente, para cerrar el estudio multivariante queda analizar la relación entre las variables categóricas y numéricas.
Este es el análisis a mayor detalle que podemos llevar a cabo. Para ello, simplemente hemos de calcular las correlaciones entre las variables, ya que es el mayor indicativo sobre las relaciones. Así, una vez comprobemos que existe alguna relación, podremos profundizar en el estudio. Otro elemento que nos puede ser de mucha ayuda es obtener las relaciones dos a dos entre todos los datos del dataset. Esto es, en parte, redundante porque hay muchas cosas que ya hemos calculado antes y por eso es opcional.
fig, axis = plt.subplots(figsize = (10, 7))
sns.heatmap(total_data[["Age", "Fare", "Sex_n", "Pclass", "Embarked_n", "SibSp", "Parch", "Survived"]].corr(), annot = True, fmt = ".2f")
plt.tight_layout()
plt.show()
Existe una relación entre la tipología de clase (Pclass
) y la edad del pasajero (Age
) fuertemente negativa (los que viajaban en primera clase eran personas con alta edad) y entre la clase y la tarifa pagada (Fare
), algo que tiene mucho sentido. El resto de correlaciones se mantienen con respecto a lo visto anteriormente.
Una vez analizada la correlación, analicemos los dos casos vistos para corroborar la teoría:
fig, axis = plt.subplots(figsize = (10, 5), ncols = 2)
sns.regplot(ax = axis[0], data = total_data, x = "Age", y = "Pclass")
sns.regplot(ax = axis[1], data = total_data, x = "Fare", y = "Pclass").set(ylabel = None, ylim = (0.9, 3.1))
plt.tight_layout()
plt.show()
En el primer gráfico vemos que cuando la edad avanza, la presencia de billetes de primera clase se hace más notoria, y conforme la edad decrece, los billetes de tercera clase se hacen más presentes, reforzando la relación negativa entre las variables observadas. El segundo gráfico también refuerza lo observado, ya que los billetes de mejor clase deben ser más caros.
Una vez calculada la correlación podemos dibujar el pairplot
(es un paso opcional):
sns.pairplot(data = total_data)
¡Ahora, trabajemos y practiquemos la lección de hoy para afianzar lo aprendido!
La ingeniería de características (feature engineering) es un proceso que implica la creación de nuevas características (o variables) a partir de las existentes para mejorar el rendimiento del modelo. Esto puede implicar una variedad de técnicas como la normalización, la transformación de los datos, etcétera. El objetivo es mejorar la precisión del modelo y/o reducir la complejidad del mismo, facilitando así su interpretación.
En los pasos previos hemos empezado a trabajar con los datos eliminando duplicados, contabilizando los valores nulos e, incluso, para calcular correlaciones, transformando Sex
y Embarked
en categorías numéricas. Si bien esto podríamos haberlo hecho en este paso, ya que forma parte de la ingeniería de características, normalmente suele hacerse antes de analizar las variables, separando este proceso en uno previo y este que vamos a ver a continuación.
Un valor atípico (outlier) es un punto de datos que se desvía significativamente de los demás. Es un valor que es notablemente diferente de lo que sería de esperar dada la tendencia general de los datos. Estos outliers pueden ser causados por errores en la recolección de datos, variaciones naturales en los datos, o pueden ser indicativos de algo significativo, como una anomalía o evento extraordinario.
El análisis descriptivo es una poderosa herramienta para caracterizar el conjunto de datos: la media, desviación y los cuartiles nos brindan una poderosa información sobre cada variable. La función describe()
de un DataFrame nos ayuda a calcular en unos tiempos muy reducidos todos estos valores.
total_data.describe()
Si bien la experiencia es un componente importante en el análisis de los resultados de la tabla anterior, podemos utilizar ciertas reglas para detectarlos, como ver el valor mínimo y máximo de una característica específica y compararlo con su percentil 25% y 75%. Por ejemplo, todo parece normal salvo para la columna Fare
que tiene una media de 32,20 pero su percentil del 50% es 14 y su valor máximo es 512. Podríamos decir que 512 parece ser un valor atípico, pero podría ser un error de transcripción. También es posible que el billete más caro tuviera ese precio. Sería útil investigar un poco y confirmar o desmentir esa información.
Dibujar los diagramas de cajas de las variables también nos da una información muy poderosa sobre los valores atípicos que se salen de las regiones de confianza:
fig, axis = plt.subplots(3, 3, figsize = (15, 10))
sns.boxplot(ax = axis[0, 0], data = total_data, y = "Survived")
sns.boxplot(ax = axis[0, 1], data = total_data, y = "Pclass")
sns.boxplot(ax = axis[0, 2], data = total_data, y = "Age")
sns.boxplot(ax = axis[1, 0], data = total_data, y = "SibSp")
sns.boxplot(ax = axis[1, 1], data = total_data, y = "Parch")
sns.boxplot(ax = axis[1, 2], data = total_data, y = "Fare")
sns.boxplot(ax = axis[2, 0], data = total_data, y = "Sex_n")
sns.boxplot(ax = axis[2, 1], data = total_data, y = "Embarked_n")
plt.tight_layout()
plt.show()
Podemos determinar fácilmente que las variables afectadas por outliers son Age
, SibSp
, Parch
y Fare
. En el caso anterior sobre el billete del crucero (columna Fare
) parece que la tarifa del billete de 512 dólares no es muy común. Deberíamos establecer algunos límites superior e inferior para determinar si un punto de datos debe considerarse o no un valor atípico.
Para lidiar con ellos hay muchas técnicas, y puedes encontrar más información aquí, pero que se resume en los siguientes puntos:
Por ejemplo, si queremos aplicar el segundo punto de los anteriores para el caso de la columna Fare
:
fare_stats = total_data["Fare"].describe()
fare_stats
fare_iqr = fare_stats["75%"] - fare_stats["25%"]
upper_limit = fare_stats["75%"] + 1.5 * fare_iqr
lower_limit = fare_stats["25%"] - 1.5 * fare_iqr
print(f"Los límites superior e inferior para la búsqueda de outliers son {round(upper_limit, 2)} y {round(lower_limit, 2)}, con un rango intercuartílico de {round(fare_iqr, 2)}")
Basándonos en estos resultados, deberíamos eliminar los registros de los pasajeros cuyo importe de billete supere los 65 dólares. Sin embargo, nuestro criterio es muy importante aquí y, según los precios que vimos en el diagrama de caja, los valores más extremos están por encima de 300. Veamos cuántos valores representan ese valor extremo de 512:
total_data[total_data["Fare"] > 300]
En este caso vemos que todos ellos sobrevivieron, quizá sí que hay un impacto real sobre el precio de billete tan elevado y la supervivencia final. Por lo tanto, sumado al análisis univariante anterior, existe una implicación entre el precio del billete y el resultado final de supervivencia, por lo que decidimos que mantenemos los valores atípicos.
Un valor faltante (missing value) es un espacio que no tiene valor asignado en la observación de una variable específica. Este tipo de valores son bastante comunes y pueden surgir por muchas razones. Por ejemplo, podría haber un error en la recopilación de datos, alguien podría haberse negado a responder una pregunta en una encuesta, o simplemente podría ser que cierta información no esté disponible o no sea aplicable.
La función isnull()
es una poderosa herramienta para obtener esta información:
total_data.isnull().sum().sort_values(ascending=False)
Además, podemos dividir ese resultado por la longitud de nuestro DataFrame (número de filas) para obtener el porcentaje de valores faltantes en cada columna. Los valores faltantes generalmente se representan en forma de Nan
, Null
(nulo) o None
(ninguno) en el conjunto de datos:
total_data.isnull().sum().sort_values(ascending=False) / len(total_data)
Para lidiar con ellos hay muchas técnicas, y puedes encontrar más información aquí, pero que se resume en los siguientes puntos:
Para los casos con datos faltantes observados en las variables Age
, Embarked
y Fare
, vamos a utilizar la imputación numérica a través de la función fillna()
. En este caso vamos a utilizar algunas de las distintas opciones que se pueden escoger para rellenar valores:
total_data["Age"].fillna(total_data["Age"].median(), inplace = True)
total_data["Embarked"].fillna(total_data["Embarked"].mode()[0], inplace = True)
total_data["Fare"].fillna(total_data["Fare"].mean(), inplace = True)
total_data.isnull().sum()
Vemos cómo los valores han sido imputados correctamente y ya no existen faltantes.
Otro uso típico en esta ingeniería es la de la obtención de nuevas características mediante la "fusión" de dos o más ya existentes. Por ejemplo, en este caso de uso, el del análisis del Titanic, hay dos variables que representan los acompañantes de un pasajero. Por un lado, SibSp
contabiliza el número de hermanos que acompañaban al pasajero (incluyendo a su cónyuge, si aplica) y, por otro, Parch
contabiliza el número de acompañantes que eran padres e hijos. Uniendo estas dos variables y sumándolas podemos obtener una tercera, que nos informa sobre los acompañantes de un pasajero determinado, sin distinción entre los vínculos que pudieran tener.
total_data["FamMembers"] = total_data["SibSp"] + total_data["Parch"]
total_data.head()
De esta forma podemos simplificar el número de variables y trazar nuevas relaciones con la clase predictora.
El escalado de valores (feature scaling) es un paso crucial en el preprocesamiento de datos para muchos algoritmos de Machine Learning. Es una técnica que cambia el rango de los valores de los datos para que puedan ser comparables entre sí. El escalado normalmente implica la normalización, que es el proceso de cambiar los valores para que tengan una media de 0 y una desviación estándar de 1. Otra técnica común es el escalado mínimo-máximo, que transforma los datos para que todos los valores estén entre 0 y 1.
Antes de escalar los valores, debemos dividir convenientemente el conjunto en train y test, lo que prevendrá que los datos de entrenamiento no se contaminen con los datos del conjunto de pruebas. El escalador (scaler) será el encargado de escalar las variables en función del conjunto de entrenamiento.
from sklearn.model_selection import train_test_split
num_variables = ["Pclass", "Age", "Fare", "Sex_n", "Embarked_n", "FamMembers"]
# Dividimos el conjunto de datos en muestras de train y test
X = total_data.drop("Survived", axis = 1)[num_variables]
y = total_data["Survived"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 42)
X_train.head()
NOTA: Sólo se deben escalar las variables predictoras, nunca la objetivo.
A continuación detallaremos cómo podemos aplicar cada una de ellas, pero recordemos que depende mucho del modelo que vayamos a querer entrenar:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X_train)
X_train_norm = scaler.transform(X_train)
X_train_norm = pd.DataFrame(X_train_norm, index = X_train.index, columns = num_variables)
X_test_norm = scaler.transform(X_test)
X_test_norm = pd.DataFrame(X_test_norm, index = X_test.index, columns = num_variables)
X_train_norm.head()
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
scaler.fit(X_train)
X_train_scal = scaler.transform(X_train)
X_train_scal = pd.DataFrame(X_train_scal, index = X_train.index, columns = num_variables)
X_test_scal = scaler.transform(X_test)
X_test_scal = pd.DataFrame(X_test_scal, index = X_test.index, columns = num_variables)
X_train_scal.head()
NOTA: En este paso debemos asegurar que todas nuestras variables son numéricas, y, si no, como hemos visto en los pasos del principio, deberíamos transformarlas, como hemos hecho con
Sex
yEmbarked
.
¡Ahora, trabajemos y practiquemos la lección de hoy para afianzar lo aprendido!
La selección de características (feature selection) es un proceso que implica seleccionar las características (variables) más relevantes de nuestro conjunto de datos para usarlas en la construcción de un modelo de Machine Learning, desechando el resto.
Existen varias razones para incluirlo en nuestro análisis exploratorio:
Además, existen diversas técnicas para la selección de características. Muchas de ellas se basan a su vez en modelos supervisados, entrenados o de clustering, tienes más información aquí.
La librería sklearn
contiene gran parte de las mejores alternativas para llevarla a cabo. Una de las herramientas que más se utilizan para realizar procesos de selección de características rápidos y con buenos resultados es SelectKBest
. Esta función selecciona las k
mejores características de nuestro conjunto de datos basándose en una función de un test estadístico. Este test estadístico normalmente es un ANOVA o un Chi-Cuadrado:
from sklearn.feature_selection import f_classif, SelectKBest
# Con un valor de k = 5 decimos implícitamente que queremos eliminar 2 características del conjunto de datos
selection_model = SelectKBest(f_classif, k = 5)
selection_model.fit(X_train, y_train)
ix = selection_model.get_support()
X_train_sel = pd.DataFrame(selection_model.transform(X_train), columns = X_train.columns.values[ix])
X_test_sel = pd.DataFrame(selection_model.transform(X_test), columns = X_test.columns.values[ix])
X_train_sel.head()
X_test_sel.head()
X_train_sel["Survived"] = list(y_train)
X_test_sel["Survived"] = list(y_test)
X_train_sel.to_csv("/workspaces/machine-learning-content/assets/clean_titanic_train.csv", index=False)
X_test_sel.to_csv("/workspaces/machine-learning-content/assets/clean_titanic_test.csv", index=False)
La selección de características, al igual que el entrenamiento del modelo en general, se debe realizar solo en el conjunto de datos de entrenamiento y no en la totalidad. Si la llevásemos a cabo en todo el conjunto, podríamos introducir un sesgo que se conoce como contaminación de datos (data leakage), que ocurre cuando la información del conjunto de prueba se utiliza para tomar decisiones durante el entrenamiento, lo que puede llevar a una estimación demasiado optimista del rendimiento del modelo.
Por eso, la mejor práctica es dividir los datos en dos conjuntos: de entrenamiento y de prueba antes de la selección de características, que se realizará solo con los datos de entrenamiento y que luego se aplicará a ambos. De esta manera, aseguramos que este proceso y el modelo están siendo validados de manera justa.
En este caso, utilizando la selección de características de Chi cuadrado, las características más importantes son Pclass
, Sex
, Parch
, Fare
y Embarked
.
Ahora ya conocemos cómo llevar a cabo un estudio profundo y en detalle para conocer completamente nuestro conjunto de datos. Recordemos los pasos que tenemos que seguir para llevarlo a cabo:
Tras la implementación y adopción de estos pasos, estaremos preparados para entrenar el modelo.