How to Build a scikit-learn Pipeline: Step by Step
Building a scikit-learn Pipeline bundles preprocessing and the model into a single object: whatever is applied to the training data is applied automatically to new data. It is the simplest way to prevent data leakage and to make sure that what goes to production is exactly what you validated.
Prerequisites
- Python 3.10 or later with scikit-learn and pandas installed (
pip install scikit-learn pandas). - A CSV file with tabular data and a target column (here we use
clientes.csvand thechurncolumn). - Basic familiarity with training and testing models.
Step 1: Split train and test before anything else
The golden rule: the test set must influence nothing, not even the mean used for scaling. So split the data before any transformation.
import pandas as pd
from sklearn.model_selection import train_test_split
df = pd.read_csv("clientes.csv")
X = df.drop(columns=["churn"])
y = df["churn"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
Step 2: Build the simplest Pipeline
A Pipeline is a named list of steps. Every step except the last one must transform the data; the last one is usually the model.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = Pipeline(steps=[
("scaler", StandardScaler()),
("modelo", LogisticRegression(max_iter=1000)),
])
The names ("scaler", "modelo") are yours to choose, and they will come in handy later.
Step 3: Train and predict in one line
When you call fit, the Pipeline learns the scaling from the training set and trains the model. When you call predict, it applies the same scaling to new data without you having to remember it.
pipe.fit(X_train, y_train)
y_pred = pipe.predict(X_test)
from sklearn.metrics import accuracy_score
print(accuracy_score(y_test, y_pred))
Step 4: Handle numeric and categorical columns with ColumnTransformer
In real life, columns are not all alike. ColumnTransformer applies a different path to each group of columns and plugs into the Pipeline like any other step.
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.impute import SimpleImputer
numericas = ["idade", "mensalidade", "meses_cliente"]
categoricas = ["plano", "regiao"]
pre = ColumnTransformer(transformers=[
("num", Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
]), numericas),
("cat", OneHotEncoder(handle_unknown="ignore"), categoricas),
])
pipe = Pipeline([
("pre", pre),
("modelo", LogisticRegression(max_iter=1000)),
])
pipe.fit(X_train, y_train)
handle_unknown="ignore" prevents the classic error of a category showing up in production that never appeared during training.
Step 5: Validate and tune the whole Pipeline
Because a Pipeline is a regular estimator, you can hand it straight to cross-validation. On every fold the preprocessing is relearned from the training part only — and that is precisely what removes the leakage.
from sklearn.model_selection import cross_val_score, GridSearchCV
scores = cross_val_score(pipe, X_train, y_train, cv=5, scoring="f1")
print(scores.mean())
grelha = {"modelo__C": [0.1, 1, 10]}
busca = GridSearchCV(pipe, grelha, cv=5, scoring="f1")
busca.fit(X_train, y_train)
print(busca.best_params_)
Note the double underscore: modelo__C means "parameter C of the step named modelo". That is how you tune hyperparameters inside a Pipeline.
Step 6: Save the complete Pipeline
Save the Pipeline, not just the model. That way the file already contains the preprocessing and accepts raw data.
import joblib
joblib.dump(busca.best_estimator_, "modelo_churn.joblib")
modelo = joblib.load("modelo_churn.joblib")
print(modelo.predict(X_test.head()))
Check the result
Three quick checks:
pipe.named_stepsshows the steps in the right order.pipe.predict(X_test.head())works on the raw DataFrame, with no manual scaling or encoding. If it runs, the Pipeline is doing all the work.- If you hit could not convert string to float, a text column was left out of the ColumnTransformer; if you hit columns are missing, the new data does not have the same columns as the training data.
Compare the cross-validation score with and without the Pipeline. If the number "improves" when you scale everything before splitting, that is not a better model — that is leakage.
Conclusion
In half a dozen lines you moved from a set of loose steps to a single object that can be validated and served. The natural next step is to add feature selection or a TargetEncoder to the Pipeline, and to register the model in an MLOps platform. One question to take with you: if a new row arrives tomorrow with a category never seen before, does your Pipeline survive it — or break?