sábado, 5 de diciembre de 2020

Preparación de datos para los algoritmos de Machine Learning

Vamos a aprender a preparar nuestro set de datos para que pueda ser manejado fácilmente por los algoritmos de Machine Learning, en vez de hacerlo manualmente siempre es interesante tener escritos nuestros scripts que nos ayuden en esta tarea. De este modo, podremos reproducir estas transformaciones  en otros sets de datos  o en el mismo set de datos cada vez que dispongamos de nuevos datos y nos permitirá acumular una librería de transformaciones que podremos utilizar en ocasiones futuras.

Limpieza de datos

La mayoría de algoritmos de Machine Learning ML, no trabajan bien con datos faltantes, de modo que crearemos algunas funciones que lo tengan en cuenta para hacer la correspondiente limpieza de valores faltantes. Podemos tomar varios caminos:
Eliminar las filas con datos faltantes
Eliminar los conjuntos de filas con datos faltantes (Por ej. si estamos trabajando con números de teléfono de una ciudad, en vez de eliminar sólo la fila sin información, podemos optar por eliminar el distrito entero de la ciudad en el que pertenezcan los datos faltantes)
Asignar un valor a los datos faltantes (Por ej. Ponerlos a cero o asignarles el valor medio que le corresponda, la mediana, etc.)
En nuestro ejemplo, tenemos un archivo .csv con datos planos del registro de temperaturas del observatorio de Madrid-Retiro, con datos para 1853 sólo para los meses de enero y octubre, el resto están vacíos.

Preparación de datos para los Algoritmos de Machine Learning


Si eliminamos los datos vacíos de enero, nos sigue apareciendo 1853, pues enero de este año tiene su valor correspondiente.
datos.dropna(subset=["ENE"])

Eliminar datos vacíos de un dataset


Para eliminar 1853 debemos elegir uno de los meses que no tenga datos, por ejemplo febrero
datos.dropna(subset=["FEB"])

Eliminar datos vacíos de un dataset


Ahora ya si podemos ver que ha desparecido 1853

Si queremos rellenarlo con la mediana de cada mes correspondiente podemos hacer 

media = datos["FEB"].median()
datos["FEB"].fillna(media, inplace=True)
datos.head()

Rellenar datos en un dataset para Machine Learning


En este caso, vemos como ha puesto la mediana de febrero de toda la serie al dato de febrero de 1853.Si hacemos esto no debemos olvidarnos de calcular la mediana en la fila anterior.
Si queremos eliminar una columna entera
datos.drop("FEB",axis=1)

Eliminar una columna de un Dataset


Para operar más rápidamente podemos utilizar la librería de Sciki-Learn SimpleImputer que es muy útil para estos propósitos.
En este caso calcularemos las medianas de nuestro dataset

from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
#esta línea elimina los datos no numéricos del dataset
datos_num = datos.select_dtypes(include=[np.number])
#ajustamos la instancia imputer entrenándola con nuestros
#datos, para ello utilizamos la función fit() que calculará #la mediana de cada columna sobre nuestros datos
imputer.fit(datos_num)
#podemos ver los resultados con statistics_
imputer.statistics_

Rellenar datos de un Dataset con Scikit-Learn

Si hubiera datos faltantes podríamos utilizar imputer para rellenar los huecos con estas medianas calculadas.
Para ellos hacemos esto

X = imputer.transform(datos_num)
datos_tr = pd.DataFrame(X, columns=datos_num.columns,index=datos.index)

En la figura vemos que el dato faltante se ha rellenado con su mediana correspondiente.
datos_tr.head()

Rellenar datos de un Dataset con Scikit-Learn


Si no recordamos con que tipo de dato estamos rellenando los datos faltantes, podemos escribir 
imputer.strategy  
En nuestro caso nos devuelve median


Manejar texto y atributos de categoría en Machine Learning

En Machine Learning, manejaremos muchos datos numéricos, pero otras veces tendremos que tratar datos de texto. Por ejemplo en nuestro dataset numérico tenemos una columna de carácter que es texto.
Para cargar el dataset hacemos


import pandas as pd
def load_retiro():

    return pd.read_csv("Nombre_archivo_dataset.csv")

buscamos la columna de texto sabiendo que se llama "Caracter". 

  datos_cat =                    datos[["Caracter"]]
  datos_cat.head(10)

Manejar texto y atributos de categoría en Machine Learning


A los algoritmos de Machine Learning  ML no le gusta el texto, así que transformaremos esta columna de texto en una column numérica, para ello pondremos

