37  Introducción al preprocesamiento y la calidad de datos

Preprocesar datos es convertir registros capturados para operar un negocio en entradas coherentes para una pregunta analítica. Antes de transformar una columna, preguntamos qué representa, cómo se obtuvo y qué información estará disponible cuando se use el modelo. La limpieza ayuda a evitar errores conocidos; no garantiza buenas predicciones.

En BigMart, LF y Low Fat pueden significar lo mismo, un peso negativo requiere revisión y un precio extraordinario podría ser un error o un producto especial. Una regla automática no puede decidir lo último sin contexto. Tampoco todos los modelos necesitan el mismo tratamiento: algunos aceptan valores ausentes o categorías directamente, mientras que otros requieren imputación o codificación.1

1 La necesidad de codificar o imputar depende del estimador y de su implementación; esta unidad muestra un flujo tabular general, no una obligación para todo modelo.

NotaAntes de limpiar

Conserva una copia del valor original y registra la regla aplicada. Convierte en ausente un valor imposible según una regla de negocio documentada; investiga un valor atípico plausible antes de eliminarlo.

37.1 Ejemplo: de una tabla cruda a variables utilizables

Esta muestra inventada para aprender contiene problemas de completitud, validez y consistencia. Los límites de peso y precio que usaremos son supuestos del ejercicio, no reglas generales de BigMart.

Producto Item_Weight Item_MRP Outlet_Establishment_Year Item_Fat_Content
FDA15 9.30 249.80 1999 Low Fat
DRC01 -5.00 0.00 09-Jan-2009 low fat
FDN15 NA 1200000.00 1998 LF
FDX07 19.20 NA 14/02/1998 Regular
NCD19 200.00 53.86 1985-01-20 reg

El orden de trabajo es: definir qué valores violan reglas del dominio; unificar etiquetas y formatos; separar entrenamiento y prueba; ajustar las estadísticas y categorías con entrenamiento; aplicar esas mismas transformaciones a ambos conjuntos. El identificador del producto no es una medida continua: aquí lo conservamos para auditoría, pero no lo enviamos al modelo.

37.1.1 Una transformación a mano

Consideremos tres pesos de entrenamiento: \(10\), ausente y \(20\). Si elegimos imputación por media, calculamos \((10+20)/2=15\) con los dos pesos observados. La columna resultante es \((10,15,20)\). Su media es \(15\) y su desviación estándar poblacional2 es \(\sqrt{\tfrac{(10-15)^2+(15-15)^2+(20-15)^2}{3}}=\sqrt{50/3}\approx4.082\). Al estandarizarla obtenemos \(\big(\tfrac{10-15}{4.082},\,0,\,\tfrac{20-15}{4.082}\big)\approx(-1.225,0,1.225)\).

2 Desviación estándar poblacional: raíz del promedio de las desviaciones al cuadrado respecto a la media, dividiendo entre \(n\). Es la convención de StandardScaler; la versión muestral divide entre \(n-1\).

Ahora llega un producto nuevo de prueba con peso \(40\). Si por descuido «ajustamos» con las cuatro filas, la media de imputación pasa a \((10+20+40)/3\approx23.33\): el valor que asignamos a una fila de entrenamiento depende de un dato que no deberíamos haber visto. Esta es la idea de fuga que formalizaremos enseguida y que el laboratorio permite manipular.

Si las categorías son Low Fat, LF y Regular, primero unificamos LF con Low Fat. Después, una codificación one hot3 da las columnas Fat_Low y Fat_Regular:

3 One hot encoding crea una columna indicadora por categoría. Documentación de OneHotEncoder.

Peso estandarizado Fat_Low Fat_Regular
-1.225 1 0
0 1 0
1.225 0 1

Esta matriz solo ilustra un posible preprocesador. El escalado es útil para muchos modelos sensibles a magnitudes o distancias, pero no es un requisito universal.4

4 StandardScaler calcula parámetros con el conjunto usado en fit; los modelos basados en árboles suelen ser menos sensibles a la escala. Documentación de preprocesamiento de scikit-learn.

37.2 Formalización: reglas, parámetros y calidad

Un pipeline es una composición5 ordenada \(T=T_k\circ\cdots\circ T_1\). Algunas etapas aplican reglas establecidas por el dominio; otras aprenden parámetros6 \(\theta\) a partir de datos. Para evaluar un modelo, ajustamos \(\hat\theta=\operatorname{fit}(X_{\mathrm{train}})\) y usamos el mismo estado en \(T_{\hat\theta}(X_{\mathrm{train}})\) y \(T_{\hat\theta}(X_{\mathrm{test}})\). Ajustar con \(X_{\mathrm{test}}\) incorpora información de evaluación al entrenamiento, una forma de fuga de datos.7

