DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on

MLZC25-06. Preprocesamiento de Datos: La Cocina del Machine Learning

🍳 Preprocesamiento: Donde la Magia Realmente Sucede

Imagina que eres un chef estrella. Tienes ingredientes increíbles, pero antes de crear un platillo exquisito, necesitas:

  • Lavar y cortar las verduras
  • Marinar la carne
  • Medir las especias
  • Preparar todos los utensilios

En Machine Learning, el preprocesamiento es exactamente eso: preparar tus datos para que los algoritmos puedan cocinar los mejores modelos. Los datos en bruto raramente están listos para ser usados directamente. Necesitan ser limpiados, transformados y preparados.

🎯 ¿Por qué el Preprocesamiento es Crítico?

1. Los algoritmos son exigentes

# ❌ Esto fallará
from sklearn.linear_model import LinearRegression

# Datos con valores faltantes y strings
X = [[1, None, "alto"], [2, 5.5, "bajo"]]
y = [100, 200]

modelo = LinearRegression()
modelo.fit(X, y)  # ValueError: Input contains NaN
Enter fullscreen mode Exit fullscreen mode
# ✅ Esto funcionará
X_limpio = [[1, 3.0, 1], [2, 5.5, 0]]  # NaN reemplazado, string codificado
y = [100, 200]

modelo = LinearRegression()
modelo.fit(X_limpio, y)  # ¡Funciona!
Enter fullscreen mode Exit fullscreen mode

2. La calidad del modelo depende de la calidad de los datos

  • Datos malosModelo malo
  • Datos buenosModelo bueno
  • Datos excelentesModelo excelente

3. El preprocesamiento es donde se gana o pierde la batalla

  • 80% del tiempo en proyectos de ML se gasta en preprocesamiento
  • Los errores aquí se propagan a todo el pipeline
  • Es la diferencia entre un modelo que funciona y uno que no

🧹 1. Limpieza de Datos

Manejo de Valores Faltantes

import pandas as pd
import numpy as np
from sklearn.impute import SimpleImputer, KNNImputer

def manejar_valores_faltantes(df, estrategia='promedio'):
    """
    Maneja valores faltantes según la estrategia elegida

    Estrategias:
    - 'eliminar': Eliminar filas con valores faltantes
    - 'promedio': Imputar con promedio (solo numéricas)
    - 'mediana': Imputar con mediana (solo numéricas)
    - 'moda': Imputar con moda
    - 'knn': Imputar usando K-Nearest Neighbors
    - 'eliminar_columna': Eliminar columnas con muchos faltantes
    """

    print(f"Valores faltantes antes: {df.isnull().sum().sum()}")

    if estrategia == 'eliminar':
        df_limpio = df.dropna()

    elif estrategia == 'eliminar_columna':
        # Eliminar columnas con más del 50% de valores faltantes
        umbral = 0.5
        columnas_a_eliminar = df.columns[df.isnull().sum() / len(df) > umbral]
        df_limpio = df.drop(columns=columnas_a_eliminar)
        print(f"Columnas eliminadas: {columnas_a_eliminar.tolist()}")

    else:
        # Imputación
        columnas_numericas = df.select_dtypes(include=[np.number]).columns
        columnas_categoricas = df.select_dtypes(include=['object']).columns

        df_limpio = df.copy()

        # Imputar numéricas
        if len(columnas_numericas) > 0:
            if estrategia == 'promedio':
                imputer = SimpleImputer(strategy='mean')
            elif estrategia == 'mediana':
                imputer = SimpleImputer(strategy='median')
            elif estrategia == 'knn':
                imputer = KNNImputer(n_neighbors=5)
            else:
                imputer = SimpleImputer(strategy='mean')

            df_limpio[columnas_numericas] = imputer.fit_transform(df[columnas_numericas])

        # Imputar categóricas
        if len(columnas_categoricas) > 0:
            imputer_cat = SimpleImputer(strategy='most_frequent')
            df_limpio[columnas_categoricas] = imputer_cat.fit_transform(df[columnas_categoricas])

    print(f"Valores faltantes después: {df_limpio.isnull().sum().sum()}")
    return df_limpio

