← All Posts
Deep Learning · Data Preparation and Preprocessing · Pipelines and Reproducibility

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 → impute → scale → model
Raw inputAfter imputeAfter scaleTarget y
1010−1.41421
missing3002
303003
50501.41424
Fit: the final estimator learns from the scaled column and target y. The last column contains observed targets, not predictions.
The numerical transformers prepare the inputs. The targets remain paired with their rows. Prediction uses the same saved imputer, scaler, and regression parameters.

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

StepDuring pipeline.fitDuring pipeline.predict
ImputerFit on raw training inputs; return filled training inputs.Transform with the saved replacement mean.
ScalerFit on the imputer’s training output; return scaled training inputs.Transform the filled new inputs with saved statistics.
EstimatorFit 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?

01Imputer stateReplacement mean: 30. Future missing values reuse it.
02Scaler stateCenter: 30. Divisor: √200. Keep the same feature order and units.
03Estimator stateIntercept: 2.5. Coefficient on the scaled feature: about 0.8485.
The fitted predictor includes every learned stage. The final coefficient expects standardized inputs. Passing the raw value 70 directly to that estimator would skip the representation it learned from.

Write $z$ for the standardized feature and $\hat y$ for the prediction. The fitted regression in this example is simply

$$\hat y=2.5+0.848528\,z.$$

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.9000

References and further reading