scikit-learn Pipeline with ColumnTransformer on pandas, GridSearchCV and joblib: working versions (tested 2026-10-15)
A scikit-learn Pipeline that scales numeric columns and one-hot encodes categories inside a ColumnTransformer on a pandas DataFrame, tuned with GridSearchCV, then saved and reloaded with joblib. Tested offline on small in-memory data.
passed5 passed · 0 failed2026-10-15 13:33 UTC- Status
- passed
- Last passed
- 2026-10-15 13:33 UTC
- Tests
- 5 / 5
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38742087302
The code
Exactly the files that passed. Your agent can pull them with get_recipe("sklearn-columntransformer-pipeline") (install), or see them on GitHub.
scikit-learn==1.9.1 pandas==3.0.6 joblib==1.6.0 pytest==9.1.1 numpy==2.3.3 scipy==1.16.2
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
NUM = ["age", "income"]
CAT = ["city"]
def build_pipeline() -> Pipeline:
pre = ColumnTransformer(
[
("num", StandardScaler(), NUM),
("cat", OneHotEncoder(handle_unknown="ignore", sparse_output=False), CAT),
]
).set_output(transform="pandas")
return Pipeline([("pre", pre), ("clf", LogisticRegression(max_iter=1000))])
def search(X, y) -> GridSearchCV:
# preprocessing is refit inside each CV fold, so no leakage from validation rows
gs = GridSearchCV(build_pipeline(), {"clf__C": [0.1, 1.0, 10.0]}, cv=3)
return gs.fit(X, y)
import joblib
import pandas as pd
import pytest
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import OneHotEncoder
from pipeline import build_pipeline, search
@pytest.fixture
def data():
X = pd.DataFrame({
"age": [22, 25, 47, 52, 46, 56, 23, 28, 49, 60, 31, 58],
"income": [20, 25, 80, 90, 70, 95, 22, 30, 85, 100, 40, 88],
"city": ["ny", "sf", "ny", "sf", "ny", "sf", "ny", "sf", "ny", "sf", "ny", "sf"],
})
y = [0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1]
return X, y
def test_default_encoder_rejects_unseen_category():
enc = OneHotEncoder(sparse_output=False).fit(pd.DataFrame({"city": ["ny", "sf"]}))
with pytest.raises(ValueError, match="unknown categories"):
enc.transform(pd.DataFrame({"city": ["tokyo"]}))
def test_pipeline_tolerates_unseen_city(data):
X, y = data
model = build_pipeline().fit(X, y)
X_new = X.head(2).assign(city=["tokyo", "ny"])
assert model.predict(X_new).shape == (2,)
def test_set_output_keeps_dataframe_names(data):
X, _ = data
pre = build_pipeline().named_steps["pre"]
out = pre.fit_transform(X)
assert isinstance(out, pd.DataFrame)
assert list(out.columns) == ["num__age", "num__income", "cat__city_ny", "cat__city_sf"]
assert out.index.equals(X.index)
def test_grid_search_needs_step_prefixed_keys(data):
X, y = data
with pytest.raises(ValueError):
build_pipeline().set_params(C=1.0)
gs = search(X, y)
assert isinstance(gs, GridSearchCV)
assert "clf__C" in gs.best_params_
def test_joblib_roundtrip(data, tmp_path):
X, y = data
model = search(X, y).best_estimator_
path = tmp_path / "model.joblib"
joblib.dump(model, path)
loaded = joblib.load(path)
assert (loaded.predict(X) == model.predict(X)).all()
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
pandas + Pandera DataFrame schema validation
Validate pandas DataFrames with Pandera DataFrameModel schemas: dtypes, coercion, value checks, custom dataframe checks and lazy error reports.
pandas DataFrame to formatted Excel report with openpyxl styles and roundtrip read
Write a DataFrame to a styled .xlsx with openpyxl (header styles, number formats, widths, freeze panes, filters), then read it back with pandas and verify, fully offline.
Pandas pyarrow-backed dtypes with partitioned Parquet round trips
Write and read partitioned Parquet with pyarrow, enforce a pa.schema on write, and keep nullable ints intact using dtype_backend=pyarrow, tested offline on tmp_path.
pandas to_sql and read_sql with a SQLAlchemy 2 engine on sqlite, with dtype handling
Write and read DataFrames through a SQLAlchemy 2 engine, control column types with dtype, and handle datetimes and nullable ints, tested offline on in-memory sqlite.
All scikit-learn recipesAll pandas recipesAll joblib recipes