Identificación e imputación de valores perdidos en Python
Los valores perdidos (missing values en inglés) están presente en la mayoría de los conjuntos de datos con los que trabajemos en nuestro día a día. Son aquellos en los que no se almacena ningún valor de datos en una observación. Es de vital importancia identificar y ocuparse, por ejemplo, mediante su imputación, de estos valores para poder seguir con nuestro análisis o entrenamiento de modelo de machine learning.
Es por eso que dedicaremos esta nueva publicación a explicar cómo detectar éstos missing values en nuestros conjuntos de datos en Python mediante un ejemplo práctico. Además, también mostraremos como podemos eliminar o imputar estos valores de varias maneras (media, moda, k-nearest neighbors, etc). Comencemos conociendo el conjunto de datos con el que trabajaremos en el tutorial.
El conjunto de datos
Para este tutorial vamos a trabajar con el conjunto de datos público Automobile, que podéis encontrar en el repositorio UCI Machine Learning. Éste dataset contiene 205 instancias con diferentes características de automóviles como el número de puertas (number-of-doors
), los caballos (horsepower
) o el precio (price
). Para cargar el dataset en Python hacemos uso de la función read_csv
de la librería Pandas, a la que le pasamos la ruta donde se alojan los datos y nos devuelve un objeto DataFrame
. Una vez tenemos nuestro DataFrame
, le asignamos el nombre de cada variable como podéis ver en el siguiente código:
import pandas as pd
# Cargamos el dataset
df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning'
'-databases/autos/imports-85.data', encoding = 'utf-8',
header = None)
# Añadimos el nombre de cada variable
df.columns = ["symboling", "normalized-losses", "make", "fuel-type",
"aspiration", "num-of-doors", "body-style",
"drive-wheels", "engine-location", "wheel-base", "length",
"width", "height", "curb-weight", "engine-type",
"num-of-cylinders", "engine-size", "fuel-system", "bore",
"stroke", "compression-ratio", "horsepower", "peak-rpm",
"city-mpg", "highway-mpg", "price"]
Seguidamente, echamos un primer vistazo a estos datos y mostramos las primeras cinco observaciones:
df.head()
De entrada, observamos varias columnas como normalized-losses
con valores perdidos y representados como ?
.
Identificación de missing values en Python
En la libraría Pandas de Python, los valores perdidos se representan con None
y NaN
(acrónimo de Not a Number). Este último es un valor especial de punto flotante reconocido por todos los sistemas que utilizan la representación estándar de punto flotante IEEE. Además, Pandas asigna automáticamente NaN
si el valor de una celda es un string vacío ''
, NA
o NaN
.
Sin embargo, hay ocasiones en las que los valores perdidos se representan con un valor diferente a los antes mencionados, por ejemplo, con un 0
en el caso de una variable numérica o ?
, como en el caso de nuestro conjunto de datos. En este último caso, lo que debemos hacer es reemplazar el valor de estos valores perdidos representados con ?
por el valor NaN
, y para ello Pandas nos permite pasar una lista de valores que queremos considerar en todas las columnas como valores perdidos mediante el parámetro na_values
:
import pandas as pd
na_values = ["?"]
# Cargamos el dataset
df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning'
'-databases/autos/imports-85.data', encoding = 'utf-8',
na_values = na_values, header = None)
# Añadimos el nombre de cada variable
df.columns = ["symboling", "normalized-losses", "make", "fuel-type",
"aspiration", "num-of-doors", "body-style",
"drive-wheels", "engine-location", "wheel-base", "length",
"width", "height", "curb-weight", "engine-type",
"num-of-cylinders", "engine-size", "fuel-system", "bore",
"stroke", "compression-ratio", "horsepower", "peak-rpm",
"city-mpg", "highway-mpg", "price"]
Volvemos a mostrar las primeras observaciones para asegurarnos que los valores con ?
han sido reemplazados correctamente por NaN
:
df.head()
También existe una función replace
que podemos usar para reemplazar un valor por otro sin tener que cargar el dataset de nuevo. El siguiente paso consiste en conocer el número de missing values que hay en cada variable. La manera más sencilla para ello consiste en usar función isnull
de Pandas, que devuelve True
cuando se trata de un valor perdido y False
en caso contrario, junto con la función sum
para saber cuántos valores perdidos hay en cada columna. Veamos como funciona en el siguiente código:
# Identificamos el número de missing values en cada columna
df.isnull().sum()
Podemos observar que la variable normalized-losses
tiene 41 valores perdidos. bore
, stroke
y price
cuentan con 4 missing values. Finalmente num-of-doors
, horsepower
y peak-rpm
tienen solo 2 valores perdidos. Otra forma de visualizar los missing values en un gráfico es utilizar los mapas de calor que nos proporciona la librería seaborn como podéis ver a continuación:
import seaborn as sns
# Identificamos los missing values visualmente
sns.heatmap(df.isnull(), cbar=False)
Las marcas blancas del anterior gráfico representan los valores perdidos. Mediante este gráfico es más fácil encontrar patrones y vínculos existentes entre los missing values en las diferentes variables.
Una vez sabemos los valores perdidos que hay en las variables y están identificadas correctamente en el Dataframe
, es hora de ocuparnos de ellos mediante su eliminación o imputación.
Eliminación de valores perdidos
En primer lugar explicaremos como eliminar estos valores perdidos en Python. Esta opción no es recomendable para conjuntos de datos pequeños o con un alto porcentaje de valores perdidos. Dicho esto y si estamos seguros de que queremos eliminar estos valores, tan solo tenemos que usar la función dropna
que nos proporciona Pandas.
Así, podemos, por ejemplo, eliminar las observaciones que contengan valores perdidos en todas las columnas ajustando el argumento how
a 'all'
(how='all'
) o mantener sólo aquellas con al menos 3 valores que no sean missing values (thresh=3
). Para este caso, le vamos a definir en qué columnas buscar los valores perdidos y para ello usamos el parámetro subset
:
# Eliminamos las filas con missing values en horsepower o peak-rpm
df.dropna(subset=['horsepower', 'peak-rpm'], inplace=True)
# Comprobamos que se han eliminado
print("valores perdidos en horsepower: " +
str(df['horsepower'].isnull().sum()))
print("valores perdidos en peak-rpm: " +
str(df['peak-rpm'].isnull().sum()))
Imputación de los valores perdidos
Hay dos aproximaciones comúnmente usadas para la imputación de los valores perdidos.
La primera técnica consiste en rellenar estos valores con la media (o mediana) de los datos de la variable en el caso de que se trate de una variable numérica. Para el caso de las variables categóricas imputamos los valores perdidos con la moda de la variable.
De nuevo Pandas nos ofrece una función (fillna
) para realizar esta imputación de manera sencilla. A continuación podéis ver como imputamos la variable numérica bore
con la media y la variable categórica num-of-doors
con el valor mas frecuente:
# Imputamos la variable bore con la media
df['bore'].fillna(df['bore'].mean(), inplace=True)
print("valores perdidos en bore: " +
str(df['bore'].isnull().sum()))
# Imputamos la variable num-of-doors con la moda
df['num-of-doors'].fillna(df['num-of-doors'].mode()[0], inplace=True)
print("Valores perdidos en num-of-doors: " +
str(df['num-of-doors'].isnull().sum()))
Para este ejemplo práctico hemos calculado la media/moda de todos los datos. Sin embargo, en un ejemplo real y para evitar la fuga de datos (data leakage en inglés), es importante dividir los datos en datos de entrenamiento y datos de test, calcular la media/moda únicamente de los datos de entrenamiento y aplicarla a los datos de prueba.
Otra técnica más avanzada consiste en el uso de modelos predictivos para estimar los valores perdidos. Un modelo no paramétrico muy popular para estos casos es el k-nearest neighbors, donde se estima el valor perdido como la media (en el caso de las variables numéricas) de los valores de los k vecinos u observaciones mas cercanos. Asimismo, para las variables categóricas, se utiliza las clase mayoritaria de entre los k mas cercanos.
La librería scikit-learn nos proporciona la clase KNNImputer
para hacer uso de este modelo en la imputación de missing values. Esta clase usa por defecto la distancia euclidiana, pero podemos elegir la que prefiramos modificando el parámetro metric
. Asimismo, también podemos elegir el número de vecinos con el argumento n_neighbors
y en esta ocasión usaremos 5. Podeís ver en el siguiente código como utilizamos este método para imputar los valores perdidos en la variable normalized-losses
:
from sklearn.impute import KNNImputer
# Construimos el modelo
imputer = KNNImputer(n_neighbors=5, weights="uniform")
# Ajustamos el modelo e imputamos los missing values
imputer.fit(df[["normalized-losses"]])
df["normalized-losses"] = imputer.transform(df[["normalized-losses"]]).ravel()
print("Valores perdidos en normalized-losses: " +
str(df['normalized-losses'].isnull().sum()))
De nuevo recordamos que, aunque para este tutorial hemos entrenado el modelo con todos los datos, en un problema real es recomendable hacerlo solo con los del conjunto de entrenamiento.
En esta ocasión hemos aprendido como identificar los valores perdidos en Python y dos técnicas para imputarlos. El código usado en este artículo lo podéis obtener desde nuestra cuenta de GitHub. ¡Hasta la próxima!