# Ejemplo de uso
df_limpio = manejar_valores_faltantes(df, estrategia='promedio')
Enter fullscreen mode Exit fullscreen mode

Manejo de Duplicados

def manejar_duplicados(df, subset=None, keep='first'):
    """
    Maneja filas duplicadas

    Args:
        subset: Columnas a considerar para detectar duplicados
        keep: 'first', 'last', o False (eliminar todos)
    """
    duplicados_antes = df.duplicated(subset=subset).sum()
    print(f"Duplicados encontrados: {duplicados_antes}")

    if duplicados_antes > 0:
        df_limpio = df.drop_duplicates(subset=subset, keep=keep)
        print(f"Duplicados eliminados: {duplicados_antes}")
        return df_limpio
    else:
        print("No se encontraron duplicados")
        return df

# Ejemplo de uso
df_sin_duplicados = manejar_duplicados(df_limpio)
Enter fullscreen mode Exit fullscreen mode

Detección y Manejo de Outliers

def manejar_outliers(df, columnas_numericas, metodo='iqr', factor=1.5):
    """
    Detecta y maneja outliers

    Métodos:
    - 'iqr': Rango intercuartílico
    - 'zscore': Puntuación Z
    - 'isolation_forest': Bosque de aislamiento
    """

    df_limpio = df.copy()
    outliers_info = {}

    if metodo == 'iqr':
        for col in columnas_numericas:
            Q1 = df[col].quantile(0.25)
            Q3 = df[col].quantile(0.75)
            IQR = Q3 - Q1

            limite_inferior = Q1 - factor * IQR
            limite_superior = Q3 + factor * IQR

            outliers = (df[col] < limite_inferior) | (df[col] > limite_superior)
            outliers_info[col] = {
                'cantidad': outliers.sum(),
                'porcentaje': (outliers.sum() / len(df)) * 100,
                'limites': (limite_inferior, limite_superior)
            }

            # Opción 1: Eliminar outliers
            # df_limpio = df_limpio[~outliers]

            # Opción 2: Cap outliers (más conservador)
            df_limpio[col] = df_limpio[col].clip(limite_inferior, limite_superior)

    elif metodo == 'zscore':
        from scipy import stats

        for col in columnas_numericas:
            z_scores = np.abs(stats.zscore(df[col]))
            outliers = z_scores > factor  # factor = 3 es común

            outliers_info[col] = {
                'cantidad': outliers.sum(),
                'porcentaje': (outliers.sum() / len(df)) * 100
            }

            # Cap outliers usando percentiles
            p5, p95 = df[col].quantile([0.05, 0.95])
            df_limpio[col] = df_limpio[col].clip(p5, p95)

    print("=== INFORMACIÓN DE OUTLIERS ===")
    for col, info in outliers_info.items():
        print(f"{col}: {info['cantidad']} outliers ({info['porcentaje']:.2f}%)")

    return df_limpio, outliers_info

# Ejemplo de uso
df_sin_outliers, info_outliers = manejar_outliers(df_sin_duplicados, num_cols, metodo='iqr')
Enter fullscreen mode Exit fullscreen mode

🔄 2. Transformación de Datos

Codificación de Variables Categóricas

from sklearn.preprocessing import LabelEncoder, OneHotEncoder, OrdinalEncoder

def codificar_categoricas(df, columnas_categoricas, metodo='onehot'):
    """
    Codifica variables categóricas

    Métodos:
    - 'label': Label Encoding (0, 1, 2, ...)
    - 'onehot': One-Hot Encoding (columnas binarias)
    - 'ordinal': Ordinal Encoding (para categorías ordenadas)
    """

    df_codificado = df.copy()

    if metodo == 'label':
        encoders = {}
        for col in columnas_categoricas:
            le = LabelEncoder()
            df_codificado[col] = le.fit_transform(df[col].astype(str))
            encoders[col] = le

    elif metodo == 'onehot':
        # One-hot encoding
        df_codificado = pd.get_dummies(df, columns=columnas_categoricas, prefix=columnas_categoricas)

    elif metodo == 'ordinal':
        # Para variables ordinales (ej: 'bajo', 'medio', 'alto')
        ordinal_mappings = {
            'categoria_ordenada': ['bajo', 'medio', 'alto'],
            'satisfaccion': ['muy_bajo', 'bajo', 'medio', 'alto', 'muy_alto']
        }

        for col in columnas_categoricas:
            if col in ordinal_mappings:
                oe = OrdinalEncoder(categories=[ordinal_mappings[col]])
                df_codificado[col] = oe.fit_transform(df[[col]])

    print(f"Forma antes: {df.shape}")
    print(f"Forma después: {df_codificado.shape}")

    return df_codificado

