6.4. Imputación de valores faltantes

Por varias razones, muchos conjuntos de datos del mundo real contienen valores faltantes, a menudo codificados como espacios en blanco, NaNs u otros marcadores de posición. Sin embargo, estos conjuntos de datos son incompatibles con los estimadores de scikit-learn, que asumen que todos los valores de un arreglo son numéricos y que todos tienen y contienen un significado. Una estrategia básica para utilizar conjuntos de datos incompletos es descartar filas y/o columnas enteras que contienen valores faltantes. Sin embargo, esto tiene el riesgo de perder datos que pueden ser valiosos (aunque sean incompletos). Una estrategia mejor es imputar los valores que faltan, es decir, deducirlos de la parte conocida de los datos. Consulta la entrada de Glosario de Términos Comunes y Elementos de la API sobre la imputación.

6.4.1. Imputación univariante frente a multivariante

Un tipo de algoritmo de imputación es el univariante, que imputa los valores en la i-ésima dimensión de la característica utilizando sólo los valores no faltantes en esa dimensión de la característica (por ejemplo, impute.SimpleImputer). Por el contrario, los algoritmos de imputación multivariante utilizan todo el conjunto de dimensiones de características disponibles para estimar los valores faltantes (por ejemplo, impute.IterativeImputer).

6.4.2. Imputación de características univariantes

La clase SimpleImputer proporciona estrategias básicas para la imputación de valores faltantes. Los valores faltantes se pueden imputar con un valor constante proporcionado, o utilizando los estadísticos (media, mediana o más frecuente) de cada columna en la que se encuentran los valores faltantes. Esta clase también permite diferentes codificaciones de los valores faltantes.

El siguiente fragmento de código demuestra cómo reemplazar los valores faltantes, codificados como np.nan, utilizando el valor medio de las columnas (eje 0) que contienen los valores faltantes:

>>> import numpy as np
>>> from sklearn.impute import SimpleImputer
>>> imp = SimpleImputer(missing_values=np.nan, strategy='mean')
>>> imp.fit([[1, 2], [np.nan, 3], [7, 6]])
SimpleImputer()
>>> X = [[np.nan, 2], [6, np.nan], [7, 6]]
>>> print(imp.transform(X))
[[4.          2.        ]
 [6.          3.666...]
 [7.          6.        ]]

La clase SimpleImputer también soporta matrices dispersas:

>>> import scipy.sparse as sp
>>> X = sp.csc_matrix([[1, 2], [0, -1], [8, 4]])
>>> imp = SimpleImputer(missing_values=-1, strategy='mean')
>>> imp.fit(X)
SimpleImputer(missing_values=-1)
>>> X_test = sp.csc_matrix([[-1, 2], [6, -1], [7, 6]])
>>> print(imp.transform(X_test).toarray())
[[3. 2.]
 [6. 3.]
 [7. 6.]]

Debemos tener en cuenta que este formato no está pensado para almacenar implícitamente los valores faltantes en la matriz porque la densificaría en el momento de la transformación. Los valores faltantes codificados por 0 deben utilizarse con una entrada densa.

La clase SimpleImputer también soporta datos categóricos representados como valores de cadena o categóricos de pandas cuando se utiliza la estrategia 'most_frequent' o 'constant':

>>> import pandas as pd
>>> df = pd.DataFrame([["a", "x"],
...                    [np.nan, "y"],
...                    ["a", np.nan],
...                    ["b", "y"]], dtype="category")
...
>>> imp = SimpleImputer(strategy="most_frequent")
>>> print(imp.fit_transform(df))
[['a' 'x']
 ['a' 'y']
 ['a' 'y']
 ['b' 'y']]

6.4.3. Imputación de características multivariantes

Un enfoque más sofisticado es utilizar la clase IterativeImputer, que modela cada característica con valores faltantes como una función de otras características, y utiliza esa estimación para la imputación. Lo hace de forma iterativa rotatoria: en cada paso, una columna de características se designa como salida y y las otras columnas de características se tratan como entradas X. Se ajusta un regresor en (X, y) para conocer y. A continuación, el regresor se utiliza para predecir los valores perdidos de y. Esto se hace para cada característica de forma iterativa, y luego se repite para las rondas de imputación max_iter. Se devuelven los resultados de la última ronda de imputación.

Nota

Este estimador sigue siendo experimental por ahora: los parámetros predeterminados o los detalles de comportamiento podrían cambiar sin ningún ciclo de obsolescencia. Resolver los siguientes problemas ayudaría a estabilizar IterativeImputer: criterios de convergencia (#14338), estimadores por defecto (#13286), y uso de estado aleatorio (#15611). Para utilizarlo, es necesario importar explícitamente enable_iterative_imputer.

>>> import numpy as np
>>> from sklearn.experimental import enable_iterative_imputer
>>> from sklearn.impute import IterativeImputer
>>> imp = IterativeImputer(max_iter=10, random_state=0)
>>> imp.fit([[1, 2], [3, 6], [4, 8], [np.nan, 3], [7, np.nan]])
IterativeImputer(random_state=0)
>>> X_test = [[np.nan, 2], [6, np.nan], [np.nan, 6]]
>>> # the model learns that the second feature is double the first
>>> print(np.round(imp.transform(X_test)))
[[ 1.  2.]
 [ 6. 12.]
 [ 3.  6.]]

Tanto SimpleImputer como IterativeImputer se pueden utilizar en un Pipeline como una forma de construir un estimador compuesto que soporte la imputación. Consulta Imputar valores faltantes antes de crear un estimador.

6.4.3.1. Flexibilidad de IterativeImputer

Hay muchos paquetes de imputación bien establecidos en el ecosistema de ciencia de datos de R: Amelia, mi, mice, missForest, etc. missForest es popular, y resulta ser una instancia particular de diferentes algoritmos de imputación secuencial que pueden ser implementados con IterativeImputer pasando diferentes regresores que se utilizarán para predecir los valores de las características que faltan. En el caso de missForest, este regresor es un Random Forest. Consulta Imputar valores faltantes con variantes de IterativeImputer.

6.4.3.2. Imputación múltiple vs. única

En la comunidad estadística, es una práctica común realizar múltiples imputaciones, generando, por ejemplo, m imputaciones separadas para una única matriz de características. Cada una de estas m imputaciones se somete a un análisis posterior de pipeline (por ejemplo, ingeniería de características, agrupamiento, regresión, clasificación). Los m resultados del análisis final (por ejemplo, los errores de validación apartados) permiten al científico de datos comprender cómo pueden diferir los resultados analíticos como consecuencia de la incertidumbre inherente causada por los valores faltantes. Esta práctica se denomina imputación múltiple.

Nuestra implementación de IterativeImputer se inspira en el paquete R MICE (Multivariate Imputation by Chained Equations) 1, pero difiere de él al devolver una única imputación en lugar de múltiples imputaciones. Sin embargo, IterativeImputer también puede utilizarse para imputaciones múltiples aplicándolo repetidamente al mismo conjunto de datos con diferentes semillas aleatorias cuando sample_posterior=True. Consulta 2, capítulo 4 para más discusión sobre imputaciones múltiples vs. simples.

Sigue siendo un problema abierto la utilidad de la imputación simple frente a la múltiple en el contexto de la predicción y la clasificación cuando el usuario no está interesado en medir la incertidumbre debida a los valores faltantes.

Ten en cuenta que una llamada al método transform de IterativeImputer no puede cambiar el número de muestras. Por lo tanto, no se pueden realizar múltiples imputaciones con una sola llamada a transform.

6.4.4. Referencias

1

Stef van Buuren, Karin Groothuis-Oudshoorn (2011). «mice: Multivariate Imputation by Chained Equations in R». Journal of Statistical Software 45: 1-67.

2

Roderick J A Little and Donald B Rubin (1986). «Statistical Analysis with Missing Data». John Wiley & Sons, Inc., New York, NY, USA.

6.4.5. Imputación de vecinos más cercanos

La clase KNNImputer proporciona la imputación para rellenar los valores faltantes utilizando el enfoque de k-Nearest Neighbors. Por defecto, se utiliza una métrica de distancia euclidiana que soporta los valores faltantes, nan_euclidean_distances, para encontrar los vecinos más cercanos. Cada característica que falta se imputa utilizando los valores de los «vecinos más cercanos» que tienen un valor para la característica. La característica de los vecinos se promedia uniformemente o se pondera por la distancia a cada vecino. Si a una muestra le falta más de una característica, los vecinos de esa muestra pueden ser diferentes en función de la característica concreta que se impute. Cuando el número de vecinos disponibles es inferior a n_neighbors y no hay distancias definidas con el conjunto de entrenamiento, se utiliza la media del conjunto de entrenamiento para esa característica durante la imputación. Si hay al menos un vecino con una distancia definida, durante la imputación se utilizará la media ponderada o no ponderada de los vecinos restantes. Si una característica siempre falta en el entrenamiento, se elimina durante la transformn. Para más información sobre la metodología, consulta la ref. [OL2001].

El siguiente fragmento de código demuestra cómo reemplazar los valores faltantes codificados como np.nan, utilizando el valor medio de la característica de los dos vecinos más cercanos de las muestras con valores faltantes:

>>> import numpy as np
>>> from sklearn.impute import KNNImputer
>>> nan = np.nan
>>> X = [[1, 2, nan], [3, 4, 3], [nan, 6, 5], [8, 8, 7]]
>>> imputer = KNNImputer(n_neighbors=2, weights="uniform")
>>> imputer.fit_transform(X)
array([[1. , 2. , 4. ],
       [3. , 4. , 3. ],
       [5.5, 6. , 5. ],
       [8. , 8. , 7. ]])
OL2001

Olga Troyanskaya, Michael Cantor, Gavin Sherlock, Pat Brown, Trevor Hastie, Robert Tibshirani, David Botstein and Russ B. Altman, Missing value estimation methods for DNA microarrays, BIOINFORMATICS Vol. 17 no. 6, 2001 Pages 520-525.

6.4.6. Marcar los valores imputados

El transformador MissingIndicator es útil para transformar un conjunto de datos en la correspondiente matriz binaria que indica la presencia de valores faltantes en el conjunto de datos. Esta transformación es útil junto con la imputación. Cuando se utiliza la imputación, conservar la información sobre los valores que faltan puede ser informativo. Tenga en cuenta que tanto el SimpleImputer como el IterativeImputer tienen el parámetro booleano add_indicator (False por defecto) que cuando se establece en True proporciona una forma conveniente de apilar la salida del transformador MissingIndicator con la salida del imputador.

NaN se utiliza normalmente como marcador de posición para los valores faltantes. Sin embargo, obliga a que el tipo de datos sea de punto flotante (float). El parámetro missing_values permite especificar otros marcadores de posición, como los enteros. En el siguiente ejemplo, utilizaremos -1 como valores faltantes:

>>> from sklearn.impute import MissingIndicator
>>> X = np.array([[-1, -1, 1, 3],
...               [4, -1, 0, -1],
...               [8, -1, 1, 0]])
>>> indicator = MissingIndicator(missing_values=-1)
>>> mask_missing_values_only = indicator.fit_transform(X)
>>> mask_missing_values_only
array([[ True,  True, False],
       [False,  True,  True],
       [False,  True, False]])

El parámetro features se utiliza para elegir las características para las que se construye la máscara. Por defecto, es 'missing-only' que devuelve la máscara de imputación de las características que contienen valores faltantes en el momento de fit:

>>> indicator.features_
array([0, 1, 3])

El parámetro features puede establecerse como 'all' para devolver todas las características, contengan o no valores faltantes:

>>> indicator = MissingIndicator(missing_values=-1, features="all")
>>> mask_all = indicator.fit_transform(X)
>>> mask_all
array([[ True,  True, False, False],
       [False,  True, False,  True],
       [False,  True, False, False]])
>>> indicator.features_
array([0, 1, 2, 3])

Cuando se utiliza el MissingIndicator en un Pipeline, asegúrate de utilizar el FeatureUnion o el ColumnTransformer para añadir las características del indicador a las características regulares. Primero obtenemos el conjunto de datos iris y le añadimos algunos valores faltantes.

>>> from sklearn.datasets import load_iris
>>> from sklearn.impute import SimpleImputer, MissingIndicator
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.pipeline import FeatureUnion, make_pipeline
>>> from sklearn.tree import DecisionTreeClassifier
>>> X, y = load_iris(return_X_y=True)
>>> mask = np.random.randint(0, 2, size=X.shape).astype(bool)
>>> X[mask] = np.nan
>>> X_train, X_test, y_train, _ = train_test_split(X, y, test_size=100,
...                                                random_state=0)

Ahora creamos una FeatureUnion. Todas las características se imputarán utilizando SimpleImputer, para que los clasificadores puedan trabajar con estos datos. Además, añade las variables indicadoras de MissingIndicator.

>>> transformer = FeatureUnion(
...     transformer_list=[
...         ('features', SimpleImputer(strategy='mean')),
...         ('indicators', MissingIndicator())])
>>> transformer = transformer.fit(X_train, y_train)
>>> results = transformer.transform(X_test)
>>> results.shape
(100, 8)

Por supuesto, no podemos utilizar el transformador para hacer predicciones. Deberíamos incluirlo en un Pipeline con un clasificador (por ejemplo, un DecisionTreeClassifier) para poder hacer predicciones.

>>> clf = make_pipeline(transformer, DecisionTreeClassifier())
>>> clf = clf.fit(X_train, y_train)
>>> results = clf.predict(X_test)
>>> results.shape
(100,)