Frontend Labs
⋯

56. scikit-learn: the fit/predict contract, pipelines, and leakage

56.1 The contract

Every scikit-learn estimator follows one interface: fit(X, y) learns from data, predict(X) (or transform(X)) applies what was learned. Learn it once, and you can swap a logistic regression for a random forest by changing one line.

56.2 Real-world example: the help-desk ticket classifier

The AI guide's running example is routing help-desk tickets. Here it is as a real pipeline:

# labs/tickets.py
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.pipeline import make_pipeline

rng = np.random.default_rng(42)
templates = {
    "billing":   ["refund for order {n}", "charged twice on invoice {n}", "update my credit card",
                  "wrong amount on bill {n}", "cancel subscription and refund"],
    "technical": ["app crashes on login", "error 500 when saving", "cannot reset password",
                  "page loads forever on {n}", "sync fails with timeout error"],
    "shipping":  ["package {n} not delivered", "tracking shows no update", "delivered to wrong address",
                  "order {n} arrived damaged", "when will my order ship"],
}
noise = ["please help", "urgent", "thanks", "hi team", "asap", ""]
texts, labels = [], []
for label, ts in templates.items():
    for _ in range(200):
        t = rng.choice(ts).format(n=rng.integers(1000, 9999))
        texts.append(f"{rng.choice(noise)} {t} {rng.choice(noise)}".strip())
        labels.append(label)

X_train, X_test, y_train, y_test = train_test_split(
    texts, labels, test_size=0.2, stratify=labels, random_state=0)

model = make_pipeline(
    TfidfVectorizer(ngram_range=(1, 2), min_df=2, sublinear_tf=True),
    LogisticRegression(max_iter=1000),
)
print("5-fold CV accuracy:", cross_val_score(model, X_train, y_train, cv=5).mean().round(3))
model.fit(X_train, y_train)
pred = model.predict(X_test)
print(classification_report(y_test, pred, digits=3))
print(confusion_matrix(y_test, pred, labels=list(templates)))

for t in ["I was charged twice, need my money back", "the app shows an error after update",
          "my parcel never arrived"]:
    probs = model.predict_proba([t])[0]
    print(f"{model.classes_[probs.argmax()]:<10} {probs.max():.2f}  {t}")

vec, clf = model.named_steps["tfidfvectorizer"], model.named_steps["logisticregression"]
terms = np.array(vec.get_feature_names_out())
for i, cls in enumerate(clf.classes_):
    print(cls, "<-", ", ".join(terms[np.argsort(clf.coef_[i])[-5:]]))   # explainability for free

This is the AI guide's first model and TF-IDF as working code. A TF-IDF + logistic-regression baseline trains in milliseconds, is explainable (the last loop prints the words that drive each class), and is surprisingly hard to beat on short, domain-specific text. Always build this baseline before reaching for an LLM.

56.3 Leakage: the bug that makes models look great

# WRONG: the vectorizer sees the test set's vocabulary and document frequencies
# vec = TfidfVectorizer().fit(all_texts); X = vec.transform(all_texts); then split X

Any statistic computed on the full dataset before splitting (vocabulary, mean and standard deviation for scaling, imputation values, feature selection) leaks information from the test set into training. Your offline accuracy goes up, and production accuracy does not. The fix is the pipeline: put every preprocessing step inside it, so fit only ever sees training data, including inside each cross-validation fold. The AI guide Ch 13 covers evaluation in depth. Other leaks to watch for: duplicates across train and test (Ch 60 deduplicates), random splits on time-series data (split by time instead), and features that are only known after the label (a "refund_issued" column in a churn model).

56.4 Saving the model

import joblib
# joblib.dump(model, "tickets.joblib")      # a pickle: load only your own files (Ch 47.1)

For models that leave your machine, prefer an export format: skops for scikit-learn with a safer loader, or ONNX (skl2onnx) to run the model from Go, Rust, or C++ with ONNX Runtime, which fits the "Python offline, another language online" split from §0.5.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36