| SOCR ≫ | DSPA ≫ | DSPA3 Topics ≫ |
library(ggplot2)
library(dplyr)
library(tidyr)
library(patchwork)
library(plotly) # interactive figures and ALL 3-D graphics
library(class) # knn
library(caret) # confusionMatrix, train, resampling
library(pROC) # ROC / AUCHow this chapter uses graphics
Every two-dimensional figure is drawn with
ggplot2and rendered statically. Immediately after each one, the equivalentplot_ly()code appears in a chunk markedeval=FALSE, echo=TRUE, printed in these notes, ready to paste into a live session.Every three-dimensional figure is drawn with
plot_ly()and evaluated. That matters unusually much here: a classifier is a posterior probability surface over feature space, and the difference between \(k=1\) and \(k=100\), or between a single tree and a forest, is a difference in the roughness of that surface. A fixed projection hides exactly the property being taught.
The DSPA3 Labs include a number of interactive webapps, which are build as portable, self-contained, and browser accessible applications. These apps demonstrate various concepts discussed in this chapter, including
(Ch 5 · 5.6): Accuracy Comparator Trap: Metrics Under Class Imbalance
[(Ch 5 · 5.3): Exploring k-NN Boundaries, Bias–Variance, and the Bayes Floor}(https://dspa3-apps.predictive.space/labs/knn-boundaries)
After completing this chapter you will be able to:
Estimated time: 10–13 hours including exercises. Prerequisites: Chapter 2 (contingency tables, class imbalance), Chapter 3 (trees, ensembles, complexity), and Chapter 4, in particular distance concentration (§4.3.1), which is the reason kNN degrades in high dimensions.
Machine learning divides along whether the algorithm is shown the answers.
Supervised learning provides input variables \(X\) and an outcome \(Y\), and seeks a mapping \(\hat f: \mathcal{X}\to\mathcal{Y}\) that generalizes to unseen data. It is “supervised” because training labels correct the learner. When \(Y\) is quantitative the task is regression (Chapter 3); when \(Y\) is categorical it is classification, the subject of this chapter.
Unsupervised learning provides only \(X\) and asks the algorithm to find structure, groupings, associations, low-dimensional coordinates, with no correct answer to compare against. Clustering and association rule mining are the canonical problems (Chapter 8, Chapter 7).
The distinction is about supervision, not about the algorithms themselves. Gaussian mixtures cluster when labels are absent and classify when they are present; the same tree machinery does regression and classification.
| Outcome type | Supervised methods | Unsupervised counterparts |
|---|---|---|
| Binary | kNN, naive Bayes, logistic regression, LDA/QDA, decision trees, SVM, boosting | k-means, Gaussian mixtures, spectral clustering |
| Multi-class | kNN, naive Bayes, multinomial logistic, trees, random forest, neural networks | Hierarchical clustering, DBSCAN, HDBSCAN |
| Ordinal | Ordinal logistic, ordinal forests, cumulative-link models | Ordinal MDS |
| Quantitative | Linear regression, regression trees, random forest, BART, SVR | PCA, factor analysis, autoencoders |
| None (structure) | – | Apriori / association rules, t-SNE, UMAP |
Before any algorithm, there is a target. Without it, “this classifier is 72% accurate” has no reference point.
Suppose \((X, Y)\) are drawn from a joint distribution with \(Y\in\{1,\dots,K\}\). Under 0–1 loss, every error costs the same, the expected loss of a classifier \(g\) is its misclassification rate
\[R(g)=P\big(g(X)\ne Y\big)=E_X\Big[1-P\big(Y=g(X)\mid X\big)\Big].\]
Minimizing pointwise gives the Bayes classifier
\[\boxed{\;g^{*}(x)=\arg\max_{k\in\{1,\dots,K\}} P(Y=k\mid X=x)\;}\]
and its risk is the Bayes error
\[R^{*}=E_X\Big[1-\max_k P(Y=k\mid X=x)\Big].\]
No classifier can do better than \(R^{*}\). The Bayes error is irreducible: it is the overlap between the classes in the true joint distribution, not a deficiency of any method. If two patients have identical measured features and different outcomes, no algorithm can separate them.
Every method in this chapter is a plug-in rule: estimate \(\hat P(Y=k\mid X=x)\) somehow, then take the argmax. They differ only in how they estimate the posterior.
| Method | How it estimates \(P(Y=k\mid x)\) |
|---|---|
| kNN | Local vote fraction among the \(k\) nearest training points |
| Naive Bayes | \(\propto \hat P(k)\prod_j \hat P(x_j\mid k)\), assuming conditional independence |
| LDA / QDA | Gaussian class-conditionals with shared / separate covariance |
| Logistic regression | Direct parametric model of the log-odds |
| Decision tree | Class proportions in the leaf containing \(x\) |
| Random forest | Average of many trees’ leaf proportions |
The Bayes error is normally unknowable, because the joint distribution is unknown. On simulated data we know it exactly, which makes simulation the only setting where “how close to optimal is this classifier?” has an answer.
# Two Gaussian classes with a KNOWN generating process
mu1 <- c(-1, -0.6); mu2 <- c(1.1, 0.7)
S1 <- matrix(c(1.0, 0.35, 0.35, 0.9), 2)
S2 <- matrix(c(1.2, -0.30, -0.30, 0.7), 2)
pi1 <- 0.45
# Posterior P(Y = 2 | x) from the true densities
post2 <- function(x1, x2) {
z <- cbind(x1, x2)
d1 <- mvtnorm::dmvnorm(z, mu1, S1)
d2 <- mvtnorm::dmvnorm(z, mu2, S2)
(1 - pi1) * d2 / (pi1 * d1 + (1 - pi1) * d2)
}
set.seed(21)
n_sim <- 1200
lab <- rbinom(n_sim, 1, 1 - pi1) # 0 -> class 1, 1 -> class 2
Xsim <- rbind(MASS::mvrnorm(sum(lab == 0), mu1, S1),
MASS::mvrnorm(sum(lab == 1), mu2, S2))
ysim <- factor(c(rep("A", sum(lab == 0)), rep("B", sum(lab == 1))))
sim <- data.frame(x1 = Xsim[, 1], x2 = Xsim[, 2], y = ysim)
# Monte-Carlo estimate of the Bayes error
set.seed(22)
mc <- 2e5
lab_mc <- rbinom(mc, 1, 1 - pi1)
Xmc <- rbind(MASS::mvrnorm(sum(lab_mc == 0), mu1, S1),
MASS::mvrnorm(sum(lab_mc == 1), mu2, S2))
p_mc <- post2(Xmc[, 1], Xmc[, 2])
bayes_error <- mean(1 - pmax(p_mc, 1 - p_mc))
c(bayes_error = round(bayes_error, 4))#> bayes_error
#> 0.1004
g1 <- seq(-4.5, 4.5, length.out = 220)
g2 <- seq(-4, 4, length.out = 220)
grid_df <- expand.grid(x1 = g1, x2 = g2)
grid_df$p <- post2(grid_df$x1, grid_df$x2)
ggplot(grid_df, aes(x1, x2)) +
geom_raster(aes(fill = p)) +
geom_contour(aes(z = p), breaks = 0.5, colour = "black", linewidth = 1) +
geom_point(data = sim, aes(x1, x2, shape = y), size = 1.1, alpha = 0.55,
colour = "grey15") +
scale_fill_gradient2(low = "#3B7DD8", mid = "white", high = "#D8433B",
midpoint = 0.5, name = "P(Y = B | x)") +
scale_shape_manual(values = c(A = 1, B = 3), name = "Class") +
coord_fixed() +
labs(title = "The Bayes-optimal decision boundary",
subtitle = sprintf("Black contour is P = 0.5. Bayes error = %.3f -- no classifier can beat this",
bayes_error),
x = expression(x[1]), y = expression(x[2])) +
theme_dspa()Because the posterior is a surface over feature space, it is worth rotating:
Zp <- matrix(grid_df$p, nrow = length(g1))
plot_ly(x = g2, y = g1, z = Zp, type = "surface", colorscale = "RdBu",
reversescale = TRUE, opacity = 0.92,
colorbar = list(title = "P(Y=B|x)")) |>
add_trace(x = sim$x2, y = sim$x1,
z = post2(sim$x1, sim$x2),
type = "scatter3d", mode = "markers", name = "Observations",
marker = list(size = 1.8, opacity = 0.45, color = "black")) |>
layout(title = "True posterior probability surface",
scene = list(xaxis = list(title = "x2"), yaxis = list(title = "x1"),
zaxis = list(title = "P(Y = B | x)", range = c(0, 1))))