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.
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.
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()
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.
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])
Si tenemos datos nulos, tenemos 3 posibilidades para solucionar el problema:
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
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])
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
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
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.
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.
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)
Codifica etiquetas con valores entre 0 y n_clases - 1.
Esta clase tiene, entre otros, los siguientes métodos:
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()
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)
¿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:
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()
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>
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.]]
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()
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')
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))
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