← Back to list

Build a Fit-Ready Preprocessor & Nothing Else

One file, one responsibility. Here’s why that constraint will save your entire pipeline.

ait-lahssen Ismail · 2026-04-11 14:58 · 1 claps · 2.7 min read
#machine-learning #python #mlops #credit-risk-modelling #data-pipeline
Open on Medium ↗
Wiki topics: OPS · LLMOps & Inference ML · Machine Learning EDU · Education & Learning 🔧 · Data Engineering

Build a Fit-Ready Preprocessor & Nothing Else

One file, one responsibility. Here’s why that constraint will save your entire pipeline.

Photo de Conny Schneidersur Unsplash

Photo de Conny Schneidersur Unsplash

TL;DR: src/features/preprocessor.py does exactly one thing : build a fit-ready preprocessing object. No fitting, no training, no evaluation. The discipline is the point.

When building ML pipelines, one mistake shows up everywhere: people mix preprocessing definition, fitting, and model training in the same place.

That leads to data leakage, messy code, and pipelines you can’t reuse without fear.

So in my project, I enforced one rule:

*src/features/preprocessor.py does one thing only build a fit-ready preprocessing object.*

Not a fitted preprocessor. A fit-ready one. The distinction matters more than it sounds.

What this file is actually responsible for

The goal is simple: build an object that knows which columns are numeric, which are categorical, and what transformations to apply to each. Then return it.

The learning, the actual fitting, comes later, inside the training pipeline on X_train.

Two data types, two pipelines

At this stage, the data is already split conceptually into numeric and categorical features. So the preprocessing logic splits into two paths, not because that’s how sklearn works, but because these two types of data require fundamentally different transformations.

The numeric path

Numeric columns may contain missing values. The real question isn’t whether to impute it’s which strategy to use.

Here, median imputation makes sense: it’s robust to outliers and learned from training data at fit time. No scaling yet. Just what’s needed.

The categorical path

Categorical columns need more care, not just because models can’t read strings, but because:

  • They may contain missing values
  • They must be converted to numeric representations
  • The set of valid categories must be learned from training data, not hardcoded

That last point is critical. The encoder doesn’t just transform data, it learns the category structure from X_train. That's why encoding belongs inside the fitted pipeline, not outside it.

One more detail: unseen categories at inference time must not crash the pipeline. So we explicitly handle that with handle_unknown="ignore".

Combining both paths

Now we have a numeric pipeline and a categorical pipeline. But the model expects one input. So we combine both into a single object that routes each column type to the right transformer, that’s the role of ColumnTransformer.

Function design

We wrap everything into one function:

  • Input: numeric_features, categorical_features
  • Output: one preprocessing object

Column selection is handled upstream. Preprocessing logic stays isolated. That’s clean separation.

The implementation

from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
def build_preprocessor(numeric_features, categorical_features):
    """
    Build and return a fit-ready preprocessing object.
    Parameters
    ----------
    numeric_features : list
        List of numeric feature names.
    categorical_features : list
        List of categorical feature names.
    Returns
    -------
    ColumnTransformer
        A preprocessing object ready to be fit on X_train.
    """
    # Numeric pipeline: median imputation only
    numeric_pipeline = Pipeline(
        steps=[
            ("imputer", SimpleImputer(strategy="median")),
        ]
    )
    # Categorical pipeline: fill then encode
    categorical_pipeline = Pipeline(
        steps=[
            ("imputer", SimpleImputer(strategy="constant", fill_value="missing")),
            ("encoder", OneHotEncoder(handle_unknown="ignore")),
        ]
    )
    # Combine both into a single routing object
    preprocessor = ColumnTransformer(
        transformers=[
            ("num", numeric_pipeline, numeric_features),
            ("cat", categorical_pipeline, categorical_features),
        ]
    )
    return preprocessor

The distinction people get wrong

This file does not return a fitted preprocessor. It returns a fit-ready object.

The actual fitting happens later:

preprocessor.fit(X_train)
# then:
X_train_transformed = preprocessor.transform(X_train)
X_test_transformed  = preprocessor.transform(X_test)

If you mix these responsibilities, you risk leakage and lose control of your pipeline. The training set bleeds into the test set before you’ve even noticed.

What this unlocks

The training pipeline becomes clean and linear:

  1. Build preprocessor
  2. Fit on X_train
  3. Transform X_train and X_test
  4. Train model

No confusion. No hidden behavior. No leakage.

If you look at this carefully, this file isn’t really about preprocessing.It’s about discipline in pipeline design.

And that’s where most projects quietly fail.

This is Part of a series on building clean ML pipelines from raw data to production. Next up: where .fit() should live and how to structure train_pipeline.py without leaking anything.

Ismail ait-lahssen


메타데이터
post_id
c39d790935e4
slug
build-a-fit-ready-preprocessor-nothing-else-c39d790935e4
url
https://medium.com/@ftdjxcx/build-a-fit-ready-preprocessor-nothing-else-c39d790935e4
canonical_url
https://medium.com/@ftdjxcx/build-a-fit-ready-preprocessor-nothing-else-c39d790935e4
author_url
https://medium.com/@ftdjxcx
status
ok
fetched_at
2026-06-13 16:00:06