En la lección anterior terminamos con varios NaN sembrados a propósito: convertimos edades imposibles y fechas futuras en valores faltantes porque era más honesto que inventar un número. Ahora toca decidir qué hacer con esos huecos, y con todos los que ya venían de origen en los datos de MercaFresh. Los datos faltantes son inevitables en cualquier proyecto real, y la forma de tratarlos puede cambiar por completo las conclusiones de un modelo: eliminar filas puede sesgar la muestra, imputar mal puede fabricar patrones falsos, y hacerlo en el momento equivocado puede filtrar información que el modelo no debería tener. En esta lección aprenderás a diagnosticar por qué faltan los datos, a visualizar sus patrones y a elegir entre eliminar e imputar con criterio.

Contenido

  1. Por qué faltan datos: MCAR, MAR y MNAR
  2. Detección y visualización de nulos
  3. Eliminación: filas y columnas
  4. Imputación simple: media, mediana, moda y constantes
  5. SimpleImputer de scikit-learn
  6. Imputación avanzada: KNNImputer e indicadores de ausencia
  7. El riesgo silencioso: fuga de información

Por qué faltan datos: MCAR, MAR y MNAR

Antes de rellenar un hueco hay que preguntarse por qué está vacío. La estadística distingue tres mecanismos de ausencia, y la diferencia no es académica: determina qué estrategias son válidas.

MCAR: Missing Completely At Random

La ausencia no depende de nada: es puro azar. En MercaFresh, un fallo intermitente del servidor perdió el campo tiempo_entrega de un 2 % de pedidos elegidos al azar. Ningún tipo de pedido tenía más probabilidad de perderlo que otro.

  • Consecuencia: los datos que quedan siguen siendo representativos. Eliminar esas filas no sesga; solo reduce la muestra.

MAR: Missing At Random (condicionado a lo observado)

La ausencia depende de otras columnas que sí tenemos. En MercaFresh, la edad falta mucho más entre los clientes que se registraron por la app móvil (donde el campo era opcional) que entre los del formulario web (donde era obligatorio). Sabiendo el canal de registro, la ausencia ya no aporta información extra.

  • Consecuencia: eliminar filas sesga la muestra (perderíamos usuarios de app, que quizá tienen otro perfil de churn), pero se puede imputar bien usando las columnas relacionadas.

MNAR: Missing Not At Random

La ausencia depende del propio valor que falta. Ejemplo clásico en MercaFresh: la valoración post-entrega (satisfaccion) falta sobre todo entre los clientes descontentos, que no se molestan en responder la encuesta. El hueco es información: quien no responde tiende a estar insatisfecho.

  • Consecuencia: es el caso más peligroso. Imputar la media fabricaría clientes "medianamente satisfechos" que en realidad estaban enfadados — justo los que van a hacer churn. Aquí los indicadores de ausencia (los veremos más abajo) son casi obligatorios.
Mecanismo ¿De qué depende la ausencia? Ejemplo MercaFresh Estrategia razonable
MCAR De nada (azar puro) Fallo aleatorio del servidor Eliminar o imputar: ambas seguras
MAR De otras columnas observadas Edad falta según canal de registro Imputar usando esas columnas
MNAR Del propio valor ausente No responden los insatisfechos Indicador de ausencia + cautela

En la práctica no hay un test definitivo para distinguirlos: se razona con conocimiento del negocio. La pregunta útil es siempre: ¿quién o qué decidió no rellenar este campo, y tiene eso relación con lo que quiero predecir?

Detección y visualización de nulos

Reconstruyamos un extracto de clientes de MercaFresh, ya limpio de duplicados y valores imposibles (herencia de 03-01), pero con sus nulos:

import pandas as pd
import numpy as np

df = pd.DataFrame({
    "id_cliente": [101, 104, 105, 106, 107, 108, 109, 110, 111, 112],
    "edad": [34, np.nan, 41, 38, np.nan, 45, 31, 27, np.nan, 52],
    "canal_registro": ["web", "app", "web", "app", "app", "web", "app", "web", "app", "web"],
    "gasto_total": [1250.5, 0.0, 2100.75, 310.2, 15400.0, 670.4, 0.0, 980.1, 45.9, 1530.0],
    "satisfaccion": [4.5, np.nan, 4.8, np.nan, 4.2, 3.9, np.nan, 4.6, np.nan, 4.1],
    "tiempo_entrega_min": [42, 55, np.nan, 61, 38, 47, 52, np.nan, 66, 44],
})

El recuento básico:

# Nulos por columna, en absoluto y en porcentaje
print(df.isna().sum())
print((df.isna().mean() * 100).round(1))
# edad                 30.0 %
# satisfaccion         40.0 %
# tiempo_entrega_min   20.0 %

