Pipelines y preprocesado automatizado
1 El problema del preprocesado manual
Cuando aplicas múltiples transformaciones (escalado, imputación, codificación, selección de variables) de forma manual, es fácil cometer errores:
- Fuga de datos (data leakage): calcular la media con todos los datos (incluido test) antes de separar train/test.
- Código desordenado: 20 líneas de
fit_transformytransformrepetidas por todas partes. - Difícil de reproducir: si cambias el orden de las transformaciones, cambian los resultados.
- Imposible en cross-validation: cada fold debería calcular sus propias estadísticas, no las del dataset completo.
Los Pipelines de scikit-learn resuelven todos estos problemas.
2 Pipeline básico
Un Pipeline encadena transformaciones y un modelo final en un único objeto que se comporta como un estimador cualquiera:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
pipe = Pipeline([
('scaler', StandardScaler()), # Paso 1: escalar
('pca', PCA(n_components=0.95)), # Paso 2: reducir dimensionalidad
('classifier', RandomForestClassifier()) # Paso 3: clasificar
])
# Se usa como un solo modelo
pipe.fit(X_train, y_train)
score = pipe.score(X_test, y_test)
Tip
Ventaja clave: En cross-validation, cada fold aplica el fit solo en los datos de entrenamiento de ese fold, evitando fugas de información. Sin pipelines, esto requeriría código manual muy propenso a errores.
2.1 Acceder a pasos individuales
# Acceder al scaler ajustado
scaler = pipe.named_steps['scaler']
print("Media:", scaler.mean_)
print("Std:", scaler.scale_)
# Acceder al PCA ajustado
pca = pipe.named_steps['pca']
print("Varianza explicada:", pca.explained_variance_ratio_.sum())
# Predicciones
y_pred = pipe.predict(X_test)
2.2 Búsqueda de hiperparámetros con GridSearch
Una de las grandes ventajas de los pipelines es que puedes buscar hiperparámetros de todas las etapas en un solo GridSearchCV:
from sklearn.model_selection import GridSearchCV
param_grid = {
'pca__n_components': [10, 20, 30, 0.95], # hiperparámetro de PCA
'classifier__n_estimators': [100, 200], # hiperparámetro del clasificador
'classifier__max_depth': [5, 10, None] # otro del clasificador
}
grid = GridSearchCV(pipe, param_grid, cv=5, scoring='accuracy')
grid.fit(X_train, y_train)
print(f"Mejores parámetros: {grid.best_params_}")
print(f"Mejor accuracy: {grid.best_score_:.4f}")
3 ColumnTransformer: transformaciones diferentes por tipo de columna
Los datasets reales mezclan columnas numéricas y categóricas que necesitan transformaciones distintas. ColumnTransformer aplica transformaciones diferentes a grupos de columnas:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
# Definir qué columnas son numéricas y cuáles categóricas
num_features = ['edad', 'ingresos', 'antiguedad']
cat_features = ['ciudad', 'genero', 'educacion']
preprocessor = ColumnTransformer([
('num', Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
]), num_features),
('cat', Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
]), cat_features)
])
# Pipeline completo
pipe = Pipeline([
('preprocess', preprocessor),
('classifier', RandomForestClassifier(n_estimators=200))
])
pipe.fit(X_train, y_train)
score = pipe.score(X_test, y_test)
3.1 Pipeline completo con ColumnTransformer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import GradientBoostingClassifier
num_features = ['edad', 'ingresos']
cat_features = ['ciudad', 'genero']
# Transformador para numéricas
num_transformer = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# Transformador para categóricas
cat_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# Combinar
preprocessor = ColumnTransformer([
('num', num_transformer, num_features),
('cat', cat_transformer, cat_features)
])
# Pipeline completo: preprocesado + modelo
full_pipe = Pipeline([
('preprocessor', preprocessor),
('classifier', GradientBoostingClassifier())
])
full_pipe.fit(X_train, y_train)
print(f"Accuracy: {full_pipe.score(X_test, y_test):.4f}")
4 FeatureUnion: combinar features
FeatureUnion aplica múltiples transformadores en paralelo y concatena sus resultados:
from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA
from sklearn.feature_selection import SelectKBest
# Combinar PCA con selección de características
combined = FeatureUnion([
('pca', PCA(n_components=10)),
('select', SelectKBest(k=5))
])
X_combined = combined.fit_transform(X_train, y_train)
print(f"Dimensiones originales: {X_train.shape[1]}")
print(f"Dimensiones combinadas: {X_combined.shape[1]}")
5 Pipelines en producción
Nota
¿Por qué los pipelines son esenciales en producción? Porque garantizan que exactamente el mismo preprocesado aplicado al entrenamiento se aplique a los datos nuevos en inferencia. Si escalas con la media del train, necesitas guardar esa media y reusarla. Los pipelines lo hacen automáticamente.
5.1 Guardar y cargar un pipeline
import joblib
# Guardar el pipeline entrenado
joblib.dump(full_pipe, 'pipeline_modelo.pkl')
# Cargar y usar en producción
pipe_cargado = joblib.load('pipeline_modelo.pkl')
predicciones = pipe_cargado.predict(X_nuevos)
5.2 Integración con Flask/FastAPI
# En una API de inferencia
from fastapi import FastAPI
import joblib
app = FastAPI()
pipe = joblib.load('pipeline_modelo.pkl')
@app.post("/predict")
def predict(datos: dict):
X = pd.DataFrame([datos])
prediccion = pipe.predict(X)[0]
return {"clase": prediccion}
6 Errores comunes
| Error | Causa | Solución |
|---|---|---|
| Data leakage | fit_transform con todos los datos |
Usar Pipeline con train/test split |
| Columnas inconsistentes | ColumnTransformer no maneja columnas faltantes | handle_unknown='ignore' |
| Memoria agotada | Pipeline demasiado grande | Usar memory= para cachear transformaciones |
| Transformadores custom | Necesitas lógica propia | Crear clase con fit() y transform() |
6.1 Transformador custom
Para crear un transformador compatible con scikit-learn, hereda de BaseEstimator (para get_params, set_params) y TransformerMixin (para fit_transform automático):
from sklearn.base import BaseEstimator, TransformerMixin
class DeleteNanRows(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
return self
def transform(self, X):
return X.dropna()
# Uso
delete_nan = DeleteNanRows()
X_train_prep = delete_nan.fit_transform(X_train)
6.1.1 Escalar solo columnas seleccionadas
from sklearn.preprocessing import RobustScaler
import pandas as pd
class CustomScaler(BaseEstimator, TransformerMixin):
def __init__(self, attributes):
self.attributes = attributes
def fit(self, X, y=None):
return self
def transform(self, X):
X_copy = X.copy()
scaler = RobustScaler()
X_scaled = scaler.fit_transform(X_copy[self.attributes])
X_scaled = pd.DataFrame(X_scaled, columns=self.attributes, index=X_copy.index)
for attr in self.attributes:
X_copy[attr] = X_scaled[attr]
return X_copy
# Uso: escalar solo columnas específicas
scaler = CustomScaler(['ingresos', 'edad'])
X_train_prep = scaler.fit_transform(X_train)
6.1.2 Codificar solo columnas categóricas
from sklearn.preprocessing import OneHotEncoder
class CustomOneHotEncoding(BaseEstimator, TransformerMixin):
def __init__(self):
self._oh = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
self._columns = None
def fit(self, X, y=None):
X_cat = X.select_dtypes(include=['object'])
self._columns = pd.get_dummies(X_cat).columns
self._oh.fit(X_cat)
return self
def transform(self, X):
X_copy = X.copy()
X_cat = X_copy.select_dtypes(include=['object'])
X_num = X_copy.select_dtypes(exclude=['object'])
X_cat_oh = self._oh.transform(X_cat)
X_cat_oh = pd.DataFrame(X_cat_oh, columns=self._columns, index=X_copy.index)
X_copy.drop(list(X_cat), axis=1, inplace=True)
return X_copy.join(X_cat_oh)
# Uso
encoder = CustomOneHotEncoding()
X_train_encoded = encoder.fit_transform(X_train)
6.2 FunctionTransformer: transformaciones simples
Para transformaciones que no necesitan fit() (son puramente matemáticas), FunctionTransformer envuelve cualquier función en un estimador compatible con pipelines:
from sklearn.preprocessing import FunctionTransformer
import numpy as np
# Transformación de valores absolutos
abs_transformer = FunctionTransformer(np.abs, validate=False)
abs_transformer.fit_transform(np.array([[-1, 2], [-3, 4]]))
# [[1, 2], [3, 4]]
# Función personalizada: elevar al cuadrado
def cuadrado(x):
return x ** 2
cuadrado_transformer = FunctionTransformer(cuadrado, validate=False)
cuadrado_transformer.fit_transform(np.array([[1, 2], [3, 4]]))
# [[1, 4], [9, 16]]
# Logaritmo (para datos con cola pesada)
log_transformer = FunctionTransformer(np.log1p, validate=False)
# Integrado en un pipeline
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
pipe = Pipeline([
('log', FunctionTransformer(np.log1p, validate=False)),
('scaler', StandardScaler()),
('model', RandomForestClassifier())
])
Tip
¿Cuándo usar FunctionTransformer en vez de BaseEstimator? Cuando la transformación es pura función (log, abs, cuadrado) y no necesitas aprender nada de los datos. Si necesitas fit() para calcular algo (media, frecuencias, etc.), usa BaseEstimator + TransformerMixin.
7 Resumen
| Componente | Función | Ejemplo |
|---|---|---|
| Pipeline | Encadenar pasos secuencialmente | Escalar → PCA → Clasificar |
| ColumnTransformer | Transformaciones diferentes por columna | Num → StandardScaler, Cat → OneHot |
| FeatureUnion | Combinar features en paralelo | PCA + SelectKBest |
| Transformador custom | Lógica de preprocesado propia | CustomScaler, CustomOneHotEncoding |
| FunctionTransformer | Envolver funciones simples | log, abs, cuadrado |
Tip
Regla de oro: Un pipeline expone los métodos del último estimador. Si el último es un clasificador → fit, predict. Si es un transformador → fit, transform, fit_transform.
Los pipelines son obligatorios en cualquier proyecto serio de machine learning. Evitan fugas de datos, simplifican el código y hacen que el modelo sea fácil de desplegar en producción.