from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
datos_cat_encoded = ordinal_encoder.fit_transform(datos_cat)

datos_cat_encoded[:10]

Manejar texto y atributos de categoría en Machine Learning

Si queremos listar en modo texto las diferentes categorías que componen nuestro dataset, lo hacemos con la variable de la instancia  ordinal_encoder.categories_


categories_ordinal_encoder.categories_ing

  
Puede haber casos en los que esta clasificación no nos satisfaga, pues las categorías podrían no ser contiguas como en este ejemplo, tal vez deseemos asignar un 1 a una categoría y un 0 a cualquier categoría que no sea la elegida. Este tipo de clasificación se llama codificación “one-hot” porque sólo un atributo toma valor 1 (hot) y el resto toman valor cero. Scikit-Learn provee la clase OneHotEncoder para convertir los valores categóricos en sus respectivos vectores one-hot. Por defecto la clase OneHotEncoder devuelve un array escaso (sparse array) que contiene un valor relleno como 1, y el resto va con ceros. Si deseamos cargar todos nuestro sparse arrays en un array denso, podemos utilizar el método toarray().


from sklearn.preprocessing import OneHotEncoder
cat_codificador = OneHotEncoder()
datos_cat_1hot = cat_codificador.fit_transform(datos_cat)
datos_cat_1hot

datos_cat_1hot.toarray()

OneHotEncoder


OneHotEncoder carga para cada fila de nuestro dataset, un sparse array con un 1 en la posición que corresponde al valor codificado, en este caso, la primera posición corresponde a cálido y la tercera a muy cálido. Como se puede observar, el orden de las categorías está ordenado según aparecen en el dataset y no en un orden natural como podría ser “muy cálido-> calido -> normal -> fresco-> muy fresco”.
En un dataset con miles de categorías diferentes obtendríamos un array relleno con miles de ceros, esto puede ocuparnos demasiado espacio inútil en la memoria. La función toarray() nos muestra una visión de la matriz cargada de ceros, pero en realidad sólo almacena los unos y su posición.
Una alternativa a toarray() es poner la variable sparse a false, el resultado será idéntico al mostrado arriba.


cat_codificador = OneHotEncoder(sparse=False)
datos_cat_1hot = cat_codificador.fit_transform(datos_cat)

datos_cat_1hot

Si queremos revisar las categorías, siempre podemos hacer

cat_codificador.categories_


Transformadores personalizados

Aunque Scikit_Learn tiene muchos transformadores, es posible que deseemos un tipo de transformación a medida, personalizado según nuestros requerimientos pero que sea compatible con las funcionalidades de Scikit-Learn. Para ello, todo lo que tenemos que hacer, es implementar una clase que contenga tres métodos: fit() que retorne self, transform() y fit_transform(). Este último podemos obtenerlo simplemente añadiendo una clase base llamada TransformerMixin. Si añadimos baseEstimator como clase base y los evitar los parámetros *args y **kargs en el constructor tendremos además dos métodos extra más get_params() y set_params() que serán útiles para ajustar automáticamente los hiperparámetros.


from sklearn.base import BaseEstimator, TransformerMixin

# indices de columnas
años_ix, enero_ix, agosto_ix, total_ix = 1, 2, 9, 14

class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_parametro_personalizado = True): # sin *args or **kargs
        self.add_parametro_personalizado = add_parametro_personalizado
    def fit(self, X, y=None):
        return self  # nada que hacer
    def transform(self, X):
        parametro_personalizado = X[:, enero_ix]
        parametro_personalizado2 = X[:, total_ix]
        if self.add_parametro_personalizado:
            parametro_personalizado3 = X[:, enero_ix] / X[:, agosto_ix]
            return np.c_[X, parametro_personalizado,parametro_personalizado2,
                         parametro_personalizado3]
        else:
            return np.c_[X, parametro_personalizado,parametro_personalizado2]

attr_adder = CombinedAttributesAdder(add_parametro_personalizado=False)
datos_extra_attribs = attr_adder.transform(datos.values)

datos_extra_attribs = pd.DataFrame(
    datos_extra_attribs,
    columns=list(datos.columns)+["relacion_enero_agosto", "total"],
    index=datos.index)

datos_extra_attribs.head()

Transformadores personalizados

En este ejemplo hemos creado una nueva columna con la relación de datos enero/agosto pero podemos crear cualquier relación que consideremos relevante.


Escalado de características

