Classical ML: Trees, Ensembles, k-NN, SVM & Clustering

The workhorses of tabular data. Learn decision trees (Gini impurity, entropy, information gain, depth and pruning), ensembles (random forests, bagging, gradient boosting — XGBoost/LightGBM), k-nearest neighbours and distance metrics, support vector machines and kernels, Naive Bayes, unsupervised learning (k-means, hierarchical clustering, DBSCAN, PCA), the bias–variance trade-off, feature engineering and pipelines — with NumPy code for k-means and a Gini decision stump.

Before deep learning, and still today for most business (tabular) data, the best models are trees and their ensembles. Gradient-boosted trees win many Kaggle competitions and run fraud detection, credit risk and pricing systems.

Unsupervised methods like k-means and PCA find structure in unlabelled data — customer segments, anomalies, compressed representations.

An analogy

A decision tree is like the game 'twenty questions': each question splits the possibilities until the answer is clear. A random forest asks hundreds of slightly different players and takes a vote — collectively wiser than any single player.

Decision trees

Trees split data on feature thresholds to make groups purer (Gini or entropy for classification, variance for regression). They handle mixed feature types, need no scaling, capture interactions and are easy to explain — but deep trees overfit. Control with max depth, minimum samples per leaf and pruning.

Ensembles

Combining many models:

Distance, margin and probability models

k-NN predicts from the k closest training points (needs scaling; slow at prediction time; suffers in high dimensions). SVMs find the maximum-margin boundary; kernels (RBF, polynomial) create non-linear boundaries. Naive Bayes applies Bayes' theorem assuming feature independence — fast, strong baseline for text.

Unsupervised learning and bias–variance

k-means (choose k with elbow/silhouette; sensitive to initialisation — use k-means++), hierarchical clustering (dendrograms), DBSCAN (density-based, finds arbitrary shapes and outliers), Gaussian mixtures, PCA (variance-maximising projection via eigenvectors) and t-SNE/UMAP for visualisation. The bias–variance trade-off: simple models underfit (high bias), complex ones overfit (high variance); choose complexity with cross-validation. Pipelines bundle preprocessing and models to prevent leakage.

Formulas

Worked example: Gini impurity

Given: Node with 6 positives, 4 negatives

Find: G.

Answer: 0.48

Worked example: Entropy

Given: 50/50 node

Find: H.

Answer: 1 bit (maximum)

Worked example: k-NN vote

Given: k = 5 neighbours: 3 A, 2 B

Find: Prediction.

Answer: A (probability 0.6)

k-means clustering and a Gini split

import numpy as np
# k-means clustering and a Gini-impurity decision stump
rng = np.random.default_rng(1)
pts = np.vstack([rng.normal(c, 0.6, (50, 2)) for c in ((0, 0), (5, 5), (0, 6))])
centroids = pts[[0, 50, 100]].copy()
for it in range(10):
    labels = np.argmin(((pts[:, None, :] - centroids[None]) ** 2).sum(-1), axis=1)
    centroids = np.array([pts[labels == k].mean(0) for k in range(3)])
print("k-means centroids:", np.round(centroids, 2).tolist())
print("cluster sizes:", np.bincount(labels).tolist())
# Decision stump: best split on one feature by Gini impurity
x = np.array([2, 3, 4, 5, 6, 7, 8, 9]); y = np.array([0, 0, 0, 1, 0, 1, 1, 1])
def gini(v):
    if len(v) == 0: return 0.0
    p = np.bincount(v, minlength=2) / len(v)
    return 1 - np.sum(p ** 2)
best = min(((t, (gini(y[x <= t]) * (x <= t).sum() + gini(y[x > t]) * (x > t).sum()) / len(y)) for t in x[:-1]), key=lambda z: z[1])
print(f"root Gini {gini(y):.3f}; best split x <= {best[0]} with weighted Gini {best[1]:.3f}")
# k-means centroids: [[-0.01, -0.08], [4.92, 4.99], [-0.18, 5.99]]
# cluster sizes: [50, 50, 50]
# root Gini 0.500; best split x <= 4 with weighted Gini 0.200

k-means alternates assigning points to the nearest centroid and moving centroids to the mean — it recovers the three true centres (0,0), (5,5), (0,6). The decision stump tries every threshold and picks the one that most reduces impurity: splitting at x ≤ 4 leaves one pure side and one mostly-pure side (weighted Gini 0.2). A tree repeats this recursively.

Common mistakes

Applications

Key takeaways

Going further