ProvenPin Install

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
pinned versions6
scikit-learn@1.9.1pandas@3.0.6joblib@1.6.0pytest@9.1.1numpy@2.3.3scipy@1.16.2
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
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.

requirements.txt
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
pipeline.py
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)
tests/test_pipeline.py
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.ini
[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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.