Inteligencia artificiacial preprocesamiento de datasets

Preprocesamiento de datasets con Python, Scikit-learn y Pandas (Parte #4)

Contenido

Hasta ahora hemos visto problemas de clasificación, regresión y clusterización con Scikit-learn. Sin embargo, siempre hemos utilizado datasets que ya vienen definidos dentro de la propia librería y que ya están bien procesadas. No obstante, cuando encontramos un conjunto de datos en Internet o recogemos esos datos, no vendrán los datos procesados. Será necesario un proceso de preprocesamiento de datasets antes de que podamos aplicar algún algoritmo de machine learning.

En el presente artículo vamos a presentar una serie de técnicas de preprocesamiento de datasets con Python, Scikit-learn y Pandas. Estos métodos se usan cuando queremos preparar un dataset para aplicar un algoritmo de aprendizaje automático.

Obteniendo un dataset no preprocesado

Existen en Internet bastantes lugares desde los que obtener conjuntos de datos para utilizarlos en proyectos de aprendizaje automático. Para mostrar las técnicas que vamos a aprender en el presente artículo, vamos a usar un dataset no procesado. Para ello usaremos la página Kaggle que contiene datasets y retos para practicar las técnicas de machine learning. En concreto usaremos el dataset de clasificación de estrellas que podemos encontrar en el siguiente enlace.

Preprocesamiento de datasets con Python, ¿por dónde empiezo?

Cuando nos enfrentamos a un conjunto de datos sin procesar podemos esperar encontrarnos cualquier cosa dentro del mismo. Así, es frecuente encontrar caracteres extraños o no imprimibles, algunos datos que nos faltan, etc. Por todo ello es importante, realizar una primera visualización rápida para empezar a ver los primeros errores que nos podamos encontrar.

El dataset seleccionado viene en formato de valores separados por comas (Comma-separated values). Esto significa que es un fichero de texto con los valores separados por un separador, que en general suele ser una coma. Así, si abrimos el fichero de texto con nuestro editor favorito veremos algo así:

Temperature,L,R,A_M,Color,Spectral_Class,Type
 3068,0.0024,0.17,16.12,Red,M,0
 3042,0.0005,0.1542,16.6,Red,M,0
 2600,0.0003,0.102,18.7,Red,M,0
 2800,0.0002,0.16,16.65,Red,M,0
 1939,0.000138,0.103,20.06,Red,M,0
 2840,0.00065,0.11,16.98,Red,M,0
 2637,0.00073,0.127,17.22,Red,M,0
 2600,0.0004,0.096,17.4,Red,M,0
 2650,0.00069,0.11,17.45,Red,M,0
 2700,0.00018,0.13,16.05,Red,M,0

En la primera línea del fichero vemos un encabezado en el que tenemos indicado el nombre del atributo. En las siguientes líneas tendremos los valores de cada atributo para una instancia de nuestro dataset.

Como podemos apreciar, el formato de valores separados por coma (CSV en adelante) es un formato bastante simple de leer. Es por ello, que es bastante frecuente en el campo del machine learning, aunque no es el único formato que nos podemos encontrar para trabajar con datasets.

El dataset que hemos elegido para este artículo plantea un problema de clasificación, en el que disponemos de 6 atributos para cada instancia y una etiqueta («Type»). El dataset contiene 240 instancias que deberemos repartir en un conjunto de entrenamiento y otro de test tal y como vimos en otros artículos.

Leyendo el dataset usando Pandas

Lo primero que tendremos que hacer para trabajar con el dataset será leerlo desde el fichero CSV para usarlo en Python. Así, existen distintas alternativas, aunque en este artículo usaremos la librería de Python Pandas. Esta librería ofrece una gran cantidad de herramientas y funcionalidades que nos harán la vida más fácil a la hora de preprocesar datasets. Para cargar el dataset usaremos el siguiente código:

import pandas as pd
import sklearn as skl

url = "https://raw.githubusercontent.com/AprendeConEjemplos/aprendizaje-automatico-con-scikit-learn/main/04_Preprocesamiento/Stars.csv"
dataframe = pd.read_csv(url)
print(dataframe)

Como vemos en las dos primeras líneas cargamos las librerías Pandas y Scikit-learn y les ponemos un alias más corto (pd y skl respectivamente). Definimos una url desde la que vamos a cargar el dataset que vamos a utilizar en el código de este artículo. A continuación, leemos el fichero CSV desde la url definida con el método read_csv. Estamos leyendo desde una URL remota, pero si tenemos el fichero localmente en nuestra máquina lo podremos leer igual usando la ruta al mismo. Por último mostramos el objeto devuelto, que en este caso se llama dataframe, que Pandas usa como unidad de información fundamental, y que podemos asimilar a una tabla de datos. El resultado de imprimir el dataframe es el siguiente:

Contenido del dataframe con el dataset cargado antes de iniciar el preprocesamiento de datasets
Contenido del dataframe con el dataset cargado

Un primer acercamiento al contenido del dataset

Ahora que tenemos cargado el dataset dentro de un dataframe, podemos empezar a usar la gran cantidad de funcionalidad de la librería Pandas. Algo bastante útil es usar el método «describe» del dataframe que nos indica un resumen del contenido del mismo. Así si ejecutamos el siguiente código, obtendríamos como salida:

print(dataframe.describe())
Salida del método describe con un resumen del contenido del dataframe.
Salida del método describe con un resumen del contenido del dataframe

Se puede apreciar los siguientes valores:

  • count: Indica la suma de los valores no nulos de cada atributo. Los valores nulos/no presentes no suman.
  • mean: Valor medio de ese atributo.
  • std: Desviación estándar de los valores de ese atributo.
  • min: Valor mínimo del atributo.
  • 25%, 50%, 75%: Se corresponde con los valores correspondientes con esos percentiles. Así, las instancias que están dentro del percentil más bajo tienen una temperatura menor de 3344.25 grados.
  • max: Valor máximo para el atributo.

Iniciando el preprocesamiento del dataset

Ya tenemos cargado el dataset, ahora empieza el trabajo de preprocesamiento de datasets. Lo primero que vamos a realizar, es separar los atributos de las instancias del dataset de la etiqueta que queremos usar para el entrenamiento. Esto lo realizaremos con las siguientes líneas:

dataset = dataframe.drop("Type", axis=1)
label = dataframe["Type"].copy()

Como vemos la primera línea usa la función «drop» que elimina una línea o columna de la tabla especificando una etiqueta y un eje. En nuestro caso eliminamos la columna (axis=1) que se etiqueta como «Type», y el resultado se asigna a la variable dataset. A continuación seleccionamos la columna «Type» y la copiamos a la variable labels. La forma de seleccionar las columnas de un dataframe es similar a como se selecciona un valor de un diccionario de Python, usando como key el nombre de la columna.

Limpiando el dataset

La mayor parte de los algoritmos de aprendizaje automático tienen problemas o no funcionan bien cuando las instancias tienen atributos con datos nulos. Es decir, si en nuestro dataset tenemos un atributo para el que algunas instancias tienen un valor pero otras no, tendremos un problema.

En el caso de nuestro dataset esto no sucede. Hemos comprobado que el número de valores no nulos para todos los todos los atributos es igual que es el número de instancias del dataset.

Sin embargo en el caso de que tuviéramos este problema se pueden seguir tres opciones diferentes:

  • Eliminar las instancias con los valores nulos, reduciendo así el número de instancias.
  • Eliminar por completo, el atributo que contiene valores nulos, reduciendo el número de atributos de las instancias del dataset.
  • Asignar un valor al atributo de las instancias con valores nulos, por ejemplo el valor medio.

Ejemplo de limpieza de datasets

Imaginemos por un momento que el atributo «Temperature» de nuestro dataset tuviera valores nulos para algunas instancias. Para realizar algunas de las opciones anteriores usaríamos el siguiente código:

dataframe_op1 = dataframe.dropna(subset=["Temperature"])    # Opción 1, eliminamos las instancias con valores nulos
dataframe_op2 = dataframe.drop("Temperature", axis=1)       # Opción 2, eliminamos el atributo que contiene valores nulos
mean_temp = dataframe["Temperature"].mean()
dataframe_op3 = dataframe["Temperature"].fillna(mean_temp)  # Opción 3, asignamos el valor medio en los valores nulos

Como vemos en el primer caso se utiliza el método «dropna» que elimina valores nulos. Usamos el parámetro «subset» para indicar la columna que puede contener valores nulos. En el segundo ejemplo usamos el método «drop», indicando que eliminamos todos los valores del eje 1 (valores en la columna). Por último calculamos el valor medio (mean) para la columna «Temperature». Mediante el método «fillna» rellenamos los valores nulos de la columna «Temperature» con el valor medio.

Preprocesando atributos con texto en el dataset

En general, los algoritmos de machine learning no trabajan bien con atributos con valores que no sean numéricos. Es por ello, que aquellos atributos con valores que contengan un texto o sean una categoría, debemos transformarlos a valores numéricos.

En el caso que nos ocupa tenemos dos atributos no numéricos: «Color» que nos indica el color de la estrella en inglés (Red, Blue, etc) y «Spectral_Class» que es otro atributo categórico (M, O, A, etc).

Seleccionando los atributos de texto del dataset

Lo primero que vamos a hacer es seleccionar los atributos que tienen valores de texto. Para ello, vamos a utilizar el siguiente código:

color_cat = dataframe[['Color']]
spectral_cat = dataframe[['Spectral_Class']]
print(color_cat.head(10))
print(spectral_cat.head(10))

En este código seleccionamos todos los valores de los atributos «Color» y «Spectral_Class» del dataframe. A continuación mostramos los diez primeros valores usando el método «head», obteniendo la siguiente salida:

Salida de los valores de texto para los atributos "Color" y "Spectral_Class".
Salida de los valores de texto para los atributos «Color» y «Spectral_Class».

Pasando los atributos de texto a número

Nuestra primera opción, es hacer una transformación directa de cada valor del atributo de texto a un número correlativo. Es decir, si tenemos los valores «A, B, C, A, …» pasar los mismos valores a valor numérico «0, 1, 2, 0, …».

Para realizar esta conversión podemos utilizar las herramientas de preprocesamiento que nos brinda Scikit-learn. Para ello usaremos el siguiente código:

from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
color_cat_encoded = ordinal_encoder.fit_transform(color_cat)
print(color_cat_encoded[:10])

En el código importamos el codificador «OrdinalEncoder» desde el paquete de preprocesamiento de Scikit-learn (sklearn.preprocessing). Posteriormente creamos un objeto de tipo «OrdinalEncoder» y mediante el método «fit_transform» obtenemos la transformación de los valores de texto a un valor numérico.

Además el objeto que hemos creado tendrá una serie de valores interesantes. Por ejemplo, las categorías que se han creado, que podremos verlas accediendo a la variable «categories_» del objeto ordinal_encoder.

Sin embargo, existe un problema con esta codificación. La mayor parte de algoritmos de aprendizaje automático van a asumir que valores cercanos van a ser similares. Esto puede estar bien para atributos con valores como «MAL», «REGULAR», «BIEN», «EXCELENTE». Pero en nuestro caso y con los colores no tiene porque ser una buena idea. Por ello debemos utilizar otra forma de codificación.

Codificando atributos de texto con One Hot Encoding

Como hemos definido anteriormente, cuando no existe una relación de cercanía entre los valores de texto de un atributo, no es buena idea pasar directamente a valores numéricos consecutivos. En estos casos, lo que hacemos es crear un atributo con valores binarios para cada valor de texto. Por ejemplo, en el atributo «COLOR» tenemos el valor «RED». Así, creamos un atributo binario de nombre «RED» siendo 1 para las instancias cuyo color sea «RED» y 0 en otro caso. Veamos el código que necesitamos para realizar esta tarea.

from sklearn.preprocessing import OneHotEncoder
one_hot_encoder = OneHotEncoder()
color_cat_one_hot = one_hot_encoder.fit_transform(color_cat)
print(color_cat_one_hot.toarray().shape)
print(color_cat_one_hot.toarray())

Lo primero que hemos hecho en el código anterior es importar el codificador «OneHotEncoder» que realizará la codificación definida. A continuación creamos el objeto de tipo «OneHotEncoder». Después realizamos la codificación usando el método «fit_transform» igual que con el anterior codificador. Esto nos devuelve una codificación en la que se indica el número de instancia dentro del dataset, número de atributo binario y el valor. Con el método «toarray()» convertimos esa representación a una matriz y mostramos sus valores y tamaño («shape»). A continuación vemos la salida del código.

(240, 17) 
[[0. 0. 0. ... 0. 0. 0.]  
 [0. 0. 0. ... 0. 0. 0.] 
 [0. 0. 0. ... 0. 0. 0.] 
 ... 
 [0. 0. 0. ... 0. 0. 0.] 
 [0. 0. 0. ... 0. 0. 0.] 
 [1. 0. 0. ... 0. 0. 0.]]

Al igual que con el otro codificador también podemos obtener algunos valores útiles como las categorías que hemos codificado, mediante la variable «categories_» del objeto «one_hot_encoder».

La principal pega de este tipo de codificación es que si tenemos un gran número de valores de texto, generaremos un número igual de atributos binarios. Esto puede provocar que el algoritmo de machine learning se resienta y el desempeño sea peor: tiempos largos de entrenamiento, inferencia, etc. En estos caso es mejor cambiar estos valores de texto por un atributo numérico que sea apropiado. En el caso de los colores podríamos utilizar un modelo de colores.

Escalado de atributos

Este será una de las principales tareas que realicemos en el preprocesamiento de datasets. Los principales algoritmos de machine learning que existen no funcionan muy bien cuando existen una gran diferencia entre los valores de un atributo. En el caso de nuestro dataset, hemos comprobado que tenemos algunos atributos como por ejemplo el atributo «L» que contiene valores muy distantes. En concreto ese atributo tiene un valor mínimo de 0 y máximo de 849820. Esto es algo que debemos evitar, ya que los algoritmos de aprendizaje no funcionan bien en estos casos.

Existen dos formas claras de solucionar estos problemas de escalas: la normalización de valores y la estandarización.

Normalización de valores de un atributo

El escalado min-max o normalización, es una técnica común a la hora de solucionar el problema de tener diferentes escalas en los valores de un atributo. El objetivo que se consigue con esta técnica es que todos los valores de un atributo estén comprendido en el intervalo [0-1]. De forma matemática, lo que estamos haciendo es a cada valor le restamos el mínimo y lo dividimos entre el valor máximo.

Scikit-learn contiene funcionalidad para poder obtener el valor normalizado de un atributo numérico que presenta el problema de escalas. Vamos a ver un ejemplo de código de como lo haríamos.

from sklearn.preprocessing import MinMaxScaler

min_max_scaler = MinMaxScaler()
l_values = dataframe[['L']]
scaled_values = min_max_scaler.fit(l_values)
print(min_max_scaler.transform(l_values)[0:10])

En el código anterior importamos el algoritmo para escalar valores de un atributo. A continuación creamos el objeto de tipo «MinMaxScaler». Obtenemos los valores del atributo «L» que queremos escalar usando el acceso a columnas del dataframe. Usamos el método «fit» para obtener el mínimo y el máximo que se usarán para realizar los cálculos. Posteriormente usamos el método «transform» para obtener los valores del atributo. La última línea muestra los diez primeros valores escalados.

[[2.73127546e-09] 
 [4.94455040e-10] 
 [2.59000259e-10] 
 [1.41272869e-10] 
 [6.82818865e-11] 
 [6.71046126e-10] 
 [7.65228038e-10] 
 [3.76727649e-10] 
 [7.18137082e-10] 
 [1.17727390e-10]]

Usando la estandarización de valores de un atributo

La otra forma de realizar el escalado de valores que vamos a ver se denomina estandarización. Matemáticamente hablando, lo que estamos haciendo en este proceso es restar la media de los valores y dividir por la desviación estándar de los mismos. De esta forma los valores obtenidos tendrán una media de cero y una varianza de uno.

Existen algunas diferencias notables con respecto a la normalización. En primer lugar los valores obtenidos por la estandarización no están acotados en ningún rango ([0-1] por ejemplo).

En segundo lugar, este método consigue solucionar el problema de valores atípicos u outliers que presenta la normalización. Por ejemplo, supongamos que un atributo de temperatura contiene valores entre 0 y 100 por regla general. Por un error de medición, tenemos un valor de 10000 que se consideraría un outlier. Si aplicamos la normalización, la mayor parte de valores estarían en el rango 0-0.1. Sin embargo, esto no se da con la estandarización.

Al igual que en caso anterior, vamos a utilizar un ejemplo para ver como podríamos aplicar este proceso a los valores de un atributo. A continuación presentamos el código de ejemplo.

from sklearn.preprocessing import StandardScaler

standard_scaler = StandardScaler()
l_values = dataframe[['L']]
scaled_values = standard_scaler.fit(l_values)
print(standard_scaler.transform(l_values)[0:10])

Como puede apreciarse, lo que hacemos en primer lugar es importar la clase para realizar el escalado. Después creamos un objeto de tipo «StandardScaler». A continuación, obtenemos los valores del atributo «L» de nuestro dataset. Usamos el método «fit» que lo que hace es calcular la media y la desviación estándar de los valores del atributo. Posteriormente, con el método «transform», obtenemos los valores ya escalados. Mostramos los diez primeros valores ya escalados usando el «print» de Python.

[[-0.59862356] 
 [-0.59862357] 
 [-0.59862357] 
 [-0.59862357] 
 [-0.59862357] 
 [-0.59862357] 
 [-0.59862357] 
 [-0.59862357] 
 [-0.59862357] 
 [-0.59862357]]

Automatizando los pasos de preprocesamiento de datasets con Scikit-learn

Como hemos visto durante el artículo a la mayor parte de datasets les debemos aplicar varios pasos de procesamiento antes de que estén listos. Aplicar estos pasos de preprocesamiento a cada atributo de forma manual, puede resultar engorroso y con tendencia a cometer fallos.

Afortunadamente, la librería Scikit-learn nos brinda una funcionalidad para automatizar el proceso de preprocesamiento de datasets. Para ello usaremos lo que se conocen como tuberías de procesamiento (transformation pipelines).

Debemos pensar en estas tuberías como en una lista de tareas que se ejecuta de forma secuencial sobre un atributo. Así, imaginemos que a nuestro atributo le queremos aplicar un proceso de transformación para establecer los valores nulos como la media y posteriormente estandarizar los valores. Consistiría en ir indicando en la lista el transformador que queremos usar y sobre que atributo debería aplicarse.

Sin embargo, aún tendríamos que ir aplicando pipelines a cada atributo de forma secuencial. Para solucionar esto, Scikit-learn, presenta un transformador de columnas especialmente diseñado para trabajar con datos en forma de tabla, como los dataframes de Pandas.

Un ejemplo de preprocesamiento de datasets automatizado

Veamos un ejemplo para ilustrar la explicación:

from sklearn.compose import ColumnTransformer
num_attrs = ["Temperature", "L", "R"]
text_attrs = ["Color", "Spectral_Class"]

pipeline = ColumnTransformer([
                              ("numeric", StandardScaler(), num_attrs),
                              ("text", OneHotEncoder(), text_attrs)
])
preprocessed_dataset = pipeline.fit_transform(dataset)

En el código vemos como inicialmente se importa el «ColumnTransformer» que nos ayudará a automatizar el proceso de preprocesamiento de datasets. A continuación definimos los nombre de los atributos sobre los que vamos a realizar las diferentes tareas de preprocesado. En nuestro caso, vamos a separar atributos de texto a los que les aplicaremos el «OneHotEncoder»; y los atributos numéricos a los que realizaremos un proceso de estadarización. Posteriormente definimos el objeto de tipo «ColumnTransformer» usando una lista en la que cada item contiene: un nombre, el proceso que queremos aplicar y los atributos sobre los que va a actuar. Así, el primer item sería: («numeric», StandardScaler(), num_attrs), siendo el nombre «numeric» el que queremos indicarle, StandardScaler() la estadarización y «num_attrs» una lista de los nombre de los atributos numéricos.

Finalmente en la variable «preprocessed_dataset» obtendremos el dataset preprocesado, que será una matriz dispersa. Este objeto puede ser usado directamente con los algoritmos de machine learning que dispone la librería Scikit-learn.

Usando el dataset preprocesado

Una vez que hemos finalizado la fase de preprocesamiento de datasets, podemos aplicar el algoritmo de machine learning adecuado a nuestro problema. En el caso del ejemplo que hemos usado es de un problema de clasificación de estrellas en función de los atributos definidos para cada instancia.

Si revisamos el artículo de clasificación que ya hemos visto anteriormente, podemos usar el código que ya definimos adaptándolo un poco. Así tendremos el siguiente código:

from sklearn.model_selection import train_test_split
# Realizamos la partición de nuestro dataset en un conjunto de entrenamiento y otro de test (20%)
X_train, X_test, y_train, y_test = train_test_split(preprocessed_dataset, label, test_size=0.2, random_state=42)

Primero importamos el algoritmo que se usa para partir nuestro conjunto de datos en entrenamiento y test, tal y como vimos en el artículo de clasificación. Con el método «train_test_split» partimos nuestro conjunto de datos en entrenamiento y test. Como puede apreciarse usamos un 20% del total como test («test_size=0.2») y un valor de «random_state» para reproducir resultados.

Por último realizamos el entrenamiento y vemos los resultados usando para ello el siguiente código de ejemplo:

from sklearn.svm import SVC

# Creamos el clasificador SVM lineal
classifier =  SVC()

# Realizamos el entrenamiento
classifier.fit(X_train, y_train)

# Obtenemos el accuracy de nuestro modelo para el conjunto de test
print(classifier.score(X_test, y_test))

Lo primero que hacemos es importar el algoritmo de clasificación que vamos a usar, que en nuestro caso será una máquina de vector de soporte para clasificación. Posteriormente creamos un objeto de tipo SVC (máquina de vector de soporte), con los parámetros por defecto. En la siguiente línea de código entrenamos usando para ello el método «fit». Por último, calculamos el score del clasificador usando para ello el conjunto de datos para test (X_test e y_test) usando el método «score». Este valor nos indicará el porcentaje de instancias bien clasificadas. En nuestro caso concreto obtenemos casi un 84% de accuracy. Este valor, aunque bueno, aún podría mejorarse modificando los parámetros del clasificador o usando otro algoritmo de clasificación.

Resumiendo

En este artículo hemos visto diferentes métodos empleados durante la fase de preprocesamiento de datasets. Hemos empezado leyendo un dataset en formato CSV, usando para ello la librería Pandas y sus dataframes. Posteriormente, hemos empezado viendo los procesos de limpieza de datasets de valores extraños o inválidos. A continuación, hemos visto como tratar con los atributos con valores de texto. Por último, hemos preprocesado aquellos datos numéricos que presentan problemas de escala. Todo ello lo hemos automatizado usando los métodos que nos brinda Scikit-learn para dicho propósito. Finalmente hemos conseguido entrenar de forma satisfactoria un clasificador con nuestro dataset preprocesado.

Esperamos que el artículo de preprocesamiento de datasets te haya resultado interesante y de utilidad. Recuerda que dispones del código fuente empleado en este artículo en el repositorio de código fuente de la web.

Sugerencias

Para cualquier consulta o sugerencia no dudes en ponerte en contacto con nosotros a través del siguiente formulario.

    captcha

    Bibliografía

    Comparte el artículo

    Entradas relacionadas