# Ejemplo de uso
df_codificado = codificar_categoricas(df_sin_outliers, cat_cols, metodo='onehot')
Enter fullscreen mode Exit fullscreen mode

Escalado y Normalización

from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

def escalar_datos(X_train, X_test, columnas_numericas, metodo='standard'):
    """
    Escala datos numéricos

    Métodos:
    - 'standard': Z-score (media=0, std=1)
    - 'minmax': Min-Max (0-1)
    - 'robust': Robusto a outliers (mediana, IQR)
    """

    X_train_escalado = X_train.copy()
    X_test_escalado = X_test.copy()

    if metodo == 'standard':
        scaler = StandardScaler()
    elif metodo == 'minmax':
        scaler = MinMaxScaler()
    elif metodo == 'robust':
        scaler = RobustScaler()

    # Escalar solo columnas numéricas
    X_train_escalado[columnas_numericas] = scaler.fit_transform(X_train[columnas_numericas])
    X_test_escalado[columnas_numericas] = scaler.transform(X_test[columnas_numericas])

    print(f"Método de escalado: {metodo}")
    print(f"Media después del escalado: {X_train_escalado[columnas_numericas].mean().mean():.3f}")
    print(f"Desviación estándar: {X_train_escalado[columnas_numericas].std().mean():.3f}")

    return X_train_escalado, X_test_escalado, scaler

# Ejemplo de uso
X_train_scaled, X_test_scaled, scaler = escalar_datos(X_train, X_test, num_cols, metodo='standard')
Enter fullscreen mode Exit fullscreen mode

Transformaciones de Distribución

from sklearn.preprocessing import PowerTransformer, QuantileTransformer

def transformar_distribucion(df, columnas_numericas, metodo='boxcox'):
    """
    Transforma distribuciones para que sean más normales

    Métodos:
    - 'boxcox': Transformación Box-Cox
    - 'yeojohnson': Yeo-Johnson (maneja valores negativos)
    - 'quantile': Transformación cuantil
    """

    df_transformado = df.copy()
    transformers = {}

    if metodo == 'boxcox':
        transformer = PowerTransformer(method='box-cox')
    elif metodo == 'yeojohnson':
        transformer = PowerTransformer(method='yeo-johnson')
    elif metodo == 'quantile':
        transformer = QuantileTransformer(output_distribution='normal')

    for col in columnas_numericas:
        # Aplicar transformación
        datos_transformados = transformer.fit_transform(df[[col]])
        df_transformado[col] = datos_transformados.flatten()
        transformers[col] = transformer

        # Visualizar antes y después
        fig, axes = plt.subplots(1, 2, figsize=(12, 4))

        # Antes
        axes[0].hist(df[col], bins=30, alpha=0.7)
        axes[0].set_title(f'{col} - Antes')

        # Después
        axes[1].hist(df_transformado[col], bins=30, alpha=0.7)
        axes[1].set_title(f'{col} - Después ({metodo})')

        plt.tight_layout()
        plt.show()

    return df_transformado, transformers

# Ejemplo de uso
df_transformado, transformers = transformar_distribucion(df_codificado, num_cols, metodo='yeojohnson')
Enter fullscreen mode Exit fullscreen mode

🎯 3. Feature Engineering

Creación de Variables Derivadas