isna() devuelve un DataFrame de booleanos (True donde hay nulo); sum() los cuenta y mean() da directamente la proporción. Pero los totales no lo cuentan todo: importa el patrón. ¿Faltan siempre en las mismas filas? ¿Falta satisfaccion en los mismos clientes que edad?

# ¿Cuántas columnas faltan por fila?
print(df.isna().sum(axis=1).value_counts())

# ¿Coinciden los nulos de dos columnas?
print(pd.crosstab(df["edad"].isna(), df["satisfaccion"].isna()))

# Pista sobre el mecanismo: ¿la ausencia de edad depende del canal?
print(df.groupby("canal_registro")["edad"].apply(lambda s: s.isna().mean()))
# app    0.6   <- ¡falta el 60 % en app y el 0 % en web: huele a MAR!
# web    0.0

Ese último groupby es la herramienta detective: cruza la ausencia con otras columnas para intuir el mecanismo. Para datasets grandes, un mapa visual ayuda:

import matplotlib.pyplot as plt

plt.imshow(df.isna(), aspect="auto", cmap="gray_r")
plt.xticks(range(len(df.columns)), df.columns, rotation=45)
plt.ylabel("Fila")
plt.title("Mapa de nulos (negro = faltante)")
plt.tight_layout()
plt.show()

Cada celda negra es un hueco: bandas verticales indican columnas problemáticas; patrones alineados entre columnas, ausencias correlacionadas. (La librería missingno automatiza estos gráficos, pero con imshow te bastan las herramientas que ya tienes.)

Eliminación: filas y columnas

La opción más simple es borrar. dropna la implementa con varios matices:

# Eliminar toda fila con AL MENOS un nulo (agresivo: aquí perderíamos 7 de 10)
df_sin_nulos = df.dropna()

# Eliminar solo filas donde falte una columna crítica concreta
df_edad = df.dropna(subset=["edad"])

# Eliminar filas con más de 2 nulos (thresh = mínimo de valores NO nulos exigido)
df_thresh = df.dropna(thresh=len(df.columns) - 2)

# Eliminar una COLUMNA entera
df_sin_satisf = df.drop(columns=["satisfaccion"])

¿Cuándo es aceptable eliminar?

  • Filas: cuando los nulos son pocos (regla orientativa: < 5 % de las filas) y el mecanismo es MCAR. Si es MAR o MNAR, eliminar filas distorsiona la muestra: en nuestro ejemplo, borrar las filas sin edad eliminaría sobre todo clientes de app.
  • Columnas: cuando falta una proporción enorme (> 50-60 %) y la columna no es crítica para el negocio. Ojo con la excepción MNAR: satisfaccion falta un 40 %, pero que falte puede predecir churn — antes de tirarla, considera conservar su indicador de ausencia.
  • Nunca elimines la fila si el nulo está en la variable objetivo de un dato de producción que quieres predecir; solo en entrenamiento tiene sentido descartar filas sin etiqueta.

Imputación simple: media, mediana, moda y constantes

Imputar es rellenar el hueco con un valor estimado. Las recetas básicas con pandas:

# Media: para numéricas simétricas
df["edad"] = df["edad"].fillna(df["edad"].mean())

# Mediana: para numéricas con outliers o asimetría (¡recuerda 02-01!)
df["tiempo_entrega_min"] = df["tiempo_entrega_min"].fillna(
    df["tiempo_entrega_min"].median())

# Moda: para categóricas ([0] porque mode() puede devolver varios empates)
df["canal_registro"] = df["canal_registro"].fillna(
    df["canal_registro"].mode()[0])

# Constante con significado de negocio
df["satisfaccion"] = df["satisfaccion"].fillna(-1)   # -1 = "no respondió"

¿Media o mediana? El criterio viene directo del módulo 2: la media es sensible a outliers, la mediana no. Con el cliente-restaurante de 15.400 € en el dataset, imputar gasto_total con la media inflaría todos los huecos; la mediana es la elección robusta por defecto.

Una imputación por grupos aprovecha la estructura MAR: si la edad falta según el canal, imputa con la mediana de cada canal:

df["edad"] = df["edad"].fillna(
    df.groupby("canal_registro")["edad"].transform("median"))

Coste oculto de toda imputación simple: reduce artificialmente la varianza. Cada hueco rellenado con la mediana es un punto clavado en el centro de la distribución; si imputas el 30 % de una columna, su histograma tendrá un pico artificial y sus correlaciones (02-03) se debilitarán. Imputar mucho no es gratis.

SimpleImputer de scikit-learn

¿Por qué usar scikit-learn si fillna ya funciona? Porque SimpleImputer memoriza el valor con el que imputa (lo aprende en fit) y lo reaplica idéntico a datos nuevos (transform). Esa separación fit/transform es la clave del apartado final de esta lección, y el patrón general de toda la librería.

