Scikit-learn Workflow¶
Scikit-learn is built around one idea: every algorithm, regardless of how different it is mathematically, speaks the same interface. A logistic regression, a random forest, and a support vector machine are all called the same way. Once you learn the API contract, you can swap algorithms with a single line of code and focus your energy on what actually matters — data quality, features, and evaluation.
This note walks you through the complete sklearn workflow end-to-end, from raw data to a defensible evaluation score.
Learning Objectives¶
- Explain the sklearn estimator API:
fit,predict,transform,fit_transform,score - Distinguish transformers, estimators, and pipelines
- Build a complete end-to-end ML pipeline with preprocessing and a model
- Use
cross_val_scoreandStratifiedKFoldfor reliable evaluation - Handle mixed-type data (numeric + categorical) inside a single pipeline
- Interpret evaluation metrics appropriate to the task type
The Estimator API Contract¶
Sklearn has a single, consistent interface across all of its objects. Learn this once and you can use any algorithm.
| Method | Who has it | What it does |
|---|---|---|
fit(X, y) |
All estimators | Learn parameters from training data |
predict(X) |
Models (classifiers, regressors) | Return predictions for new data |
predict_proba(X) |
Classifiers that support it | Return class probabilities |
transform(X) |
Transformers (scalers, encoders) | Apply the learned transformation |
fit_transform(X) |
Transformers | Fit then transform in one step (train only) |
score(X, y) |
Most estimators | Compute default metric (accuracy or R²) |
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
import numpy as np
# Transformers: fit learns parameters, transform applies them
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # learn mean/std, then scale
X_test_scaled = scaler.transform(X_test) # apply learned mean/std, no refit
# Models: fit learns from data, predict applies
model = LogisticRegression(max_iter=10000, random_state=42)
model.fit(X_train_scaled, y_train) # learn weights
y_pred = model.predict(X_test_scaled) # apply weights to new data
proba = model.predict_proba(X_test_scaled) # class probabilities
print("Prediction shape:", y_pred.shape) # Output: (114,)
print("Probability shape:", proba.shape) # Output: (114, 2)
print("Classes:", model.classes_) # Output: [0 1]
Info
The consistency of this API is why sklearn dominates industry for tabular ML. You can write a function that accepts any sklearn-compatible model, and it works with logistic regression, random forests, gradient boosting, SVMs — all of them. This is the power of a well-designed interface.
Complete End-to-End Workflow¶
Here is a full ML workflow on the Titanic dataset — a realistic example with missing values, categorical features, and class imbalance.
Step 1: Load and inspect¶
import pandas as pd
import numpy as np
# Load Titanic data — a classic binary classification dataset
url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
# If offline, use this minimal synthetic version:
np.random.seed(42)
n = 891
titanic = pd.DataFrame({
"Pclass": np.random.choice([1, 2, 3], n, p=[0.24, 0.21, 0.55]),
"Age": np.where(np.random.rand(n) < 0.2, np.nan,
np.random.normal(30, 14, n).clip(1, 80)),
"SibSp": np.random.poisson(0.5, n),
"Fare": np.random.exponential(32, n).round(2),
"Sex": np.random.choice(["male", "female"], n, p=[0.65, 0.35]),
"Embarked": np.random.choice(["S", "C", "Q", None], n, p=[0.72, 0.18, 0.08, 0.02]),
"Survived": np.random.choice([0, 1], n, p=[0.62, 0.38]),
})
print(titanic.dtypes)
# Output:
# Pclass int64
# Age float64
# SibSp int64
# Fare float64
# Sex object
# Embarked object
# Survived int64
print(titanic.isnull().sum())
# Output:
# Age ~178 (20% missing)
# Embarked ~2
Step 2: Define features and target¶
feature_cols = ["Pclass", "Age", "SibSp", "Fare", "Sex", "Embarked"]
target_col = "Survived"
X = titanic[feature_cols]
y = titanic[target_col]
print("Class balance:")
print(y.value_counts(normalize=True).round(3))
# Output:
# 0 0.621
# 1 0.379
Step 3: Split¶
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=y # preserve class balance in both sets
)
print(f"Train: {X_train.shape[0]}, Test: {X_test.shape[0]}")
# Output: Train: 712, Test: 179
Step 4: Build preprocessing pipeline for mixed data¶
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
# Separate columns by type
numeric_cols = ["Age", "Fare", "SibSp"]
categorical_cols = ["Pclass", "Sex", "Embarked"]
# Preprocessing for numeric features:
# 1. Impute missing values with median
# 2. Scale to zero mean, unit variance
numeric_transformer = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
])
# Preprocessing for categorical features:
# 1. Impute missing values with the most frequent value
# 2. One-hot encode categories to integers
categorical_transformer = Pipeline([
("imputer", SimpleImputer(strategy="most_frequent")),
("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
])
# ColumnTransformer applies different pipelines to different columns
preprocessor = ColumnTransformer([
("num", numeric_transformer, numeric_cols),
("cat", categorical_transformer, categorical_cols),
])
Step 5: Build full model pipeline¶
from sklearn.linear_model import LogisticRegression
model_pipe = Pipeline([
("preprocessor", preprocessor),
("classifier", LogisticRegression(max_iter=10000, random_state=42)),
])
model_pipe.fit(X_train, y_train)
Step 6: Evaluate¶
from sklearn.metrics import (
accuracy_score,
classification_report,
confusion_matrix,
roc_auc_score,
)
y_pred = model_pipe.predict(X_test)
y_proba = model_pipe.predict_proba(X_test)[:, 1] # probability of class 1
print("Accuracy:", accuracy_score(y_test, y_pred).round(4))
# Output: Accuracy: 0.7989
print("\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=["Died", "Survived"]))
# Output:
# precision recall f1-score support
# Died 0.83 0.87 0.85 111
# Survived 0.75 0.69 0.72 68
# accuracy 0.80 179
print("ROC-AUC:", roc_auc_score(y_test, y_proba).round(4))
# Output: ROC-AUC: 0.8541
Tip
ROC-AUC is often more informative than accuracy for classification. Accuracy collapses when classes are imbalanced — a model that predicts "died" for everyone gets 62% accuracy on Titanic. AUC measures the model's ability to rank positives above negatives, regardless of threshold.
Reading the Classification Report¶
# The classification report gives four numbers per class
#
# precision: of all predicted positive, how many were actually positive?
# TP / (TP + FP)
#
# recall: of all actual positives, how many did the model find?
# TP / (TP + FN)
#
# f1-score: harmonic mean of precision and recall
# 2 * (precision * recall) / (precision + recall)
#
# support: number of true instances in this class
#
# When to optimise for precision: when false positives are costly
# (e.g. flagging a good customer as fraudulent)
#
# When to optimise for recall: when false negatives are costly
# (e.g. missing a cancer diagnosis)
# Confusion matrix: rows are actual, columns are predicted
cm = confusion_matrix(y_test, y_pred)
print("Confusion matrix:")
print(cm)
# Output:
# [[TN FP]
# [FN TP]]
#
# Top-left: correctly predicted "Died"
# Bottom-right: correctly predicted "Survived"
# Top-right: predicted "Survived" but actually "Died" (false positives)
# Bottom-left: predicted "Died" but actually "Survived" (false negatives)
Cross-Validation for Reliable Evaluation¶
A single train/test split gives you one number. Cross-validation gives you a distribution — how much does performance vary across different subsets of the data? That variance is information.
from sklearn.model_selection import cross_val_score, StratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(
model_pipe,
X, y,
cv=cv,
scoring="roc_auc",
)
print("Fold AUC scores:", scores.round(4))
# Output: Fold AUC scores: [0.8534 0.8412 0.8701 0.8623 0.8488]
print(f"Mean AUC: {scores.mean():.4f}") # Output: Mean AUC: 0.8552
print(f"Std AUC: {scores.std():.4f}") # Output: Std AUC: 0.0100
Interpreting cross-validation output¶
Mean = 0.8552, Std = 0.0100
- Good mean, low variance → stable, reliable model
- Good mean, high variance → model works but results depend heavily on which data you use
- Low mean → model needs improvement (features, algorithm, hyperparameters)
- Very high mean (> 0.99) on a real dataset → check for leakage
Warning
A standard deviation close to zero is not always good. If all 5 folds give exactly 0.97, it might mean your dataset has very low variance — or that leakage is making every fold trivially easy to predict. Investigate either case.
Comparing Multiple Models¶
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
candidates = {
"Logistic Regression": LogisticRegression(max_iter=10000, random_state=42),
"Decision Tree": DecisionTreeClassifier(max_depth=5, random_state=42),
"Random Forest": RandomForestClassifier(n_estimators=100, random_state=42),
"SVM": SVC(probability=True, random_state=42),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
results = {}
for name, clf in candidates.items():
pipe = Pipeline([
("preprocessor", preprocessor),
("classifier", clf),
])
scores = cross_val_score(pipe, X, y, cv=cv, scoring="roc_auc")
results[name] = scores
print(f"{name:<25} AUC: {scores.mean():.4f} ± {scores.std():.4f}")
# Output (approximate):
# Logistic Regression AUC: 0.8552 ± 0.0100
# Decision Tree AUC: 0.8101 ± 0.0231
# Random Forest AUC: 0.8712 ± 0.0119
# SVM AUC: 0.8634 ± 0.0142
Tip
Choose your model based on cross-validation results, not test set results. After comparing models on cross-validation, pick one, retrain it on all training data, and evaluate it once on the test set. That single final test set evaluation is your honest reported performance.
Common Workflow Mistakes¶
Warning
Calling fit on test data. Even for a transformer: scaler.fit_transform(X_test) recomputes the scaler on test data. This is leakage and will go silently wrong. Always: fit on train, transform test.
Warning
Forgetting to include preprocessing in cross-validation. If you scale before passing to cross_val_score, all five folds use the same scaler fitted on the full dataset — that scaler has seen validation fold data. Always wrap preprocessing in a Pipeline before cross-validation.
Warning
Interpreting a high score() result without checking what metric it uses. LogisticRegression.score() returns accuracy. On an imbalanced dataset, accuracy is misleading. Always specify the metric explicitly: cross_val_score(..., scoring="roc_auc").
Warning
Using fit_transform on test data. fit_transform(X_test) is equivalent to fit(X_test).transform(X_test) — it refits the transformer on the test set. For test data, always use .transform() only.
The Workflow Checklist¶
Before you submit or present any ML result, verify each item:
[ ] Problem is clearly defined (what decision does the model support?)
[ ] Target column is correct and not leaky
[ ] Identifier columns removed from features
[ ] Data split BEFORE any preprocessing
[ ] Preprocessing is inside a Pipeline (or manually done on train, applied to test)
[ ] Model evaluated on held-out data (test set or cross-validation)
[ ] Reported metric is appropriate for the task and class balance
[ ] Test set was touched only once
[ ] Model limitations are stated (where does it fail? what is the error rate?)
Interview Questions¶
Q1: What does fit() do? What does transform() do?
Show answer
fit() learns parameters from training data — for a StandardScaler, it computes the mean and standard deviation; for a LogisticRegression, it learns the weights. These parameters are stored in the object.
transform() applies the previously learned transformation to new data without relearning parameters. For a StandardScaler, it subtracts the mean and divides by the standard deviation computed during fit().
fit_transform() is a convenience that calls both in sequence. It should only be used on training data.
Q2: Why use a Pipeline instead of preprocessing manually?
Show answer
Two reasons. First, correctness: a Pipeline ensures that preprocessing is always fitted on training data only. When you pass a pipeline to cross_val_score, each fold refits the transformers on its own training fold — preventing leakage. If you preprocess manually before cross-validation, all folds see the same transformers fitted on the full dataset, which is leakage.
Second, reproducibility: a pipeline bundles all preprocessing and modelling decisions into a single serialisable object. You can save and load it with joblib.dump(), and know that predictions will always use the same preprocessing steps applied in the same order.
Q3: You cross-validate a model and get mean AUC 0.92 ± 0.18. What does that tell you?
Show answer
The high standard deviation (0.18) is concerning. It means model performance varies enormously across folds — some folds may score 0.74 and others 1.00. This suggests the model is unstable: it works well on some data distributions and poorly on others. Possible causes include: small dataset making each fold very different; class imbalance causing some folds to have very few positive examples; or leakage affecting some folds but not others. Investigate before trusting the mean.
Q4: What is ColumnTransformer and why do you need it?
Show answer
ColumnTransformer applies different preprocessing pipelines to different columns simultaneously. In most real datasets, numeric and categorical features need different treatment — numerics need imputation and scaling, categoricals need imputation and one-hot encoding. ColumnTransformer lets you specify which transformer to apply to which columns, then combines the outputs into a single feature matrix. Without it, you would have to manually split, transform, and recombine columns — which is error-prone and breaks the Pipeline pattern.
What's Next¶
You've covered the complete sklearn workflow from data loading through ColumnTransformer pipelines to model comparison and the pre-submission checklist — including fit/transform semantics, cross-validation interpretation, and classification report reading. Next up: 05-exercises — where you'll apply the full workflow to real datasets, building pipelines from scratch and diagnosing common mistakes under exam-style constraints.
Optional Deep Dive
Work through the sklearn tutorial "An introduction to machine learning with scikit-learn" at https://scikit-learn.org/stable/tutorial/basic/tutorial.html — it uses the Iris and digits datasets to walk through the exact fit/predict/score workflow covered here, reinforcing the API patterns with hands-on examples you can run in a notebook.