5 Composición de funciones: \((T_2\circ T_1)(X)=T_2(T_1(X))\); se aplica primero \(T_1\) y su salida alimenta a \(T_2\). El orden importa: imputar y luego escalar no da, en general, lo mismo que escalar y luego imputar.

6 Parámetro aprendido frente a regla fija: una media o una lista de categorías son estadísticos calculados con datos; si cambian las filas, cambian ellos. Una regla como «LF significa Low Fat» no depende de las filas y puede aplicarse a cualquier conjunto sin riesgo de fuga.

7 Fuga de datos: información que no estaría disponible al entrenar influye en alguna etapa del ajuste o de la selección del modelo. Errores comunes y prácticas recomendadas de scikit-learn.

Un preprocesador suele combinar comprobaciones de calidad distintas:

Dimensión Pregunta práctica Ejemplo de control
Completitud ¿Faltan valores requeridos? Proporción de Item_Weight ausente
Validez ¿Respeta tipos y rangos acordados? Peso positivo dentro de un rango de negocio
Consistencia ¿Coinciden representaciones equivalentes? LF y Low Fat
Unicidad ¿Hay registros duplicados de una entidad? Producto y tienda repetidos sin justificación
Exactitud ¿Se aproxima el registro a la realidad? Contrastar un precio con la fuente original
Oportunidad ¿Llegó a tiempo para la decisión? Retraso entre venta y captura

Para una columna requerida, una medida simple de completitud es \(C=1-\frac{n_{\mathrm{ausentes}}}{n}\). Las demás dimensiones necesitan reglas y fuentes de referencia propias; una sola puntuación no las resume.

TipPráctica en equipos de datos

Documenta el esquema y las reglas de calidad que afectan al modelo, valida los lotes nuevos y observa cómo cambian faltantes, rangos y categorías con el tiempo. Una alerta debe conducir a investigar la causa; una variación de distribución por sí sola no demuestra pérdida de rendimiento.8

8 Un cambio en \(P(X)\) describe deriva de las variables de entrada; una variación en \(P(Y\mid X)\) describe cambio de la relación con la respuesta. La relevancia depende de la tarea y de la evidencia disponible.

37.3 Laboratorio: ¿qué cambia si ajustamos con datos de prueba?

Objetivo. Identificar qué estadísticas pertenecen al entrenamiento y observar cómo se altera una imputación cuando se usan datos de prueba para calcularlas.

Hipótesis. Con pesos observados de entrenamiento \(10\) y \(20\), la media para imputar debe ser \(15\). Si se incorpora el peso \(40\) de prueba al ajuste, la media cambia.

Pasos. Con «Imputar peso» activo, compara ambos modos de ajuste y observa cómo se mueve la línea naranja. Activa «Estandarizar precio» y anota μ y σ en cada modo. Por último, activa «Codificar tienda»: la fila de prueba pertenece a una tienda Hypermarket que no aparece en entrenamiento. ¿Qué columnas existen en cada modo y cuál de las dos matrices podría producirse realmente en producción?

Laboratorio · Ajustar y aplicar

Simulador de entrenamiento y prueba

Tres filas de entrenamiento y una de prueba. Activa transformaciones y cambia qué filas se usan para aprender sus parámetros.

Filas usadas en .fit()
Transformaciones

Parámetros aprendidos

peso de entrenamientopeso de pruebamedia usada para imputar

Matriz resultante

Comprobación. La media con prueba es \(23.33\); la media de entrenamiento es \(15\). El simulador muestra además que el precio de prueba también altera los parámetros del escalado si se incluye en .fit().

Reflexión. ¿Por qué una transformación que usa reglas fijas para LF puede aplicarse a prueba, mientras que la media de imputación debe aprenderse en entrenamiento? ¿Cambiaría la conclusión si el conjunto de prueba tuviera cientos de filas?

37.4 Implementación con pandas y scikit-learn

El ejemplo aplica reglas del ejercicio a peso y precio, luego imputa, escala y codifica categorías. Separamos filas antes de ajustar el ColumnTransformer. Usamos pocas filas para poder inspeccionar el resultado, no para estimar rendimiento predictivo.

Código
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, RobustScaler, StandardScaler

X_raw = pd.DataFrame({
    'Item_Identifier': ['FDA15', 'DRC01', 'FDN15', 'FDX07', 'NCD19', 'FDP10', 'FDZ20'],
    'Item_Weight': [9.30, -5.00, np.nan, 19.20, 200.00, 12.0, 10.5],
    'Item_MRP': [249.80, 0.00, 1200000.0, np.nan, 53.86, 180.0, 190.0],
    'Item_Fat_Content': ['Low Fat', 'low fat', 'LF', 'Regular', 'reg', 'LF', 'Unknown'],
})

