Preprocessing Pipelines
A trained model often expects inputs that have already been imputed, scaled, or encoded. Those transforms contain learned information too: a replacement mean, a standard deviation, a category vocabulary. The deployable predictor is the fitted sequence, not just its final estimator.
A preprocessing pipeline packages that sequence behind one interface. During fit, each step learns from the output of the previous one. During predict, the saved transforms prepare new inputs before the estimator runs. This chapter connects the imputation and scaling notes with a complete example.
Follow one feature through three fitted steps
Use four training inputs: [10, missing, 30, 50], with targets [1, 2, 3, 4]. We will fit a mean imputer, a standardizer, and a ridge regression model. Ridge is a linear model with a penalty on coefficient size; we use penalty strength alpha=1.
The observed inputs average to 30, so the imputer produces [10, 30, 30, 50]. The standardizer learns from these four filled values: mean 30 and standard deviation √200, approximately 14.1421. Its outputs are approximately [−1.4142, 0, 0, 1.4142]. Only then does ridge fit those transformed inputs against the targets.
| Raw input | After impute | After scale | Target y |
|---|---|---|---|
| 10 | 10 | −1.4142 | 1 |
| missing | 30 | 0 | 2 |
| 30 | 30 | 0 | 3 |
| 50 | 50 | 1.4142 | 4 |
For a future row with a missing input, the imputer supplies 30 and the scaler produces 0. The fitted model predicts 2.5. For a future input of 70, the scaler produces about 2.8284 and the model predicts 4.9. These numbers illustrate the call sequence; they do not establish prediction quality.
What fit and predict actually call
| Step | During pipeline.fit | During pipeline.predict |
|---|---|---|
| Imputer | Fit on raw training inputs; return filled training inputs. | Transform with the saved replacement mean. |
| Scaler | Fit on the imputer’s training output; return scaled training inputs. | Transform the filled new inputs with saved statistics. |
| Estimator | Fit on transformed training inputs and their targets. | Predict from transformed new inputs. |
Intermediate steps must be transformers. The final step can be a predictor or another transformer. A pipeline ending in ridge exposes predict; the preprocessing-only slice pipe[:-1] exposes transform. Calling predict on raw inputs preserves the full sequence.
Why fit_transform is not always fit followed by transform
For intermediate steps, scikit-learn uses fit_transform when available. Simple imputers and standardizers produce the familiar fit-then-transform result. A supervised transformer can deliberately do something different for training rows.
For example, TargetEncoder uses internal cross-fitting during fit_transform, then saves reference statistics for later transform calls. Replacing that training operation with fit(X, y).transform(X) changes the values supplied to the model. A pipeline preserves the transformer’s defined training interface.
Order changes what later steps learn
The scaler above sees four values after imputation. If it were fitted directly on the three observed values, its standard deviation would be about 16.3299 instead of 14.1421. Filling the missing row with the mean adds a zero deviation to the variance calculation and changes its denominator.
That is a concrete reason to record the sequence. “Mean imputation plus standardization” is not a complete specification unless their order is fixed. The imputation chapter discusses how filling values changes a distribution.
One fitted object for training and prediction
Run the complete scikit-learn pipeline
import numpy as np
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import Ridge
from sklearn.pipeline import Pipeline
train = np.array([[10.], [np.nan], [30.], [50.]])
target = np.array([1., 2., 3., 4.])
pipe = Pipeline([
("impute", SimpleImputer(strategy="mean")),
("scale", StandardScaler()),
("model", Ridge(alpha=1.0)),
])
pipe.fit(train, target)
future = np.array([[np.nan], [70.]])
print(pipe.named_steps["impute"].statistics_.tolist())
print(pipe.named_steps["scale"].scale_.round(4).tolist())
print(pipe[:-1].transform(train).ravel().round(4).tolist())
print(pipe.predict(future).round(4).tolist())
[30.0]
[14.1421]
[-1.4142, 0.0, 0.0, 1.4142]
[2.5, 4.9]
named_steps lets us inspect the fitted components. Step names also identify parameters: model__alpha refers to ridge’s penalty, while impute__strategy refers to the imputer. Two underscores separate the step name from its parameter.
For tuning, pass the whole pipeline to the search procedure. Each candidate and training fold must fit its own preprocessing and estimator. The cross-validation tuning notes explain how to compare candidates and reserve final evaluation data.
What must survive after training?
Write $z$ for the standardized feature and $\hat y$ for the prediction. The fitted regression in this example is simply
The coefficient is rounded here; the runnable pipeline retains full precision. Save the fitted sequence together so the replacement value, scale, and coefficient stay compatible. Input schema and software versions also matter; the saving-transformers chapter covers that deployment boundary.
A pipeline cannot fix the rows or information you give it
Calling pipe.fit on the whole dataset still fits every stage on the whole dataset. A feature computed using future outcomes is still a leaking feature. A random split of repeated users can still put the same user on both sides. Pipelines organize fitting; the leakage chapter separates that protection from the other evaluation decisions.
The ordinary pipeline also expects intermediate feature transforms to preserve the pairing between rows and targets. A step that drops or resamples rows needs an interface that handles targets consistently. For different operations on numerical and categorical columns, use ColumnTransformer inside the pipeline.
C++: return one object containing the fitted state
This one-feature implementation makes the order explicit: learn the fill, fill training inputs, fit the scaler on those filled values, then fit ridge on standardized inputs. Its prediction method only reuses saved state. It supports NaN as missing, rejects an entirely missing reference, and uses a divisor of 1 for a constant filled feature.
Compile the complete small pipeline
#include <cmath>
#include <iomanip>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <vector>
struct FittedPipeline {
double fill, center, scale, coefficient, intercept;
double predict(double raw) const {
const double x = std::isnan(raw) ? fill : raw;
if (!std::isfinite(x)) throw std::invalid_argument("infinite input");
return intercept + coefficient * ((x - center) / scale);
}
};
// One feature, NaN = missing. Alpha penalizes the slope, not the intercept.
FittedPipeline fit_pipeline(const std::vector<double>& raw,
const std::vector<double>& y, double alpha) {
if (raw.empty() || raw.size() != y.size() ||
!(alpha > 0) || !std::isfinite(alpha))
throw std::invalid_argument("invalid training input");
double observed_sum = 0;
std::size_t observed_count = 0;
for (std::size_t i = 0; i < raw.size(); ++i) {
if (!std::isfinite(y[i])) throw std::invalid_argument("invalid target");
if (std::isnan(raw[i])) continue;
if (!std::isfinite(raw[i])) throw std::invalid_argument("infinite input");
observed_sum += raw[i]; ++observed_count;
}
if (observed_count == 0) throw std::invalid_argument("all inputs missing");
const double fill = observed_sum / observed_count;
std::vector<double> x = raw;
double center = 0, y_mean = 0;
for (std::size_t i = 0; i < x.size(); ++i) {
if (std::isnan(x[i])) x[i] = fill;
center += x[i]; y_mean += y[i];
}
center /= x.size(); y_mean /= x.size();
double variance = 0;
for (double v : x) variance += (v - center) * (v - center);
double scale = std::sqrt(variance / x.size());
if (scale == 0) scale = 1;
double cross = 0, squares = 0;
for (std::size_t i = 0; i < x.size(); ++i) {
const double z = (x[i] - center) / scale;
cross += z * (y[i] - y_mean); squares += z * z;
}
return {fill, center, scale, cross / (squares + alpha), y_mean};
}
int main() {
const double missing = std::numeric_limits<double>::quiet_NaN();
const auto pipeline = fit_pipeline({10,missing,30,50}, {1,2,3,4}, 1.0);
std::cout << std::fixed << std::setprecision(4);
std::cout << pipeline.predict(missing) << " " << pipeline.predict(70) << "\n";
}
2.5000 4.9000References and further reading
- Scikit-learn: Pipeline API — fit, transform, prediction, and fitted-step access.
- Scikit-learn: chaining estimators — sequential composition, slicing, nested parameters, and evaluation.
- Scikit-learn: Ridge — the regression objective used by the numerical example.
- Scikit-learn: TargetEncoder — why cross-fitted training transformations can differ from later transforms.