The scikit-learn API
Every scikit-learn estimator follows the same three-method contract. Once you see it, the whole library makes sense.
What you'll learn
- The fit / predict / transform pattern that every estimator follows
- Why Pipelines are the single most underrated tool in sklearn
- How ColumnTransformer handles mixed numeric and categorical features
- The shape of a real end-to-end workflow — load, split, preprocess, fit, predict
Before you start
The first time you read scikit-learn code, every model looks suspiciously
similar. LinearRegression, RandomForestClassifier, KMeans,
StandardScaler — they all have the same handful of methods. That’s not a
coincidence. It’s the estimator pattern: an estimator is any sklearn object that learns from data — models, scalers, encoders, all of it. This shared interface is the reason sklearn
is the lingua franca of tabular ML.
The contract
Throughout sklearn, X is the feature matrix (rows = samples, columns = input variables) and y is the label vector (the target you’re trying to predict). Every estimator implements at least one of these methods:
| Method | What it does | Who has it |
|---|---|---|
fit(X, y) | Learn parameters from training data | Everything |
predict(X) | Produce predictions | Supervised models |
transform(X) | Modify features (scale, encode, reduce dims) | Preprocessors, dim reducers |
predict_proba(X) | Class probabilities | Classifiers |
fit_transform(X) | fit then transform in one call | Preprocessors |
That’s it. Logistic regression, a random forest, and a standard scaler all expose this same interface, which is why you can swap them in and out of a pipeline without changing the surrounding code.
A minimal end-to-end run
Predicting whether a credit card transaction will be flagged as risky — a mix of numeric and categorical features, the bread and butter of any fraud team:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
# Toy dataset — 6 transactions, 2 numeric features, binary label
df = pd.DataFrame({
"amount": [12.50, 850.00, 45.00, 2100.00, 9.99, 320.00],
"hours_old": [0.1, 23.0, 3.5, 71.0, 0.2, 12.0],
"is_risky": [0, 1, 0, 1, 0, 0],
})
X = df[["amount", "hours_old"]]
y = df["is_risky"]
# Same three lines every time:
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=0)
scaler = StandardScaler().fit(X_train) # learn means + stds on TRAIN ONLY
X_train_s = scaler.transform(X_train)
X_test_s = scaler.transform(X_test)
model = LogisticRegression().fit(X_train_s, y_train)
print("scaler.mean_ (learned on train):", scaler.mean_.round(2).tolist())
print("test predictions:", model.predict(X_test_s).tolist())
scaler.mean_ (learned on train): [743.12, 23.58]
test predictions: [0, 0]
Notice the discipline: the scaler is .fit on the training data only,
then .transform is applied to both. If you fit the scaler on the full
dataset you’ve leaked test information into preprocessing — a subtle bug
that inflates your metrics and falls apart in production.
Pipelines — the underrated superpower
Doing that manual scaler-then-model dance is fine for one model. The
moment you cross-validate, tune hyperparameters, or save to disk, it gets
tedious and error-prone. Pipeline wraps the whole sequence into a
single estimator that itself has .fit / .predict.
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
df = pd.DataFrame({
"amount": [12.50, 850.00, 45.00, 2100.00, 9.99, 320.00, 75.00, 1500.0],
"hours_old": [0.1, 23.0, 3.5, 71.0, 0.2, 12.0, 1.0, 48.0],
"is_risky": [0, 1, 0, 1, 0, 0, 0, 1],
})
X, y = df[["amount", "hours_old"]], df["is_risky"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
pipe = Pipeline([
("scale", StandardScaler()),
("clf", LogisticRegression()),
])
pipe.fit(X_train, y_train)
print("score:", pipe.score(X_test, y_test))
print("preds:", pipe.predict(X_test).tolist())
score: 1.0
preds: [0, 0]
The pipeline IS an estimator. You call .fit once, and at predict time
the same scaler trained on X_train is applied to whatever you pass in.
No leakage, no bookkeeping.
Mixed types — ColumnTransformer
Real data is messy: numeric columns, categorical columns, maybe text. You
want different preprocessing per column. ColumnTransformer is how.
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
df = pd.DataFrame({
"amount": [12.5, 850, 45, 2100, 9.99, 320, 75, 1500],
"hours_old": [0.1, 23, 3.5, 71, 0.2, 12, 1, 48],
"merchant": ["food", "elec", "food", "elec", "food", "ride", "food", "elec"],
"is_risky": [0, 1, 0, 1, 0, 0, 0, 1],
})
X, y = df.drop(columns=["is_risky"]), df["is_risky"]
pre = ColumnTransformer([
("num", StandardScaler(), ["amount", "hours_old"]),
("cat", OneHotEncoder(handle_unknown="ignore"), ["merchant"]),
])
pipe = Pipeline([("pre", pre), ("clf", LogisticRegression())])
pipe.fit(X, y)
print("score on train:", pipe.score(X, y))
print("predict new row:",
pipe.predict(pd.DataFrame([{"amount": 9.0, "hours_old": 0.5, "merchant": "food"}])).tolist())
score on train: 1.0
predict new row: [0]
ColumnTransformer slices columns out by name, applies the transformer
you specified, then stacks the results back together. The Pipeline
wraps that with a classifier. The whole thing is now ONE object —
serialize it, ship it, done.
Why this matters
The estimator pattern means experimentation is cheap. Want to try a
random forest instead? Replace LogisticRegression() with
RandomForestClassifier(). Want to compare scalers? Swap
StandardScaler for RobustScaler. The surrounding code does not
change. This composability is what makes sklearn productive — and
it’s why every downstream library (XGBoost, LightGBM, scikit-learn-contrib
packages) mimics the same interface.
In one breath
- Every scikit-learn estimator (model, scaler, encoder) follows one contract:
fit(learn from data),predict(supervised output),transform(modify features), pluspredict_probaandfit_transform. - Because the interface is shared, you swap estimators without touching the surrounding code — the source of sklearn’s composability.
- Fit preprocessing on train only, then transform both; fitting on the full data leaks test info and inflates metrics.
- A
Pipelinebundles preprocessing + model into one estimator with its own.fit/.predict— no leakage, no bookkeeping; treat “wrap it in a Pipeline” as non-negotiable before deploying. ColumnTransformerroutes different transforms to different columns (scale numerics, one-hot categoricals) and stacks them — wrap it in a Pipeline and you have one serializable, shippable object.
Quick check
Quick check
Practice this in an interview
All questionsK-means partitions n points into k clusters by alternating between two steps: assigning each point to its nearest centroid, then recomputing each centroid as the mean of its assigned points. It repeats until assignments stop changing, which guarantees convergence but not a globally optimal solution.
Standardization rescales features to zero mean and unit variance; normalization squashes values into a fixed range, usually [0, 1]. Distance-based and gradient-based models are sensitive to scale and require one of these; tree-based models split on rank order and are scale-invariant.
ML CI/CD must validate not just code correctness but also model quality — automated retraining triggers, data validation, model evaluation gates, and canary deployment checks that standard software pipelines have no equivalent for. A regression in model AUC is as much a deployment failure as a 500 error.
Full ML reproducibility requires locking three layers: the random seed across all frameworks, the software environment via pinned dependency manifests or container images, and the training data via content-addressed versioning. Missing any one layer means the same code can produce different models on different runs or machines.