5 Mid-Tier ML Concepts You Should Know for Interviews
Bias and variance, L1 versus L2 regularisation, data leakage and honest cross validation, metrics for imbalanced data, and gradient descent: the questions that come after "name an algorithm".

After "name an algorithm"
The first ML interview question is usually some version of "what would you use for this". The second one is where it gets interesting: "it overfit, what do you do", "why did it look great offline and fail in production", "your fraud model is 99% accurate, is that good". Those are diagnosis questions, and they're built on five concepts.
I've hit every one of these on real models. Each section has the idea, why it matters, code I ran before posting (scikit-learn 1.7, numpy 2.2), and the trap. Save it and come back the night before.
5 mid tier ML concepts

A 36 second version of this article

1. Bias versus variance
A model's expected error on new data splits into three parts: bias, variance and irreducible noise. Bias is error from the model being too simple to capture the pattern (a straight line through a curve). Variance is error from the model being too sensitive to the particular training sample (a deep tree that changes completely when you add ten rows). Noise is the part no model can remove. Making a model more flexible usually lowers bias and raises variance, which is the tradeoff.
The practical value is diagnosis. Plot a learning curve: training and validation score as the training set grows.
- Both scores low and close together: high bias. More data won't help. Use a more expressive model, add features, or reduce regularisation.
- Training score high, validation much lower, a big gap: high variance. More data helps, and so do regularisation, a simpler model, or bagging.
Bias versus variance

A learning curve for a deliberately overfit tree: training accuracy near 1.0, validation well below. That gap is variance.
from sklearn.datasets import make_classification
from sklearn.model_selection import learning_curve
from sklearn.tree import DecisionTreeClassifier
X, y = make_classification(n_samples=1500, n_features=20, n_informative=5, flip_y=0.1, random_state=0)
sizes, train, val = learning_curve(DecisionTreeClassifier(random_state=0), X, y, cv=5,
train_sizes=[0.1, 0.4, 1.0])
for n, t, v in zip(sizes, train.mean(1), val.mean(1)):
print(f"{n:5d} rows train {t:.2f} val {v:.2f}")The trap: "get more data" is the reflexive answer, and it only works for variance. A high bias model given ten times the data stays exactly as wrong.
2. L1 versus L2 regularisation
Regularisation adds a penalty on the size of the weights to the loss, so the model trades a little training fit for simpler weights that generalise better.
- L2 (Ridge) penalises the sum of squared weights. It shrinks every weight toward zero smoothly and rarely makes any exactly zero. With correlated features it spreads weight across them instead of picking one arbitrarily.
- L1 (Lasso) penalises the sum of absolute weights. Its geometry (the corners of the diamond shaped constraint) drives some weights to exactly zero, so it performs feature selection as a side effect.
- Elastic net mixes both, which helps when you want sparsity but have groups of correlated features.
L1 versus L2

On data where only 5 of 30 features matter, Lasso zeroes most of the useless ones; Ridge zeroes none.
from sklearn.datasets import make_regression
from sklearn.linear_model import Lasso, Ridge
X, y = make_regression(n_samples=500, n_features=30, n_informative=5, noise=10, random_state=0)
lasso = Lasso(alpha=1.0).fit(X, y)
ridge = Ridge(alpha=1.0).fit(X, y)
print((lasso.coef_ == 0).sum(), "of 30 weights zeroed by L1")
print((ridge.coef_ == 0).sum(), "of 30 weights zeroed by L2")The traps: the penalty is on raw weight size, so a feature measured in thousands gets penalised differently from one measured in fractions. Standardise first. And watch the parameter direction: alpha in Ridge and Lasso is the penalty strength, but C in LogisticRegression and SVMs is its inverse, so a smaller C means more regularisation. People get that backwards in interviews constantly.
3. Data leakage and honest cross validation
Leakage is any path by which information that wouldn't be available at prediction time gets into training. The model looks brilliant in validation and disappoints in production, and it's the most common reason I've seen an offline score not survive contact with real traffic.
The usual suspects:
- Preprocessing leakage. Fitting a scaler, an imputer or a feature selector on the whole dataset before splitting. The training folds have now seen statistics of the validation fold. The fix is a
Pipeline, so every step is fit inside each training fold only. - Target leakage. A feature that's a consequence of the label. "Refund issued" predicts churn wonderfully, because it happens after the customer complained.
- Time leakage. Shuffling time series, so the model trains on next week to predict last week. Use
TimeSeriesSplitor a strict cutoff date. - Group leakage. The same user, patient or device in both train and test, so the model recognises the entity instead of learning the pattern. Use
GroupKFold.
Data leakage

