Part 8 · Machine Learning · Module 56
scikit-learn in Practice
In M55 you built models by hand. Professionals use scikit-learn: one consistent API for dozens of models, preprocessing that cannot leak, honest cross-validation, and tuning in a few lines. This module teaches the library the way it is used on real tabular data, and ends with an end-to-end workflow you can reuse.
- Use the estimator API (
fit,predict,transform) and tell hyperparameters from learnedattributes_ - Preprocess mixed numeric and categorical data with
SimpleImputer,StandardScaler,OneHotEncoder, andColumnTransformer - Wrap preprocessing and a model in a
Pipelineand explain how that prevents leakage - Choose among linear models, k-nearest neighbors, decision trees, random forests, gradient boosting, and SVMs
- Estimate performance with
KFold/StratifiedKFoldcross-validation and tune withGridSearchCVandRandomizedSearchCV - Handle class imbalance and explain models with impurity and permutation importance
- Save and load models safely with joblib or skops, knowing why unpickling untrusted files is dangerous
The estimator API
scikit-learn's great idea is that every algorithm is an object with the same few methods. Learn the pattern once and you can use any of its hundreds of classes:
- An estimator is any object with
fit(X, y)(orfit(X)for unsupervised methods).fitlearns from data and returnsself, so calls can be chained. - A predictor adds
predict(X)(and for classifiers oftenpredict_proba(X)andscore(X, y)). - A transformer adds
transform(X), which returns a new feature matrix, and the shortcutfit_transform(X). Scalers, encoders, and imputers are transformers.
Two kinds of numbers live on an estimator, and the naming convention tells them apart.
Hyperparameters are settings you choose, passed to the constructor: LogisticRegression(C=0.5).
Learned attributes are computed by fit and end in an underscore: coef_,
intercept_, classes_, mean_. If an attribute ends in _ and you
have not called fit, it does not exist yet.
Remember: fit learns, predict/transform apply.
Constructor arguments are hyperparameters; trailing-underscore attributes are learned.
Preprocessing mixed tabular data
Real tables mix numbers ("age", "monthly_spend"), categories ("plan", "country"), and holes (missing values). Most models need a purely numeric matrix with no missing values, so we transform each column type differently:
SimpleImputer(strategy="median")fills missing numbers with the training median (robust to outliers);strategy="most_frequent"works for categories.StandardScalerrescales numbers to mean 0, standard deviation 1 (M55's standardization).OneHotEncoderturns a category with k values into k 0/1 columns.handle_unknown="ignore"makes a category never seen in training encode as all zeros instead of crashing in production.ColumnTransformerapplies a different transformer to each group of columns and concatenates the results side by side.
Remember: impute, then scale numbers; impute, then one-hot categories; glue them with
ColumnTransformer; set handle_unknown="ignore" for production safety.
Pipelines prevent leakage
A Pipeline chains transformers and a final estimator into one estimator. Calling
pipe.fit(X_train, y_train) fits each step on the training data in order; calling
pipe.predict(X_test) only applies the already-fitted steps. That ordering is exactly the
"statistics from train only" rule of M55, enforced by code instead of discipline.
The danger is subtle, so here is a dramatic demonstration. We generate pure noise: 60 samples, 2,000 random features, random labels. There is nothing to learn; honest accuracy is 50%. If we select the 20 "best" features on all the data and then cross-validate, the selection step has already peeked at every test fold's labels.
Remember: put every data-dependent step inside the Pipeline, and cross-validate the Pipeline, not pre-transformed data.
A tour of the models
Linear models
LinearRegression, Ridge,
Lasso, LogisticRegression. Fast, interpretable coefficients, good baselines. Need scaled
features and cannot bend unless you add features.
k-nearest neighbors
KNeighborsClassifier(n_neighbors=5) predicts the
majority label of the k closest training points. No training cost, slow prediction on big data,
sensitive to scaling and to many features.
Decision trees
DecisionTreeClassifier(max_depth=3) asks yes/no questions
("petal length ≤ 2.45?"). Readable, no scaling needed, but a deep tree overfits badly.
Random forests
RandomForestClassifier averages many deep trees, each on
a bootstrap sample with random feature subsets. Averaging cancels their variance. Strong default, little tuning.
Gradient boosting
HistGradientBoostingClassifier adds shallow trees one at
a time, each fixing the previous ensemble's errors. Usually the most accurate choice on tabular data; handles
missing values natively. GradientBoostingClassifier is the older, slower version.
Support vector machines
SVC(kernel="rbf") finds the widest-margin
boundary, with kernels for curved boundaries. Excellent on small/medium data; scales poorly beyond ~100k rows;
needs scaling.
A decision tree is the easiest model to look inside. Each box shows the question, the impurity (Gini: the chance two random samples in the box have different labels), how many samples reach it, and the class counts.
Different models draw different decision boundaries. On the curved make_moons data, compare:
Remember: start with a linear baseline, then try a random forest and a histogram gradient booster; scale features for linear models, k-NN, and SVMs.
Cross-validation
A single validation split is noisy: with 150 samples, a lucky split can swing accuracy by several points. k-fold cross-validation splits the data into k folds, trains k times, each time holding out a different fold, and averages the k scores. Every sample is used for validation exactly once, and the spread of the scores tells you how stable the estimate is.
KFold(n_splits=5, shuffle=True, random_state=0): plain folds. Use for regression.StratifiedKFold: each fold keeps the class proportions. The default for classifiers when you passcv=5; essential with imbalanced classes.GroupKFoldkeeps all rows of one group (customer, patient) in the same fold;TimeSeriesSplitalways validates on data later than the training data.
Notice "k-NN unscaled": wine's features range from about 0.1 to over 1,000, so the large ones dominate the distance. Scaling is not optional for distance-based models.
Remember: report the mean and spread of CV scores; stratify classification folds; group or time-order folds when rows are not independent.
Hyperparameter tuning
GridSearchCV tries every combination in a grid, scoring each with cross-validation, then refits the
best on all the data you gave it. RandomizedSearchCV samples a fixed number of combinations from
distributions, which finds good settings far faster when there are many hyperparameters (most of them usually do not
matter much).
Remember: grids for one or two knobs, randomized search for many; use step__param
names; keep a test set the search never touches.
Class imbalance
Fraud, churn, defects, disease: the interesting class is usually rare. With 5% positives, a model can reach 95% accuracy by never predicting the positive class. Tools that help:
- Better metrics: recall, precision, F1,
balanced_accuracy(mean recall across classes), andaverage_precision(area under the precision-recall curve, which is more informative than ROC-AUC when positives are rare). - Stratified splits and folds so every fold has some positives.
- Class weights:
class_weight="balanced"makes each mistake on a rare sample count more in the loss. - Threshold tuning: 0.5 is arbitrary. Pick the probability threshold that fits the business cost
of false positives vs false negatives, on validation data. (scikit-learn 1.5+ also offers
TunedThresholdClassifierCV.) - Resampling (oversampling the minority, for example with SMOTE from the separate imbalanced-learn package) is sometimes useful, but try weights and thresholds first.
Remember: with rare classes, judge by recall/precision/average precision, stratify, use class weights, and tune the threshold to the costs.
Feature importance and permutation importance
Stakeholders ask "why?". Tree ensembles expose feature_importances_: how much each feature reduced
impurity across all splits. It is free but biased: it favors features with many distinct values (an ID column can
look important) and is computed on training data.
Permutation importance is model-agnostic and more honest: on held-out data, shuffle one column (breaking its relationship to the target) and measure how much the score drops. A big drop means the model relies on that feature. If two features are strongly correlated, shuffling one barely hurts because the other carries the same information, so read low scores with care.
Remember: importance explains the model, not the world; prefer permutation importance on held-out data; correlated features share credit.
Saving and loading models
Training can take hours; serving must take milliseconds. So we fit once, save the fitted pipeline to a file, and load it in the serving process. scikit-learn's documented options:
- joblib (installed with scikit-learn):
joblib.dump(pipe, "model.joblib"),joblib.load("model.joblib"). Efficient for large NumPy arrays. Built on pickle. - skops:
skops.io.dump/skops.io.loaduse a format that does not execute arbitrary code when loading, and you list which types you trust. Add it withpixi add skops(it is on conda-forge). - ONNX (via
skl2onnx) exports to a portable format served without Python.
Remember: save the whole fitted pipeline; joblib for your own files, skops when files cross trust
boundaries; pin versions with pixi.lock.
An end-to-end workflow
Here is the shape of a real project on a tabular dataset: predict whether a subscriber will cancel ("churn")
from mixed numeric and categorical data with missing values. We generate the data so the cell works offline, but
every step is what you would do with a real CSV loaded by pd.read_csv.
- Hold out a test set first, stratified.
- Build a preprocessing
ColumnTransformerand a baseline model inside aPipeline. - Compare candidates with cross-validation on the training set.
- Tune the best candidate.
- Evaluate once on test; inspect errors and importances.
- Save the fitted pipeline with its metadata.
Remember: split first, pipeline everything, compare with CV against a dummy baseline, tune, test once, save the pipeline.
Practice
scikit-learn, NumPy, and pandas are available in these exercises.
Write build_preprocessor(numeric, categorical) returning an unfitted ColumnTransformer
that, for the numeric column names, imputes the median then standardizes, and for the
categorical names imputes the most frequent value then one-hot encodes with
handle_unknown="ignore" and sparse_output=False.
Hint
Two tuples of (name, transformer, columns); each
transformer is a small Pipeline of an imputer and a scaler/encoder.
Show solution
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
def build_preprocessor(numeric, categorical):
num = Pipeline([("impute", SimpleImputer(strategy="median")), ("scale", StandardScaler())])
cat = Pipeline([
("impute", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
return ColumnTransformer([("num", num, list(numeric)), ("cat", cat, list(categorical))])
Write make_model(C=1.0) returning a Pipeline with two steps named
"scale" (a StandardScaler) and "clf" (a LogisticRegression
with the given C and max_iter=2000). Then write cv_accuracy(model, X, y)
that returns the mean accuracy of 5-fold StratifiedKFold(shuffle=True, random_state=0)
cross-validation as a float.
Hint
cross_val_score(model, X, y, cv=StratifiedKFold(5, shuffle=True,
random_state=0)).mean(), wrapped in float(...).
Show solution
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
def make_model(C=1.0):
return Pipeline([("scale", StandardScaler()), ("clf", LogisticRegression(C=C, max_iter=2000))])
def cv_accuracy(model, X, y):
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
return float(cross_val_score(model, X, y, cv=cv).mean())
Write tune_knn(X, y) that grid-searches n_neighbors over
[1, 3, 5, 7, 9, 11, 15] for a pipeline make_pipeline(StandardScaler(), KNeighborsClassifier())
with cv=5, and returns a tuple (best_k, best_cv_score) (an int and a float).
Hint
make_pipeline names the step
"kneighborsclassifier", so the grid key is "kneighborsclassifier__n_neighbors".
Show solution
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
def tune_knn(X, y):
pipe = make_pipeline(StandardScaler(), KNeighborsClassifier())
grid = GridSearchCV(pipe, {"kneighborsclassifier__n_neighbors": [1, 3, 5, 7, 9, 11, 15]}, cv=5)
grid.fit(X, y)
return int(grid.best_params_["kneighborsclassifier__n_neighbors"]), float(grid.best_score_)
Write top_features(model, X_val, y_val, names, k=3) that runs
permutation_importance(model, X_val, y_val, n_repeats=10, random_state=0) and returns the
k feature names with the largest mean importance, most important first.
Hint
np.argsort(result.importances_mean)[::-1][:k] gives the
indexes of the top k.
Show solution
import numpy as np
from sklearn.inspection import permutation_importance
def top_features(model, X_val, y_val, names, k=3):
result = permutation_importance(model, X_val, y_val, n_repeats=10, random_state=0)
order = np.argsort(result.importances_mean)[::-1][:k]
return [str(names[i]) for i in order]
Write threshold_for_recall(y_true, proba, target_recall) that returns the largest
threshold t among the distinct values of proba such that predicting
proba >= t achieves recall of at least target_recall. A larger threshold means fewer
false alarms, so we want the largest one that still meets the recall goal.
Hint
Loop over sorted(set(proba), reverse=True) and return the
first threshold whose recall np.mean(proba[y_true == 1] >= t) reaches the target.
Show solution
import numpy as np
def threshold_for_recall(y_true, proba, target_recall):
y_true, proba = np.asarray(y_true), np.asarray(proba)
positives = proba[y_true == 1]
for t in sorted(set(proba.tolist()), reverse=True):
if np.mean(positives >= t) >= target_recall:
return t
return float(proba.min())
Mini-project
Quiz
Recap
- Every estimator has
fit; predictors addpredict; transformers addtransform. Learned attributes end in_. - Impute, scale, and one-hot encode with a
ColumnTransformer; sethandle_unknown="ignore". - Pipelines tie preprocessing to the model so fitting happens only on training folds: no leakage.
- Linear models are fast baselines; random forests are robust; histogram gradient boosting usually wins on tables; scale for k-NN, SVM, and linear models.
- Cross-validate (stratified for classification) and report mean ± std.
- Tune with GridSearchCV or RandomizedSearchCV using
step__paramnames; keep a final test set. - For imbalance: better metrics, class weights, and threshold tuning.
- Explain with permutation importance on held-out data.
- Persist the whole pipeline; never load untrusted pickles; pin versions with
pixi.lock.