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
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:
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.
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:
- Imputa valores faltantes con la media (inicialización).
- 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.
- 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.