# División ilustrativa explícita: las dos últimas filas simulan datos futuros.
X_train = X_raw.iloc[:5].copy()
X_test = X_raw.iloc[5:].copy()
display(X_raw)
Item_Identifier Item_Weight Item_MRP Item_Fat_Content
0 FDA15 9.3 249.80 Low Fat
1 DRC01 -5.0 0.00 low fat
2 FDN15 NaN 1200000.00 LF
3 FDX07 19.2 NaN Regular
4 NCD19 200.0 53.86 reg
5 FDP10 12.0 180.00 LF
6 FDZ20 10.5 190.00 Unknown

Estas clases conservan el formato tabular que recibe ColumnTransformer y no modifican el original. Las reglas de rango son decisiones del ejemplo; en un proyecto real requieren revisión con especialistas del dominio.

Código
class RangeToMissing(BaseEstimator, TransformerMixin):
    def __init__(self, lower, upper):
        self.lower = lower
        self.upper = upper

    def fit(self, X, y=None):
        return self

    def transform(self, X):
        result = X.copy()
        column = result.columns[0]
        values = pd.to_numeric(result[column], errors='coerce')
        result[column] = values.where(values.between(self.lower, self.upper))
        return result

    def get_feature_names_out(self, input_features=None):
        return np.asarray(input_features, dtype=object)


class FatContentStandardizer(BaseEstimator, TransformerMixin):
    def fit(self, X, y=None):
        return self

    def transform(self, X):
        result = X.copy()
        column = result.columns[0]
        normalized = result[column].astype('string').str.strip().str.lower()
        result[column] = normalized.map({
            'lf': 'Low Fat', 'low fat': 'Low Fat',
            'reg': 'Regular', 'regular': 'Regular',
        }).fillna('Unknown').astype(str)
        return result

    def get_feature_names_out(self, input_features=None):
        return np.asarray(input_features, dtype=object)


weight_pipeline = Pipeline([
    ('range', RangeToMissing(1, 50)),
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler()),
])
price_pipeline = Pipeline([
    ('range', RangeToMissing(1, 1000)),
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', RobustScaler()),
])
fat_pipeline = Pipeline([
    ('normalize', FatContentStandardizer()),
    ('encode', OneHotEncoder(handle_unknown='ignore', sparse_output=False)),
])
preprocessor = ColumnTransformer([
    ('weight', weight_pipeline, ['Item_Weight']),
    ('price', price_pipeline, ['Item_MRP']),
    ('fat', fat_pipeline, ['Item_Fat_Content']),
])

train_matrix = preprocessor.fit_transform(X_train)
test_matrix = preprocessor.transform(X_test)
columns = preprocessor.get_feature_names_out()
display(pd.DataFrame(train_matrix, columns=columns, index=X_train['Item_Identifier']))
display(pd.DataFrame(test_matrix, columns=columns, index=X_test['Item_Identifier']))
weight__Item_Weight price__Item_MRP fat__Item_Fat_Content_Low Fat fat__Item_Fat_Content_Regular
Item_Identifier
FDA15 -1.581139 97.97 1.0 0.0
DRC01 0.000000 0.00 1.0 0.0
FDN15 0.000000 0.00 1.0 0.0
FDX07 1.581139 0.00 0.0 1.0
NCD19 0.000000 -97.97 0.0 1.0
weight__Item_Weight price__Item_MRP fat__Item_Fat_Content_Low Fat fat__Item_Fat_Content_Regular
Item_Identifier
FDP10 -0.718699 28.17 1.0 0.0
FDZ20 -1.197832 38.17 0.0 0.0

El precio de \(1{,}200{,}000\) se convierte en ausente por la regla supuestamente válida en este ejercicio y se imputa antes de escalar. RobustScaler usa mediana y rango intercuartílico; no corrige por sí mismo un valor erróneo.9 La categoría Unknown de prueba se codifica según las categorías aprendidas en entrenamiento; handle_unknown='ignore' evita un error si aparece una categoría nueva, aunque requiere seguimiento.10

10 One hot encoding crea una columna indicadora por categoría. Documentación de OneHotEncoder.

ImportanteEvaluación sin fuga de datos

Separa entrenamiento y prueba antes de aprender medias, medianas, escalas o categorías. En validación cruzada, coloca preprocesador y estimador en un mismo Pipeline para que cada pliegue ajuste solo con sus filas de entrenamiento.11

11 Fuga de datos: información que no estaría disponible al entrenar influye en alguna etapa del ajuste o de la selección del modelo. Errores comunes y prácticas recomendadas de scikit-learn.

37.5 Para recordar

  • Define el significado y las reglas de calidad antes de modificar valores.
  • Comprueba las transformaciones con un ejemplo pequeño y conserva trazabilidad.
  • Ajusta parámetros con entrenamiento; aplica el resultado a prueba y a datos nuevos.
  • Vigila cambios de esquema y distribución después del despliegue, y comprueba su efecto con métricas del caso de uso.