def crear_features_derivadas(df):
    """Crea nuevas variables a partir de las existentes"""

    df_nuevo = df.copy()

    # Ejemplos de features derivadas

    # 1. Ratios
    if 'ingresos' in df.columns and 'gastos' in df.columns:
        df_nuevo['ratio_gasto_ingreso'] = df['gastos'] / df['ingresos']

    # 2. Diferencias temporales
    if 'fecha' in df.columns:
        df_nuevo['fecha'] = pd.to_datetime(df['fecha'])
        df_nuevo['dia_semana'] = df_nuevo['fecha'].dt.dayofweek
        df_nuevo['mes'] = df_nuevo['fecha'].dt.month
        df_nuevo['año'] = df_nuevo['fecha'].dt.year

    # 3. Interacciones
    if 'edad' in df.columns and 'experiencia' in df.columns:
        df_nuevo['edad_experiencia_interaccion'] = df['edad'] * df['experiencia']

    # 4. Agrupaciones
    if 'categoria' in df.columns and 'precio' in df.columns:
        precio_por_categoria = df.groupby('categoria')['precio'].transform('mean')
        df_nuevo['precio_vs_categoria'] = df['precio'] / precio_por_categoria

    # 5. Variables binarias
    if 'edad' in df.columns:
        df_nuevo['es_mayor_edad'] = (df['edad'] >= 18).astype(int)

    print(f"Features originales: {len(df.columns)}")
    print(f"Features después: {len(df_nuevo.columns)}")
    print(f"Nuevas features: {set(df_nuevo.columns) - set(df.columns)}")

    return df_nuevo

# Ejemplo de uso
df_con_features = crear_features_derivadas(df_transformado)
Enter fullscreen mode Exit fullscreen mode

Selección de Features

from sklearn.feature_selection import SelectKBest, f_regression, mutual_info_regression

def seleccionar_features(X, y, metodo='mutual_info', k=10):
    """
    Selecciona las mejores features

    Métodos:
    - 'mutual_info': Información mutua
    - 'f_regression': F-score
    - 'correlation': Correlación con target
    """

    if metodo == 'mutual_info':
        selector = SelectKBest(score_func=mutual_info_regression, k=k)
    elif metodo == 'f_regression':
        selector = SelectKBest(score_func=f_regression, k=k)
    elif metodo == 'correlation':
        # Selección por correlación
        correlaciones = X.corrwith(y).abs().sort_values(ascending=False)
        features_seleccionadas = correlaciones.head(k).index.tolist()
        return X[features_seleccionadas], features_seleccionadas

    X_seleccionado = selector.fit_transform(X, y)
    features_seleccionadas = X.columns[selector.get_support()].tolist()

    print(f"Features seleccionadas: {features_seleccionadas}")

    return pd.DataFrame(X_seleccionado, columns=features_seleccionadas), features_seleccionadas

# Ejemplo de uso
X_seleccionado, features_importantes = seleccionar_features(X, y, metodo='mutual_info', k=5)
Enter fullscreen mode Exit fullscreen mode

🔧 4. Pipeline de Preprocesamiento

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer

def crear_pipeline_preprocesamiento(columnas_numericas, columnas_categoricas):
    """Crea un pipeline completo de preprocesamiento"""

    # Preprocesamiento para columnas numéricas
    numeric_transformer = Pipeline(steps=[
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', StandardScaler())
    ])

    # Preprocesamiento para columnas categóricas
    categorical_transformer = Pipeline(steps=[
        ('imputer', SimpleImputer(strategy='most_frequent')),
        ('onehot', OneHotEncoder(handle_unknown='ignore'))
    ])

    # Combinar transformadores
    preprocessor = ColumnTransformer(
        transformers=[
            ('num', numeric_transformer, columnas_numericas),
            ('cat', categorical_transformer, columnas_categoricas)
        ]
    )

    return preprocessor

# Ejemplo de uso
preprocessor = crear_pipeline_preprocesamiento(num_cols, cat_cols)

# Aplicar pipeline
X_processed = preprocessor.fit_transform(X_train)
X_test_processed = preprocessor.transform(X_test)
Enter fullscreen mode Exit fullscreen mode

📊 5. Validación del Preprocesamiento