The pipeline version is the honest one: the scaler and the selector are refit on each training fold, never on the rows they are scored on.
from sklearn.datasets import make_classification
from sklearn.feature_selection import SelectKBest
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = make_classification(n_samples=300, n_features=2000, n_informative=5, random_state=0)
# leaky: feature selection saw every row, including the ones used for scoring
X_sel = SelectKBest(k=20).fit_transform(X, y)
leaky = cross_val_score(LogisticRegression(max_iter=1000), X_sel, y, cv=5).mean()
# honest: selection happens inside each training fold
pipe = make_pipeline(StandardScaler(), SelectKBest(k=20), LogisticRegression(max_iter=1000))
honest = cross_val_score(pipe, X, y, cv=5).mean()
print(f"leaky {leaky:.2f} honest {honest:.2f}")With many features and few rows, the leaky score comes out noticeably higher than the honest one, for a model that is exactly the same. That gap is the leakage.
4. Imbalanced data: 99% accurate, 100% useless
If 1% of transactions are fraud, a model that always answers "not fraud" is 99% accurate and catches nothing. Accuracy is the wrong question when classes are imbalanced. Better ones:
- Precision: of everything you flagged, what fraction was really fraud? Low precision means analysts wading through false alarms.
- Recall: of all the fraud that happened, what fraction did you catch? Low recall means losses.
- F1 combines the two into one number, the harmonic mean.
- PR-AUC (average precision) summarises precision and recall across every threshold.
The threshold is a business decision, not a default. 0.5 is arbitrary; choose it from the cost of a false alarm against the cost of a miss. class_weight="balanced" tells many scikit-learn models to weigh the rare class more during training.
Metrics for imbalanced data
![Slide 5 of 8: 99% ACCURATE, 100% USELESS. With 1% fraud, predicting "never fraud" scores 99% accuracy. Use precision, recall and PR-AUC. Why it matters: Precision: of what you flagged, how much was right. Recall: of what was there, how much you caught. The threshold trades one for the other. A code card (python) shows: from sklearn.metrics import ( average_precision_score, classification_report) p = clf.predict_proba(X_test)[:, 1] print(average_precision_score(y_test, p)) print(classification_report(y_test, p > 0.3)) Interview trap: ROC-AUC can look excellent on heavy imbalance. PR-AUC shows the pain. And never oversample before the split.](https://media.designsbyduhart.org/viral/d32f6e7bd53e9759.png)
A 1% positive class. The always-negative baseline is 99% accurate with zero recall; the model is judged on average precision and a chosen threshold instead.
from sklearn.datasets import make_classification
from sklearn.dummy import DummyClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, average_precision_score, recall_score
from sklearn.model_selection import train_test_split
X, y = make_classification(n_samples=20000, weights=[0.99], flip_y=0, random_state=0)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, stratify=y, random_state=0)
dummy = DummyClassifier(strategy="most_frequent").fit(X_tr, y_tr)
print("dummy accuracy", accuracy_score(y_te, dummy.predict(X_te)),
"recall", recall_score(y_te, dummy.predict(X_te)))
clf = LogisticRegression(class_weight="balanced", max_iter=1000).fit(X_tr, y_tr)
p = clf.predict_proba(X_te)[:, 1]
print("PR-AUC", round(average_precision_score(y_te, p), 3))
for t in (0.5, 0.8, 0.95):
print(t, "recall", round(recall_score(y_te, p > t), 2))The traps. ROC-AUC can look excellent on heavily imbalanced data because the huge number of true negatives keeps the false positive rate tiny; Saito and Rehmsmeier (PLOS ONE, 2015) showed the precision recall plot is more informative in exactly that setting. And if you oversample (SMOTE or plain duplication), do it inside the training fold only. Oversampling before the split copies minority rows into the test set, which is leakage again.
5. Gradient descent
Most models with a differentiable loss are trained the same way: compute the gradient of the loss with respect to the weights, take a step in the opposite direction, repeat. The learning rate is the step size. Too large and the loss bounces around or diverges; too small and training crawls or stalls.
The variants are about how much data each step sees. Batch gradient descent uses the whole dataset per step (stable, expensive). Stochastic uses one example (cheap, noisy). Mini-batch, the default in deep learning, uses a few dozen to a few thousand. Momentum and Adam smooth the noisy steps and adapt the step size per weight.
Gradient descent
![Slide 6 of 8: GRADIENT DESCENT. Step downhill on the loss, a learning rate at a time. Too big diverges, too small crawls. Why it matters: Every neural network and many classic models train this way. Batch size, momentum and Adam are all tweaks on these four lines. A code card (python) shows: w = np.zeros(X.shape[1]) for step in range(500): grad = X.T @ (X @ w - y) / len(y) # MSE w -= lr * grad Interview trap: Unscaled features stretch the loss into a long narrow valley and descent zig-zags. Scale before you blame the learning rate.](https://media.designsbyduhart.org/viral/9b17459813316b9e.png)
Watch the learning rate

Plain batch gradient descent for linear regression with numpy. On standardised features it converges; feed it raw features with wildly different scales and the same learning rate blows up.
import numpy as np
rng = np.random.default_rng(0)
X = rng.normal(size=(1000, 3)) * [1.0, 50.0, 0.01] # three very different scales
y = X @ np.array([2.0, 0.1, 300.0]) + rng.normal(size=1000)
def fit(X, y, lr, steps=500):
w = np.zeros(X.shape[1])
for _ in range(steps):
grad = X.T @ (X @ w - y) / len(y) # gradient of mean squared error / 2
w -= lr * grad
return np.mean((X @ w - y) ** 2)
Xs = (X - X.mean(0)) / X.std(0)
print("scaled MSE", round(fit(Xs, y, lr=0.1), 2))
with np.errstate(over="ignore", invalid="ignore"):
print("unscaled MSE", fit(X, y, lr=0.1)) # diverges: inf or nanThe trap: when descent zig-zags or diverges, people reach for the learning rate first. Often the real problem is that features on very different scales stretch the loss surface into a long, narrow valley, so any single step size is too big in one direction and too small in another. Standardise the features and the same learning rate behaves.
The one line answers
If you remember nothing else, remember these. They're the slide I'd want in my pocket walking into the room.
The one line answers

Every one of these is about the same habit: diagnose before you tune. Read the learning curve, keep the validation honest, choose the metric that matches the problem, and fix the data before you blame the optimiser.
Next in the series: CI/CD pipelines, and the GitLab CI file I'd show in an interview.
Save it for your next interview

Next in the interview series: CI/CD pipelines, and the GitLab CI file I would show in an interview.
More: LinkedIn · Instagram. Portfolio and case studies: designsbyduhart.org.
If any of this saved you an afternoon, Buy me a coffee.