pipelines_and_composition.md
631 lines · skills/scikit-learn/references/pipelines_and_composition.md at 5f2bc03
Pipelines and Composite Estimators Reference
Targets scikit-learn 1.9.1. Snippets with caller-supplied data/columns are illustrative; fit all learned preprocessing inside the training folds when estimating predictive performance.
Raw lines 1 to 4Overview
Pipelines chain multiple processing steps into a single estimator, preventing data leakage and simplifying code. They enable reproducible workflows and seamless integration with cross-validation and hyperparameter tuning.
Raw lines 5 to 8Pipeline Basics
Raw lines 9 to 10Creating a Pipeline
**Pipeline (sklearn.pipeline.Pipeline)**
- Chains transformers with a final estimator
- All intermediate steps must implement
fitandtransform(fit_transformis optional) - Final step can be any estimator (transformer, classifier, regressor, clusterer)
- Example:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
pipeline = Pipeline([
('scaler', StandardScaler()),
('pca', PCA(n_components=10)),
('classifier', LogisticRegression())
])
# Fit the entire pipeline
pipeline.fit(X_train, y_train)
# Predict using the pipeline
y_pred = pipeline.predict(X_test)
y_proba = pipeline.predict_proba(X_test)Raw lines 11 to 37Using make_pipeline
make_pipeline
- Convenient constructor that auto-generates step names
- Example:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
pipeline = make_pipeline(
StandardScaler(),
PCA(n_components=10),
SVC(kernel='rbf')
)
pipeline.fit(X_train, y_train)Raw lines 38 to 56Accessing Pipeline Components
Raw lines 57 to 58Accessing Steps
# By index
scaler = pipeline.steps[0][1]
# By name
scaler = pipeline.named_steps['scaler']
pca = pipeline.named_steps['pca']
# Using indexing syntax
scaler = pipeline['scaler']
pca = pipeline['pca']
# Get all step names
print(pipeline.named_steps.keys())Raw lines 59 to 76Setting Parameters
# Set parameters using double underscore notation
pipeline.set_params(
pca__n_components=15,
classifier__C=0.1
)
# Or during creation
pipeline = Pipeline([
('scaler', StandardScaler()),
('pca', PCA(n_components=10)),
('classifier', LogisticRegression(C=1.0))
])Raw lines 77 to 93Accessing Attributes
# Access fitted attributes
pca_components = pipeline.named_steps['pca'].components_
explained_variance = pipeline.named_steps['pca'].explained_variance_ratio_
# Access intermediate transformations
X_scaled = pipeline.named_steps['scaler'].transform(X_test)
X_pca = pipeline.named_steps['pca'].transform(X_scaled)Raw lines 94 to 105Hyperparameter Tuning with Pipelines
Raw lines 106 to 107Grid Search with Pipeline
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
pipeline = Pipeline([
('scaler', StandardScaler()),
('classifier', SVC())
])
param_grid = {
'classifier__C': [0.1, 1, 10, 100],
'classifier__gamma': ['scale', 'auto', 0.001, 0.01],
'classifier__kernel': ['rbf', 'linear']
}
grid_search = GridSearchCV(pipeline, param_grid, cv=5, n_jobs=-1)
grid_search.fit(X_train, y_train)
print(f"Best parameters: {grid_search.best_params_}")
print(f"Best score: {grid_search.best_score_:.3f}")Raw lines 108 to 133Tuning Multiple Pipeline Steps
pipeline = Pipeline([
('scaler', StandardScaler()),
('pca', PCA()),
('classifier', SVC())
])
# Every PCA candidate must fit within min(n_training_fold, n_features).
param_grid = {
# PCA parameters
'pca__n_components': [5, 10, 20, 50],
# Classifier parameters
'classifier__C': [0.1, 1, 10],
'classifier__kernel': ['rbf', 'linear']
}
grid_search = GridSearchCV(pipeline, param_grid, cv=5)
grid_search.fit(X_train, y_train)Raw lines 134 to 155ColumnTransformer
Raw lines 156 to 157Basic Usage
**ColumnTransformer (sklearn.compose.ColumnTransformer)**
- Apply different preprocessing to different columns
- Prevents preprocessing leakage only when placed inside the estimator passed to cross-validation
- Example:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
# Define column groups
numeric_features = ['age', 'income', 'hours_per_week']
categorical_features = ['gender', 'occupation', 'native_country']
# Create preprocessor
preprocessor = ColumnTransformer(
transformers=[
('num', StandardScaler(), numeric_features),
('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features)
],
remainder='passthrough' # Keep other columns unchanged
)
X_transformed = preprocessor.fit_transform(X)Raw lines 158 to 184With Pipeline Steps
from sklearn.pipeline import Pipeline
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
]
)
# Full pipeline with model
full_pipeline = Pipeline([
('preprocessor', preprocessor),
('classifier', LogisticRegression())
])
full_pipeline.fit(X_train, y_train)Raw lines 185 to 215Using make_column_transformer
from sklearn.compose import make_column_transformer
preprocessor = make_column_transformer(
(StandardScaler(), numeric_features),
(OneHotEncoder(handle_unknown='ignore'), categorical_features),
remainder='passthrough'
)Raw lines 216 to 227Column Selection
# By column names (if X is DataFrame)
preprocessor = ColumnTransformer([
('num', StandardScaler(), ['age', 'income']),
('cat', OneHotEncoder(), ['gender', 'occupation'])
])
# By column indices
preprocessor = ColumnTransformer([
('num', StandardScaler(), [0, 1, 2]),
('cat', OneHotEncoder(), [3, 4])
])
# By boolean mask
numeric_mask = [True, True, True, False, False]
categorical_mask = [False, False, False, True, True]
preprocessor = ColumnTransformer([
('num', StandardScaler(), numeric_mask),
('cat', OneHotEncoder(), categorical_mask)
])
# By callable
def is_numeric(X):
return X.select_dtypes(include=['number']).columns.tolist()
preprocessor = ColumnTransformer([
('num', StandardScaler(), is_numeric)
])Raw lines 228 to 260Getting Feature Names
# Fit before requesting names
preprocessor.fit(X_train)
output_features = preprocessor.get_feature_names_out()
print(f"Input features: {X_train.columns.tolist()}")
print(f"Output features: {output_features}")Raw lines 261 to 270Remainder Handling
# Drop unspecified columns (default)
preprocessor = ColumnTransformer([...], remainder='drop')
# Pass through unchanged
preprocessor = ColumnTransformer([...], remainder='passthrough')
# Apply transformer to remaining columns
preprocessor = ColumnTransformer([...], remainder=StandardScaler())Raw lines 271 to 283FeatureUnion
Raw lines 284 to 285Basic Usage
**FeatureUnion (sklearn.pipeline.FeatureUnion)**
- Concatenates results of multiple transformers
- Transformers are applied in parallel
- Example:
from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA
from sklearn.feature_selection import SelectKBest
# Combine PCA and feature selection
feature_union = FeatureUnion([
('pca', PCA(n_components=10)),
('select_best', SelectKBest(k=20))
])
X_combined = feature_union.fit_transform(X_train, y_train)
print(f"Combined features: {X_combined.shape[1]}") # 10 + 20 = 30Raw lines 286 to 306With Pipeline
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA, TruncatedSVD
# Create feature union
feature_union = FeatureUnion([
('pca', PCA(n_components=10)),
('svd', TruncatedSVD(n_components=10))
])
# Full pipeline
pipeline = Pipeline([
('scaler', StandardScaler()),
('features', feature_union),
('classifier', LogisticRegression())
])
pipeline.fit(X_train, y_train)Raw lines 307 to 329Weighted Feature Union
# Apply weights to transformers
feature_union = FeatureUnion(
transformer_list=[
('pca', PCA(n_components=10)),
('select_best', SelectKBest(k=20))
],
transformer_weights={
'pca': 2.0, # Give PCA features double weight
'select_best': 1.0
}
)Raw lines 330 to 345Advanced Pipeline Patterns
Raw lines 346 to 347Caching Pipeline Steps
from sklearn.pipeline import Pipeline
from tempfile import mkdtemp
from shutil import rmtree
# Cache intermediate results
cachedir = mkdtemp()
pipeline = Pipeline([
('scaler', StandardScaler()),
('pca', PCA(n_components=50)),
('classifier', LogisticRegression())
], memory=cachedir)
pipeline.fit(X_train, y_train)
# Clean up cache
rmtree(cachedir)Raw lines 348 to 368Nested Pipelines
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
# Inner pipeline for text processing
text_pipeline = Pipeline([
('vect', CountVectorizer()),
('tfidf', TfidfTransformer())
])
# Outer pipeline combining text and numeric features
full_pipeline = Pipeline([
('features', ColumnTransformer([
('text', text_pipeline, 'text'), # Scalar selects 1D text input
('numeric', StandardScaler(), ['age', 'income'])
])),
('classifier', LogisticRegression())
])Raw lines 369 to 393Custom Transformers in Pipelines
from sklearn.base import BaseEstimator, TransformerMixin
class TextLengthExtractor(TransformerMixin, BaseEstimator):
def fit(self, X, y=None):
return self
def transform(self, X):
return [[len(text)] for text in X]
pipeline = Pipeline([
('length', TextLengthExtractor()),
('scaler', StandardScaler()),
('classifier', LogisticRegression())
])Raw lines 394 to 412Slicing Pipelines
# Get sub-pipeline
sub_pipeline = pipeline[:2] # First two steps
# Get specific range
middle_steps = pipeline[1:3]Raw lines 413 to 422TransformedTargetRegressor
Raw lines 423 to 424Basic Usage
TransformedTargetRegressor
- Transforms target variable before fitting
- Automatically inverse-transforms predictions
- Example:
from sklearn.compose import TransformedTargetRegressor
from sklearn.preprocessing import QuantileTransformer
from sklearn.linear_model import LinearRegression
model = TransformedTargetRegressor(
regressor=LinearRegression(),
transformer=QuantileTransformer(output_distribution='normal')
)
model.fit(X_train, y_train)
y_pred = model.predict(X_test) # Automatically inverse-transformedRaw lines 425 to 444With Functions
import numpy as np
model = TransformedTargetRegressor(
regressor=LinearRegression(),
func=np.log1p,
inverse_func=np.expm1
)
model.fit(X_train, y_train)Raw lines 445 to 458Complete Example: End-to-End Pipeline
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
# Define feature types
numeric_features = ['age', 'income', 'hours_per_week']
categorical_features = ['gender', 'occupation', 'education']
# Numeric preprocessing pipeline
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# Categorical preprocessing pipeline
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])
# Combine preprocessing
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
]
)
# Full pipeline
pipeline = Pipeline([
('preprocessor', preprocessor),
('pca', PCA(n_components=0.95)), # Keep 95% variance
('classifier', RandomForestClassifier(random_state=42))
])
# Hyperparameter tuning
param_grid = {
'preprocessor__num__imputer__strategy': ['mean', 'median'],
'pca__n_components': [0.90, 0.95, 0.99],
'classifier__n_estimators': [100, 200],
'classifier__max_depth': [10, 20, None]
}
grid_search = GridSearchCV(
pipeline, param_grid,
cv=5, scoring='accuracy',
n_jobs=-1, verbose=1
)
grid_search.fit(X_train, y_train)
print(f"Best parameters: {grid_search.best_params_}")
print(f"Best CV score: {grid_search.best_score_:.3f}")
print(f"Test score: {grid_search.score(X_test, y_test):.3f}")
# Make predictions
best_pipeline = grid_search.best_estimator_
y_pred = best_pipeline.predict(X_test)
y_proba = best_pipeline.predict_proba(X_test)Raw lines 459 to 527Visualization
Raw lines 528 to 529Displaying Pipelines
# In Jupyter notebooks, pipelines display as diagrams
from sklearn import set_config
set_config(display='diagram')
pipeline # Displays visual diagramRaw lines 530 to 539Text Representation
# Print pipeline structure
print(pipeline)
# Get detailed parameters
print(pipeline.get_params())Raw lines 540 to 549Best Practices
Raw lines 550 to 551Always Use Pipelines
- Prevents data leakage
- Ensures consistency between training and prediction
- Makes code more maintainable
- Enables easy hyperparameter tuning
Proper Pipeline Construction
# Good: Preprocessing inside pipeline
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression())
])
pipeline.fit(X_train, y_train)
# Unsafe if these once-scaled values are subsequently passed to CV/search
X_train_scaled = StandardScaler().fit_transform(X_train)
model = LogisticRegression()
model.fit(X_train_scaled, y_train)Raw lines 558 to 572Use ColumnTransformer for Mixed Data
Always use ColumnTransformer when you have both numerical and categorical features:
preprocessor = ColumnTransformer([
('num', StandardScaler(), numeric_features),
('cat', OneHotEncoder(), categorical_features)
])Raw lines 573 to 581Name Your Steps Meaningfully
# Good
pipeline = Pipeline([
('imputer', SimpleImputer()),
('scaler', StandardScaler()),
('pca', PCA(n_components=10)),
('rf_classifier', RandomForestClassifier())
])
# Bad
pipeline = Pipeline([
('step1', SimpleImputer()),
('step2', StandardScaler()),
('step3', PCA(n_components=10)),
('step4', RandomForestClassifier())
])Raw lines 582 to 600Cache Expensive Transformations
For repeated fitting (e.g., during grid search), cache expensive steps:
from tempfile import mkdtemp
cachedir = mkdtemp()
pipeline = Pipeline([
('expensive_preprocessing', ExpensiveTransformer()),
('classifier', LogisticRegression())
], memory=cachedir)Raw lines 601 to 612Test Pipeline Compatibility
Ensure all steps are compatible:
- All intermediate steps must have fit() and transform()
- Final step needs fit() and predict() (or transform())
- Use set_output(transform='pandas') for DataFrame output
# This is a transformer-only pipeline; a final classifier has no transform().
transformers = Pipeline([
('imputer', SimpleImputer()),
('scaler', StandardScaler())
]).set_output(transform='pandas')
X_transformed = transformers.fit_transform(X_train[numeric_features])
# OneHotEncoder must use sparse_output=False for pandas output.Raw lines 613 to 627Upstream references
- https://scikit-learn.org/stable/modules/compose.html
- https://scikit-learn.org/stable/modules/generated/sklearn.pipeline.Pipeline.html