Herramientas de usuario

Herramientas del sitio


clase:ia:saa:1eval:preparacion_datos

06 - EDA: Preparación de datos

Información de los datos

Columnas del dataset

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()

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)

Información detallada sobra cada columna:

df.info()

<class 'pandas.core.frame.DataFrame'>
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()

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()

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()

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})

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,
)

Si nos fijamos, podemos establecer regiones en el gráfico:

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()

Boxplots, para ver los cuartiles, dispersión de los datos, valores anómalos…:

sns.boxplot(y=df["age"])

plt.show()

sns.boxplot(x='gains', y ='age', data = df)

plt.show()

sns.boxplot(x='gains', y='age', hue='sex', data=df)

plt.show()

Distribuciones:

_ = sns.kdeplot(df['age'])
plt.show()

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])

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()]

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()]

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()

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()

df.head()

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()

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()

df.head()

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()

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()

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:

Si lo codificamos con OneHotEncoder, nuestro dataset quedaría:

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 '<class 'numpy.float64'>'
	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()

df_c = df_c.join(df_encoder)
df_c.head()

df_c.drop(['workclass'], axis = 1, inplace = True)
df_c.head()

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()

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: 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.
clase/ia/saa/1eval/preparacion_datos.txt · Última modificación: 2023/11/06 13:09 por cesguiro