Build a Fit-Ready Preprocessor & Nothing Else
One file, one responsibility. Here’s why that constraint will save your entire pipeline.
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
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.pydoes 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:
- Build preprocessor
- Fit on
X_train - Transform
X_trainandX_test - 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