Es una de las más importantes transformaciones que podemos aplicar, pues los algoritmos de ML no trabajan bien con datos de diferentes escalas.
Hay dos formas comunes de tener todos los datos con la misma escala: escalado  min-max y estandarización.
El escalado min-max (mucha gente lo llama normalización) es el más simple. Los valores se re-escalan a valores entre 0 y 1. Esto se consigue restando el valor mínimo al máximo y dividiendo entre el máximo menos en mínimo. Scikit-Learn tiene un transformador llamado MinMaxScaler. Esta clase tiene un hiperparámetro que nos permite cambiar el rango si por alguna razón no queremos que sea 0-1.
La estandarización es diferente. Primero resta el valor medio (de modo que la media de todos los valores sea 0) entonces divide entre la desviación estándar resultando que la distribución tiene varianza unidad. Al contrario que min-max, la estandarización no constriñe los valores entre un rango específico. Esto puede ser un problema para algunos algoritmos, por ejemplo las redes neuronales esperan valores entre 0 y 1) sin embargo la estandarización es mucho menos afectada por los valores atípicos, pues por ejemplo en nuestro caso de temperaturas todas oscilan entre poco más de cero y 30 ºC. si por error ponemos 100 ºC el sistema min-max actuaría en consecuencia y asignaría 1 a los 100 ºC, con lo que el resto de valores quedarían entre 0 y 0,3. Mientras que utilizando la estandarización, el resto de valores no se vería afectado.

Para la estandarización Scikit_lear dispone del transformador StandardScaler.

Tuberías de Transformación (pipelines)
En este post hemos visto diferentes transformaciones que podemos realizar sobre los datos, que además deber ser realizadas en el orden correcto. Para esto Scikit-Learn dispone de la clase Pipeline que nos ayuda en estas secuencias de transformación. Aquí hay una pequeña tubería para adaptar los atributos numéricos.


datos_num = datos.select_dtypes(include=[np.number])
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

num_pipeline = Pipeline([
        ('imputador', SimpleImputer(strategy="median")),
        ('añade_atributos', CombinedAttributesAdder()),
        ('escalador_estandard', StandardScaler()),
    ])


datos_num_tr = num_pipeline.fit_transform(datos_num)

El constructor de la tubería toma una lista de pares nombre/estimador que definen la secuencia de pasos que seguirá. El último estimador debe ser un transformador, es decir debe poseer el método fit_transform(). Los nombres pueden ser  los que queramos pero no se permiten dos barras bajas seguidas “__”.

Cuando llamamos al método fit()de la tubería, este llama  a fit_transform() secuencialmente en todos los transformadores, pasando la salida de cada transformador como la entrada del siguiente.
Hay que tener en cuenta que debemos tratar las columnas numéricas y las categóricas  de forma separada. Si queremos tratarlas conjuntamente Scikit-Learn dispone de la clase ColumnTransformer que permite transformar al mismo tiempo varias columnas.


from sklearn.compose import ColumnTransformer
num_attribs = list(datos_num)
cat_attribs = ["Caracter"]

full_pipeline = ColumnTransformer([
        ("num", num_pipeline, num_attribs),
        ("cat", OneHotEncoder(), cat_attribs),
    ])

datos_prepared = full_pipeline.fit_transform(datos)

podemos ver el resultado de la ejecución poniendo

datos_prepared

Pipelines en Scikit-Learn Machine Learning


aquí primero importamos la clase ColumnTransformer, seguida de la lista de las columnas numéricas y las categóricas, luego construimos un constructor ColumnTransformer que requiere una lista de tuplas, cada una de las cuales contiene un nombre, un transformador y una lista de nombres (o índices) de las columnas sobre las que se va a aplicar la tubería.
En este ejemplo hemos definido que las columnas numéricas sean transformadas usando el num_pipeline definido más arriba y las categorías sean transformadas utilizando OneHotEncoder finalmente aplicamos este ColumnTransformer  a los datos cargados de nuestro dataset llamado datos.
Hay que tener en cuenta que OneHotEncoder devuelve un array sparse, mientras que num-pipeline devuelve un array denso. ColumnTransformer estima la densidad de la matriz final (la relacción de celdas que no son cero) y devuelve una matriz sparse si la densidad es inferior a un umbral dado.

En vez de utilizar un transformador, también podríamos haber utilizado la cadena drop para eliminar las columnas deseadas o passthrough si queremos dejar las columnas sin tocar.


No hay comentarios:

Publicar un comentario