====== 06 - EDA: Preparación de datos ======
===== Información de los datos =====
==== Columnas del dataset ====
{{ :clase:ia:saa:4_preparacion_datos:adult.zip |}}
Lo primero que hay que hacer es entender los datos con los que vamos a trabajar. Podemos usar varias funciones para mostrar información relevante sobre los datos.
import pandas as pd
import numpy as np
df = pd.read_csv('adult.csv')
**Primeras filas de un dataframe**:
df.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_014.png?400 |}}
La función **head()** nos muestra las 5 primeras columnas de nuestro dataframe. Podemos pasarle un número para mostrar las n primeras filas:
df.head(7)
{{ :clase:ia:saa:4_preparacion_datos:selection_016.png?400 |}}
**Información detallada sobra cada columna**:
df.info()
RangeIndex: 32560 entries, 0 to 32559
Data columns (total 15 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 age 32559 non-null float64
1 workclass 32560 non-null object
2 fnlwgt 32560 non-null int64
3 education 32560 non-null object
4 education-num 32554 non-null float64
5 marital-status 32560 non-null object
6 occupation 32560 non-null object
7 relationship 32560 non-null object
8 race 32560 non-null object
9 sex 32560 non-null object
10 capital-gain 32560 non-null int64
11 capital-loss 32560 non-null int64
12 hours-per-week 32552 non-null float64
13 native-country 32560 non-null object
14 gains 32560 non-null object
dtypes: float64(3), int64(3), object(9)
memory usage: 3.7+ MB
La función **info()** nos muestra el número de datos no nulos y el tipo de datos de cada columna.
**Información estadística de cada columna**:
df.describe()
{{ :clase:ia:saa:4_preparacion_datos:selection_015.png?400 |}}
La función **describe()** nos muestra diferentes parámetros estadísticos (media, cuartiles, desviación estándar...) de cada columna.
**Valores nulos**:
df.isnull().sum()
age 1
workclass 0
fnlwgt 0
education 0
education-num 6
marital-status 0
occupation 0
relationship 0
race 0
sex 0
capital-gain 0
capital-loss 0
hours-per-week 8
native-country 0
gains 0
dtype: int64
aunque con **info()** podemos saber el número de nulos de cada columna, nos puede resultar más cómodo hacerlo de esta forma.
La mayoría de modelos de ML no admiten datos nulos ni datos que no sean numéricos, con lo que antes de aplicar el modelo deberemos preparar los datos para que sean válidos.
También podemos ver los valores de la etiqueta. En nuestro caso, la idea es calcular el sueldo de una persona en función de las variables dadas (edad, estudios, sexo...)
df['gains'].unique()
array([' <=50K', ' >50K'], dtype=object)
La funcion **unique()** nos muestra los posibles valores de nuestra etiqueta, con lo que en nuestro caso el modelo deberá tratar de inferir si una persona con unas características dadas ganará más o menos de 50.000 $ al año.
==== Inspección visual de los datos ====
Otra cosa que nos puede ayudar es mostrar diferente información con gráficas. Por ejemplo, podemos mostrar la distribución de cada columna con respecto a la etiqueta:
import matplotlib.pyplot as plt
import seaborn as sns
df_numerics = df.select_dtypes(include = np.number)
figure=plt.figure(figsize = (15, 6))
for i, column in enumerate(df_numerics.columns, 1):
axes = figure.add_subplot(3,3,i)
sns.kdeplot(x = df_numerics[column], hue = df['gains'], fill = True, ax = axes)
figure.tight_layout()
{{ :clase:ia:saa:4_preparacion_datos:grafica1.png?400 |}}
Para mostrar la gráfica anterior, las columnas deben ser de tipo numérico, de ahí que seleccionemos sólo las columnas que cumplan ese requisito con **df.select_dtypes(include = np.number)**
También podríamos mostrar los diferentes histogramas de las columnas:
figure=plt.figure(figsize = (15, 20))
for i, column in enumerate(df.columns, 1):
axes = figure.add_subplot(8,2,i)
sns.histplot(x = df[column], ax = axes)
figure.tight_layout()
{{ :clase:ia:saa:4_preparacion_datos:grafica2.png?400 |}}
Relaciones entre las variables:
n_samples_to_plot = 5000
columns = ['age', 'education-num', 'hours-per-week']
_ = sns.pairplot(data=df[:n_samples_to_plot], vars=columns,
hue="gains", plot_kws={'alpha': 0.2},
height=3, diag_kind='hist', diag_kws={'bins': 30})
{{ :clase:ia:saa:4_preparacion_datos:grafica3.png?400 |}}
Vamos a fijarnos en el gráfico anterior que relaciona horas trabajadas a la semana y edad con el sueldo:
ax = sns.scatterplot(
x="age", y="hours-per-week", data=df[:n_samples_to_plot],
hue="gains", alpha=0.5,
)
{{ :clase:ia:saa:4_preparacion_datos:grafica4.png?400 |}}
Si nos fijamos, podemos establecer regiones en el gráfico:
{{ :clase:ia:saa:4_preparacion_datos:grafica5.png?400 |}}
De esta forma, de manera visual vemos que la mayoría de gente menor de 27 años o que trabajen menos de 40 horas a la semana gana menos de 50K.
El objetivo de nuestros modelos de ML es encontrar esos patrones de forma automática.
Matriz de correlación:
corr_df = df.corr(method='pearson')
plt.figure(figsize=(8, 6))
sns.heatmap(corr_df, annot=True)
plt.show()
{{ :clase:ia:saa:4_preparacion_datos:grafica6.png?400 |}}
Boxplots, para ver los cuartiles, dispersión de los datos, valores anómalos...:
sns.boxplot(y=df["age"])
plt.show()
{{ :clase:ia:saa:4_preparacion_datos:grafica7.png?400 |}}
sns.boxplot(x='gains', y ='age', data = df)
plt.show()
{{ :clase:ia:saa:4_preparacion_datos:grafica8.png?400 |}}
sns.boxplot(x='gains', y='age', hue='sex', data=df)
plt.show()
{{ :clase:ia:saa:4_preparacion_datos:grafica9.png?400 |}}
Distribuciones:
_ = sns.kdeplot(df['age'])
plt.show()
{{ :clase:ia:saa:4_preparacion_datos:grafica10.png?400 |}}
n_rows=2
n_cols=3
fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols, figsize=(30,15))
for i, column in enumerate(df_numerics):
sns.kdeplot(df[column],ax=axes[i//n_cols,i%n_cols])
{{ :clase:ia:saa:4_preparacion_datos:grafica11.png?400 |}}
===== Datos nulos =====
Si tenemos datos nulos, tenemos 3 posibilidades para solucionar el problema:
* Eliminar todas las filas con valores nulos
* Inferir su valor
* Eliminar la columna
Dependiendo del número de la situación (número de filas afectadas, tipo de datos de la columna...) utilizaremos una estrategia u otra.
Por ejemplo, en nuestro dataset adult.csv, podemos mirar las filas que tienen edad nulo con la función **isnull()**:
df[df['age'].isnull()]
{{ :clase:ia:saa:4_preparacion_datos:selection_017.png?400 |}}
Vemos que sólo hay una fila con la edad nula, con lo que podemos borrarla sin que afecte a nuestro entrenamiento del modelo:
df.drop(df[df['age'].isnull()].index, inplace = True)
df['age'].isnull().sum()
0
La opción **inplace = True** en la función **drop()** hace que el borrado se haga directamente en el dataframe, sin realizar una copia.
Vamos a comprobar ahora el campo education_num.
df[df['education-num'].isnull()]
{{ :clase:ia:saa:4_preparacion_datos:selection_018.png?400 |}}
Podríamos borrar las filas afectadas igual que hemos hecho antes, pero si nos fijamos, tenemos otro campo (**education**) que podría estar relacionado con él. Veamos si es así:
df['education-num'].unique()
array([13., 9., 7., 14., 5., 10., 12., 11., 4., 16., nan, 15., 3.,
6., 2., 1., 8.])
df['education'].unique()
array([' Bachelors', ' HS-grad', ' 11th', ' Masters', ' 9th',
' Some-college', ' Assoc-acdm', ' Assoc-voc', ' 7th-8th',
' Doctorate', ' Prof-school', ' 5th-6th', ' 10th', ' 1st-4th',
' Preschool', ' 12th'], dtype=object)
Como vemos, el campo **education_num** es un valor que va del 1 al 16 (sin contar el nulo **nan**). La característica **education** es un cadena que nos indica el tipo de educación de la persona (preescolar, bachillerato, primaria....), y también tiene 16 posibles valores. Parece claro que **education-num** es el código del tipo de educación. Para comprobarlo, podemos sacar los valores únicos de **education** con los diferentes números del campo **education-num**-
df[df['education-num'] == 13]['education'].unique()
array([' Bachelors'], dtype=object)
Si lo hacemos al revés, nos da el mismo resultado (sin contar los valores **nan**), con lo que nuestra deducción parece correcta y podemos rellenar los datos que faltan con su valor correspondiente.
df[df['education'] == ' Bachelors']['education-num'].unique()
array([13., nan])
En realidad, para estar seguros deberíamos comprobar el resto de códigos (o, por lo menos, un número significativo de ellos)
df.loc[df['education'] == ' Bachelors', 'education-num'] = 13
df.loc[df['education'] == ' Some-college', 'education-num'] = 10
df.loc[df['education'] == ' HS-grad', 'education-num'] = 9
df['education-num'].isnull().sum()
0
Para intentar inferir los valores nulos, podemos utilizar diferentes técnicas. En el caso anterior estaba bastante claro como sacar los códigos faltantes, pero también podemos utilizar, por ejemplo, modelos de Machine Learning que nos calcule los valores más posibles de los datos nulos
Puede que te estés preguntando para qué queremos dos columnas con la misma información (**education** y **education-num**). En realidad, deberíamos borrar una de las dos (**education**) para que nuestro modelo sea más eficiente, como veremos en el tema **mejora de datos**.
En nuestro dataframe, sólo nos queda una columna con datos nulos (**hours-per-week**). Como son pocos, podemos borrar las filas afectadas como hemos hecho con el campo **age**.
df.drop(df[df['hours-per-week'].isnull()].index, inplace = True)
df['hours-per-week'].isnull().sum()
0
df.isnull().sum()
age 0
workclass 0
fnlwgt 0
education 0
education-num 0
marital-status 0
occupation 0
relationship 0
race 0
sex 0
capital-gain 0
capital-loss 0
hours-per-week 0
native-country 0
gains 0
dtype: int64
En este caso, no hemos borrado ninguna columna, ya que los datos nulos no eran mucho. En el caso de que hubiera una columna con bastantes datos nulos, igual nos interesa más borrar directamente la característica de nuestro modelo
Aunque pueda parecer que hemos solucionado los valores faltantes, no tiene porqué ser así. Vamos a ver los valores de la columna **workclass**, por ejemplo:
df['workclass'].unique()
array([' Self-emp-not-inc', ' Private', ' State-gov', ' Federal-gov',
' Local-gov', ' ?', ' Self-emp-inc', ' ', ' Without-pay',
' Never-worked'], dtype=object)
Como vemos, entre los valores tenemos **?** y un espacio en blanco, con lo que tendríamos que tratar esos datos también.
Tenemos que mirar todas las columnas de nuestro dataframe para asegurarnos que no nos faltan datos (o no son datos anómalos). Si son numéricos, con mirar el valor más bajo y el más alto (la función **describe()** nos lo indica) y ver si hay nulos puede ser suficiente, pero tenemos que prestar atención, sobre todo, a los valores de los datos de tipo cadena de caracteres.
Para saber cuántos valores anómalos hay en la columna, podemos usar la función **value_counts()**:
df['workclass'].value_counts()
Private 22687
Self-emp-not-inc 2541
Local-gov 2093
? 1836
State-gov 1296
Self-emp-inc 1114
Federal-gov 960
Without-pay 14
Never-worked 7
3
Name: workclass, dtype: int64
Como vemos, tenemos 1836 registros con valor **?** en el campo **workclass** y 3 con espacio en blanco. A partir de aquí, adoptaremos alguna de las estrategias anteriores según el caso.
===== Datos categóricos =====
Las variables categóricas tienen valores discretos tomados de una lista finita de valores.
Por ejemplo, la columna workclass toma un valor de 7 posibles:
df['workclass'].value_counts()
Private 22276
Self-emp-not-inc 2499
Local-gov 2067
State-gov 1277
Self-emp-inc 1072
Federal-gov 943
Without-pay 14
Name: workclass, dtype: int64
Para saber que variables son categóricas, podemos mirar el tipo de datos de cada característica:
df.dtypes
age float64
workclass object
fnlwgt int64
education object
education-num float64
marital-status object
occupation object
relationship object
race object
sex object
capital-gain int64
capital-loss int64
hours-per-week float64
native-country object
gains object
dtype: object
Las columnas con tipo de dato **object** contienen valores de tipo cadena. Podemos aprovechar ésto para seleccionar las columnas categóricas según su tipo de datos con la función **make_column_selector** de la librería **scikit-learn**:
from sklearn.compose import make_column_selector as selector
categorical_columns_selector = selector(dtype_include=object)
categorical_columns = categorical_columns_selector(df)
categorical_columns
['workclass',
'education',
'marital-status',
'occupation',
'relationship',
'race',
'sex',
'native-country',
'gains']
Ahora podemos crear un dataset sólo con los datos categóricos:
df_categorical = df[categorical_columns]
df_categorical.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_028.png?400 |}}
Vamos a ver diferentes estrategias para codificar variables categóricas. Para hacer pruebas, vamos a hacer una copia de nuestro dataset categórico primero:
# Copiamos el dataframe para ir haciendo pruebas.
# deep = True para que los cambios en df_c no se reflejen de df_categorical
df_c = df_categorical.copy(deep = True)
==== LabelEncoder ====
Codifica etiquetas con valores entre 0 y n_clases - 1.
Esta clase tiene, entre otros, los siguientes métodos:
* **fit**: Entrena nuestro codificador con los datos que le pasamos
* **transform**: Codifica los datos que le pasamos según nuestro codificador entrenado
* **fit_transform**: Entrena nuestro codificador, y devuelve las categorías codificadas (una mezcla de los dos anteriores)
Usamos el método **fit_transform** para cambiar directamente los valores en nuestro dataset y ver el resultado:
from sklearn.preprocessing import LabelEncoder
enc_le = LabelEncoder()
df_c['education'] = enc_le.fit_transform(df_c['education'])
df_c['education'].unique()
array([ 9, 11, 1, 12, 6, 15, 7, 5, 10, 8, 14, 4, 0, 13, 2, 3])
El problema de esta función es que codifica los valores por orden alfabético. En nuestro caso, existe un orden en el tipo de educación (de menos a más), con lo que perderíamos esa característica. Podemos ver la diferencia si nos fijamos en las columnas **education** y **education-num** de las primeras filas del dataset original y el dataset copiado:
df_c.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_029.png?400 |}}
df.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_030.png?400 |}}
**LabelEncoder** sólo se debe utilizar para codificar **variables objetivo** (**target**). Para codificar características ordinales, usaremos la siguiente función (**OrdinalEncoder**)
==== OrdinalEncoder ====
Codifica características como una matriz de enteros.
La entrada a este transformador debe ser una matriz de enteros o cadenas, que denota los valores tomados por características categóricas (discretas). Las entidades se convierten en números enteros ordinales. Esto da como resultado una sola columna de números enteros (0 a n_categorías - 1) por característica.
Igual que con **LabelEncoder**, usamos el método **fit_transform**, con la diferencia que esta vez la entrada debe ser una matriz.
from sklearn.preprocessing import OrdinalEncoder
df_c = df_categorical.copy(deep = True)
enc_ord = OrdinalEncoder()
df_c["education"] = enc_ord.fit_transform(df_c[["education"]])
Si queremos ver las categorías que ha codificado, usamos **categories_**:
enc_ord.categories_
[array([' 10th', ' 11th', ' 12th', ' 1st-4th', ' 5th-6th', ' 7th-8th',
' 9th', ' Assoc-acdm', ' Assoc-voc', ' Bachelors', ' Doctorate',
' HS-grad', ' Masters', ' Preschool', ' Prof-school',
' Some-college'], dtype=object)]
Vamos a ver como ha quedado nuestro dataset:
df_c.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_031.png?400 |}}
El codificador ha ordenado las categorías igual que antes (por orden alfabético), con lo que seguimos teniendo el mismo problema. Para solucionarlo, podemos pasarle la lista de categorías ordenadas como parámetro con **categories**. Este parámetro será una matriz con un array con las categorías ordenadas por cada columna del dataset que queramos codificar.
df_c = df_categorical.copy(deep = True)
education_order = [' Preschool', ' 1st-4th', ' 5th-6th', ' 7th-8th', ' 9th', ' 10th', ' 11th', ' 12th',
' HS-grad', ' Some-college', ' Assoc-voc', ' Assoc-acdm', ' Bachelors', ' Masters', ' Prof-school',
' Doctorate']
# Categoríes es una matriz con cada una de las categorías de los features
enc_ord = OrdinalEncoder(categories = [education_order])
df_c["education"] = enc_ord.fit_transform(df_c[["education"]])
Si miramos ahora las categorías del codificador, ya están ordenadas:
enc_ord.categories_
[array([' Preschool', ' 1st-4th', ' 5th-6th', ' 7th-8th', ' 9th', ' 10th',
' 11th', ' 12th', ' HS-grad', ' Some-college', ' Assoc-voc',
' Assoc-acdm', ' Bachelors', ' Masters', ' Prof-school',
' Doctorate'], dtype=object)]
Si miramos los datasets y comparamos las columnas education y education-num como antes, veremos que no coinciden los códigos.
df_c.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_032.png?400 |}}
df.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_030.png?400 |}}
Esto pasa porque las categorías de nuestro dataset original empiezan por el 1, mientras que el codificador empieza las categorías por el 0 (aunque se mantiene el orden de las mismas, que es lo importante)
=== Categorías nuevas ===
¿Qué pasa si después de entrenar nuestro codificador con las categorías de los datos de entrenamiento le pasamos datos con categorías nuevas? Vamos a probarlo con la columna **education**.
Primero creamos unos datos con un valor nuevo en la columna.
df_c = df_categorical.copy(deep = True)
df_test = df_c.loc[0:3, :].copy(deep = True)
df_test['education'] = 'new_value'
df_test.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_033.png?400 |}}
Volvemos a entrenar a nuestro codificador (en este caso vamos a utilizar los métodos **fit** y **transform**, ya que no queremos cambiar los datos de nuestro dataset original, sólo entrenar al codificador y modificar los nuevos datos):
enc_ord = OrdinalEncoder(categories = [education_order])
enc_ord.fit(df_c[["education"]])
Vamos a aplicar nuestro codificador entrenado con los nuevos datos:
df_test["education"] = enc_ord.transform(df_test[["education"]])
...
--> 136 raise ValueError(msg)
137 else:
138 # Set the problematic rows to an acceptable value and
ValueError: Found unknown categories ['new_value'] in column 0 during transform
El codificador da un error, al encontrar categorías que no ha visto en su entrenamiento (**new_value**).
Para solucionarlo, debemos usar el parámetro **handle_unknow** cuando construimos el codificador. Este parámetro acepta dos valores:
* **error**: Comportamiento por defecto. Mostrará un error si encuentra nuevas categorías
* **use_encoded_value**: Las nuevas categorías serán codificadas con el valor que le pasemos al parámetro **unknown_value**
* Cualquier otro valor, codificará las nuevas categorías como cero
Vamos a volver a crear nuestro codificador, pero esta vez poniendo un valor fijo (-1) a las nuevas categorías:
df_test = df_c.loc[0:3, :].copy(deep = True)
df_test['education'] = 'new_value'
enc_ord = OrdinalEncoder(categories = [education_order], handle_unknown='use_encoded_value', unknown_value=-1)
enc_ord.fit(df_c[["education"]])
df_test["education"] = enc_ord.transform(df_test[["education"]])
df_test.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_034.png?400 |}}
**OrdinalEnconder** es útil cuando nuestras variables categóricas tienen un orden y queremos que nuestro modelo de ML lo tenga en cuenta. Pero, ¿qué pasa si la característica no tiene ningún orden y aplicamos el codificador? En este caso, corremos el riesgo que nuestro modelo aprenda que los valores tienen un cierto orden, lo cual podría ser engañoso. Para estas situaciones, mejor usar el siguiente codificador: **OneHotEncoder**.
==== OneHotEncoder ====
La idea de este codificador, es crear una columna por cada valor diferente que tengamos en nuestras características. Las columnas tendrán un valor de 0, excepto aquella que coincida con la característica del registro, que valdrá 1.
Por ejemplo, supongamos que tenemos este dataset:
{{ :clase:ia:saa:4_preparacion_datos:selection_035.png?200 |}}
Si lo codificamos con OneHotEncoder, nuestro dataset quedaría:
{{ :clase:ia:saa:4_preparacion_datos:selection_036.png?200 |}}
Para hacerlo con sklearn, usamos la clase **OneHotEncoder**:
from sklearn.preprocessing import OneHotEncoder
Primero creamos nuestro codificador que usaremos sobre la columna **workclass**:
df_c = df_categorical.copy(deep = True)
enc_ohe = OneHotEncoder()
enc_ohe.fit(df_c[["workclass"]])
Codificamos los datos y vemos como quedan:
data_encoder = enc_ohe.transform(df_c[['workclass']])
print(data_encoder)
(0, 4) 1.0
(1, 2) 1.0
(2, 2) 1.0
(3, 2) 1.0
(4, 2) 1.0
(5, 2) 1.0
(6, 4) 1.0
(7, 2) 1.0
(8, 2) 1.0
(9, 2) 1.0
(10, 5) 1.0
(11, 2) 1.0
...
En principio, no se parece en nada a nuestro objetivo final. Esto es porque OneHotEncoder almacena los datos como una matriz dispersa.
data_encoder
<30148x7 sparse matrix of type ''
with 30148 stored elements in Compressed Sparse Row format>
Una matriz dispersa es una matriz de gran tamaño en la que la mayor parte de sus elementos son cero. Para ahorrar memoria y tiempo de proceso, OneHotEncoder almacena por defecto sólo las posiciones donde hay un 1
Si queremos ver las columnas con un formato más legible, podemos usar la función **toarray()**:
data_encoder = enc_ohe.transform(df_c[['workclass']]).toarray()
print(data_encoder)
[[0. 0. 0. ... 1. 0. 0.]
[0. 0. 1. ... 0. 0. 0.]
[0. 0. 1. ... 0. 0. 0.]
...
[0. 0. 1. ... 0. 0. 0.]
[0. 0. 1. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]
También podemos decirle a **OneHotEncoder** que no utilice matrices dispersas si se lo indicamos directamente al crear el codificador con el parámetro **sparse = False**:
df_c = df_categorical.copy(deep = True)
enc_ohe = OneHotEncoder(sparse=False)
enc_ohe.fit(df_c[["workclass"]])
data_encoder = enc_ohe.transform(df_c[['workclass']])
print(data_encoder)
[[0. 0. 0. ... 1. 0. 0.]
[0. 0. 1. ... 0. 0. 0.]
[0. 0. 1. ... 0. 0. 0.]
...
[0. 0. 1. ... 0. 0. 0.]
[0. 0. 1. ... 0. 0. 0.]
[0. 0. 0. ... 0. 0. 0.]]
usamos **sparse = False** por motivos didácticos para una mayor legibilidad de los datos. En realidad, almacenar los datos como matriz dispersa es más eficiente
Ahora podemos transformar la nueva matriz a un dataset de Pandas, unirlo a nuestro dataset original y borrar el campo original:
df_encoder = pd.DataFrame(data_encoder)
df_encoder.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_037.png?400 |}}
df_c = df_c.join(df_encoder)
df_c.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_038.png?400 |}}
df_c.drop(['workclass'], axis = 1, inplace = True)
df_c.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_039.png?400 |}}
Si nos fijamos, las nuevas columnas tienen números como títulos (0, 1, 2, 3, 4, 5 y 6). Podemos ponerles un nombre más indicativo con la función **get_feature_names** de **OneHotEncoder**:
df_c = df_categorical.copy(deep = True)
enc_ohe = OneHotEncoder(sparse=False)
enc_ohe.fit(df_c[["workclass"]])
feature_names = enc_ohe.get_feature_names(input_features=["workclass"])
feature_names
array(['workclass_ Federal-gov', 'workclass_ Local-gov',
'workclass_ Private', 'workclass_ Self-emp-inc',
'workclass_ Self-emp-not-inc', 'workclass_ State-gov',
'workclass_ Without-pay'], dtype=object)
df_encoder = pd.DataFrame(data_encoder, columns = feature_names)
df_encoder.head()
{{ :clase:ia:saa:4_preparacion_datos:selection_040.png?400 |}}
La función **get_feature_names** está obsoleta a partir de la versión 1.0 y será eliminada en la versión 1.2. En su lugar, debemos usar **get_feature_names_out**.
Igual que **OrdinalEncoder**, **OneHotEncoder** también tiene el parámetro **handle_unknown**. En este caso, los valores posibles son "error" (se comporta igual) o "ignore" (todos los valores de las columnas serían 0).
Si un atributo categórico tiene una cantidad grande de categorías posibles (países, por ejemplo), entonces la codificación one-hot tendrá como resultado una cantidad grande de características de entrada. Esto puede ralentizar el entrenamiento y deteriorar el rendimiento. Si eso ocurre, conviene sustituir la entrada categórica por características útiles relacionadas con las categorías: por ejemplo, podríamos sustituir los países por continentes, regiones...
===== Preparar datos =====
Como hemos visto en el punto anterior, tenemos que hacer una serie de pasos para preparar nuestros datos (usar varios codificadores para las variables categóricas, por ejemplo).
Hacerlos uno a uno puede resultar tedioso, pero, por suerte tenemos una clase **sklearn** que nos puede facilitar mucho el trabajo: [[https://scikit-learn.org/stable/modules/generated/sklearn.compose.ColumnTransformer.html|ColumnTransformer]].
Vamos a realizar todo el proceso de preparado de datos y entrenaremos un modelo con RandomForest para predecir nuevos datos. No te preocupes si no entiendes el código relativo al modelo (separación de datos, entrenamiento, predicción, métricas...), lo iremos viendo en apartados posteriores.
import pandas as pd
import numpy as np
df = pd.read_csv('adult2.csv')
Después de tratar los datos nulos, guardamos el dataset resultante en otro diferente llamado **adult2.csv**, por eso cargamos éste y no **adult.csv**.
Cargamos las librerías necesarias para nuestros codificadores:
from sklearn.preprocessing import LabelEncoder, OrdinalEncoder, OneHotEncoder
Especificamos el orden de las categorías de la columna **education** y las características que vamos a codificar con OrdinalEncoder y OneHotEncoder:
education_order = [' Preschool', ' 1st-4th', ' 5th-6th', ' 7th-8th', ' 9th', ' 10th', ' 11th', ' 12th',
' HS-grad', ' Some-college', ' Assoc-voc', ' Assoc-acdm', ' Bachelors', ' Masters', ' Prof-school',
' Doctorate']
features_ohe = [
'workclass',
'marital-status',
'occupation',
'relationship',
'race',
'sex',
'native-country',
]
features_ord = ["education"]
Construimos nuestros codificadores:
enc_le = LabelEncoder()
enc_ord = OrdinalEncoder(categories = [education_order], handle_unknown='use_encoded_value', unknown_value=-1)
enc_ohe = OneHotEncoder(handle_unknown="ignore")
En este punto, es cuando utilizamos la clase **ColumnTransformer**. Esta clase nos permite especificar una serie de pasos (en nuestro caso los codificadores que usaremos) que aplicaremos a las columnas especificadas de nuestro dataset.
from sklearn.compose import ColumnTransformer
my_pipeline = ColumnTransformer(
[
('cat_Ord', enc_ord, features_ord),
('cat_Ohe', enc_ohe, features_ohe)
],
remainder='passthrough',
sparse_threshold=0
)
Vamos a comentar varias cosas del código anterior.
Si te fijas, los codificadores que vamos a usar están en una lista. Cada item de esa lista consiste en un nombre (puedes poner el que quieras, siempre que no se repita), el codificador a utilizar y sobre qué columnas lo vamos a usar.
('cat_Ord', enc_ord, features_ord),
('cat_Ohe', enc_ohe, features_ohe)
A continuación, usamos la propiedad **remainder = 'passthrough'**. Por defecto, las columnas a las que no se aplican los codificadores se borran del dataset resultante. Si no queremos que pase ésto (en nuestro caso borraría todas las columnas numéricas a las que no aplicamos ningún codificador), debemos poner esa propiedad con valor **passthrough**.
Por último, también ponemos el parámetro **sparse_threshold=0**. El problema es que cuando tienes muchas categorías en una columna (por ejemplo, en native-country), **ColumnTransformer** puede dar un error al almacenar los valores como matrices dispersas. Con esta propiedad le podemos indicar que devuelva siempre matrices densas, con lo que solucionamos el error.
Otra cosa que hay que tener en cuenta, es que sólo indicamos los codificadores que vamos a utilizar sobre las características de nuestro dataset, no sobre la etiqueta (**gains**). A la etiqueta le aplicaremos **LabelEncoder** más adelante.
Ahora vamos a separar los datos en características y etiqueta:
X = df.drop('gains', axis = 1)
y = df['gains']
Después, volvemos a separar en datos de test y entrenamiento (usaremos los datos de entrenamiento para entrenar a nuestro modelo y los datos de test para probarlo):
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
Si miramos las dimensiones de nuestros datos de entrenamiento, vemos que tienen 21103 filas y 14 columnas (características)
print(X_train.shape)
print(y_train.shape)
(21103, 14)
(21103,)
Una vez tenemos los datos separados, es cuando les aplicamos los codificadores a nuestros datos de entrenamiento:
X_train = pd.DataFrame(my_pipeline.fit_transform(X_train))
y_train = enc_le.fit_transform(y_train)
Si miramos las dimensiones ahora de nuestros datos, vemos como han aumentado las características hasta 88, por la aplicación de la codificación **OneHotEncoder** sobre algunas columnas.
print(X_train.shape)
print(y_train.shape)
(21103, 88)
(21103,)
Vamos a crear nuestro modelo y a entrenarlo con nuestros datos de entrenamiento:
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(max_depth=2, random_state=0)
model.fit(X_train, y_train)
Ahora que ya tenemos nuestro modelo entrenado, vamos a probarlo con nuestros datos de test. Primero tendremos que aplicarle las mismas transformaciones que le hemos aplicado a nuestros datos de entrenamiento:
X_test = pd.DataFrame(my_pipeline.transform(X_test))
y_test = pd.DataFrame(enc_le.transform(y_test))
Fíjate que antes usábamos el método **fit_transform** porque queríamos entrenar nuestros codificadores con los datos de entrenamiento y transformarlos. Ahora usamos **transform** porque no queremos volver a entrenar los codificadores, sólo transformar los datos.
\\ \\
Si usáramos de nuevo **fit_transform**, podríamos encontrar categorías en alguna columna que no estaban en los datos de entrenamiento, con lo que el número de características serían diferente en los datos de test y entrenamiento y no funcionaría nuestro modelo.
Si miramos las dimensiones de los datos de test, vemos que coinciden el número de características (88):
print(X_test.shape)
(9045, 88)
Lo último que nos queda es hacer nuestras predicciones y ver el rendimiento:
from sklearn.metrics import accuracy_score
pred = model.predict(X_test)
print(accuracy_score(pred,y_test))
0.7679380873410724
En este caso, hemos utilizado la exactitud (número de aciertos / total de casos) como métrica. Dependiendo del problema, no siempre es la mejor métrica, como veremos en el tema de //Clasificación//.