Classical Supervised ML
The models you can explain end-to-end on a whiteboard. On tabular data, also the ones that still beat deep learning without breaking a sweat.
Every model on this page is the same three-part recipe from 01, applied to a different function shape: pick a function family (a line, a tree, a margin, a neighborhood vote), pick a loss, and fit the parameters to minimize it.
Linear and logistic regression are the interpretable baseline. Decision trees learn splits in place of coefficients, which buys nonlinearity at the cost of high variance. Ensembling reduces that variance in two different ways.
Bagging averages many independent trees in parallel (random forests). Boosting chains many weak trees sequentially, each correcting the previous ensemble's mistakes (gradient boosting, XGBoost). SVMs and k-NN round out the toolkit.
One sentence for an interview: on tabular data, gradient-boosted trees are still the default that wins competitions and production pipelines, precisely because tabular features don't have the spatial or sequential structure that gives neural networks their edge.
01Intuition
Every model in this section answers the same question differently: what function shape am I allowed to draw through the data?
Start with the simplest possible answer: a straight line. Linear regression asks for the line (or, with more features, the hyperplane) that comes closest to every training point, where "closest" is measured by squared vertical distance. There's a unique best answer, it has a closed form, and every coefficient tells you exactly how much the prediction moves per unit change in a feature, holding everything else fixed.
That interpretability is the whole appeal, and the whole limitation. A straight line can't bend. So it can't represent a relationship where the effect of one feature depends on the value of another, unless you hand-build that interaction as a new column.
Now suppose the target is a class, such as spam or not spam. You could fit a straight line to 0/1 labels and threshold the output at 0.5, but that breaks immediately.
The raw output is unbounded. A very confident, very correct prediction (say, a score of 4.0 for an obvious spam email) gets penalized by squared error exactly as if it were wrong, because squared error has no idea that "greater than 1" isn't a meaningful direction to keep pushing.
Logistic regression fixes this by squashing the linear output through a sigmoid, which maps any real number into (0, 1) and reads naturally as a probability.
Once the output is a probability, cross-entropy becomes the natural loss in place of squared error. The reason is worked out in 01: cross-entropy is the maximum-likelihood loss for a class label, and it doesn't saturate the way squared error does when the model is confidently wrong. This page applies the same argument without re-deriving it.
Every other model in this page is a variation on "what's the function shape, and how do I fit it." A decision tree's shape is a set of axis-aligned rectangular regions, fit by greedily choosing the split that most improves purity. A support vector machine's shape is a linear boundary (or a nonlinear one, via the kernel trick) chosen to maximize the margin to the nearest points of each class.
A k-nearest-neighbors "model" barely has a function shape at all. It defers the decision to prediction time and asks the neighborhood what it thinks. Keep that framing running underneath everything below: same recipe, different function family, different way of fitting it.
The simplest learning algorithm fits a line. Here it is on five made-up students: hours studied against exam score.
Try it Fit a line to five exam scores with least squares
import numpy as np
hours = np.array([1, 2, 3, 4, 5], dtype=float) # hours studied
score = np.array([52, 55, 61, 64, 70], dtype=float) # exam score
X = np.column_stack([hours, np.ones_like(hours)]) # slope and intercept
slope, intercept = np.linalg.lstsq(X, score, rcond=None)[0]
pred = slope * hours + intercept
print(f"score = {slope:.2f} x hours + {intercept:.2f}")
print("predictions:", np.round(pred, 1))
print("mean absolute error:", np.round(np.abs(pred - score).mean(), 2))
print("prediction for 6 hours:", round(slope * 6 + intercept, 1))
score = 4.50 x hours + 46.90. Its predictions land within an average of 0.72 points of the real scores, and it predicts about 73.9 for a student who studies 6 hours. Learning here is just choosing the two numbers, slope and intercept, that make the errors smallest.02Timeline
Linear and logistic regression are fast and interpretable but draw only straight boundaries, so nonlinear relationships and feature interactions had to be engineered by hand.
Decision trees (CART, 1984; ID3, 1986) found nonlinear splits on their own, and random forests (Breiman, 2001) and gradient boosting (Friedman, ~1999–2001) fixed their instability by averaging many trees or fitting them one after another.
XGBoost (Chen & Guestrin, 2016) made boosting fast and regularized, with LightGBM and CatBoost as siblings, and gradient-boosted trees still dominate tabular data in 2026 because that data rarely has the structure CNNs and Transformers exploit.
03The model zoo: how each one decides
Regularization, geometrically
Both linear and logistic regression are usually fit with a penalty added to the loss, because the unpenalized least-squares solution overfits whenever features are correlated or numerous relative to the number of examples. Ridge (L2) adds $\lambda \sum_j \beta_j^2$ to the loss; lasso (L1) adds $\lambda \sum_j |\beta_j|$. Both shrink coefficients toward zero as $\lambda$ grows, but they shrink differently.
Ridge shrinks every coefficient smoothly. Lasso can zero coefficients out entirely, performing feature selection as a side effect of fitting. The geometric reason is worked out in the "why" section below, and it is one of the most commonly asked interview questions on this page.
Polynomial regression and feature engineering
A straight line is a strict function-shape limitation, but it's a limitation you can partially engineer your way around: add $x^2$, $x^3$, or an interaction term $x_1 x_2$ as new columns, and linear regression on the expanded feature set can now fit curves and interactions. It's still linear in the parameters, which is all the closed-form solution requires.
The tradeoff is that you now choose which nonlinear terms to add by hand, and high-degree polynomial terms overfit badly outside the range of the training data: small differences in the fit near the training points can blow up into huge swings just past the boundary. This is the manual-feature-engineering step that tree-based models below make largely unnecessary.
Decision trees: splitting criteria and pruning
A decision tree builds its function shape by repeatedly asking the single best yes/no question about one feature, splitting the training data into two purer subsets, and recursing. "Best" is measured by how much a split reduces impurity. Two criteria dominate:
- Information gain. The drop in entropy, $H(p) = -\sum_i p_i \log p_i$. Literally how much less uncertain you are about the class label after the split.
- Gini impurity. $G = 1 - \sum_i p_i^2$, the probability that two randomly drawn examples from the node would be misclassified if you labeled them according to the node's class distribution.
They're both measuring "how mixed is this node," just with different curvature. In practice they usually produce very similar trees. Gini is marginally cheaper to compute (no logarithm) and is the default in most libraries, but the choice rarely changes the tree's structure or accuracy meaningfully.
Left alone, a tree will keep splitting until every leaf is pure. In the limit that means memorizing individual training points, including their noise. Pruning is the fix, and it comes in two forms.
Stop early (a max depth, a minimum number of samples per leaf, a minimum impurity decrease required to accept a split). Or grow the full tree and then cut back branches whose removal doesn't hurt validation performance (cost-complexity pruning). Either way, the goal is the same: trade a little training accuracy for a tree that generalizes.
Bagging and random forests
A single unpruned decision tree is a low-bias, high-variance model. It can represent almost any function shape, but small changes to the training data produce very different splits, especially near the root.
Bagging (bootstrap aggregating) tackles that variance head-on: train many trees, each on a different bootstrap resample of the training set (sample $n$ rows with replacement), and average their predictions (or vote, for classification). Averaging independent, unbiased estimators reduces variance without touching bias.
Random forests add one more ingredient: at every split, each tree is only allowed to consider a random subset of the features. This matters more than it looks, as the variance formula below shows.
Bootstrapping alone still tends to produce correlated trees. If one feature is unusually predictive, nearly every bootstrap sample will pick it as the very first split, so the trees end up structurally similar near the root and their errors stay correlated.
Correlated errors don't average away. The variance of an average of $M$ trees with average pairwise correlation $\rho$ and individual variance $\sigma^2$ works out to $\rho\sigma^2 + \frac{1-\rho}{M}\sigma^2$.
The second term shrinks toward zero as you add more trees. The first term, $\rho\sigma^2$, is a floor. No amount of additional trees gets you below it.
So random feature subsampling exists to lower $\rho$. By occasionally hiding the dominant feature from a tree, it forces different trees to discover different, diverse splits. It is the only lever that keeps helping as $M$ grows.
Boosting: sequential error correction
Boosting starts from the opposite end of the bias-variance spectrum. Instead of averaging many strong, independent, high-variance trees, it chains many weak, high-bias trees (often just a few levels deep) one after another. Each new tree is trained specifically to predict what the current ensemble is still getting wrong. Concretely, for regression with squared-error loss:
- Start simple. An initial prediction $F_0$, often just the mean of the target.
- Compute the residual for every training example, $r = y - F_0$.
- Fit a new small tree to predict that residual, and not the original target.
- Add a shrunk version of that tree's prediction to the running total, $F_1 = F_0 + \eta \cdot \text{tree}_1$, where $\eta$ (the learning rate) is typically small, 0.01 to 0.3.
- Recompute the residual against the new, better prediction, and repeat.
Each round, the ensemble's remaining error gets a little smaller. And each new tree only ever has to solve the much easier problem of predicting what's left over.
XGBoost, LightGBM, CatBoost — and why gradient-boosted trees still win on tabular data
XGBoost (Chen & Guestrin, 2016) didn't reinvent gradient boosting so much as engineer it properly. Three changes carried the weight:
- A regularized objective that penalizes tree complexity directly, in addition to shrinkage.
- Native missing-value handling, learning a default split direction so imputation is not required.
- Systems-level optimization (parallel split-finding, cache-aware access patterns) to make it fast at scale.
LightGBM and CatBoost are later, actively used siblings. LightGBM is optimized for speed on very large datasets via histogram-based splitting, and CatBoost for datasets with many categorical features. All three are gradient boosting underneath and differ in engineering.
The deeper question interviewers want answered is why tree ensembles still beat deep neural networks on tabular data, years into the deep learning era. Three reasons, and they all point the same direction.
- No structure to exploit. Tabular features usually have no useful spatial or sequential structure. A CNN's convolution assumes nearby pixels are related; a Transformer's attention assumes tokens form a meaningful sequence. Column order in a spreadsheet is arbitrary, so those inductive biases have nothing to grab onto.
- Mixed types come free. Trees handle mixed feature types and missing values natively. A categorical column, a numeric column, and a sparsely-populated column all just become "is this value above/below/equal to some threshold." No embedding table or imputation pipeline required.
- The error-correction shape fits. Boosting's sequential error-correction is a very good structural fit for the kind of data tabular problems usually present: a moderate number of features with complex but relatively low-order interactions, exactly what a shallow tree can carve out a few splits at a time.
Support vector machines
An SVM's core idea is margin maximization. Among all the lines (or hyperplanes) that separate two classes, pick the one that leaves the widest possible street between the boundary and the nearest point of either class. Those nearest points are the support vectors. They're the only points that determine where the boundary sits, which is why the model can be so much sparser than it looks.
When classes aren't linearly separable, the soft-margin formulation allows some points to sit inside the margin or even on the wrong side, penalized by a cost. That gives you a tunable tradeoff between a wide margin and a few tolerated mistakes.
The kernel trick is what lets SVMs draw nonlinear boundaries without ever leaving linear machinery. Stated plainly: a kernel function computes the similarity two points would have if you'd projected them into some higher-dimensional space, without ever forming that projection. The SVM optimization only ever needs dot products between points, never the raw coordinates in that higher-dimensional space.
So you can substitute a kernel function for the dot product and get the effect of a much richer feature space at the computational cost of the original one.
k-NN and the curse of dimensionality
k-nearest-neighbors defers almost all of the work to prediction time. To classify a new point, find the $k$ closest training points and let them vote. There's no training phase to speak of, which makes it a clean lens for bias-variance intuition.
- Small $k$ (like $k=1$) lets the prediction follow every quirk of the single nearest point. Low bias, high variance, a decision boundary that snakes around individual noisy examples.
- Large $k$ averages over many neighbors, smoothing the boundary out, which gives higher bias and lower variance. In the extreme where $k$ equals the whole training set, the prediction collapses to always guessing the majority class.
The curse of dimensionality is what breaks k-NN in high-dimensional feature spaces. It is worth stating concretely. As the number of dimensions grows, the volume of the space grows so much faster than the data filling it that distances between points stop being informative.
For many common distance metrics, the ratio between the distance to the nearest neighbor and the distance to the farthest neighbor converges toward 1. Everything ends up roughly equidistant from everything else. "Nearest" stops meaning "similar" and starts meaning "arbitrary," and k-NN's entire premise quietly stops holding.
Try it Watch nearest and farthest converge as dimensions pile up
import numpy as np
rng = np.random.default_rng(0)
n = 500 # 500 points, one query, unit cube
print(" d nearest farthest ratio")
for d in [2, 5, 20, 100, 1000, 10000]:
P = rng.random((n, d)) # (500, d) the "training set"
q = rng.random(d) # (d,) the point to classify
dist = np.linalg.norm(P - q, axis=1) # (500,) one per point
lo, hi = dist.min(), dist.max()
print(f"{d:5d} {lo:9.3f} {hi:9.3f} {lo / hi:8.3f}")
print("\nshapes on the last row: P", P.shape, " q", q.shape,
" dist", dist.shape)
d=2 the nearest point sits at 0.024 of the farthest distance, so "nearest" means something. By d=10000 the ratio is 0.967 — the closest of 500 points is barely 3% nearer than the most distant one. Watch the nearest distance itself climb from 0.030 to 40.210 while the gap to the farthest hardly widens at all: every point has drifted out to roughly the same radius. Nothing is broken: a unit cube in 10,000 dimensions looks like this, and it is why k-NN stops carrying signal long before you notice it has.Naive Bayes
Naive Bayes applies Bayes' theorem (see 01) to classification with one deliberate simplification. It assumes every feature is conditionally independent of every other feature, given the class. That assumption is almost always technically false. In a document, the word "loan" and the word "interest" are obviously not independent given the class "finance."
Classification only needs the correct class to come out on top. It does not need a correctly calibrated probability.
The independence assumption does distort the actual probability values. But it tends to distort them in a similar, consistent direction across classes. So the ranking between classes survives even when the magnitudes don't.
Combined with how cheap and stable it is to fit on high-dimensional, sparse bag-of-words features, that's enough to make naive Bayes a strong, fast baseline.
The perceptron — the bridge to everything in 04
The perceptron (Rosenblatt, 1958) is the historical hinge this whole page pivots on. It's a single linear classifier: weighted sum of inputs plus a bias, passed through a step function, trained with a simple rule that nudges weights toward misclassified points. Structurally it's almost identical to logistic regression.
It differs in two ways. Its output is a hard 0/1 decision and not a smooth probability, and its update rule is not derived from a loss gradient the way logistic regression's is.
Minsky and Papert showed in 1969 that a single perceptron can't learn XOR. It can only separate linearly separable data. That limitation is exactly why nobody stopped there. Stack perceptron-like units into layers, replace the step function with a smooth activation, and add backpropagation to train the whole stack. That's the multilayer perceptron, and it's where 04 (Neural Network Fundamentals) picks up the story directly from here.
04The equations
Least squares: the closed-form solution
Linear regression minimizes the sum of squared residuals, $\|y - X\beta\|^2$. Setting the gradient of that expression with respect to $\beta$ to zero and solving gives the normal equation:
- X the design matrix, shape
(n_examples, n_features)— one row per training example, one column per feature (plus a constant column for the intercept). - y the target vector, shape
(n_examples,). - β̂ the closed-form optimal coefficients — no iteration required, assuming $X^{\top}X$ is invertible.
Sometimes $X^{\top}X$ isn't invertible, or is too large to invert directly (many correlated features, or more features than examples). Then the same objective is minimized iteratively instead, by gradient descent on the mean squared error: $\nabla_\beta \text{MSE} = \frac{2}{n}X^{\top}(X\beta - y)$, followed by the usual $\beta \leftarrow \beta - \alpha \nabla_\beta \text{MSE}$ update. Same objective, two ways to reach the minimum.
Logistic regression: the sigmoid link
The linear part, $w^{\top}x + b$, is unbounded, exactly like linear regression's output. The sigmoid $\sigma$ is the link function that squashes it into $(0,1)$, so the output reads as a genuine probability. Fitting $w$ and $b$ minimizes cross-entropy between this predicted probability and the true label — see 01 for why that loss, specifically, is the right one and how its gradient behaves.
L1 vs. L2 regularization
- λ regularization strength — $\lambda = 0$ recovers plain least squares; larger $\lambda$ shrinks coefficients harder.
- Σβⱼ² the L2 (ridge) penalty — smooth, differentiable everywhere, shrinks every coefficient continuously toward zero.
- Σ|βⱼ| the L1 (lasso) penalty — has a kink at zero for every coefficient, which is exactly what makes it capable of setting coefficients to exactly zero and not just small. The geometric reason is in the next section.
05Why this shape
Why lasso gives you sparsity and ridge doesn't
Both penalties can be understood as the same constrained optimization: minimize squared error subject to the coefficients staying inside some budget region. The region just has a different shape for each.
- Ridge. The constraint $\sum_j \beta_j^2 \le t$ is a circle (a sphere, in higher dimensions).
- Lasso. The constraint $\sum_j |\beta_j| \le t$ is a diamond, with sharp corners sitting exactly on the coordinate axes. Those corners are the points where one or more coefficients are exactly zero.
Picture the unconstrained least-squares solution as the center of a set of elliptical contours of equal squared error, radiating outward. The regularized solution is wherever those growing ellipses first touch the constraint region's boundary.
A circle has no corners. So that first point of contact is, generically, some point on the smooth curve where every coordinate is nonzero. Coefficients get shrunk, but not zeroed. A diamond has corners sticking out right where an ellipse is likely to touch it first, and those corners are exactly the points where one or more coordinates are zero.
Geometrically, the elliptical contours are much more likely to first touch a pointy corner than a smooth curve is to touch anywhere in particular. So lasso routinely zeroes out coefficients entirely, and ridge, no matter how large $\lambda$ gets, almost never does. This is a common interview question because the geometric argument is short and visual, and you can either draw it or you can't.
Try it Fit the same data with ridge and lasso and compare the coefficients
import numpy as np
rng = np.random.default_rng(0)
n, d, lam = 60, 8, 12.0
X = rng.normal(size=(n, d))
X[:, 3] = X[:, 0] + 0.05 * rng.normal(size=n) # a near-duplicate
beta = np.array([3., 0., 0., 0., -2., 0., 1.5, 0.])
y = X @ beta + 0.5 * rng.normal(size=n)
# Ridge is smooth everywhere, so there is no kink to land on.
ridge = np.linalg.solve(X.T @ X + lam * np.eye(d), X.T @ y)
# Lasso has no closed form. The max(., 0) below IS the diamond's
# corner: once a feature's pull drops under lam/2, it snaps flat.
lasso = np.zeros(d)
for _ in range(400):
for j in range(d):
rho = X[:, j] @ (y - X @ lasso + X[:, j] * lasso[j])
pull = max(abs(rho) - lam / 2, 0.0)
lasso[j] = np.sign(rho) * pull / (X[:, j] @ X[:, j])
print("X", X.shape, " truly nonzero:", np.flatnonzero(beta).tolist())
print("ridge:", np.round(ridge, 3) + 0.)
print("lasso:", np.round(lasso, 3) + 0.)
print("exactly 0.0 -> ridge", int((ridge == 0).sum()),
" lasso", int((lasso == 0).sum()))
0, lasso 5. The ridge value is exactly 0., and the three lasso kept are indices 0, 4, 6, the three that were ever real. Then look at columns 0 and 3, which are near-duplicates by construction: ridge splits the credit between them, 1.292 and 1.255, while lasso gives one of them 2.813 and the other nothing. Ridge's junk coefficients (-0.081, 0.054, 0.015) are tiny, but tiny still means you ship all eight features and compute all eight at inference time. This is the practical difference the diamond's corners buy you.Why boosting and bagging are fundamentally different moves
It's tempting to think of bagging and boosting as two flavors of "combine a bunch of trees." They're attacking opposite ends of the bias-variance tradeoff. Bagging starts from strong, low-bias, high-variance base learners (deep, largely unpruned trees) and averages many independent copies of them.
Averaging is a variance-reduction operation, and it does almost nothing for bias: if every individual tree is systematically wrong in the same direction, averaging them just reproduces that same systematic error.
That's precisely why random feature subsampling matters so much for bagging specifically. It's the mechanism that keeps the trees' errors decorrelated enough for averaging to keep paying off as you add more of them.
Boosting starts from weak, high-bias, low-variance base learners (shallow trees, sometimes just stumps) and chains them sequentially, each one targeting exactly the error the current ensemble hasn't explained yet. This is a bias-reduction operation, and the ensemble's representational capacity grows with every round, letting it fit relationships no single weak learner could capture alone.
The cost shows up when you stop regularizing. Unless you rein it in (small learning rate, limited tree depth, early stopping, row/column subsampling), boosting will keep driving training error toward zero by continuing to fit whatever is left, including noise. That's exactly the overfitting risk bagging mostly doesn't have. Adding more trees to a random forest almost never hurts. Adding more rounds to an unregularized boosted model eventually does.
Try it Give bagging and boosting the same 30 stumps and compare
import numpy as np
rng, n = np.random.default_rng(0), 200
x = np.sort(rng.random(n))
y = np.sin(4 * x) + 0.1 * rng.normal(size=n) # the target curve
def stump(xs, rs, grid): # depth-1 tree: one split, two means
best = (np.inf, None)
for t in np.quantile(xs, np.linspace(.05, .95, 19)):
L = xs <= t
a, b = rs[L].mean(), rs[~L].mean()
sse = ((rs - np.where(L, a, b)) ** 2).sum()
if sse < best[0]:
best = (sse, np.where(grid <= t, a, b))
return best[1]
# Bagging: 30 stumps on bootstrap resamples, averaged in parallel.
bag = np.array([stump(x[i], y[i], x)
for i in rng.integers(0, n, (30, n))])
# Boosting: 30 stumps in series, each fitting what is left over.
F = np.full(n, y.mean())
for _ in range(30):
F = F + 0.3 * stump(x, y - F, x)
print("bag members", bag.shape, " they disagree by",
round(bag.std(0).mean(), 3))
for tag, p in [("one stump", stump(x, y, x)),
("bagged x30", bag.mean(0)), ("boosted x30", F)]:
print(f"{tag:12s} MSE {((y - p) ** 2).mean():.4f}")
0.0903 to 0.0807. The same thirty stumps chained instead of averaged reach 0.0156, roughly six times better, with no extra parameters. The reason is on the first printed line: the 30 bagged members disagree with each other by only 0.065 on average, so there was hardly any variance for averaging to remove. A stump is already a low-variance model and its error is almost all bias — which is exactly the error boosting attacks. Averaging only pays when the base learner is high-variance, which is why bagging is paired with deep unpruned trees and boosting with shallow ones.06Tradeoffs, failure modes, and choosing a metric
| Bagging (random forests) | Boosting (gradient boosting / XGBoost) | |
|---|---|---|
| Primary effect | Reduces variance | Reduces bias |
| Base learners | Deep, low-bias, high-variance trees | Shallow, high-bias, low-variance trees |
| Training | Parallel — every tree is independent | Sequential — each tree depends on the last |
| Overfitting risk from adding more trees | Low — more trees rarely hurt | Real — needs learning rate, depth limits, early stopping |
| Sensitivity to label noise | Robust — averaging dilutes noisy points | Higher — will keep trying to fit noise as "residual" |
| Examples | Random Forest, Extra Trees | AdaBoost, Gradient Boosting, XGBoost, LightGBM, CatBoost |
Failure modes
- Decision trees without pruning. Grown all the way out, a tree can carve a leaf for almost every training point and hit near-zero training error. That is memorization. Max depth, minimum samples per leaf, or cost-complexity pruning are the fix, and any of them trades a bit of training accuracy for real generalization.
- k-NN and the curse of dimensionality. In high-dimensional feature spaces, distances stop discriminating. The nearest and farthest neighbors become nearly equidistant, so the "nearest neighbors" carry almost no signal. Dimensionality reduction or a learned lower-dimensional embedding is usually required before k-NN is useful on anything but a small, carefully chosen feature set.
- Naive Bayes' independence assumption. It's almost always technically wrong, since features are rarely conditionally independent given the class. But classification only needs the correct class ranked first, and a calibrated probability is not required, so the model keeps performing well on tasks like text classification despite the wrong assumption underneath it.
- Data leakage. The single most common way a model looks great offline and fails in production. Two concrete examples:
- Fitting a scaler or imputer on the entire dataset before splitting into train/test. The training statistics have already "seen" information from the test rows, quietly inflating validation performance.
- Including a feature that's only populated after the outcome is already known. A "days since last late payment" column will look like an extremely strong predictor of default in historical data, but it isn't available at the moment you'd need to make the prediction.
Choosing a metric
For classification:
- Precision. Of the things I flagged positive, how many were.
- Recall. Of the things that were positive, how many did I catch.
- ROC-AUC. Ranking quality across all thresholds, and a reasonable default.
- PR-AUC. Precision-recall AUC, which ignores true negatives entirely.
The tradeoff between precision and recall is a business decision as much as a modeling one. ROC-AUC can look deceptively good under severe class imbalance, because it's partly driven by the true-negative rate. When negatives vastly outnumber positives, even a mediocre model racks up a huge number of easy true negatives.
PR-AUC focuses on how well the model does specifically on the positive class, which makes it the honest choice whenever positives are rare: fraud, disease screening, defect detection.
For regression:
- MAE. Penalizes every error linearly, so it's robust to outliers but gives no extra urgency to large mistakes.
- MSE / RMSE. Penalizes errors quadratically, so a few large errors dominate the loss. (RMSE is the square root, back in the original units.) Appropriate when big mistakes are disproportionately costly, risky when a few noisy outliers shouldn't be allowed to dominate training.
- Huber loss. It is the compromise: quadratic for small errors and linear beyond a threshold, giving you MSE's smooth gradient near zero without MSE's outlier sensitivity far from it.
- MAPE. Mean absolute percentage error reports error as a percentage of the true value. Easy to communicate, but it behaves badly, or is undefined, when the true value is near zero.
Structured, tabular data (spreadsheets, transaction logs, customer records) is where trees almost always win, usually with less tuning and less data than a neural net would need. That's where a boosted tree's inductive biases match the data.
Unstructured data with real spatial or sequential structure (images, text, audio, video) is where deep nets win. Convolution and attention are inductive biases built specifically for the kind of structure that data has, and trees have no mechanism to exploit it at all. The honest answer in an interview isn't "deep learning is more advanced." It's "match the inductive bias to the structure present in the data."
07Build this
Boosting is easy to state and easy to misremember as "average a lot of trees". Watching a stack of stumps eat a residual, one stump at a time, is what makes the sequential part stick.
Fit a one-dimensional curve with depth-1 trees, written from the five-step loop in section 03. One input feature, one output, a few hundred noisy points. Small enough to plot the ensemble's prediction over the target after any round you like. That turns "each tree fits what the last total got wrong" from a sentence into a picture.
- Generate the data: a few hundred points from a smooth curve with a couple of bends, plus Gaussian noise. Hold a third of them out.
- Write the stump by hand. Scan every candidate split threshold on the single feature, keep the one that minimizes squared error, and predict the mean of each side. That is the whole weak learner, with no library call.
- Run the loop from section 03. Start at the mean of the target, compute residuals, fit a stump to those residuals, add $\eta$ times its prediction to the running total, repeat. Use $\eta = 0.1$.
- Plot two things at rounds 1, 5, 20, and a few hundred: the ensemble's prediction over the target, and a histogram of the residuals.
- Plot training and held-out squared error against round number, on the same axes, all the way out.
- Now remove the learning rate. Set $\eta = 1$ and rerun every plot above.
Where this runs in production
A typical fraud or credit-risk pipeline built on XGBoost has three moving parts:
- Engineer features from transaction and account history. Aggregations over recent activity, velocity features like "transactions in the last hour," categorical merchant and device fields.
- Train a gradient-boosted tree model on these features.
- Evaluate it knowing the positive class, actual fraud, is a small minority of all transactions.
Because positives are rare, ROC-AUC can look reassuringly high while the model is still missing most of the fraud that matters. It's partly propped up by the huge number of easy true negatives. PR-AUC is the honest metric here, because it only credits the model for how well it finds and ranks the rare positive cases.
Class imbalance itself is usually handled with a combination of three levers: class weighting (XGBoost's scale_pos_weight), threshold tuning against the actual business cost of a false positive (blocking a legitimate purchase) versus a false negative (missing real fraud), and sometimes resampling.
Leakage is easy to introduce by accident in a pipeline like this, and a feature derived from data only available after a chargeback is filed is a textbook example. Every preprocessing and feature-engineering step needs to be fit strictly inside the training fold, and audited for whether it could exist at the moment a real prediction has to be made.
08Interview questions
BeginnerWhy can't you just threshold a linear regression's output to do classification?
Linear regression's output is unbounded and its loss (squared error) has no notion that "more positive than 1" or "more negative than 0" isn't a meaningful direction — a very confidently correct prediction still gets penalized for being far from the label. Logistic regression fixes this by passing the linear output through a sigmoid, bounding it into (0,1) so it reads as a probability, and pairing it with cross-entropy loss, which is the correct maximum-likelihood loss for a class label and doesn't have that saturation problem.
BeginnerWhat happens to bias and variance as you increase k in k-nearest-neighbors?
Small k follows the nearest point(s) closely — low bias, high variance, a jagged decision boundary sensitive to individual noisy points. Large k averages over more neighbors — higher bias, lower variance, a smoother boundary. At the extreme, k equal to the whole training set just always predicts the majority class: maximum bias, minimum variance.
BeginnerEntropy/information gain vs. Gini impurity for splitting a tree — does the choice actually matter?
Both measure how mixed a node's class distribution is — entropy via $-\sum p_i \log p_i$, Gini via $1 - \sum p_i^2$ — just with slightly different curvature. In practice they usually produce very similar trees. Gini is marginally cheaper (no logarithm) and is the default in most libraries, but the choice rarely changes accuracy meaningfully.
IntermediateWhy does lasso produce sparse solutions and ridge doesn't?
Both are equivalent to minimizing squared error subject to a budget constraint on the coefficients — ridge's constraint region is a circle/sphere, lasso's is a diamond with corners sitting exactly on the coordinate axes, where one or more coefficients equal zero. The regularized solution sits wherever the growing elliptical error contours first touch that region's boundary. A smooth circle has no corners, so contact generically happens where every coordinate is nonzero — coefficients shrink but rarely hit exactly zero. A diamond's corners are much more likely to be the first point of contact, which is exactly why lasso routinely zeroes coefficients out and ridge essentially never does.
IntermediateBagging vs. boosting — what's actually different about how they reduce error?
Bagging averages many independent, high-variance, low-bias trees trained in parallel on bootstrap resamples — it's a variance-reduction move, and does little for bias. Boosting chains many weak, high-bias, low-variance trees sequentially, each one fit to the previous ensemble's residual error — it's a bias-reduction move. That difference is also why they carry different overfitting risk: adding more trees to a random forest rarely hurts, but adding more rounds to an unregularized boosted model eventually starts fitting noise.
IntermediateWhy does random feature subsampling in a random forest help beyond what bootstrapping alone gives you?
Bootstrapping alone still tends to produce correlated trees, because a strongly predictive feature gets chosen as the top split in nearly every bootstrap sample. The variance of an average of trees works out to $\rho\sigma^2 + \frac{1-\rho}{M}\sigma^2$, where $\rho$ is the average pairwise correlation between trees — the second term vanishes as you add trees, but the $\rho\sigma^2$ floor doesn't. Random feature subsampling lowers $\rho$ directly, by occasionally hiding the dominant feature and forcing trees to find genuinely different splits — that's the only lever that keeps paying off as you add more trees.
IntermediateWhat is data leakage, and how does it typically sneak into a pipeline?
Leakage is when information that wouldn't be available at real prediction time ends up influencing training, making offline metrics look better than production performance will actually be. Two common patterns: fitting a scaler or imputer on the full dataset before splitting into train/test, so training statistics have already "seen" test data; and including a feature that's only populated after the outcome is known — like a payment-history field that only exists once a default has already occurred. The fix is fitting every preprocessing step only inside the training fold of the cross-validation loop, and auditing every feature for whether it could plausibly exist at prediction time.
DeepExplain the kernel trick in an SVM in one or two sentences.
A kernel function computes the similarity two points would have if you projected them into a higher-dimensional feature space, without ever actually forming that projection — since the SVM optimization only ever needs dot products between points, substituting a kernel for the dot product gets you the effect of a much richer, nonlinear feature space at the computational cost of the original one.
DeepWhy do gradient-boosted trees still beat deep neural networks on most tabular data problems?
Tabular features usually have no spatial or sequential structure for a neural net's inductive biases (convolution, attention) to exploit — column order is arbitrary. Trees handle mixed feature types and missing values natively, without an embedding table or imputation pipeline. And boosting's sequential, greedy error-correction is a strong structural fit for the moderate-feature-count, relatively low-order-interaction structure tabular problems tend to have. None of that changes for unstructured data — images, text, audio — which is exactly where deep nets do win, because that data actually has the spatial/sequential structure those inductive biases were built for.
DeepYour fraud model has a great ROC-AUC but the fraud team says it's missing most real fraud. What's going on?
ROC-AUC is measured against the true-negative rate too, and when the positive class (fraud) is a small fraction of all transactions, a model can rack up a huge number of easy true negatives and post a high ROC-AUC while still doing poorly on the positive class specifically. PR-AUC ignores true negatives and directly measures precision and recall on the positive class, which is the honest metric under severe imbalance — it would likely be revealing the exact gap the fraud team is describing.
09Go deeper
●Now write it yourself
Reading the derivation and being able to produce it are different skills. These are Deep-ML problems that exercise what this page covers — each one is checked against real test cases, not multiple choice.
Matched to this page from Deep-ML's catalogue of 1,380 problems. More at deep-ml.com, and Where to practise covers the other platforms and what each one trains.