from sklearn.impute import SimpleImputer

# Imputador para columnas numéricas, con estrategia mediana
imp_num = SimpleImputer(strategy="median")

cols_num = ["edad", "tiempo_entrega_min"]
df[cols_num] = imp_num.fit_transform(df[cols_num])

# El valor aprendido queda guardado, listo para aplicarse a datos futuros:
print(imp_num.statistics_)    # [37.0, 49.5] p. ej.: medianas de cada columna

Estrategias disponibles: "mean", "median", "most_frequent" (sirve para categóricas) y "constant" (con fill_value=). El mismo objeto, ya ajustado, se aplica luego con imp_num.transform(datos_nuevos) — sin recalcular nada.

Imputación avanzada: KNNImputer e indicadores de ausencia

KNNImputer

La imputación simple ignora el contexto de la fila: a un cliente joven de alto gasto y a un jubilado de compra ocasional les asigna la misma edad mediana. KNNImputer es más fino: para cada hueco busca las k filas más parecidas en las demás columnas (sus "vecinos") y promedia sus valores.

from sklearn.impute import KNNImputer

imp_knn = KNNImputer(n_neighbors=3)
df[["edad", "gasto_total", "tiempo_entrega_min"]] = imp_knn.fit_transform(
    df[["edad", "gasto_total", "tiempo_entrega_min"]])

La mecánica interna de "vecinos más cercanos" es exactamente el algoritmo K-NN que estudiaremos a fondo en la lección 04-05; por ahora basta la intuición: rellena cada hueco copiando de las filas que más se parecen. Dos advertencias prácticas: solo trabaja con columnas numéricas, y como mide distancias, las columnas con escalas grandes dominan el cálculo — un problema que resolveremos en la lección 03-05.

Indicadores de ausencia

Cuando la ausencia es informativa (MNAR), imputar y callar destruye la señal. La solución: añadir una columna booleana que recuerde dónde había un hueco, y después imputar tranquilamente.

# 1) Guardar la huella de la ausencia ANTES de imputar
df["satisfaccion_ausente"] = df["satisfaccion"].isna().astype(int)

# 2) Ahora sí, imputar la columna original
df["satisfaccion"] = df["satisfaccion"].fillna(df["satisfaccion"].median())

Así el modelo de churn puede aprender el patrón real: "los clientes que no responden la encuesta se dan de baja más". SimpleImputer(add_indicator=True) genera estos indicadores automáticamente. Es una técnica barata y sorprendentemente potente: en muchos datasets, el indicador acaba siendo mejor predictor que la columna imputada.

El riesgo silencioso: fuga de información

Queda la advertencia más importante de la lección. En el módulo 6 (lección 06-01) veremos que los datos se dividen en un conjunto de entrenamiento y otro de prueba, y que este último debe simular datos del futuro que el modelo nunca ha visto. Pues bien: si calculas la mediana para imputar usando todo el dataset y luego divides, los datos de prueba habrán influido en ese valor. El modelo habrá "espiado" el futuro. Es lo que se llama fuga de información (data leakage).

# MAL: la mediana se calcula con TODOS los datos, incluidos los de prueba
df["edad"] = df["edad"].fillna(df["edad"].median())
# ... y después se divide en train/test

# BIEN (esquema conceptual, se desarrolla en 06-01):
# 1. dividir primero en train y test
# 2. imp = SimpleImputer(strategy="median"); imp.fit(X_train)  <- aprende SOLO de train
# 3. X_train = imp.transform(X_train)
# 4. X_test  = imp.transform(X_test)   <- aplica el valor aprendido en train

Aquí es donde la pareja fit/transform de scikit-learn deja de ser una manía estilística y se convierte en la barrera contra la fuga: se aprende de train, se aplica a todo. El efecto de hacerlo mal es traicionero porque no da error: simplemente tu evaluación saldrá mejor de lo que el modelo merece, y el desengaño llegará en producción. Guarda esta idea: reaparecerá con el escalado (03-05), con el target encoding (03-04) y en general con cualquier paso que "aprenda" algo de los datos.

Errores Comunes y Consejos

  • Imputar sin preguntarse el mecanismo. Rellenar satisfaccion (MNAR) con la media fabrica clientes contentos que no existen. Primero diagnostica (MCAR/MAR/MNAR), luego actúa.
  • Usar la media en columnas con outliers. Con el cliente de 15.400 € dentro, la media de gasto no representa a nadie. Mediana por defecto; media solo en distribuciones razonablemente simétricas.
  • Eliminar filas alegremente. dropna() a secas puede vaciarte medio dataset y sesgar el resto. Mira siempre cuántas filas pierdes y quiénes son.
  • Imputar antes de dividir train/test. La fuga de información no avisa: infla tus métricas en silencio. Aprende los valores de imputación solo con datos de entrenamiento.
  • Olvidar el indicador de ausencia. Si sospechas MNAR, crea la columna _ausente antes de imputar; después ya no podrás reconstruirla.
  • Confundir nulos con centinelas. Un 999 o un -1 heredados del sistema origen no son NaN para pandas: conviértelos primero (df.replace(999, np.nan)) o tus recuentos de nulos mentirán.
  • Consejo: deja registrado en el código cuántos nulos había, qué estrategia aplicaste a cada columna y por qué. La imputación es una decisión de modelado, no una tarea de fontanería.

