🍳 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
# ✅ 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!
2. La calidad del modelo depende de la calidad de los datos
- Datos malos → Modelo malo
- Datos buenos → Modelo bueno
- Datos excelentes → Modelo 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')
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)
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')
🔄 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')
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')
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')
🎯 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)
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)
🔧 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)
📊 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)
🎯 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
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
🎯 Reflexión Práctica
Ejercicio: Toma un dataset real y aplica todos estos pasos de preprocesamiento. Al final, pregúntate:
- ¿Qué transformaciones fueron más importantes?
- ¿Cómo cambió la calidad de los datos?
- ¿Qué problemas encontraste y cómo los resolviste?
- ¿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)