def validar_preprocesamiento(X_original, X_processed, y):
    """Valida que el preprocesamiento no haya introducido problemas"""

    print("=== VALIDACIÓN DEL PREPROCESAMIENTO ===")

    # 1. Verificar que no hay valores faltantes
    faltantes = np.isnan(X_processed).sum()
    print(f"Valores faltantes: {faltantes}")

    # 2. Verificar que no hay infinitos
    infinitos = np.isinf(X_processed).sum()
    print(f"Valores infinitos: {infinitos}")

    # 3. Verificar distribuciones
    print(f"Forma original: {X_original.shape}")
    print(f"Forma procesada: {X_processed.shape}")

    # 4. Verificar correlaciones con target
    if hasattr(X_processed, 'columns'):
        correlaciones = pd.DataFrame(X_processed, columns=X_processed.columns).corrwith(y)
        print(f"Correlaciones con target: {correlaciones.head()}")

    # 5. Verificar que el modelo puede entrenar
    from sklearn.linear_model import LinearRegression
    try:
        modelo = LinearRegression()
        modelo.fit(X_processed, y)
        print("✅ El modelo puede entrenar correctamente")
    except Exception as e:
        print(f"❌ Error al entrenar: {e}")

# Ejemplo de uso
validar_preprocesamiento(X_train, X_processed, y_train)
Enter fullscreen mode Exit fullscreen mode

🎯 Checklist de Preprocesamiento

✅ Limpieza

  • [ ] Valores faltantes manejados
  • [ ] Duplicados eliminados
  • [ ] Outliers identificados y manejados
  • [ ] Inconsistencias corregidas

✅ Transformación

  • [ ] Variables categóricas codificadas
  • [ ] Datos numéricos escalados
  • [ ] Distribuciones transformadas si es necesario
  • [ ] Features derivadas creadas

✅ Validación

  • [ ] No hay valores faltantes o infinitos
  • [ ] El modelo puede entrenar
  • [ ] Las distribuciones tienen sentido
  • [ ] Las correlaciones se mantienen

✅ Documentación

  • [ ] Transformaciones documentadas
  • [ ] Pipeline reproducible
  • [ ] Parámetros guardados
  • [ ] Proceso versionado

💡 Consejos Avanzados

1. Preprocesamiento Diferente para Train/Test

def preprocesar_train_test(X_train, X_test, y_train):
    """Preprocesamiento que evita data leakage"""

    # Calcular estadísticas solo en train
    media_train = X_train.mean()
    std_train = X_train.std()

    # Aplicar a train y test usando estadísticas de train
    X_train_scaled = (X_train - media_train) / std_train
    X_test_scaled = (X_test - media_train) / std_train  # Usa estadísticas de train

    return X_train_scaled, X_test_scaled
Enter fullscreen mode Exit fullscreen mode

2. Preprocesamiento Automático

def preprocesamiento_automatico(df):
    """Preprocesamiento inteligente basado en el tipo de datos"""

    df_limpio = df.copy()

    # Identificar tipos automáticamente
    columnas_numericas = df.select_dtypes(include=[np.number]).columns
    columnas_categoricas = df.select_dtypes(include=['object']).columns
    columnas_fechas = df.select_dtypes(include=['datetime64']).columns

    print(f"Columnas numéricas: {len(columnas_numericas)}")
    print(f"Columnas categóricas: {len(columnas_categoricas)}")
    print(f"Columnas de fecha: {len(columnas_fechas)}")

    # Aplicar preprocesamiento apropiado
    # ... (implementar lógica automática)

    return df_limpio
Enter fullscreen mode Exit fullscreen mode

🎯 Reflexión Práctica

Ejercicio: Toma un dataset real y aplica todos estos pasos de preprocesamiento. Al final, pregúntate:

  1. ¿Qué transformaciones fueron más importantes?
  2. ¿Cómo cambió la calidad de los datos?
  3. ¿Qué problemas encontraste y cómo los resolviste?
  4. ¿Cómo podrías automatizar este proceso?

🔗 Lo que viene después

En el último post reflexionaremos sobre la tarea de la Semana 1, donde aplicaremos todos estos conceptos en un caso práctico real.

💭 Pregunta para reflexionar

¿Qué aspecto del preprocesamiento te parece más desafiante? ¿Es el manejo de valores faltantes, la codificación de categóricas, o la detección de outliers? ¿Has tenido alguna experiencia donde el preprocesamiento hizo la diferencia entre un modelo que funcionaba y uno que no?


El preprocesamiento no es glamuroso, pero es donde se gana la batalla del Machine Learning. Un buen preprocesamiento puede hacer que un algoritmo simple supere a uno complejo con datos mal preparados.

Top comments (0)