Ejercicios

Ejercicio 1

Para cada escenario de MercaFresh, clasifica el mecanismo (MCAR, MAR o MNAR) y justifica en una frase: (a) el peso del pedido falta en los registros de una semana en que la báscula del almacén estuvo estropeada; (b) el código postal falta más en pedidos hechos por teléfono, porque el operador a veces lo omite; (c) los ingresos declarados faltan sobre todo entre los clientes de rentas altas, que prefieren no decirlos.

Ejercicio 2

Con el DataFrame df de la lección (versión con nulos), imputa tiempo_entrega_min con la mediana por ciudad sabiendo que existe una columna ciudad. Escribe el código con groupby + transform y explica por qué puede ser mejor que la mediana global.

Ejercicio 3

Crea un SimpleImputer con estrategia mediana y add_indicator=True, ajústalo sobre las columnas ["edad", "satisfaccion"] y muestra la forma (shape) del resultado. Explica por qué salen más columnas de las que entraron.

Soluciones

Ejercicio 1

  • (a) MCAR: la avería no tiene relación con el tipo de pedido ni con el valor del peso; es azar (temporal) puro.
  • (b) MAR: la ausencia depende del canal (columna observada), no del valor del propio código postal; conociendo el canal, se puede imputar sin sesgo.
  • (c) MNAR: la ausencia depende del valor que falta (rentas altas ocultan más); imputar la media subestimaría los ingresos ausentes, y conviene un indicador de ausencia.

Ejercicio 2

df["tiempo_entrega_min"] = df["tiempo_entrega_min"].fillna(
    df.groupby("ciudad")["tiempo_entrega_min"].transform("median"))

transform("median") devuelve una Serie del mismo tamaño que df con la mediana del grupo de cada fila, así que fillna rellena cada hueco con la mediana de su ciudad. Es mejor que la global si el tiempo de entrega varía sistemáticamente por ciudad (reparto en Barcelona ≠ reparto en Sevilla): estamos explotando estructura MAR, igual que con la edad por canal.

Ejercicio 3

from sklearn.impute import SimpleImputer

imp = SimpleImputer(strategy="median", add_indicator=True)
resultado = imp.fit_transform(df[["edad", "satisfaccion"]])
print(resultado.shape)   # (10, 4)

Entraron 2 columnas y salen 4: las dos originales imputadas más un indicador binario por cada columna que tenía nulos (1 = el valor era faltante). Así se conserva la información de ausencia, esencial si el mecanismo es MNAR como sospechamos de satisfaccion.

Conclusión

Ya sabes tratar los huecos con criterio: diagnosticar el mecanismo (MCAR, MAR, MNAR) antes de actuar, cuantificar y visualizar los patrones de nulos, eliminar solo cuando es seguro, imputar con media/mediana/moda o por grupos, escalar a SimpleImputer y KNNImputer cuando hace falta, conservar la señal de la ausencia con indicadores, y —sobre todo— aprender los valores de imputación únicamente de los datos de entrenamiento para no filtrar información. El dataset de churn de MercaFresh ya está completo: sin duplicados, sin valores imposibles y sin huecos.

Pero completo no significa listo: varias de sus variables tienen distribuciones muy asimétricas (el gasto, con su larga cola de clientes-restaurante), las fechas siguen siendo poco útiles tal cual, y los pedidos aún no están resumidos por cliente. En la próxima lección, transformación de datos, remodelaremos esas variables —logaritmos, discretización, componentes de fecha, agregaciones— y conoceremos los pipelines de scikit-learn para encadenar todos estos pasos de forma ordenada.

Curso de Machine Learning

Módulo 1: Introducción al Machine Learning

Módulo 2: Fundamentos de Estadística y Probabilidad

Módulo 3: Preprocesamiento de Datos

Módulo 4: Algoritmos de Machine Learning Supervisado

Módulo 5: Algoritmos de Machine Learning No Supervisado

Módulo 6: Evaluación y Validación de Modelos

Módulo 7: Técnicas Avanzadas y Optimización

Módulo 8: Implementación y Despliegue de Modelos

Módulo 9: Proyectos Prácticos

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados