Saltar a contenido

Manejo de valores faltantes

1 ¿Por qué faltan datos?

Los valores faltantes son inevitables en datos reales. Pueden deberse a errores de medición, respuestas omitidas en encuestas, fallos de sensores o simplemente porque esa información no se recopiló. La forma de manejarlos afecta directamente al rendimiento del modelo.

1.1 Tipos de valores faltantes

Tipo Significado Ejemplo
MCAR (Missing Completely At Random) Falta al azar, sin relación con nada Error de medición aleatorio
MAR (Missing At Random) Falta relacionada con otras variables observadas Hombres no responden pregunta de salud
MNAR (Missing Not At Random) Falta relacionada con el propio valor faltante Personas con ingresos altos no declaran ingresos

Nota

MCAR es el mejor escenario: eliminar filas con faltantes no sesga los datos. MNAR es el peor: necesitas modelar explícitamente por qué faltan los datos.

2 Análisis de valores faltantes

Antes de decidir qué hacer, analiza cuántos faltan y dónde:

import pandas as pd
import numpy as np

df = pd.DataFrame(data={
    'feature_1': [np.nan, 3, 6, 9, 12, 15, np.nan],
    'feature_2': [100, np.nan, 200, 300, np.nan, np.nan, 600],
    'feature_3': [1000, 500, 2000, 3000, 4000, 6000, 8000],
})

# Valores nulos por columna
print(df.isnull().sum())
# feature_1    2
# feature_2    3
# feature_3    0

# ¿Algún nulo en cada fila?
filas_con_nulos = df.isnull().any(axis=1)
print(f"Filas con al menos un nulo: {filas_con_nulos.sum()}")  # 5

# Ratio de valores faltantes por columna
ratio_faltantes = (df.isnull().sum() / df.shape[0]).sort_values(ascending=False)
print(ratio_faltantes)
# feature_2    0.428571
# feature_1    0.285714
# feature_3    0.000000

# ¿Algún nulo en algún sitio?
print(df.isnull().any().any())  # True

2.1 Visualización con missingno

import missingno as msno

# Matriz de faltantes
msno.matrix(df)
plt.title('Patrón de valores faltantes')
plt.tight_layout()

# Barra de faltantes
msno.bar(df)
plt.title('Valores no faltantes por columna')
plt.tight_layout()

3 Estrategia 1: Eliminar

La estrategia más simple: eliminar filas o columnas con valores faltantes.

3.1 Eliminar filas (listwise deletion)

# Eliminar filas con cualquier faltante
df_dropped = df.dropna()
print(f"Filas originales: {len(df)}")
print(f"Filas tras eliminar: {len(df_dropped)}")
print(f"Porcentaje eliminado: {1 - len(df_dropped)/len(df):.1%}")

Cuándo usar: - Pocos faltantes (\< 5% del dataset). - Los datos son MCAR (faltan al azar). - Tienes suficientes datos como para perder filas.

3.2 Eliminar columnas

# Eliminar columnas con > 50% de faltantes
umbral = 0.5
cols_eliminar = df.columns[df.isnull().mean() > umbral]
df_dropped_cols = df.drop(columns=cols_eliminar)
print(f"Columnas eliminadas: {cols_eliminar.tolist()}")

3.3 Eliminar con umbral (thresh)

thresh requiere que una columna/fila tenga al menos \(N\) valores no nulos para sobrevivir:

# Eliminar columnas que no tengan al menos el 90% de valores definidos
df_filtrado = df.dropna(thresh=int(df.shape[0] * 0.9), axis=1)

# Eliminar filas que no tengan al menos 2 valores definidos
df_filtrado = df.dropna(thresh=2, axis=0)

Cuándo usar: Cuando quieres un control más fino que simplemente “eliminar si tiene cualquier nulo”. Por ejemplo, conservar columnas con al menos el 90% de datos definidos.

Cuándo usar: - Una columna tiene muchos faltantes (> 50%) y no es importante. - La columna no tiene poder predictivo.

Advertencia

Cuidado con eliminar indiscriminadamente. Si los datos no son MCAR, eliminar filas puede introducir sesgo. Por ejemplo, si las personas con ingresos altos tienden a no declarar ingresos, eliminar esas filas sesga el dataset hacia ingresos bajos.

4 Estrategia 2: Imputación por estadísticos simples

Reemplaza los valores faltantes con un estadístico resumen de la columna.

4.1 Media o mediana (numéricas)

from sklearn.impute import SimpleImputer

# Imputación por mediana (más robusta a outliers que la media)
imputer_median = SimpleImputer(strategy='median')
df['ingresos_imputados'] = imputer_median.fit_transform(df[['ingresos']])

# Imputación por media
imputer_mean = SimpleImputer(strategy='mean')
df['edad_imputados'] = imputer_mean.fit_transform(df[['edad']])

4.2 Moda (categóricas)

imputer_mode = SimpleImputer(strategy='most_frequent')
df['ciudad_imputada'] = imputer_mode.fit_transform(df[['ciudad']])

4.3 Constante

imputer_const = SimpleImputer(strategy='constant', fill_value='desconocido')
df['educacion_imputada'] = imputer_const.fit_transform(df[['educacion']])

4.4 Rellenado hacia adelante y hacia atrás (pad/bfill)

Pandas ofrece fillna con métodos que propagan el último valor válido:

# pad: rellena con el valor anterior (hacia adelante)
df_relleno = df.fillna(method='pad', limit=1)

# bfill: rellena con el valor siguiente (hacia atrás)
df_relleno = df.fillna(method='bfill')

# Ejemplo concreto
df_ejemplo = pd.DataFrame({
    'A': [1, np.nan, np.nan, 4, 5],
    'B': [np.nan, 2, np.nan, np.nan, 5]
})
print(df_ejemplo.fillna(method='pad'))
#       A    B
# 0   1.0  NaN
# 1   1.0  2.0
# 2   1.0  2.0
# 3   4.0  2.0
# 4   5.0  5.0

Cuándo usar: Cuando los datos tienen un orden inherente (series temporales, datos secuenciales). pad propaga hacia adelante; bfill hacia atrás.

4.5 Interpolación

La interpolación estima valores faltantes a partir de los valores vecinos usando métodos numéricos:

df_ejemplo = pd.DataFrame({
    'A': [1, np.nan, np.nan, 4, 5]
})

# Interpolación lineal (por defecto)
print(df_ejemplo['A'].interpolate())
# 0    1.0
# 1    2.0
# 2    3.0
# 3    4.0
# 4    5.0

# Interpolación polinómica
print(df_ejemplo['A'].interpolate(method='polynomial', order=2))

Cuándo usar: Datos ordenados temporalmente donde los valores faltantes están “entre” dos valores conocidos.

4.6 Reemplazo directo

# Reemplazar NaN por un valor específico
df_reemplazado = df.replace(np.nan, 0)

Advertencia

Cuidado con replace(np.nan, 0): Puede ser útil para datos donde 0 tiene sentido (por ejemplo, ventas), pero en muchos casos 0 introduce un sesgo artificial. Prefiere la media o mediana cuando no sabes qué valor poner.

Estrategia Tipo de dato Ventaja Desventaja
Media Numérico Simple Sensible a outliers
Mediana Numérico Robusta a outliers Pierde información de distribución
Moda Categórico Simple Puede crearategorías dominantes
Constante Cualquiera Explícito Puede ser arbitrario

Importante

Siempre aplica la imputación solo en entrenamiento:

from sklearn.model_selection import train_test_split

X_train, X_test = train_test_split(df, test_size=0.2)

imputer = SimpleImputer(strategy='median')
X_train_imp = imputer.fit_transform(X_train)  # fit + transform
X_test_imp = imputer.transform(X_test)        # solo transform

5 Estrategia 3: KNN Imputer

El KNN Imputer reemplaza cada valor faltante con la media de los \(k\) vecinos más cercanos. Asume que observaciones similares tienen valores similares.

\[ \hat{x}_{\text{faltante}} = \frac{1}{k} \sum_{i \in \text{knn}} x_i \]
from sklearn.impute import KNNImputer

knn_imputer = KNNImputer(n_neighbors=5)
X_knn = knn_imputer.fit_transform(df[['edad', 'ingresos']])

df[['edad_knn', 'ingresos_knn']] = X_knn

Ventaja sobre la media: Considera las relaciones entre variables. Si alguien tiene edad alta e ingresos altos, el KNN imputer usará personas similares para imputar.

Desventaja: Más lento que la media, especialmente con datasets grandes.

6 Estrategia 4: Iterative Imputer

El Iterative Imputer (basado en MICE - Multivariate Imputation by Chained Equations) modela cada variable con faltantes como una función de las demás variables:

  1. Imputa valores faltantes con la media (inicialización).
  2. Para cada variable con faltantes:
    • Usa las demás variables como predictores.
    • Entrena un modelo (por defecto, Ridge regression).
    • Re-imputa los valores faltantes con las predicciones.
  3. Repite hasta convergencia.
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer

iter_imputer = IterativeImputer(max_iter=10, random_state=42)
X_iter = iter_imputer.fit_transform(df[['edad', 'ingresos']])

df[['edad_iter', 'ingresos_iter']] = X_iter

Tip

¿Cuándo usar Iterative Imputer? Cuando los datos tienen patrones complejos y muchas correlaciones entre variables. Es el método más preciso pero también el más lento.

7 Comparación de métodos

from sklearn.impute import SimpleImputer, KNNImputer, IterativeImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline

# Crear pipeline con cada estrategia
strategies = {
    'Media': SimpleImputer(strategy='mean'),
    'Mediana': SimpleImputer(strategy='median'),
    'KNN': KNNImputer(n_neighbors=5),
    'Iterative': IterativeImputer(max_iter=10, random_state=42)
}

for name, imputer in strategies.items():
    pipe = Pipeline([
        ('imputer', imputer),
        ('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
    ])
    score = cross_val_score(pipe, X_train, y_train, cv=5).mean()
    print(f"{name:12s}: {score:.4f}")

8 Resumen

Método Complejidad Precisión Velocidad Cuándo usar
Eliminar filas Baja — Rápido \< 5% faltantes, MCAR
Media/Mediana Baja Media Muy rápido Primera aproximación
KNN Imputer Media Alta Medio Datos con patrones
Iterative Imputer Alta Muy alta Lento Datos complejos

Regla práctica: Empieza por la mediana (rápido y razonable). Si el rendimiento no es suficiente, prueba KNN o Iterative Imputer.