Linear and Quadratic Discriminant Analysis › Visualizing LDA and QDA

Michael BrenndoerferOctober 27, 20265 min read
Machine Learning from ScratchAll chapters
Part 7 of 15Supervised Learning Beyond Linear Models
Chapter 55Linear and Quadratic Discriminant Analysis › Visualizing LDA and QDA
Reading section 5 of 10 · Visualizing LDA and QDA · 5 minSection 5 of 10 · 5 min

Section 5 of 10 · 5 min read

Linear and Quadratic Discriminant Analysis › Visualizing LDA and QDASection view · Sections in this chapter5 / 10 ↓
Reading tools Definitions, links & notes

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Notes & comments

We now illustrate the geometric differences on a synthetic two-class problem with deliberately different covariances per class. Both classes are drawn from two-dimensional Gaussians. Class 1 has larger marginal variances and a stronger positive feature correlation than class 0. This violates LDA's shared-covariance assumption. QDA can represent the population Gaussian Bayes boundary here, although fitting its extra covariance parameters does not guarantee higher training or test accuracy.

The following cells prepare the data, fit both reference classifiers, and compute the decision grid and class probabilities before plotting. For binary classification, decision_function returns the one-dimensional log-posterior ratio log⁡p(y=1∣x)−log⁡p(y=0∣x)\log p(y=1\mid\mathbf{x})-\log p(y=0\mid\mathbf{x}). The common normalization cancels exactly in this difference. We place a zero column beside that ratio to make two class scores with the same difference, then apply softmax across the two columns to recover the posterior probabilities.

PythonIn [5]
CollapseExpand
import matplotlib.pyplot as plt
import numpy as np
from sklearn.discriminant_analysis import (
    LinearDiscriminantAnalysis,
    QuadraticDiscriminantAnalysis,
)

rng = np.random.default_rng(7)
n_per_class = 80
mean_0 = np.array([0.0, 0.0])
mean_1 = np.array([3.5, 2.5])
cov_0 = np.array([[1.0, 0.3], [0.3, 1.2]])
cov_1 = np.array([[2.8, 1.3], [1.3, 1.6]])

X0 = rng.multivariate_normal(mean_0, cov_0, size=n_per_class)
X1 = rng.multivariate_normal(mean_1, cov_1, size=n_per_class)
X_two = np.vstack([X0, X1])
y_two = np.concatenate(
    [np.zeros(n_per_class, dtype=int), np.ones(n_per_class, dtype=int)]
)

lda_sklearn = LinearDiscriminantAnalysis().fit(X_two, y_two)
qda_sklearn = QuadraticDiscriminantAnalysis().fit(X_two, y_two)


# Class posteriors from the discriminant scores, computed dynamically.
def softmax(scores):
    shifted = scores - scores.max(axis=1, keepdims=True)
    exp_scores = np.exp(shifted)
    return exp_scores / exp_scores.sum(axis=1, keepdims=True)


log_odds_lda = lda_sklearn.decision_function(X_two)
log_odds_qda = qda_sklearn.decision_function(X_two)
posterior_lda = softmax(
    np.column_stack([np.zeros_like(log_odds_lda), log_odds_lda])
)
posterior_qda = softmax(
    np.column_stack([np.zeros_like(log_odds_qda), log_odds_qda])
)
mean_posterior_lda = posterior_lda.mean(axis=0)
mean_posterior_qda = posterior_qda.mean(axis=0)
PythonIn [6]
CollapseExpand
grid_lower = X_two.min(axis=0) - 0.75
grid_upper = X_two.max(axis=0) + 0.75
xx, yy = np.meshgrid(
    np.linspace(grid_lower[0], grid_upper[0], 300),
    np.linspace(grid_lower[1], grid_upper[1], 300),
)
grid = np.c_[xx.ravel(), yy.ravel()]
Z_lda = lda_sklearn.predict(grid).reshape(xx.shape)
Z_qda = qda_sklearn.predict(grid).reshape(xx.shape)
D_lda = lda_sklearn.decision_function(grid).reshape(xx.shape)
D_qda = qda_sklearn.decision_function(grid).reshape(xx.shape)

acc_lda_sklearn = lda_sklearn.score(X_two, y_two)
acc_qda_sklearn = qda_sklearn.score(X_two, y_two)
ConsoleOut [7]
CollapseExpand
LDA training accuracy (scikit-learn): 0.944
QDA training accuracy (scikit-learn): 0.963
LDA mean class posterior (class 0, class 1): 0.543, 0.457
QDA mean class posterior (class 0, class 1): 0.504, 0.496

For this seeded training sample, the displayed accuracies show that QDA's higher value here is an observed result, not a guarantee; Gaussian likelihood fitting does not directly maximize classification accuracy, and a single training sample cannot establish a general ranking. Each mean posterior averages one class's fitted probability across all 160 training observations, and the following figures show both fitted decision regions on that same sample. An average near one-half is consistent with this balanced sample, but it does not measure confidence, class separation, or calibration: both confident predictions split between the classes and uniformly uncertain predictions can average to one-half. A held-out comparison below checks how both fitted rules classify new observations.

VisualizationOut [8]
CollapseExpand
Scatter plot of 80 observations per class with a straight LDA boundary and two lightly shaded class regions.
LDA decision regions on a synthetic two-class problem with unequal class covariances. LDA pools within-class scatter across both classes, producing a single straight boundary although the specified class-1 distribution has larger marginal variances and stronger positive feature correlation. The shared covariance cannot represent both specified population covariance matrices.
VisualizationOut [9]
CollapseExpand
Scatter plot of the same 80 observations per class with a curved QDA decision boundary between two lightly shaded class regions.
QDA decision regions on the same synthetic two-class problem. With a separate covariance for each class, the displayed boundary is curved, capturing unequal spreads that LDA's shared covariance cannot represent. The curve lets class 1 reach out along its long axis rather than sharing a single shape with class 0.

The first figure shows LDA's straight boundary, whose normal uses the pooled estimate Σ^\hat{\boldsymbol{\Sigma}} and the class means, Σ^−1(μ^0−μ^1)\hat{\boldsymbol{\Sigma}}^{-1}(\hat{\boldsymbol{\mu}}_0-\hat{\boldsymbol{\mu}}_1). Its shared covariance is estimated from the within-class scatter of both classes. It is a compromise, rather than class 0's covariance imposed on class 1. LDA can represent correlated and tilted Gaussian clouds, but it forces both clouds to have the same estimated shape and orientation. The plot compares this shared-shape restriction with QDA's class-specific fitted shapes.

The second figure shows QDA. The boundary is now curved. QDA allows class 1 to "reach out" along its long axis, so points that fall inside the elongated direction of the class-1 cloud are more likely to be attributed to class 1. QDA's distance penalty is smaller along a class's broad covariance directions than along its narrow directions. The log-determinant penalty depends on the full covariance volume and stays constant as the observation moves. The price of this flexibility is that QDA estimates K=2K = 2 full 2×22 \times 2 covariances. In higher dimensions, the per-class covariance estimates are more sensitive to sample size. This tradeoff is the subject of the next section.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy