Introducción: Por Qué Necesitas Pipelines de Machine Learning
Si alguna vez has construido un modelo de machine learning en Python, ya sabes cómo va la cosa: escalas datos por un lado, codificas variables categóricas por otro, entrenas un modelo, evalúas… y de repente tienes un script de 200 líneas donde cualquier cambio rompe todo. Pero el problema real no es solo el desorden en el código. Es algo más sutil y peligroso: la fuga de datos (data leakage). Cuando aplicas transformaciones a todo el dataset antes de dividirlo en entrenamiento y prueba, las estadísticas del conjunto de prueba contaminan el modelo sin que te des cuenta.
Y ahí es donde entran los Pipelines de scikit-learn.
Un pipeline encapsula preprocesamiento, transformación y modelado en un único objeto coherente. Nada de pasos sueltos, nada de scripts frágiles. Con la versión 1.8 de scikit-learn (lanzada en diciembre de 2025), además se añaden capacidades experimentales de cómputo en GPU mediante Array API, soporte para CPython sin GIL y cosas interesantes como la calibración por temperatura.
En esta guía vas a aprender a construir pipelines completos de clasificación y regresión, manejar datos mixtos con ColumnTransformer, evaluar modelos con validación cruzada y optimizar hiperparámetros con GridSearchCV y RandomizedSearchCV. Todo con código funcional compatible con scikit-learn 1.8. Vamos a ello.
Requisitos Previos e Instalación
Para seguir esta guía necesitas Python 3.11 o superior (scikit-learn 1.8 soporta hasta Python 3.14) y las siguientes bibliotecas:
pip install scikit-learn==1.8.0 pandas numpy matplotlib
Verifica tu instalación rápidamente:
import sklearn
print(sklearn.__version__) # 1.8.0
Si ya has trabajado con nuestra guía de ingeniería de características o la de limpieza de datos con Pandas 3.0, tienes la base perfecta para dar el salto a la modelización. Si no, no te preocupes — esta guía es autocontenida.
¿Qué es un Pipeline en Scikit-learn?
Un Pipeline es, en esencia, una cadena de pasos. Cada paso intermedio es un transformador (implementa fit() y transform()), y el último paso es un estimador que solo necesita fit().
Lo interesante es lo que pasa internamente. Cuando llamas a pipeline.fit(X_train, y_train), scikit-learn ejecuta fit_transform() en cada transformador con los datos de entrenamiento, pasando la salida al siguiente paso. El estimador final recibe los datos ya transformados y aprende los patrones. Al predecir con pipeline.predict(X_new), los datos fluyen por la misma cadena de transformaciones automáticamente. Sin código extra, sin posibilidad de olvidar un paso.
Ventajas Clave de Usar Pipelines
- Prevención de fuga de datos: las transformaciones se ajustan exclusivamente con datos de entrenamiento en cada fold de validación cruzada.
- Código reproducible: un único objeto encapsula todo el flujo de trabajo. Nada de scripts con 15 celdas que hay que ejecutar en orden.
- Optimización integrada: puedes buscar hiperparámetros de cualquier paso del pipeline con
GridSearchCV. - Despliegue simplificado: serializa el pipeline completo con
jobliby despliégalo tal cual en producción.
Tu Primer Pipeline: Clasificación con el Dataset Iris
Empecemos con algo simple para ver la mecánica. Vamos a escalar los datos y aplicar una regresión logística:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris
from sklearn.metrics import classification_report
# Cargar datos
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# Crear pipeline
pipeline = Pipeline([
('scaler', StandardScaler()),
('classifier', LogisticRegression(max_iter=200, random_state=42))
])
# Entrenar y evaluar
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
print(classification_report(y_test, y_pred))
Dos líneas de configuración, una de entrenamiento, una de predicción. Eso es todo.
Si no necesitas nombres personalizados para los pasos (algo que sí necesitarás más adelante para GridSearchCV), la función make_pipeline ofrece una sintaxis aún más concisa:
from sklearn.pipeline import make_pipeline
pipeline = make_pipeline(StandardScaler(), LogisticRegression(max_iter=200))
ColumnTransformer: Manejo de Datos Mixtos
Seamos honestos: el dataset Iris es muy limpio. En la vida real, tus datos van a tener columnas numéricas y categóricas mezcladas, valores faltantes por todas partes y tipos de dato inconsistentes. ColumnTransformer te permite definir pipelines separados para cada tipo de columna y combinar sus salidas en un único espacio de características.
Estructura del ColumnTransformer
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
# Definir columnas
numeric_features = ['age', 'fare', 'sibsp', 'parch']
categorical_features = ['sex', 'embarked', 'pclass']
# Pipeline para columnas numéricas
numeric_transformer = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# Pipeline para columnas categóricas
categorical_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])
# Combinar con ColumnTransformer
preprocessor = ColumnTransformer(transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
Fíjate en cómo cada tipo de columna tiene su propia receta. Las numéricas se imputan con la mediana y se escalan; las categóricas se imputan con la moda y se codifican con one-hot. Todo queda encapsulado.
Selección Automática de Columnas
¿No quieres listar las columnas a mano? Usa make_column_selector para seleccionarlas automáticamente por tipo de dato. En mi experiencia, esto es especialmente útil cuando trabajas con datasets que cambian de estructura frecuentemente:
from sklearn.compose import make_column_selector
preprocessor = ColumnTransformer(transformers=[
('num', numeric_transformer, make_column_selector(dtype_include='number')),
('cat', categorical_transformer, make_column_selector(dtype_include='object'))
])
Pipeline Completo: Ejemplo con el Dataset Titanic
Ahora sí, vamos a lo interesante. Construyamos un pipeline de clasificación completo con datos reales, combinando todo lo que hemos visto hasta ahora:
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer, make_column_selector
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
# Cargar dataset (ejemplo con datos similares a Titanic)
url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
df = pd.read_csv(url)
# Seleccionar características y objetivo
features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
X = df[features]
y = df['Survived']
# Dividir datos
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Pipelines de preprocesamiento
numeric_transformer = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
categorical_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('encoder', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])
preprocessor = ColumnTransformer(transformers=[
('num', numeric_transformer, make_column_selector(dtype_include='number')),
('cat', categorical_transformer, make_column_selector(dtype_include='object'))
])
# Pipeline completo
clf = Pipeline([
('preprocessor', preprocessor),
('classifier', RandomForestClassifier(n_estimators=100, random_state=42))
])
# Entrenar y evaluar
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(f"Exactitud: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred, target_names=['No Sobrevivió', 'Sobrevivió']))
Desde la carga de datos hasta la predicción final, todo en un flujo limpio. Si alguien nuevo se une al proyecto, puede entender el preprocesamiento completo leyendo un solo objeto.
Validación Cruzada con Pipelines
Entrenar y evaluar con un solo split está bien para una prueba rápida, pero no es suficiente para tomar decisiones serias. La validación cruzada te da una estimación mucho más fiable del rendimiento real de tu modelo. Y lo mejor: al combinarla con un pipeline, garantizas que todas las transformaciones se recalculan en cada fold. Cero riesgo de fuga de datos.
Validación Cruzada Básica con cross_val_score
from sklearn.model_selection import cross_val_score
scores = cross_val_score(clf, X_train, y_train, cv=5, scoring='accuracy')
print(f"Exactitud media: {scores.mean():.4f} (+/- {scores.std():.4f})")
Validación Cruzada Estratificada
Si tienes clases desbalanceadas (y casi siempre las tienes en problemas reales), usa StratifiedKFold para mantener la proporción de clases en cada fold:
from sklearn.model_selection import StratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(clf, X_train, y_train, cv=cv, scoring='f1')
print(f"F1-score medio: {scores.mean():.4f} (+/- {scores.std():.4f})")
Múltiples Métricas con cross_validate
A veces la exactitud no cuenta toda la historia. Si necesitas evaluar varias métricas a la vez, cross_validate es tu herramienta:
from sklearn.model_selection import cross_validate
scoring = ['accuracy', 'f1', 'roc_auc']
results = cross_validate(clf, X_train, y_train, cv=5, scoring=scoring)
for metric in scoring:
key = f'test_{metric}'
print(f"{metric}: {results[key].mean():.4f} (+/- {results[key].std():.4f})")
Optimización de Hiperparámetros con GridSearchCV
Aquí es donde los pipelines realmente brillan. Puedes optimizar hiperparámetros de cualquier paso — tanto del preprocesamiento como del modelo — en una sola búsqueda. La convención es simple: paso__parámetro con doble guion bajo.
Búsqueda Exhaustiva con GridSearchCV
from sklearn.model_selection import GridSearchCV
param_grid = {
'preprocessor__num__imputer__strategy': ['mean', 'median'],
'classifier__n_estimators': [50, 100, 200],
'classifier__max_depth': [5, 10, 20, None],
'classifier__min_samples_split': [2, 5, 10]
}
grid_search = GridSearchCV(
clf, param_grid, cv=5, scoring='f1', n_jobs=-1, verbose=1
)
grid_search.fit(X_train, y_train)
print(f"Mejores parámetros: {grid_search.best_params_}")
print(f"Mejor F1-score (CV): {grid_search.best_score_:.4f}")
print(f"F1-score en test: {grid_search.score(X_test, y_test):.4f}")
Fíjate en algo clave: estamos optimizando la estrategia de imputación del preprocesador junto con los hiperparámetros del modelo. Esto es algo que no podrías hacer fácilmente sin pipelines.
Búsqueda Aleatoria con RandomizedSearchCV
Cuando el espacio de hiperparámetros crece, probar todas las combinaciones deja de ser viable. RandomizedSearchCV muestrea un número fijo de configuraciones aleatorias, y en la práctica suele encontrar soluciones casi tan buenas en mucho menos tiempo:
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform
param_distributions = {
'classifier__n_estimators': randint(50, 500),
'classifier__max_depth': [5, 10, 20, 30, None],
'classifier__min_samples_split': randint(2, 20),
'classifier__min_samples_leaf': randint(1, 10),
'classifier__max_features': uniform(0.1, 0.9)
}
random_search = RandomizedSearchCV(
clf, param_distributions, n_iter=50, cv=5,
scoring='f1', n_jobs=-1, random_state=42
)
random_search.fit(X_train, y_train)
print(f"Mejores parámetros: {random_search.best_params_}")
print(f"Mejor F1-score (CV): {random_search.best_score_:.4f}")
Successive Halving: Búsqueda Eficiente
Esta es una opción que no todo el mundo conoce. HalvingGridSearchCV funciona de una forma bastante elegante: entrena todas las combinaciones con un subconjunto pequeño de datos, descarta las peores, y repite con más datos hasta quedarse con la mejor configuración. Para espacios de búsqueda grandes, puede ser significativamente más rápido:
from sklearn.experimental import enable_halving_search_cv
from sklearn.model_selection import HalvingGridSearchCV
halving_search = HalvingGridSearchCV(
clf, param_grid, cv=5, scoring='f1',
factor=3, random_state=42, n_jobs=-1
)
halving_search.fit(X_train, y_train)
print(f"Mejores parámetros: {halving_search.best_params_}")
Comparación de Múltiples Modelos en un Pipeline
Una de las cosas que más me gustan de los pipelines es lo fácil que resulta intercambiar el estimador final para comparar distintos algoritmos con exactamente el mismo preprocesamiento. Sin pipelines, esto sería un desastre de código duplicado:
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.model_selection import cross_val_score
models = {
'Logistic Regression': LogisticRegression(max_iter=200, random_state=42),
'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
'Gradient Boosting': GradientBoostingClassifier(n_estimators=100, random_state=42),
'SVM': SVC(kernel='rbf', random_state=42)
}
results = {}
for name, model in models.items():
pipe = Pipeline([
('preprocessor', preprocessor),
('classifier', model)
])
scores = cross_val_score(pipe, X_train, y_train, cv=5, scoring='f1')
results[name] = scores
print(f"{name}: F1 = {scores.mean():.4f} (+/- {scores.std():.4f})")
Novedades de Scikit-learn 1.8 Relevantes para Pipelines
La versión 1.8 de scikit-learn trajo varias mejoras que vale la pena destacar, especialmente si trabajas con pipelines en producción.
Soporte Array API para Cómputo en GPU
Esto es probablemente lo más emocionante de la versión 1.8. Ahora puedes pasar arrays de PyTorch o CuPy directamente a ciertos estimadores y métricas, habilitando cómputo en GPU sin conversiones manuales. Estimadores como StandardScaler, PolynomialFeatures y RidgeCV ya lo soportan. En benchmarks típicos, se ha observado una aceleración de hasta 10x respecto a una sola CPU.
# Ejemplo experimental con Array API (requiere PyTorch y GPU)
import sklearn
sklearn.set_config(array_api_dispatch=True)
import torch
X_gpu = torch.tensor(X_train.values, dtype=torch.float32, device='cuda')
# Los estimadores compatibles trabajarán directamente en GPU
Eso sí, ten en cuenta que es una funcionalidad experimental. No todos los estimadores la soportan todavía, pero la dirección es clara.
Calibración por Temperatura
La nueva opción method="temperature" en CalibratedClassifierCV es especialmente útil para problemas multiclase. En lugar del esquema One-vs-Rest que usan métodos como Platt o isotonic, calibra las probabilidades con un único parámetro libre. Más simple y, en muchos casos, más efectivo:
from sklearn.calibration import CalibratedClassifierCV
calibrated = CalibratedClassifierCV(
clf, method='temperature', cv=5
)
calibrated.fit(X_train, y_train)
probas = calibrated.predict_proba(X_test)
Soporte para CPython sin GIL (Free-threaded)
Scikit-learn 1.8 incluye wheels para CPython 3.14 sin GIL, lo que permite utilizar hilos en lugar de subprocesos para el paralelismo con n_jobs. La ventaja práctica es que se elimina la sobrecarga de la comunicación interprocesos, algo que se nota especialmente en búsquedas de hiperparámetros con muchas iteraciones.
Serialización y Despliegue del Pipeline
Una vez que tienes tu pipeline optimizado, guardarlo para producción es trivial. Y lo importante es que guardas todo — no solo el modelo, sino también los transformadores con sus parámetros ajustados:
import joblib
# Guardar
joblib.dump(grid_search.best_estimator_, 'pipeline_clasificacion.joblib')
# Cargar y predecir
pipeline_cargado = joblib.load('pipeline_clasificacion.joblib')
nuevas_predicciones = pipeline_cargado.predict(nuevos_datos)
Esto es mucho más seguro que serializar el modelo y los transformadores por separado. Un solo archivo, un solo objeto, cero riesgo de que algo quede desincronizado.
Buenas Prácticas para Pipelines de Producción
- Siempre usa pipelines para validación cruzada: evita aplicar transformaciones antes de dividir los datos. Este es el error más común que veo en proyectos reales.
- Prefiere
ColumnTransformersobre transformaciones manuales: es más robusto y reproducible que hacerdf['col'].apply(...)antes de entrenar. - Nombra tus pasos con claridad: facilita la referencia de hiperparámetros en búsquedas de grid. Créeme, tu yo del futuro te lo agradecerá.
- Usa
memorypara cachear transformaciones costosas:Pipeline([...], memory='cache_dir')evita recalcular pasos que no cambian durante la búsqueda de hiperparámetros. - Valida siempre con
cross_validate: no confíes en una sola partición train/test para tomar decisiones importantes sobre tu modelo. - Prueba
HalvingGridSearchCVpara espacios grandes: puede ser significativamente más rápido que la búsqueda exhaustiva y los resultados suelen ser igual de buenos. - Serializa el pipeline completo: no solo el modelo, sino también los transformadores, para garantizar reproducibilidad en producción.
Preguntas Frecuentes
¿Cuál es la diferencia entre Pipeline y make_pipeline en scikit-learn?
Pipeline requiere que asignes un nombre a cada paso mediante tuplas (nombre, estimador), lo cual es necesario cuando quieres referenciar parámetros específicos en una búsqueda de hiperparámetros. make_pipeline genera los nombres automáticamente a partir del nombre de la clase en minúsculas. Para pipelines simples donde no necesitas GridSearchCV, make_pipeline es más cómodo. Para todo lo demás, usa Pipeline con nombres explícitos.
¿Cómo evitar la fuga de datos (data leakage) al usar pipelines?
La regla es simple: incluye todas las transformaciones dentro del pipeline y usa cross_val_score o GridSearchCV en lugar de aplicar transformaciones antes de dividir los datos. Cuando el pipeline está dentro de la validación cruzada, cada fold ajusta los transformadores solo con sus datos de entrenamiento. Las estadísticas del conjunto de prueba nunca contaminan el modelo.
¿Puedo usar ColumnTransformer con DataFrames de Pandas?
Sí, sin problema. Puedes especificar columnas por nombre o usar make_column_selector para seleccionarlas automáticamente por tipo de dato. Desde scikit-learn 1.2, la opción set_output(transform="pandas") permite que las transformaciones devuelvan DataFrames en lugar de arrays NumPy, conservando los nombres de columnas. Personalmente, recomiendo activar esta opción siempre que trabajes con datos tabulares — hace la depuración mucho más fácil.
¿Cuándo debo usar RandomizedSearchCV en lugar de GridSearchCV?
Como regla general, usa GridSearchCV cuando el espacio de hiperparámetros es pequeño (menos de unos pocos cientos de combinaciones). Cuando tienes muchos parámetros o rangos amplios, RandomizedSearchCV es la mejor opción porque muestrea combinaciones aleatorias con un presupuesto fijo. Y si el espacio es realmente grande, echa un vistazo a HalvingGridSearchCV — es sorprendentemente eficiente.
¿Scikit-learn 1.8 soporta cómputo en GPU?
Sí, de forma experimental. Scikit-learn 1.8 introdujo soporte para la Array API estándar de Python, permitiendo pasar arrays de PyTorch o CuPy directamente a estimadores compatibles como StandardScaler, RidgeCV y varias métricas. Esto habilita el cómputo en GPU sin conversiones manuales, aunque hay que activarlo explícitamente con sklearn.set_config(array_api_dispatch=True). No todos los estimadores lo soportan aún, pero el rendimiento en los que sí lo hacen es notable.