Linear and Quadratic Discriminant Analysis

Michael BrenndoerferOctober 27, 202659 min read

Build LDA and QDA classifiers from Gaussian class models and Bayes' rule, with covariance estimation, shrinkage, and NumPy implementations.

Machine Learning from ScratchAll chapters
Part 7 of 15Supervised Learning Beyond Linear Models
Chapter 55Linear and Quadratic Discriminant Analysis
Reading the full chapter · 10 sections · 59 min10 sections · 59 min

All 10 sections · 59 min read

Linear and Quadratic Discriminant AnalysisFull chapter · Sections in this chapter1 / 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

Linear and Quadratic Discriminant Analysis

In the figures below, modeling the same observations with a shared covariance gives a straight separator, while allowing separate covariances gives a curved one. Suppose we need to classify continuous observations into two or more groups, and we have a modest number of labeled examples. K-nearest neighbors classified observations by local proximity; naive Bayes modeled class-conditional feature distributions under conditional independence; logistic regression gave us a probabilistic model of p(y∣x)p(y \mid x) via a link function. Discriminant analysis takes a third route. Instead of modeling the label given the features, we model the features given the label: each class is assumed to be a multivariate Gaussian cloud, and we classify by asking which cloud most plausibly produced the observation.

When the class distributions are Gaussian and their means, covariances, and priors are known, this approach gives the Bayes-optimal rule under zero-one misclassification loss. In practice, LDA and QDA estimate those parameters, so a fitted classifier does not inherit an exact finite-sample optimality guarantee. Linear discriminant analysis (LDA) and quadratic discriminant analysis (QDA) differ in one covariance assumption: LDA uses a shared covariance and has linear score differences, while QDA allows class-specific covariances and quadratic score differences. Those quadratic differences can produce curved boundaries, including degenerate cases discussed below.

A name disclaimer is worth making early. In modern machine learning, "LDA" is also used as the acronym for latent Dirichlet allocation, a text-topic model in unsupervised learning. That model is unrelated to the discriminant analysis we develop here. In this chapter, LDA always means linear discriminant analysis. Fisher (1936) introduced a supervised projection criterion. For two Gaussian classes with distinct means and a shared positive-definite covariance, a maximizing Fisher direction is normal to the Bayes decision hyperplane.

Naive Bayes models class-conditional distributions under conditional independence. Here we allow features to be correlated within each class through a full covariance matrix. Whether those matrices are shared or class specific determines whether the score differences are linear or quadratic.

We will work throughout in the two-class setting for the core derivations and extend to KK classes when it makes the mathematics clearer. Notation is standard: x∈Rd\mathbf{x} \in \mathbb{R}^d is the feature vector, y∈{1,2,…,K}y \in \{1, 2, \ldots, K\} the class label, πk\pi_k the prior probability of class kk, μk\boldsymbol{\mu}_k the class-conditional mean, and Σk\boldsymbol{\Sigma}_k the class-conditional covariance matrix. We assume πk>0\pi_k > 0 and symmetric positive-definite covariance matrices throughout the density-based derivations, so the logarithms, determinants, and inverses are defined. In the two-class derivations and examples, we label the classes 0 and 1.

Advantages

With the true class means, priors, and shared positive-definite covariance, LDA is the Bayes-optimal classifier for Gaussian classes under zero-one loss. Here, "optimal" means minimizing the expected probability of misclassification under that generative model: at each feature vector, predicting the class with the largest true posterior minimizes conditional error. The Gaussian discriminant derivation explains this population rule. Estimating its parameters introduces uncertainty, so fitted LDA need not attain the Bayes error in a finite sample.

LDA shares covariance information across classes. It estimates one d×dd \times d covariance matrix, KK mean vectors, and K−1K - 1 priors. For d=100d = 100 features and two classes (K=2K = 2), the covariance contributes 100(101)2=5,050\frac{100(101)}{2} = 5{,}050 distinct entries, the means contribute 2⋅100=2002 \cdot 100 = 200, and the priors contribute K−1=1K - 1 = 1, for exactly 5,2515{,}251 free generative parameters. Ordinary binary logistic regression has d+1=101d + 1 = 101 coefficients including its intercept; fitting two separate one-vs-rest models would store 2(d+1)=2022(d + 1) = 202 coefficients, with redundant representations for two complementary classes. The larger generative parameter count does not by itself determine classification efficiency. Efron (1975) studied the asymptotic relative classification efficiency of logistic regression and normal discriminant analysis for two multivariate normal classes with different means and a common covariance. That comparison concerns asymptotic classification performance, rather than a guarantee of more accurate finite-sample coefficient estimates.

QDA gives the corresponding population Bayes rule for Gaussian classes with their own positive-definite covariances, provided the true parameters and priors are used. Fitting it requires KK full covariance estimates, each with d(d+1)2\frac{d(d+1)}{2} free parameters, instead of one. When class covariances differ, its quadratic score differences can represent curved decision boundaries that a linear classifier cannot express. For independent Gaussian samples with a truly common covariance, the pooled covariance has lower entrywise sampling variance than an unbiased class estimate when that class has at least two observations and the other classes add residual degrees of freedom.

For dense full-covariance matrix kernels, computing within-class scatter from NN observations once class subsets are available costs O(Nd2)O(Nd^2). QDA then factors KK covariance matrices at a cost of O(Kd3)O(Kd^3) and scores one new observation in O(Kd2)O(Kd^2). LDA factors a single shared covariance and can precompute its class weights: the additional work is O(d3+Kd2)O(d^3 + Kd^2), after which a prediction costs O(Kd)O(Kd). These are matrix-kernel bounds; solver choices can change them. The primitive fits below additionally scan all labels once per class, adding O(NK) class-mask comparisons, plus separate input validation and np.unique work. Large feature counts also impose quadratic covariance storage, so an adequate sample size alone does not make either dense method practical.

Disadvantages

Skew, heavy tails, or multiple modes within a class can make a Gaussian density a poor approximation and can harm probability calibration or classification. LDA and QDA do not adapt their density family to represent arbitrary class shapes. A misspecified density can still give the correct hard-label boundary, so a failure of Gaussianity is a reason to examine held-out performance rather than proof that every prediction is wrong. These methods remain useful when their assumptions are plausible or as baselines for comparison.

Covariance estimation is a major difficulty when the feature dimension is large relative to the available observations per class. In high-dimensional settings, dd can rival the number of observations per class. A centered class sample has rank at most Nk−1N_k-1. For Nk>1N_k>1, its empirical covariance is singular when Nk≤dN_k\le d; with only one observation, the unbiased covariance is undefined. The ordinary unregularized QDA density formula is then undefined because the covariance inverse does not exist. Even when Nk>dN_k > d, dependent features or estimation noise can leave the covariance singular or poorly conditioned. LDA pools within-class scatter across classes, but its pooled covariance can have rank no greater than N−KN - K and is subject to the same rank and conditioning checks.

For two classes, a nondegenerate LDA boundary is one hyperplane regardless of sample size. It cannot represent a bounded winning region surrounded by the other class or a winning region split across two separated tails. In the multiclass case, each pairwise tie equation is linear and each class winning region is an intersection of half-spaces. QDA can represent additional geometries, but it estimates K d(d+1)2K\,\frac{d(d+1)}{2} covariance parameters rather than LDA's d(d+1)2\frac{d(d+1)}{2}. Estimating these separate matrices can add uncertainty in small samples; both methods remain sensitive to outliers, and there is no universal ordering of their fitted errors.

Unregularized LDA and QDA are equivariant under invertible affine transformations when their means and covariances are transformed consistently and calculations are exact. This algebraic property does not require the observed data to be exactly Gaussian. Feature standardization can improve numerical conditioning when units differ substantially, but it does not remove collinearity or guarantee an accurate covariance estimate. Regularization toward a fixed identity target can also change the rule under rescaling, so preprocessing and shrinkage must be considered together.

Formula

We first combine Gaussian class densities with Bayes' rule, then examine which score terms cancel under a shared covariance. This separates the population decision rule from the covariance estimates and numerical calculations used to fit it.

Gaussian class models and Bayes decision rules

A discriminant classifier begins with a generative thought experiment. Imagine that nature first chooses a class according to its prior probability, and then draws a feature vector from a probability density that belongs to that class. To classify a new observation, we reverse the order of that story: instead of asking "what does class kk tend to look like?", we ask "how plausible is this particular x\mathbf{x} under each class, after accounting for how common each class is?" That reversal is Bayes' rule, and the entire geometry of LDA and QDA follows from the density we choose.

The first modeling decision is therefore to describe each class by a full density rather than by a single prototype. An ordinary Euclidean nearest-centroid rule scores a point by its Euclidean distance from a class mean, so equal-length displacements have equal distance scores regardless of direction. A class density can instead describe one direction as tightly concentrated and another as broad or tilted. The multivariate Gaussian is the canonical choice for this job. It is specified by a center and a covariance, its level sets are ellipsoids, and its log-density is quadratic, which is exactly what makes the later decision rules tractable. We assume

p(x∣y=k)=N(x∣μk,Σk),p(\mathbf{x} \mid y = k) = \mathcal{N}(\mathbf{x} \mid \boldsymbol{\mu}_k, \boldsymbol{\Sigma}_k),

where N\mathcal{N} denotes the multivariate normal density

N(x∣μ,Σ)=(2π)−d/2∣Σ∣−1/2exp⁡ ⁣(−12(x−μ)⊤Σ−1(x−μ)).\mathcal{N}(\mathbf{x} \mid \boldsymbol{\mu}, \boldsymbol{\Sigma}) = (2\pi)^{-d/2} |\boldsymbol{\Sigma}|^{-1/2} \exp\!\left(-\tfrac{1}{2} (\mathbf{x} - \boldsymbol{\mu})^\top \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu})\right).

where:

  • μ\boldsymbol{\mu}: the mean vector of the Gaussian, which locates the center of the ellipsoidal cloud
  • Σ\boldsymbol{\Sigma}: the covariance matrix, which sets the shape and orientation of the cloud
  • ∣Σ∣|\boldsymbol{\Sigma}|: the covariance determinant; its square root is proportional to the volume of an ellipsoid with fixed Mahalanobis radius
  • (x−μ)⊤Σ−1(x−μ)(\mathbf{x} - \boldsymbol{\mu})^\top \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu}): the squared Mahalanobis distance from x\mathbf{x} to μ\boldsymbol{\mu}
  • dd: the number of feature dimensions

The mean μk\boldsymbol{\mu}_k locates the center of a class cloud, and the covariance Σk\boldsymbol{\Sigma}_k determines its shape and orientation. For ellipsoids with the same Mahalanobis radius, volume is proportional to ∣Σ∣\sqrt{|\boldsymbol{\Sigma}|}. A larger determinant therefore means a larger such ellipsoid and a lower Gaussian peak density. The quadratic form (x−μ)⊤Σ−1(x−μ)(\mathbf{x} - \boldsymbol{\mu})^\top \boldsymbol{\Sigma}^{-1}(\mathbf{x} - \boldsymbol{\mu}) is the squared Mahalanobis distance. It measures distance relative to the class covariance, so a large Euclidean displacement along a broad direction can still have a small standardized distance. The density decreases continuously with this distance. There is no single hard neighborhood boundary built into the Gaussian model.

A density alone is not yet a classifier, because classes may occur with different frequencies. Bayes' rule combines the class-conditional density with the prior:

p(y=k∣x)=p(x∣y=k) πkp(x),p(y = k \mid \mathbf{x}) = \frac{p(\mathbf{x} \mid y = k)\, \pi_k}{p(\mathbf{x})},

where:

  • p(y=k∣x)p(y = k \mid \mathbf{x}): the posterior probability that the observation belongs to class kk
  • p(x∣y=k)p(\mathbf{x} \mid y = k): the class-conditional density of x\mathbf{x} given class kk
  • πk\pi_k: the prior probability of class kk
  • p(x)=∑j=1Kp(x∣y=j) πjp(\mathbf{x}) = \sum_{j=1}^K p(\mathbf{x} \mid y = j)\, \pi_j: the marginal density of x\mathbf{x} under the generative model, shared by all classes and normalizing the posteriors to sum to one

The denominator is the same for every class, so it cannot change which class has the largest posterior probability. The rule "predict the class with the largest posterior probability" is therefore equivalent to "predict the class with the largest p(x∣y=k) πkp(\mathbf{x} \mid y = k)\, \pi_k." This is a small algebraic observation with a large practical consequence: we never need to compute the marginal density in order to classify.

Comparing products of densities is awkward, and in high dimensions the products can underflow. Taking logarithms preserves the argmax because the logarithm is monotonic. So

log⁡ ⁣[p(x∣y=k) πk]=−d2log⁡(2π)−12log⁡∣Σk∣−12(x−μk)⊤Σk−1(x−μk)+log⁡πk.\log\!\left[p(\mathbf{x} \mid y = k)\, \pi_k\right] = -\tfrac{d}{2} \log(2\pi) - \tfrac{1}{2} \log |\boldsymbol{\Sigma}_k| - \tfrac{1}{2} (\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}_k^{-1} (\mathbf{x} - \boldsymbol{\mu}_k) + \log \pi_k.

where:

  • x\mathbf{x}: the feature vector being classified
  • μk\boldsymbol{\mu}_k: the mean vector of class kk
  • Σk\boldsymbol{\Sigma}_k: the covariance matrix of class kk
  • πk\pi_k: the prior probability of class kk
  • dd: the number of features
  • ∣Σk∣|\boldsymbol{\Sigma}_k|: the determinant of the class covariance

The first term −d2log⁡(2π)-\tfrac{d}{2} \log(2\pi) is identical for every kk and drops out of any comparison. Dropping it, we obtain the discriminant function

δk(x)=−12(x−μk)⊤Σk−1(x−μk)−12log⁡∣Σk∣+log⁡πk,\delta_k(\mathbf{x}) = -\tfrac{1}{2} (\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}_k^{-1} (\mathbf{x} - \boldsymbol{\mu}_k) - \tfrac{1}{2} \log |\boldsymbol{\Sigma}_k| + \log \pi_k,

where:

  • δk(x)\delta_k(\mathbf{x}): the discriminant score for class kk at point x\mathbf{x}
  • x\mathbf{x}: the feature vector
  • μk\boldsymbol{\mu}_k: the mean vector of class kk
  • Σk\boldsymbol{\Sigma}_k: the covariance matrix of class kk
  • πk\pi_k: the prior probability of class kk
  • (x−μk)⊤Σk−1(x−μk)(\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}_k^{-1} (\mathbf{x} - \boldsymbol{\mu}_k): the squared Mahalanobis distance from x\mathbf{x} to class kk's mean, in units of class kk's own covariance
  • ∣Σk∣|\boldsymbol{\Sigma}_k|: the determinant of the class covariance, penalizing diffuse class clouds

The Bayes decision rule is then y^=arg⁡max⁡kδk(x)\hat{y} = \arg\max_k \delta_k(\mathbf{x}).

Each term has a clear interpretation, and it is worth reading them as a sequence of questions. The first term rewards closeness to the class mean, measured in the class's own Mahalanobis metric: does x\mathbf{x} fall inside the class's ellipsoidal neighborhood? The second term is a normalization penalty: at a fixed feature dimension, a larger covariance determinant lowers the Gaussian peak. The density at a particular point also depends on its Mahalanobis distance from the class mean. The third term rewards frequently observed classes, pulling the decision toward a prior that may be strong. Classification is thus a balance between geometric fit, concentration, and prior belief.

Two facts about this derivation are useful. First, adding the same quantity to every class score, even a quantity depending on x\mathbf{x}, does not change the predicted class. Second, the difference between two class scores is exactly the corresponding log-posterior ratio, because all common terms cancel:

log⁡p(y=k∣x)p(y=k′∣x)=δk(x)−δk′(x),\log \frac{p(y = k \mid \mathbf{x})}{p(y = k' \mid \mathbf{x})} = \delta_k(\mathbf{x}) - \delta_{k'}(\mathbf{x}),

where:

  • k,k′k, k': the two classes being compared
  • δk(x),δk′(x)\delta_k(\mathbf{x}), \delta_{k'}(\mathbf{x}): the discriminant scores for classes kk and k′k' at point x\mathbf{x}, each defined up to the same additive constant
  • p(y=k∣x),p(y=k′∣x)p(y = k \mid \mathbf{x}), p(y = k' \mid \mathbf{x}): the posterior probabilities of classes kk and k′k' given x\mathbf{x}

The right-hand side is a score difference, and its zero set is the pairwise tie locus. With two classes, a change in the winning label can occur only at a tie; a degenerate tie need not separate two open decision regions. With more than two classes, a tie between kk and k′k' contributes to their shared decision boundary only where their common score is at least as large as every other class score. We now examine how the covariance assumptions determine these score differences.

Shared covariance and linear discriminants

The LDA assumption is that all classes share the same covariance matrix: Σk=Σ\boldsymbol{\Sigma}_k = \boldsymbol{\Sigma} for all kk. The shared-covariance condition is a model assumption about the data-generating process. It says that all class clouds have the same shape, orientation, and volume, and differ only in where they are centered and how common they are. Under that assumption, the Gaussian ellipsoids are translated copies of one another. The discriminant simplifies because the −12log⁡∣Σ∣-\tfrac{1}{2} \log |\boldsymbol{\Sigma}| term is the same for all classes and drops out:

δk(x)=−12(x−μk)⊤Σ−1(x−μk)+log⁡πk.\delta_k(\mathbf{x}) = -\tfrac{1}{2} (\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu}_k) + \log \pi_k.

where:

  • δk(x)\delta_k(\mathbf{x}): the discriminant score for class kk at point x\mathbf{x}
  • x\mathbf{x}: the feature vector being classified
  • μk\boldsymbol{\mu}_k: the mean vector of class kk
  • Σ\boldsymbol{\Sigma}: the shared covariance matrix common to all classes
  • πk\pi_k: the prior probability of class kk
  • (x−μk)⊤Σ−1(x−μk)(\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu}_k): the squared Mahalanobis distance from x\mathbf{x} to class kk's mean in the shared metric

We now expand the quadratic form. In matrix notation,

(x−μk)⊤Σ−1(x−μk)=x⊤Σ−1x−2 μk⊤Σ−1x+μk⊤Σ−1μk.(\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}^{-1} (\mathbf{x} - \boldsymbol{\mu}_k) = \mathbf{x}^\top \boldsymbol{\Sigma}^{-1} \mathbf{x} - 2\, \boldsymbol{\mu}_k^\top \boldsymbol{\Sigma}^{-1} \mathbf{x} + \boldsymbol{\mu}_k^\top \boldsymbol{\Sigma}^{-1} \boldsymbol{\mu}_k.

After multiplying the expansion by −12-\tfrac{1}{2}, the term −12x⊤Σ−1x-\tfrac{1}{2}\mathbf{x}^\top\boldsymbol{\Sigma}^{-1}\mathbf{x} is identical for every class and can be dropped. The other terms retain their signs and coefficients; multiplying an entire score by a negative number would reverse its ranking. The shared quadratic term is the cancellation that removes curvature from the score differences. What remains is

δk(x)=μk⊤Σ−1x−12 μk⊤Σ−1μk+log⁡πk.\delta_k(\mathbf{x}) = \boldsymbol{\mu}_k^\top \boldsymbol{\Sigma}^{-1} \mathbf{x} - \tfrac{1}{2}\, \boldsymbol{\mu}_k^\top \boldsymbol{\Sigma}^{-1} \boldsymbol{\mu}_k + \log \pi_k.

This is a linear function of x\mathbf{x}. Define

wk=Σ−1μk,bk=−12 μk⊤Σ−1μk+log⁡πk,\mathbf{w}_k = \boldsymbol{\Sigma}^{-1} \boldsymbol{\mu}_k, \qquad b_k = -\tfrac{1}{2}\, \boldsymbol{\mu}_k^\top \boldsymbol{\Sigma}^{-1} \boldsymbol{\mu}_k + \log \pi_k,

where:

  • wk\mathbf{w}_k: the covariance-adjusted weight vector for class kk
  • bkb_k: the class-kk bias (intercept), combining the mean-quadratic term and the log prior
  • Σ,μk,πk\boldsymbol{\Sigma}, \boldsymbol{\mu}_k, \pi_k: the shared covariance, class-kk mean, and class-kk prior, as defined earlier

so that δk(x)=wk⊤x+bk\delta_k(\mathbf{x}) = \mathbf{w}_k^\top \mathbf{x} + b_k is a linear score. For two classes, the decision boundary between them is δ0(x)=δ1(x)\delta_0(\mathbf{x}) = \delta_1(\mathbf{x}), or

w⊤x+b=0,\mathbf{w}^\top \mathbf{x} + b = 0,

with

w=Σ−1(μ0−μ1),b=−12(μ0+μ1)⊤Σ−1(μ0−μ1)+log⁡π0π1.\mathbf{w} = \boldsymbol{\Sigma}^{-1}(\boldsymbol{\mu}_0 - \boldsymbol{\mu}_1), \qquad b = -\tfrac{1}{2}(\boldsymbol{\mu}_0 + \boldsymbol{\mu}_1)^\top \boldsymbol{\Sigma}^{-1} (\boldsymbol{\mu}_0 - \boldsymbol{\mu}_1) + \log \frac{\pi_0}{\pi_1}.

where:

  • w\mathbf{w}: the covariance-adjusted normal to the boundary, pointing toward increasing class-0-versus-class-1 score
  • bb: the bias term that shifts the boundary based on the class means and priors
  • Σ,μ0,μ1,π0,π1\boldsymbol{\Sigma}, \boldsymbol{\mu}_0, \boldsymbol{\mu}_1, \pi_0, \pi_1: the shared covariance, class means, and class priors, as defined earlier

The sign convention matters: w⊤x+b\mathbf{w}^\top\mathbf{x} + b equals δ0(x)−δ1(x)\delta_0(\mathbf{x})-\delta_1(\mathbf{x}), so a positive value favors class 0 after accounting for the priors. In general, w\mathbf{w} is not parallel to μ0−μ1\boldsymbol{\mu}_0-\boldsymbol{\mu}_1. If the means differ and the priors are equal, the boundary is the hyperplane through their midpoint on which their Mahalanobis distances are equal. Increasing the prior of one class expands its winning region and moves this hyperplane toward the other class. If the means are identical, the normal is zero and the priors alone decide, or every point is tied when the priors are also equal.

Fisher's original formulation sought a linear combination w⊤x\mathbf{w}^\top\mathbf{x} maximizing the ratio of between-class to within-class variance. In the two-class case with nonsingular within-class covariance, the between-class scatter of the projected values is (w⊤(μ0−μ1))2(\mathbf{w}^\top(\boldsymbol{\mu}_0-\boldsymbol{\mu}_1))^2 and the within-class scatter is w⊤Σw\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w}, so the ratio is

J(w)=(w⊤(μ0−μ1))2w⊤Σw,J(\mathbf{w}) = \frac{\bigl(\mathbf{w}^\top(\boldsymbol{\mu}_0-\boldsymbol{\mu}_1)\bigr)^2}{\mathbf{w}^\top\boldsymbol{\Sigma}\mathbf{w}},

where:

  • w\mathbf{w}: the direction onto which the data are projected
  • μ0−μ1\boldsymbol{\mu}_0-\boldsymbol{\mu}_1: the difference between the class means, setting the between-class separation
  • Σ\boldsymbol{\Sigma}: the shared (within-class) covariance, setting the within-class spread

When the class means differ, a maximizing nonzero direction is proportional to Σ−1(μ0−μ1)\boldsymbol{\Sigma}^{-1}(\boldsymbol{\mu}_0-\boldsymbol{\mu}_1). Its sign and nonzero scale do not change the variance ratio. If the means coincide, J(w)=0J(\mathbf w)=0 for every nonzero direction, so this criterion does not select an informative direction. Under the shared-covariance Gaussian model with distinct means, the maximizing direction gives the generative classifier when paired with its prior-dependent threshold. The direction alone does not specify that threshold.

LDA has KdKd mean parameters and d(d+1)2\frac{d(d+1)}{2} shared symmetric-covariance parameters. Estimating the class priors adds K−1K-1 free parameters under ∑kπk=1\sum_k \pi_k=1, giving Kd+d(d+1)2+(K−1)Kd+\frac{d(d+1)}{2}+(K-1) in total. For fixed KK, the covariance term grows quadratically in dd, while the mean term grows linearly. In the multiclass setting, LDA needs at most min⁡(d,K−1)\min(d,K-1) discriminant directions. The centered class means have at most K−1K-1 independent directions, and applying an invertible covariance adjustment does not change this rank. The posterior-relevant directions in the original feature coordinates are the span of Σ−1(μk−μref)\boldsymbol{\Sigma}^{-1}(\boldsymbol{\mu}_k-\boldsymbol{\mu}_{\mathrm{ref}}), where μref\boldsymbol{\mu}_{\mathrm{ref}} is one chosen reference-class mean. Projecting onto these directions retains the Gaussian LDA score differences; projecting onto the raw means need not.

Class-specific covariance and quadratic discriminants

QDA relaxes the shared-covariance assumption: each class has its own Σk\boldsymbol{\Sigma}_k. This can describe one class that is narrow and another that is broad or differently oriented. The shared-covariance cancellation is no longer available in general. Some terms can still coincide for particular parameter choices, but the general score must retain the full form

δk(x)=−12(x−μk)⊤Σk−1(x−μk)−12log⁡∣Σk∣+log⁡πk.\delta_k(\mathbf{x}) = -\tfrac{1}{2} (\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}_k^{-1} (\mathbf{x} - \boldsymbol{\mu}_k) - \tfrac{1}{2} \log |\boldsymbol{\Sigma}_k| + \log \pi_k.

where:

  • δk(x)\delta_k(\mathbf{x}): the discriminant score for class kk at point x\mathbf{x}
  • x\mathbf{x}: the feature vector being classified
  • μk\boldsymbol{\mu}_k: the mean vector of class kk
  • Σk\boldsymbol{\Sigma}_k: the class-specific covariance matrix for class kk
  • πk\pi_k: the prior probability of class kk
  • (x−μk)⊤Σk−1(x−μk)(\mathbf{x} - \boldsymbol{\mu}_k)^\top \boldsymbol{\Sigma}_k^{-1} (\mathbf{x} - \boldsymbol{\mu}_k): the squared Mahalanobis distance from x\mathbf{x} to class kk's mean, in class kk's own metric
  • ∣Σk∣|\boldsymbol{\Sigma}_k|: the determinant of the class kk covariance, penalizing diffuse class clouds

When Σk−1\boldsymbol{\Sigma}_k^{-1} varies with kk, the x⊤Σk−1x\mathbf{x}^\top\boldsymbol{\Sigma}_k^{-1}\mathbf{x} contribution is class dependent and must be retained. The log-determinant penalty must likewise be retained whenever the covariance determinants differ; different covariance matrices can still have the same determinant. The general discriminant is quadratic in x\mathbf{x}, with linear and second-order terms in its coordinates. Class-specific covariance therefore permits boundaries beyond a single flat cut, while particular parameter choices can still produce simpler or degenerate boundaries.

For two classes (k=0k = 0 and k=1k = 1), the boundary δ0(x)=δ1(x)\delta_0(\mathbf{x}) = \delta_1(\mathbf{x}) is

−12x⊤(Σ0−1−Σ1−1)x+(μ0⊤Σ0−1−μ1⊤Σ1−1)x+c0=0,-\tfrac{1}{2} \mathbf{x}^\top (\boldsymbol{\Sigma}_0^{-1} - \boldsymbol{\Sigma}_1^{-1}) \mathbf{x} + (\boldsymbol{\mu}_0^\top \boldsymbol{\Sigma}_0^{-1} - \boldsymbol{\mu}_1^\top \boldsymbol{\Sigma}_1^{-1}) \mathbf{x} + c_0 = 0,

which is the equation δ0(x)−δ1(x)=0\delta_0(\mathbf{x}) - \delta_1(\mathbf{x}) = 0 written with the class-0 terms leading.

where:

  • x\mathbf{x}: the feature vector at which the boundary is evaluated
  • Σ0,Σ1\boldsymbol{\Sigma}_0, \boldsymbol{\Sigma}_1: the covariance matrices of classes 0 and 1 (both assumed invertible)
  • μ0,μ1\boldsymbol{\mu}_0, \boldsymbol{\mu}_1: the mean vectors of classes 0 and 1
  • c0c_0: the constant term, defined explicitly below

The constant c0c_0 collects the terms that do not depend on x\mathbf{x}. Writing out δ0(x)−δ1(x)=0\delta_0(\mathbf{x}) - \delta_1(\mathbf{x}) = 0 in full (the form matching the displayed boundary equation) and keeping only the terms without x\mathbf{x} gives

c0=−12μ0⊤Σ0−1μ0+12μ1⊤Σ1−1μ1−12log⁡∣Σ0∣+12log⁡∣Σ1∣+log⁡π0π1.c_0 = -\tfrac{1}{2} \boldsymbol{\mu}_0^\top \boldsymbol{\Sigma}_0^{-1} \boldsymbol{\mu}_0 + \tfrac{1}{2} \boldsymbol{\mu}_1^\top \boldsymbol{\Sigma}_1^{-1} \boldsymbol{\mu}_1 - \tfrac{1}{2} \log |\boldsymbol{\Sigma}_0| + \tfrac{1}{2} \log |\boldsymbol{\Sigma}_1| + \log \frac{\pi_0}{\pi_1}.

where:

  • x\mathbf{x}: the feature vector
  • Σ0,Σ1\boldsymbol{\Sigma}_0, \boldsymbol{\Sigma}_1: the covariance matrices of classes 0 and 1
  • μ0,μ1\boldsymbol{\mu}_0, \boldsymbol{\mu}_1: the class mean vectors
  • π0,π1\pi_0, \pi_1: the class prior probabilities
  • c0c_0: the constant term collecting all contributions that do not depend on x\mathbf{x} (the mean-quadratic and log-determinant terms plus the log prior ratio)

The boundary equation has the form of a general quadratic in x\mathbf{x}: the symmetric matrix coefficient of the second-order term is −12(Σ0−1−Σ1−1)-\tfrac{1}{2}(\boldsymbol{\Sigma}_0^{-1} - \boldsymbol{\Sigma}_1^{-1}), the linear coefficient is μ0⊤Σ0−1−μ1⊤Σ1−1\boldsymbol{\mu}_0^\top \boldsymbol{\Sigma}_0^{-1} - \boldsymbol{\mu}_1^\top \boldsymbol{\Sigma}_1^{-1}, and c0c_0 is the scalar constant.

In two dimensions, a quadratic tie equation can describe conic sections such as ellipses, hyperbolas, and parabolas; in higher dimensions, the corresponding objects are quadrics. The eigenstructure of Σ0−1−Σ1−1\boldsymbol{\Sigma}_0^{-1}-\boldsymbol{\Sigma}_1^{-1} controls the quadratic curvature, but the linear and constant terms also matter. Together they determine the boundary's location, whether it is degenerate, and whether it exists at all. Some QDA models produce bounded class regions, with a narrow class winning near its center and a broad class winning in the tails. QDA can also reduce to a linear rule when the quadratic difference vanishes.

To see where the constants come from, write δ0(x)−δ1(x)\delta_0(\mathbf{x}) - \delta_1(\mathbf{x}) term by term, consistent with the δ0−δ1=0\delta_0 - \delta_1 = 0 sign convention used above. The x\mathbf{x}-independent contribution is

−12μ0⊤Σ0−1μ0+12μ1⊤Σ1−1μ1−12log⁡∣Σ0∣+12log⁡∣Σ1∣+log⁡π0π1,-\tfrac{1}{2} \boldsymbol{\mu}_0^\top \boldsymbol{\Sigma}_0^{-1} \boldsymbol{\mu}_0 + \tfrac{1}{2} \boldsymbol{\mu}_1^\top \boldsymbol{\Sigma}_1^{-1} \boldsymbol{\mu}_1 - \tfrac{1}{2} \log |\boldsymbol{\Sigma}_0| + \tfrac{1}{2} \log |\boldsymbol{\Sigma}_1| + \log \frac{\pi_0}{\pi_1},

matching c0c_0 above, while the x\mathbf{x}-dependent part yields the quadratic and linear coefficients already stated.

The number of free parameters in QDA is K⋅dK \cdot d (means) plus K−1K - 1 (priors) plus K⋅d(d+1)2K \cdot \frac{d(d+1)}{2} (covariances), so its covariance term grows KK times larger than LDA's. For independent Gaussian samples with a truly common covariance, the pooled covariance has lower entrywise sampling variance than an unbiased class estimate when that class has at least two observations and the other classes add residual degrees of freedom. Whether their flexibility improves held-out prediction depends on the population covariances and the available samples; parameter count alone does not determine the prediction error.

The covariance parameter count grows quadratically in the feature dimension and is multiplied by the number of classes for QDA. The next figure isolates that multiplier for a three-class problem, holding means and priors aside so the covariance cost is the only changing quantity.

VisualizationOut [4]
CollapseExpand
LDA and QDA covariance parameter counts versus feature dimension for three classes.
Covariance parameter counts for LDA and QDA as the feature dimension grows for three classes. QDA's count is exactly three times LDA's at every feature dimension, and the gap widens quadratically rather than staying constant.

Covariance estimation, shrinkage, and limitations

The formulas above describe the population rule when the means, covariances, and priors are known. In practice, these parameters must be estimated. The maximum-likelihood class priors are the empirical proportions, π^k=Nk/N\hat{\pi}_k=N_k/N, where NkN_k counts observations in class kk and N=∑jNjN=\sum_j N_j. The maximum-likelihood class means are μ^k=1Nk∑i:yi=kxi\hat{\boldsymbol{\mu}}_k=\frac{1}{N_k}\sum_{i:y_i=k}\mathbf{x}_i. For covariance estimates, this chapter uses the unbiased sample convention rather than the Gaussian maximum-likelihood convention:

Σ^k=1Nk−1∑i:yi=k(xi−μ^k)(xi−μ^k)⊤\hat{\boldsymbol{\Sigma}}_k = \frac{1}{N_k - 1} \sum_{i: y_i = k} (\mathbf{x}_i - \hat{\boldsymbol{\mu}}_k)(\mathbf{x}_i - \hat{\boldsymbol{\mu}}_k)^\top

where:

  • Σ^k\hat{\boldsymbol{\Sigma}}_k: the estimated covariance matrix of class kk
  • NkN_k: the number of observations in class kk
  • μ^k\hat{\boldsymbol{\mu}}_k: the empirical mean of class kk
  • xi\mathbf{x}_i: the ii-th feature vector, associated with label yiy_i

The Nk−1N_k-1 denominator gives the unbiased class covariance estimator for Nk>1N_k>1. When the centered class scatter is positive definite, the Gaussian covariance maximum-likelihood estimator divides that same scatter by NkN_k. If the scatter is singular, no finite positive-definite covariance maximizes this unconstrained Gaussian density likelihood. The relative difference between the two normalization factors vanishes as NkN_k grows. For LDA, the unbiased pooled covariance estimator is

Σ^=1N−K∑k=1K∑i:yi=k(xi−μ^k)(xi−μ^k)⊤,\hat{\boldsymbol{\Sigma}} = \frac{1}{N - K} \sum_{k=1}^K \sum_{i: y_i = k} (\mathbf{x}_i - \hat{\boldsymbol{\mu}}_k)(\mathbf{x}_i - \hat{\boldsymbol{\mu}}_k)^\top,

where:

  • Σ^\hat{\boldsymbol{\Sigma}}: the pooled estimate of the shared covariance matrix used by LDA
  • N=∑j=1KNjN = \sum_{j=1}^K N_j: the total number of observations across all classes
  • KK: the number of classes
  • NkN_k: the number of observations in class kk
  • μ^k\hat{\boldsymbol{\mu}}_k: the empirical mean of class kk
  • xi\mathbf{x}_i: the ii-th feature vector, associated with label yiy_i

This pools the within-class scatter and divides by the residual degrees of freedom N−KN-K. When the pooled scatter is positive definite, the shared-covariance Gaussian maximum-likelihood estimator instead divides that same scatter by NN. If the pooled scatter is singular, the unconstrained positive-definite Gaussian covariance likelihood has no finite maximum. These are different finite-sample normalization conventions; the worked example and primitive implementations below consistently use the unbiased denominators.

Pooling is the estimation analogue of the shared-covariance assumption. Under independent Gaussian samples with a truly common covariance, the pooled estimate has N−KN-K residual degrees of freedom rather than one class's Nk−1N_k-1. For a class with Nk>1N_k>1, pooling has no larger covariance-entry sampling variance under that model; the reduction is strict when the other classes add residual degrees of freedom, so N−K>Nk−1N-K>N_k-1. If class covariances differ, pooling instead estimates a compromise matrix.

For a centered class sample with Nk>1N_k>1, rank⁡(Σ^k)≤min⁡(d,Nk−1)\operatorname{rank}(\hat{\boldsymbol{\Sigma}}_k)\le\min(d,N_k-1). Thus 1<Nk≤d1<N_k\le d forces singularity: for d=100d=100 and Nk=50N_k=50, there are at most 49 nonzero eigenvalues and the inverse does not exist. Having Nk>dN_k>d is necessary, but not sufficient, for nonsingularity. LDA's pooled scatter similarly has rank at most min⁡(d,N−K)\min(d,N-K). Even when these matrices are full rank, small samples and strong feature dependencies can make covariance estimation unreliable. Rank, conditioning, and estimation uncertainty are separate checks.

Shrinkage estimators regularize the covariance by blending the empirical estimate with a low-dimensional target:

Σ^k(α)=(1−α)Σ^k+αT,\hat{\boldsymbol{\Sigma}}_k(\alpha) = (1 - \alpha) \hat{\boldsymbol{\Sigma}}_k + \alpha \mathbf{T},

where:

  • Σ^k(α)\hat{\boldsymbol{\Sigma}}_k(\alpha): the shrunk (regularized) covariance estimate for class kk at mixing level α\alpha
  • α\alpha: the mixing (shrinkage) parameter in [0,1][0, 1] controlling the bias-variance tradeoff; α=0\alpha = 0 recovers the empirical covariance and α=1\alpha = 1 yields the pure target
  • Σ^k\hat{\boldsymbol{\Sigma}}_k: the empirical covariance of class kk
  • T\mathbf{T}: the shrinkage target, for example the identity matrix I\mathbf{I} or a scaled version σ2I\sigma^2 \mathbf{I}

At α=0\alpha=0, the estimate is the empirical covariance; at α=1\alpha=1, it is the target T\mathbf{T}. If the empirical covariance is positive semidefinite and the target is positive definite, any α>0\alpha>0 makes their convex blend positive definite. A shared identity or positive scaled-identity target is spherical. The Ledoit-Wolf (2004) estimator provides a data-driven shrinkage approach. It should not be conflated with manually choosing a regularization strength. In scikit-learn 1.8, QDA's default 'svd' solver uses reg_param; its 'eigen' solver instead supports shrinkage, including 'auto'. LDA supports shrinkage='auto' with 'lsqr' or 'eigen'. The solver and rank limitations are specified below.

Two further limitations matter. A sufficiently extreme observation can dominate an empirical covariance and move the fitted boundary. The empirical mean and covariance have unbounded influence even when the reference distribution is Gaussian. Moving a contaminating point farther away makes its influence grow without bound. Gaussianity therefore does not make these estimators robust to contamination. Robust covariance estimators address a different estimation problem and are outside this chapter's scope.

Mathematical Properties

Discriminant analysis inherits several structural properties from its Gaussian foundation.

  • Invertible affine transformations preserve unregularized LDA and QDA predictions in exact arithmetic when their estimates are transformed consistently. If z=Ax+b\mathbf{z}=A\mathbf{x}+\mathbf{b} with invertible AA, then μk′=Aμk+b\boldsymbol{\mu}'_k=A\boldsymbol{\mu}_k+\mathbf{b} and Σk′=AΣkA⊤\boldsymbol{\Sigma}'_k=A\boldsymbol{\Sigma}_kA^\top. Mahalanobis distances are unchanged, while ∣Σk′∣=∣det⁡A∣2∣Σk∣|\boldsymbol{\Sigma}'_k|=|\det A|^2|\boldsymbol{\Sigma}_k|. The full Gaussian discriminants therefore change by −log⁡∣det⁡A∣-\log|\det A|, common to every class. Reduced LDA scores, after dropping their shared quadratic term, can acquire a different common term depending on x\mathbf{x}, but all score differences are still unchanged. This algebraic equivariance does not require Gaussian data; finite precision and a fixed regularization target can limit the invariance in practice.
  • Posterior probabilities are given by the softmax of the discriminant scores (the logits):
p(y=k∣x)=exp⁡(δk(x))∑j=1Kexp⁡(δj(x)).p(y = k \mid \mathbf{x}) = \frac{\exp(\delta_k(\mathbf{x}))}{\sum_{j=1}^K \exp(\delta_j(\mathbf{x}))}.

This means LDA and QDA are not just classifiers but probabilistic models: the same machinery that predicts labels can also predict class probabilities. In practice, these probabilities can be miscalibrated if the Gaussian assumption fails, which motivates the calibration methods discussed in later parts of the book. In the implementation below, we compute the class posteriors directly from the discriminant scores via this softmax.

  • Under the shared-covariance Gaussian model, the binary posterior is logistic in x\mathbf{x}. In the earlier sign convention, δ0−δ1=w⊤x+b\delta_0-\delta_1=\mathbf{w}^\top\mathbf{x}+b, so p(y=1∣x)=σ(−w⊤x−b)p(y=1\mid\mathbf{x})=\sigma(-\mathbf{w}^\top\mathbf{x}-b), where σ(t)=1/(1+exp⁡(−t))\sigma(t)=1/(1+\exp(-t)). Equivalently, its class-1 logit has coefficient Σ−1(μ1−μ0)\boldsymbol{\Sigma}^{-1}(\boldsymbol{\mu}_1-\boldsymbol{\mu}_0). Logistic regression fits this conditional linear-logit family directly. LDA obtains coefficients by fitting a joint Gaussian model; this imposes assumptions about feature distributions, rather than a smaller family of linear posterior logits.
  • QDA does not generally satisfy a linear-logistic relationship. Its log-odds is quadratic in x\mathbf{x}, so p(y=1∣x)=σ(f(x))p(y = 1 \mid \mathbf{x}) = \sigma(f(\mathbf{x})) holds with f(x)=log⁡(π1/π0)−12log⁡(∣Σ1∣/∣Σ0∣)−12(x−μ1)⊤Σ1−1(x−μ1)+12(x−μ0)⊤Σ0−1(x−μ0)f(\mathbf{x}) = \log(\pi_1/\pi_0) - \frac{1}{2}\log(|\boldsymbol{\Sigma}_1|/|\boldsymbol{\Sigma}_0|) - \frac{1}{2}(\mathbf{x}-\boldsymbol{\mu}_1)^\top\boldsymbol{\Sigma}_1^{-1}(\mathbf{x}-\boldsymbol{\mu}_1) + \frac{1}{2}(\mathbf{x}-\boldsymbol{\mu}_0)^\top\boldsymbol{\Sigma}_0^{-1}(\mathbf{x}-\boldsymbol{\mu}_0), a quadratic function of x\mathbf{x}. The quadratic term enters before the logistic transformation, so the gradient of the log-odds generally varies affinely across feature space.

Visualizing LDA and QDA

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.

Example

We work through a small one-dimensional example with concrete numbers so that every step is visible. The point of the example is not to show a realistic data analysis, but to make the discriminant scores themselves tangible: we can compute them by hand, compare the LDA and QDA decisions, and see exactly where the quadratic boundary comes from. Consider a two-class problem with the following samples:

Class 0: {0,1,2,3},Class 1: {4,6,8,10}.\text{Class 0}:\ \{0, 1, 2, 3\}, \qquad \text{Class 1}:\ \{4, 6, 8, 10\}.

Both classes have four observations, so π^0=π^1=0.5\hat{\pi}_0 = \hat{\pi}_1 = 0.5. Equal priors keep the comparison focused on the densities: any difference in the decision must come from the class means and variances, not from one class being more common than the other.

Step 1: compute means and variances.

For class 0, the mean is μ^0=(0+1+2+3)/4=1.5\hat{\mu}_0 = (0 + 1 + 2 + 3)/4 = 1.5, and the unbiased variance is

σ^02=13∑i(xi−1.5)2=(−1.5)2+(−0.5)2+(0.5)2+(1.5)23=53≈1.667.\hat{\sigma}_0^2 = \frac{1}{3}\sum_{i}\bigl(x_i - 1.5\bigr)^2 = \frac{(-1.5)^2 + (-0.5)^2 + (0.5)^2 + (1.5)^2}{3} = \frac{5}{3} \approx 1.667.

For class 1, the mean is μ^1=(4+6+8+10)/4=7\hat{\mu}_1 = (4 + 6 + 8 + 10)/4 = 7, and the unbiased variance is

σ^12=(−3)2+(−1)2+(1)2+(3)23=203≈6.667.\hat{\sigma}_1^2 = \frac{(-3)^2 + (-1)^2 + (1)^2 + (3)^2}{3} = \frac{20}{3} \approx 6.667.

The empirical variance of class 1 is four times that of class 0, by construction of this small example. These sample spreads make the two fitted covariance conventions differ visibly. Four observations per class do not establish the covariances of an underlying population, so we treat this as a worked calculation rather than evidence that a real population violates LDA's assumption.

Step 2: pooled variance for LDA.

The pooled variance divides the total within-class squared deviations by the degrees of freedom N−K=8−2=6N - K = 8 - 2 = 6. The numerator is the sum of the within-class squared deviations: class 0 contributes 55 and class 1 contributes 2020, for a total of 5+20=255 + 20 = 25. Thus

σ^pooled2=5+206=256≈4.167.\hat{\sigma}_{\text{pooled}}^2 = \frac{5 + 20}{6} = \frac{25}{6} \approx 4.167.

This pooled value is a compromise between the tight class 0 variance and the broad class 1 variance. It is the single shape that LDA will use for both classes.

Step 3: LDA discriminant scores.

Using the exact pooled estimate σ^2=25/6≈4.167\hat{\sigma}^2=25/6\approx4.167, the one-dimensional linear discriminant is

δk(x)=μ^kσ^2 x−μ^k22σ^2+log⁡π^k\delta_k(x) = \frac{\hat{\mu}_k}{\hat{\sigma}^2}\, x - \frac{\hat{\mu}_k^2}{2 \hat{\sigma}^2} + \log \hat{\pi}_k

where:

  • μ^k\hat{\mu}_k: the estimated mean of class kk
  • σ^2=25/6≈4.167\hat{\sigma}^2 = 25/6 \approx 4.167: the pooled variance shared by both classes
  • π^k=0.5\hat{\pi}_k = 0.5: the estimated prior probability of class kk
  • xx: the (scalar) feature value being classified

Working through the two classes:

δ0(x)=1.525/6x−2.252⋅(25/6)+log⁡(0.5)≈0.36x−0.27−0.693=0.36x−0.963,\delta_0(x) = \frac{1.5}{25/6}x - \frac{2.25}{2 \cdot (25/6)} + \log(0.5) \approx 0.36 x - 0.27 - 0.693 = 0.36 x - 0.963, δ1(x)=725/6x−492⋅(25/6)+log⁡(0.5)≈1.68x−5.88−0.693=1.68x−6.573.\delta_1(x) = \frac{7}{25/6}x - \frac{49}{2 \cdot (25/6)} + \log(0.5) \approx 1.68 x - 5.88 - 0.693 = 1.68 x - 6.573.

At a test point x=3x = 3:

δ0(3)≈1.08−0.963=0.117,δ1(3)≈5.04−6.573=−1.533.\delta_0(3) \approx 1.08 - 0.963 = 0.117, \qquad \delta_1(3) \approx 5.04 - 6.573 = -1.533.

The larger score is δ0\delta_0, so LDA predicts class 0. At a test point x=5x = 5:

δ0(5)≈1.80−0.963=0.837,δ1(5)≈8.40−6.573=1.827.\delta_0(5) \approx 1.80 - 0.963 = 0.837, \qquad \delta_1(5) \approx 8.40 - 6.573 = 1.827.

Here LDA predicts class 1. The boundary between the two classes is where δ0(x)=δ1(x)\delta_0(x) = \delta_1(x), i.e., 0.36x−0.963=1.68x−6.5730.36x - 0.963 = 1.68x - 6.573. Solving: subtract 0.36x0.36x from both sides to get −0.963=1.32x−6.573-0.963 = 1.32x - 6.573; add 6.5736.573 to both sides to get 5.61=1.32x5.61 = 1.32x; divide by 1.321.32 to get x≈4.25x \approx 4.25. As expected, this is the midpoint of the two class means, 12(1.5+7)=4.25\tfrac{1}{2}(1.5 + 7) = 4.25. The equal-prior assumption removes any prior-driven shift, and the shared variance makes the boundary exactly halfway between the centers.

Step 4: QDA discriminant scores.

QDA keeps the class-specific variances. The discriminant is

δkQDA(x)=−(x−μ^k)22σ^k2−12log⁡σ^k2+log⁡π^k.\delta_k^{\text{QDA}}(x) = -\frac{(x - \hat{\mu}_k)^2}{2 \hat{\sigma}_k^2} - \tfrac{1}{2} \log \hat{\sigma}_k^2 + \log \hat{\pi}_k.

For class 0:

δ0QDA(x)=−(x−1.5)22(5/3)−12log⁡(5/3)+log⁡(0.5)≈−0.3(x−1.5)2−0.949.\delta_0^{\text{QDA}}(x) = -\frac{(x-1.5)^2}{2(5/3)}-\tfrac{1}{2}\log(5/3)+\log(0.5) \approx -0.3(x-1.5)^2-0.949.

where:

  • μ^0=1.5\hat{\mu}_0 = 1.5: the estimated mean of class 0
  • σ^02=5/3≈1.667\hat{\sigma}_0^2 = 5/3 \approx 1.667: the unbiased estimated variance of class 0
  • π^0=0.5\hat{\pi}_0 = 0.5: the estimated prior probability of class 0
  • xx: the test point at which the class-0 QDA score is evaluated

For class 1:

δ1QDA(x)=−(x−7)22(20/3)−12log⁡(20/3)+log⁡(0.5)≈−0.075(x−7)2−1.642.\delta_1^{\text{QDA}}(x) = -\frac{(x-7)^2}{2(20/3)}-\tfrac{1}{2}\log(20/3)+\log(0.5) \approx -0.075(x-7)^2-1.642.

At x=3x = 3:

δ0QDA(3)=−0.675−12log⁡(5/3)+log⁡(0.5)≈−1.624,δ1QDA(3)=−1.200−12log⁡(20/3)+log⁡(0.5)≈−2.842.\begin{aligned} \delta_0^{\text{QDA}}(3) &= -0.675-\tfrac{1}{2}\log(5/3)+\log(0.5) \approx -1.624,\\ \delta_1^{\text{QDA}}(3) &= -1.200-\tfrac{1}{2}\log(20/3)+\log(0.5) \approx -2.842. \end{aligned}

The quadratic contributions are exactly −(3−1.5)2/[2(5/3)]=−0.675-(3-1.5)^2/[2(5/3)]=-0.675 for class 0 and −(3−7)2/[2(20/3)]=−1.200-(3-7)^2/[2(20/3)]=-1.200 for class 1. The combined log-variance and log-prior constants are approximately −0.948560-0.948560 and −1.641707-1.641707, respectively; retaining them until the final rounding gives the scores above.

In each score, the quadratic contribution is minus one-half of the squared standardized distance. The remaining terms are the log-variance penalty and log prior. Both classes are evaluated at the same test point x=3x=3.

QDA also predicts class 0 at x=3x=3 because −1.624>−2.842-1.624>-2.842. At x=4.25x=4.25, where the exact LDA scores tie:

δ0QDA(4.25)≈−3.217,δ1QDA(4.25)≈−2.209.\delta_0^{\text{QDA}}(4.25) \approx -3.217, \qquad \delta_1^{\text{QDA}}(4.25) \approx -2.209.

QDA favors class 1 at x=4.25x=4.25, while LDA is indifferent there; a claim about different hard labels at that exact point would require an LDA tie-breaking convention. This point is 2.75 raw units from both means. Relative to the class standard deviations, that is about 2.13 standard deviations for class 0 and 1.07 for class 1. The quadratic contributions are −2.26875-2.26875 and −0.5671875-0.5671875. Class 1 pays the larger log-variance penalty, approximately −0.949-0.949 rather than −0.255-0.255, but its less negative quadratic contribution more than offsets that penalty. Thus the broader class has the higher QDA score at the midpoint.

Step 5: QDA decision boundary.

Solving δ0QDA(x)=δ1QDA(x)\delta_0^{\text{QDA}}(x)=\delta_1^{\text{QDA}}(x) gives a quadratic equation. We keep the exact variances and logarithms until the numerical evaluation:

−(x−1.5)210/3−12log⁡(5/3)+log⁡(0.5)=−(x−7)240/3−12log⁡(20/3)+log⁡(0.5).-\frac{(x-1.5)^2}{10/3}-\frac{1}{2}\log(5/3)+\log(0.5) = -\frac{(x-7)^2}{40/3}-\frac{1}{2}\log(20/3)+\log(0.5).

The equal log-prior terms cancel. Multiplying by 40/340/3 and collecting the log-variance difference gives

4(x−1.5)2−(x−7)2=203log⁡ ⁣(20/35/3)=203log⁡4=403log⁡2.\begin{aligned} 4(x-1.5)^2-(x-7)^2 &=\frac{20}{3}\log\!\left(\frac{20/3}{5/3}\right)\\ &=\frac{20}{3}\log 4\\ &=\frac{40}{3}\log 2. \end{aligned}

The left-hand side expands to 4(x2−3x+2.25)−(x2−14x+49)=4x2−12x+9−x2+14x−49=3x2+2x−404(x^2-3x+2.25)-(x^2-14x+49)=4x^2-12x+9-x^2+14x-49=3x^2+2x-40. Define the boundary polynomial

P(x)=3x2+2x−40−403log⁡2.P(x)=3x^2+2x-40-\frac{40}{3}\log 2.

Here P(x)P(x) is the exact quadratic whose zeros are the QDA ties, and log⁡\log is the natural logarithm. Since 403log⁡2≈9.241962\frac{40}{3}\log 2\approx9.241962, the constant term is approximately −49.241962-49.241962. The quadratic formula gives

x=−2± 4+12(40+403log⁡2) 6≈−4.398441or3.731774.\begin{aligned} x &=\frac{-2\pm\sqrt{\,4+12\left(40+\frac{40}{3}\log 2\right)\,}}{6}\\ &\approx -4.398441 \quad\text{or}\quad 3.731774. \end{aligned}

To determine the winning regions, subtract the exact class-0 score from the class-1 score:

403(δ1QDA(x)−δ0QDA(x))=P(x).\frac{40}{3}\left(\delta_1^{\text{QDA}}(x)-\delta_0^{\text{QDA}}(x)\right)=P(x).

The multiplier is positive, so the score difference has the same sign as P(x)P(x). Its positive leading coefficient makes P(x)P(x) negative between the roots and positive outside them. Class 0 therefore wins between the two roots, class 1 wins outside, and the endpoints are ties. The wider class wins in both far tails because its quadratic Mahalanobis contribution grows more slowly, while the class-0 penalty along its own metric dominates far from both centers. This two-tail allocation cannot be expressed by a single nondegenerate LDA threshold.

The worked example produces more than isolated labels: it produces two score curves whose crossings define the decision regions. The figure below evaluates the fitted LDA and QDA scores on a grid whose interval spans both QDA roots and the LDA midpoint. Compare the two classes within one method: each method may discard a different term common to its classes, so score heights across methods are not on a shared probability scale.

PythonIn [10]
CollapseExpand
qda_poly = np.array([3.0, 2.0, -40.0 - (40.0 / 3.0) * np.log(2.0)])
qda_roots_example = np.sort(np.roots(qda_poly))
x_example = np.linspace(
    min(-1.0, qda_roots_example[0] - 1.0),
    max(11.0, qda_roots_example[-1] + 1.0),
    500,
)
mu0_example, mu1_example = 1.5, 7.0
var0_example, var1_example = 5.0 / 3.0, 20.0 / 3.0
pooled_example = 25.0 / 6.0
log_prior_example = np.log(0.5)

lda0_example = (
    (mu0_example / pooled_example) * x_example
    - mu0_example**2 / (2.0 * pooled_example)
    + log_prior_example
)
lda1_example = (
    (mu1_example / pooled_example) * x_example
    - mu1_example**2 / (2.0 * pooled_example)
    + log_prior_example
)
qda0_example = (
    -0.5 * (x_example - mu0_example) ** 2 / var0_example
    - 0.5 * np.log(var0_example)
    + log_prior_example
)
qda1_example = (
    -0.5 * (x_example - mu1_example) ** 2 / var1_example
    - 0.5 * np.log(var1_example)
    + log_prior_example
)

lda_boundary_example = 0.5 * (mu0_example + mu1_example)
VisualizationOut [11]
CollapseExpand
LDA and QDA score curves with one linear crossing and two quadratic crossings.
LDA and QDA discriminant scores for the worked one-dimensional example evaluated on a dense grid. LDA scores cross once at the midpoint, while QDA scores cross twice, so class 0 wins between the roots and class 1 wins in both tails. The doubled crossings show how class-specific variances produce a two-tail allocation that no single nondegenerate LDA threshold can express.

Implementation in NumPy and scikit-learn

We now implement both classifiers from first principles using NumPy, and then compare the results with scikit-learn's reference implementations. The fitting and scoring functions are defined first, then exercised on synthetic data. The from-scratch implementation is not intended to replace the library version; it is intended to make the mechanism auditable. In particular, it shows where the covariance inverse, the log-determinant, and the prior enter the score, and why numerical implementations avoid forming explicit matrix inverses whenever possible.

We compare two noisy two-dimensional Gaussian populations: the original unequal-covariance case and a shared-covariance case. Each has 80 training observations and 400 held-out observations per class. Separate random generators preserve the original unequal-covariance sample when we add the shared case.

Core LDA and QDA from NumPy primitives

The fitting routines accept a finite two-dimensional feature array X of shape (N,d)(N,d) and a one-dimensional label array y of length NN. Labels may be noncontiguous numbers or strings; the sorted classes array maps score columns back to those labels. Both routines require at least two classes and finite, positive-definite estimated covariances. Finite feature inputs can still overflow during fitting; both routines reject nonfinite fitted state with FloatingPointError. The sample-count conditions below are necessary, not sufficient, for positive definiteness.

LDA divides pooled within-class scatter by N−KN-K. It solves Σ^W⊤=M⊤\hat{\boldsymbol{\Sigma}}W^\top=M^\top once, where the rows of MM are the KK class means. The rows of WW are the weights wk⊤\mathbf{w}_k^\top from the derivation, and the length-KK intercept vector contains −12μ^k⊤wk+log⁡π^k-\frac12\hat{\boldsymbol{\mu}}_k^\top\mathbf{w}_k+\log\hat{\pi}_k. Prediction evaluates X @ coef.T + intercept, an (n,K)(n,K) score matrix for nn new observations, with no covariance solve during prediction. After a finiteness check, a Cholesky step checks numerical positive definiteness before that solve.

QDA estimates one unbiased covariance per class and stores its Cholesky factor LkL_k, satisfying Σ^k=LkLk⊤\hat{\boldsymbol{\Sigma}}_k=L_kL_k^\top, plus its log-determinant. To evaluate the Mahalanobis distance, the scoring routine solves LkZ=(X−μ^k)⊤L_k Z=(X-\hat{\boldsymbol{\mu}}_k)^\top by forward substitution. Here ZZ has shape (d,n)(d,n); summing its squared entries down each column yields the nn squared distances. This triangular solve avoids an explicit inverse and reuses the factors fitted once per class.

PythonIn [12]
CollapseExpand
import numpy as np


def feature_array(X, n_features=None):
    X = np.asarray(X, dtype=float)
    if X.ndim != 2 or X.shape[0] == 0 or X.shape[1] == 0:
        raise ValueError("X must be a nonempty two-dimensional feature array.")
    if not np.isfinite(X).all():
        raise ValueError("X must contain only finite values.")
    if n_features is not None and X.shape[1] != n_features:
        raise ValueError("X has the wrong number of features.")
    return X


def training_data(X, y):
    X = feature_array(X)
    y = np.asarray(y)
    if y.ndim != 1 or len(y) != len(X):
        raise ValueError("y must be one-dimensional with one label per row.")
    if y.dtype.kind not in "biufUS":
        raise ValueError("Use finite numeric labels or strings.")
    if y.dtype.kind in "biuf" and not np.isfinite(y).all():
        raise ValueError("Class labels must be finite.")
    classes, counts = np.unique(y, return_counts=True)
    if len(classes) < 2:
        raise ValueError("At least two classes are required.")
    return X, y, classes, counts


def class_priors(counts, priors):
    if priors is None:
        return counts / counts.sum()
    priors = np.asarray(priors, dtype=float)
    if (
        priors.shape != counts.shape
        or not np.isfinite(priors).all()
        or np.any(priors <= 0)
        or not np.isclose(priors.sum(), 1.0)
    ):
        raise ValueError(
            "priors must be positive and sum to one in classes order."
        )
    return priors / priors.sum()


def require_finite_model(*arrays):
    if any(not np.isfinite(array).all() for array in arrays):
        raise FloatingPointError(
            "Model calculations produced nonfinite values; check feature scales."
        )


def fit_lda(X, y, priors=None):
    X, y, classes, counts = training_data(X, y)
    N, d = X.shape
    K = len(classes)
    if N - K < d:
        raise ValueError("Unregularized LDA requires N - K >= d.")
    means = np.empty((K, d))
    scatter = np.zeros((d, d))
    for k, c in enumerate(classes):
        Xc = X[y == c]
        means[k] = Xc.mean(axis=0)
        centered = Xc - means[k]
        scatter += centered.T @ centered
    covariance = scatter / (N - K)
    require_finite_model(means, covariance)
    require_finite_model(
        np.linalg.cholesky(covariance)
    )  # Check the factor too.
    priors = class_priors(counts, priors)
    coef = np.linalg.solve(covariance, means.T).T  # K x d
    intercept = -0.5 * np.sum(means * coef, axis=1) + np.log(priors)
    require_finite_model(coef, intercept)
    return dict(
        classes=classes,
        means=means,
        priors=priors,
        covariance=covariance,
        coef=coef,
        intercept=intercept,
    )


def lda_score(X, model):
    X = feature_array(X, model["coef"].shape[1])
    return X @ model["coef"].T + model["intercept"]


def fit_qda(X, y, priors=None):
    X, y, classes, counts = training_data(X, y)
    K, d = len(classes), X.shape[1]
    if np.any(counts <= d):
        raise ValueError(
            "Unregularized QDA requires more than d rows per class."
        )
    means = np.empty((K, d))
    covariances = np.empty((K, d, d))
    factors = np.empty_like(covariances)
    logdets = np.empty(K)
    for k, c in enumerate(classes):
        Xc = X[y == c]
        means[k] = Xc.mean(axis=0)
        centered = Xc - means[k]
        covariances[k] = centered.T @ centered / (len(Xc) - 1)
        require_finite_model(means[k], covariances[k])
        factors[k] = np.linalg.cholesky(covariances[k])
        logdets[k] = 2 * np.log(np.diag(factors[k])).sum()
        require_finite_model(factors[k], logdets[k])
    return dict(
        classes=classes,
        means=means,
        covariances=covariances,
        priors=class_priors(counts, priors),
        factors=factors,
        logdets=logdets,
    )


def qda_score(X, model):
    X = feature_array(X, model["means"].shape[1])
    n, d = X.shape
    scores = np.empty((n, len(model["classes"])))
    for k, L in enumerate(model["factors"]):
        diff = X - model["means"][k]
        z = np.empty((d, n))
        # Forward substitution solves every observation's triangular system.
        for j in range(d):
            z[j] = (diff[:, j] - L[j, :j] @ z[:j]) / L[j, j]
        maha = np.sum(z**2, axis=0)
        scores[:, k] = (
            -0.5 * maha - 0.5 * model["logdets"][k] + np.log(model["priors"][k])
        )
    return scores

Both fits check the sample-count prerequisites before estimating covariances. These checks raise ValueError. Computed means and covariances must be finite before factorization, and factors, log-determinants, and LDA coefficients and intercepts must be finite before a model is returned; otherwise the routines raise FloatingPointError. A Cholesky step raises LinAlgError when it detects a failure of numerical positive definiteness. If factorization fails, check numerical range, feature scales, rank, and conditioning before choosing a remedy. Shrinkage toward a positive-definite target, as discussed earlier, can make a singular empirical estimate invertible. The 80 observations per class and two features in this example avoid the sample-count obstruction, but those counts alone do not guarantee positive definiteness. The finiteness and factorization checks test these numerical conditions for the actual estimates. Its quadratic form comes from a triangular-system solve, and its log-determinant comes from the factor's diagonal.

Comparing with scikit-learn

We now generate training and test data, fit both from-scratch classifiers and the scikit-learn equivalents, and report test accuracy and prediction agreement. The comparison has two purposes: it checks that our implementation recovers the same decision rule, and it separates the statistical question (which classifier fits this data better?) from the numerical question (do the implementations agree?).

PythonIn [13]
CollapseExpand
rng = np.random.default_rng(11)
n_train, n_test = 80, 400
mean_0_vec = np.array([0.0, 0.0])
mean_1_vec = np.array([3.5, 2.5])
cov_0_mat = np.array([[1.0, 0.3], [0.3, 1.2]])
cov_1_mat = np.array([[2.8, 1.3], [1.3, 1.6]])

X0_train = rng.multivariate_normal(mean_0_vec, cov_0_mat, size=n_train)
X1_train = rng.multivariate_normal(mean_1_vec, cov_1_mat, size=n_train)
X0_test = rng.multivariate_normal(mean_0_vec, cov_0_mat, size=n_test)
X1_test = rng.multivariate_normal(mean_1_vec, cov_1_mat, size=n_test)

X_train = np.vstack([X0_train, X1_train])
y_train = np.concatenate(
    [np.zeros(n_train, dtype=int), np.ones(n_train, dtype=int)]
)
X_test = np.vstack([X0_test, X1_test])
y_test = np.concatenate(
    [np.zeros(n_test, dtype=int), np.ones(n_test, dtype=int)]
)

lda_model = fit_lda(X_train, y_train)
qda_model = fit_qda(X_train, y_train)

scores_lda = lda_score(X_test, lda_model)
scores_qda = qda_score(X_test, qda_model)
pred_lda = lda_model["classes"][np.argmax(scores_lda, axis=1)]
pred_qda = qda_model["classes"][np.argmax(scores_qda, axis=1)]

from sklearn.discriminant_analysis import (
    LinearDiscriminantAnalysis,
    QuadraticDiscriminantAnalysis,
)

lda_ref = LinearDiscriminantAnalysis().fit(X_train, y_train)
qda_ref = QuadraticDiscriminantAnalysis().fit(X_train, y_train)
pred_lda_ref = lda_ref.predict(X_test)
pred_qda_ref = qda_ref.predict(X_test)

The shared-covariance experiment uses its own generator and the same means, class counts, and train/test sizes. Both classes now use cov_0_mat; their overlap and finite samples still make classification uncertain. We fit and evaluate each method before displaying any results.

PythonIn [14]
CollapseExpand
shared_rng = np.random.default_rng(12)
shared_X_train = np.vstack(
    [
        shared_rng.multivariate_normal(mean_0_vec, cov_0_mat, size=n_train),
        shared_rng.multivariate_normal(mean_1_vec, cov_0_mat, size=n_train),
    ]
)
shared_X_test = np.vstack(
    [
        shared_rng.multivariate_normal(mean_0_vec, cov_0_mat, size=n_test),
        shared_rng.multivariate_normal(mean_1_vec, cov_0_mat, size=n_test),
    ]
)
shared_lda = fit_lda(shared_X_train, y_train)
shared_qda = fit_qda(shared_X_train, y_train)
shared_pred_lda = shared_lda["classes"][
    np.argmax(lda_score(shared_X_test, shared_lda), axis=1)
]
shared_pred_qda = shared_qda["classes"][
    np.argmax(qda_score(shared_X_test, shared_qda), axis=1)
]
shared_lda_ref = LinearDiscriminantAnalysis().fit(shared_X_train, y_train)
shared_qda_ref = QuadraticDiscriminantAnalysis().fit(shared_X_train, y_train)
shared_pred_lda_ref = shared_lda_ref.predict(shared_X_test)
shared_pred_qda_ref = shared_qda_ref.predict(shared_X_test)

comparison_rows = []
for case, truth, fitted_pairs in [
    (
        "Unequal",
        y_test,
        [("LDA", pred_lda, pred_lda_ref), ("QDA", pred_qda, pred_qda_ref)],
    ),
    (
        "Shared",
        y_test,
        [
            ("LDA", shared_pred_lda, shared_pred_lda_ref),
            ("QDA", shared_pred_qda, shared_pred_qda_ref),
        ],
    ),
]:
    for method, primitive, reference in fitted_pairs:
        comparison_rows.append(
            (
                case,
                method,
                float((primitive == truth).mean()),
                float((reference == truth).mean()),
                float((primitive == reference).mean()),
            )
        )
ConsoleOut [15]
CollapseExpand
Covariance   Model   Scratch  Reference  Agreement
Unequal      LDA       0.907      0.907      1.000
Unequal      QDA       0.917      0.917      1.000
Shared       LDA       0.976      0.976      1.000
Shared       QDA       0.976      0.976      1.000

Each row compares accuracy on 800 held-out observations and label agreement between the primitive and reference implementation. The default reference LDA uses its SVD solver; our LDA precomputes linear coefficients with a solve, while our QDA stores Cholesky factors and uses forward substitution. For these full-rank samples in scikit-learn 1.8.0, the reference LDA SVD path uses the same N−KN-K within-class normalization as our LDA. The default unregularized QDA SVD path uses the same Nk−1N_k-1 class-covariance normalization as our QDA. Other LDA solvers can use a different covariance normalization, so changing solvers requires checking that convention again.

Agreement and accuracy answer different questions. Agreement measures whether the implementations return the same labels on these observations; it does not prove identical behavior for every input. Accuracy compares those labels with the generated test classes. The unequal-covariance population admits a Gaussian QDA Bayes rule, whereas LDA imposes a shared covariance. The shared-covariance population satisfies LDA's covariance assumption, while QDA estimates two covariances without a more appropriate population model. The displayed accuracies describe these two finite samples; neither covariance condition guarantees which fitted classifier will win on another held-out sample.

Key Parameters

The main parameters that affect how the fitted LDA and QDA models behave are listed below. These settings belong to the scikit-learn estimators; the primitive fits implement only the unregularized covariance estimates and an optional priors array in sorted classes order. Both primitive fits store class means and covariance arrays. LDA also stores precomputed weights (coef) and intercepts; QDA stores Cholesky factors and log-determinants. These are different return dictionaries.

  • priors: Class prior probabilities (default: None, which uses the observed class frequencies). Set them explicitly when the deployment population differs from the training sample, because the prior enters each class score. Changing prior ratios changes score differences and can move, create, or remove decision boundaries; identical class densities can still give a constant winning class.
  • shrinkage (LDA, scikit-learn): Covariance regularization (default: None). A value in [0,1][0,1] blends the pooled covariance toward a scaled identity target, while 'auto' applies the Ledoit-Wolf estimate. It is available only with the 'lsqr' or 'eigen' solver.
  • solver (LDA, scikit-learn): Factorization method (default: 'svd'). With the default store_covariance=False, the 'svd' path avoids forming the covariance explicitly; it cannot be combined with shrinkage. Use 'lsqr' or 'eigen' when shrinkage is needed.
  • reg_param (QDA, scikit-learn's default 'svd' solver): regularization strength in [0,1][0,1] (default: 0.0). It blends the retained empirical covariance eigenvalues, the squared singular values divided by Nk−1N_k-1, toward one. When the SVD contains a full feature-space basis, this corresponds to blending the class covariance toward identity. With Nk<dN_k<d, the compact SVD lacks feature directions, so this parameter cannot restore full rank; the installed 1.8 implementation raises an error. In version 1.8, solver='eigen' with a suitable shrinkage value supplies a full-covariance alternative.
  • store_covariance (LDA, scikit-learn): For the 'svd' solver, whether to compute and retain the weighted within-class covariance (default: False). The 'lsqr' and 'eigen' solvers always compute and store it. Enable it with 'svd' when covariance diagnostics such as eigenvalues or condition numbers are part of the analysis.

Practical Implications

LDA and QDA are candidates when a single Gaussian is a plausible approximation to each observed class. The largest-posterior decision rule used here assumes equal misclassification costs; with unequal costs, choose the label that minimizes posterior expected loss. A single Gaussian cannot exactly represent a multimodal or asymmetric class density, although its hard-label boundary can still be correct. Unmeasured predictors can limit achievable performance for any classifier; omitting features does not by itself invalidate Gaussian class models for the observed features. The central choice between the two methods is whether a shared covariance is credible. Comparing the estimated class covariances by scale, orientation, and uncertainty is more informative than comparing eigenvalues alone, since equal eigenvalues can hide different orientations. Similar covariances support LDA's pooled estimate; material differences support QDA when each class has enough observations to estimate its own covariance. Because both are generative models, the comparison should end with held-out classification and probability checks, not with inspection of the fitted parameters alone.

LDA is also a supervised projection. Under the shared-covariance Gaussian model, posterior comparisons depend only on the span of the covariance-adjusted mean differences, a subspace of dimension at most min⁡(d,K−1)\min(d, K - 1). Projecting onto that subspace preserves the model's class-posterior information, whereas projecting onto the raw class means generally does not. This can be useful for visualization or for reducing inputs to a downstream classifier, but an accuracy improvement is not guaranteed, especially when the Gaussian model is misspecified. QDA can produce decision regions that are curved or even disjoint, so mapping the fitted regions is useful before deployment.

Best Practices

Start with LDA as a baseline, then fit QDA when the covariance estimates are stable and validation supports the extra flexibility. Use stratified cross-validation for model selection and a separate held-out set for the final comparison. For scikit-learn's default QDA solver when every class has at least dd observations, compare reg_param values on a grid such as 0, 0.01, 0.05, 0.1, 0.2, and 0.5; retain the unregularized candidate when it is feasible. When 2≤Nk<d2 \le N_k < d, version 1.8's 'eigen' solver with suitable covariance shrinkage provides a different QDA candidate family. Every class still requires at least two observations. For LDA, shrinkage='auto' with solver='lsqr' or solver='eigen' provides a data-driven option, while manual shrinkage values can be tuned in the same way. Set priors explicitly when they are known; otherwise use the empirical priors and remember that they depend on the relative class sampling rates. Cross-validation can compare candidate regularization strengths, but a data-driven covariance estimator follows its own rule and should not be conflated with a manually tuned grid.

Inspect the fitted covariances before trusting the scores. Eigenvalues, condition numbers, and the success or failure of a Cholesky factorization are useful diagnostics; a factorization failure calls for checking rank and numerical range before choosing a remedy, such as rescaling, regularization, dimension reduction, or pooling. A large eigenvalue range is a warning to investigate, not a universal cutoff that mandates shrinkage, because it can reflect units, strong correlation, or estimation noise. Shrinkage has different endpoints that should be described accurately. Blending each class covariance toward a common pooled covariance approaches LDA at full shrinkage; blending toward identity makes the covariances spherical. A common spherical target yields ordinary Euclidean nearest-centroid classification when the priors are equal; unequal priors generally add class-specific score offsets. Class-specific spherical targets with different scales can retain quadratic boundaries. A common identity endpoint is a spherical member of the LDA model family, but it generally differs from the ordinary pooled-covariance LDA fit.

Data Requirements and Preprocessing

The methods require numeric features and labeled classes, and they assume that a Gaussian density is a reasonable description of each class. Use a task-specific precision target to plan per-class samples. Under independent, identically distributed Gaussian observations within a class, the unbiased variance estimate of one feature has relative standard error 2/(Nk−1)\sqrt{2/(N_k-1)}, meaning its standard deviation over repeated samples divided by that feature's true variance. This is approximately 14.2%14.2\% at Nk=100N_k=100 and 10.0%10.0\% at Nk=200N_k=200. These percentages concern a marginal variance; they do not guarantee accurate or well-conditioned full covariance estimates, classification, or calibration. Compare per-class covariance diagnostics and held-out classification and probability performance to decide whether more data, pooling, or shrinkage is needed. For QDA, N_k > d is necessary for a full-rank empirical covariance but not sufficient, since collinearity, outliers, and noise can still make the estimate singular or ill-conditioned. LDA's pooled covariance has the analogous requirement N - K >= d, with the same caveats. Imbalanced classes make the empirical priors reflect the sampling proportions; changing relative class sampling rates can change the empirical priors inferred from the resulting counts. Under a class-weighted generative likelihood, changing relative class weights can change the effective priors. Multiplying every class count or every class weight by the same positive factor preserves normalized proportions. If priors are supplied explicitly and held fixed, these operations do not change that supplied vector. Treat resampling and class weighting as deliberate modeling choices.

Standardization is not a mathematical prerequisite for exact unregularized affine equivariance, but it can improve numerical conditioning when feature units differ substantially. With shrinkage toward identity or a scaled identity, rescaling can change the target's geometry relative to the data and can change the fitted rule, especially under nonuniform feature rescaling. Uniform rescaling preserves equivariance for a trace-scaled identity target, T=(tr⁡Σ^/d)I\mathbf T=(\operatorname{tr}\hat{\boldsymbol\Sigma}/d)\mathbf I, when the shrinkage strength is held fixed and means and covariances are transformed consistently. Tune preprocessing and regularization together. Handle missing values before computing means and covariances; imputation can distort the covariance structure and should be checked rather than assumed harmless. A feature that is constant within one class makes that class covariance singular for QDA, and a feature constant within every class makes the pooled covariance singular for LDA. Strong collinearity and extreme outliers can also undermine the estimates; robust covariance estimation addresses a different problem and is outside the scope of this chapter.

Common Pitfalls

A frequent mistake is fitting QDA when each class has too few observations relative to the feature dimension, then treating unstable or singular covariance estimates as meaningful. Another is choosing between LDA and QDA by training accuracy or by comparing eigenvalues alone; equal eigenvalues do not imply equal covariance matrices, and different orientations matter. Ignoring priors is also common: unequal class proportions change relative class scores and can move, create, or remove decision boundaries, while changing relative class sampling rates or class weights can change the empirical or effective priors inferred from the modified data. Common multipliers preserve normalized proportions, and explicitly fixed priors do not change automatically. Assuming that QDA always improves when covariances differ is another pitfall, because the extra covariance estimates can add more variance than the additional flexibility removes.

Identity shrinkage should not be confused with a path to the ordinary pooled-covariance LDA fit: a common identity endpoint belongs to the LDA model family, but generally uses a different shared covariance. Shrinking toward a common pooled covariance approaches LDA at full shrinkage, whereas shrinking toward identity changes the geometry of the class clouds. Evaluating only hard labels is also risky when probabilities are used downstream; noisy covariance estimates can leave accuracy acceptable while calibration degrades. Preprocessing steps such as scaling or imputation should be fit inside cross-validation, not before splitting, or the covariance estimates and performance can be optimistic. Before deployment, map the fitted decision regions, since QDA can assign the same class in disjoint regions.

Computational Considerations

For dense full-covariance matrix kernels, constructing the within-class scatter once class subsets are available costs O(N d^2). LDA then factors one shared covariance at O(d^3) and can form K linear weight vectors at O(K d^2); prediction costs O(K d). QDA factors K class covariances at O(K d^3), stores O(K d^2) covariance entries, and scores each observation in O(K d^2). These matrix-kernel bounds exclude the primitive fits' O(NK) repeated class-mask comparisons and their separate input validation and np.unique work. Solver choices and regularized covariance models can change the constants and, in some cases, the scaling.

Dense covariance storage grows quadratically with feature dimension, while dense covariance factorization costs grow cubically. Use Cholesky or SVD-based solves rather than forming explicit inverses, and consider dimensionality reduction, a diagonal covariance model, or shrinkage when d is large relative to the available samples. Covariance regularization can make a full-rank fit possible within the solver's stated domain, but it does not remove the dense covariance cost. There is no hardware-independent feature-count cutoff at which one method becomes impractical; benchmark the actual solver, regularization, and preprocessing pipeline. When the number of observations is large but memory is limited, sufficient statistics can compute the scatter matrices in one pass.

Performance and Deployment Considerations

Evaluate the fitted model with stratified cross-validation and a final held-out test set. Accuracy and balanced accuracy summarize hard-label performance; log loss, the Brier score, and calibration curves assess probabilistic output; a confusion matrix shows which classes are confused. Compare LDA, QDA, and regularized variants against a simple baseline, and report uncertainty such as bootstrap confidence intervals when differences are small. If probabilities drive downstream decisions, check calibration on data not used to fit the classifier and recalibrate if necessary.

For deployment, store the class means, priors, and covariance factors rather than the raw training data. LDA can precompute its linear weights and intercepts for fast prediction, while QDA stores one factor per class. Monitor the input distribution, missingness, and class priors. Investigate detected drift and use task-relevant assessment to decide whether to refit the model or update its priors. When refitting after a population shift, check the newly estimated covariance conditioning. A fixed model's stored covariance factors do not change just because the incoming population changes. After fitting, scoring one observation reuses stored weights or factors and costs O(Kd)O(Kd) for LDA or O(Kd2)O(Kd^2) for dense QDA. For QQ observations, total scoring work grows as O(QKd)O(QKd) or O(QKd2)O(QKd^2), respectively, and can exceed the cost of one fit. Possible statistical risks include stale parameters, changed priors, and degraded calibration; monitor accuracy or balanced accuracy when only labels matter, and monitor log loss and calibration when probabilities matter.

Summary

Linear and quadratic discriminant analysis model each class as a multivariate Gaussian and compare its posterior probability. Bayes' rule turns the Gaussian log density into a Mahalanobis-distance term, a log-determinant penalty, and a log-prior term. With the true parameters and priors, choosing the largest score minimizes zero-one misclassification risk under that model; fitted versions estimate this rule. A shared covariance makes the log-determinant and feature-quadratic terms common to all classes, leaving linear score differences in LDA. Class-specific covariances retain quadratic score differences in QDA, which can produce curved or degenerate boundaries.

The two methods trade covariance flexibility against estimation uncertainty. LDA estimates one shared covariance, KK means, and K−1K-1 priors, for d(d+1)2+Kd+(K−1)\frac{d(d+1)}{2}+Kd+(K-1) generative parameters. QDA estimates KK class-specific covariances, the same number of means, and the same number of priors, for K d(d+1)2+Kd+(K−1)K\,\frac{d(d+1)}{2}+Kd+(K-1) parameters. Here dd is the feature dimension and KK the class count. QDA increases the covariance parameter count by a factor of KK, rather than multiplying every parameter count by KK. Shrinkage toward a common pooled covariance connects class-specific estimates to LDA; shrinkage toward spherical targets has different endpoints and must be interpreted with its target and prior assumptions stated.

Estimating covariances reliably becomes particularly difficult when the sample size per class is small relative to the feature dimension. When samples are small relative to dimension or features are dependent, rank deficiency, poor conditioning, and estimation uncertainty need separate attention; shrinkage is one possible response. If Gaussianity is violated by heavy tails, multimodality, or other shapes, the Bayes-optimality guarantee no longer follows from the Gaussian model, and probability calibration may suffer. The hard-label boundary can nevertheless still coincide with the true Bayes boundary, so misspecification does not prove classification failure. LDA and QDA make their distributional assumptions explicit; using them well requires checking the fitted estimates and evaluating predictions under the conditions that matter for the task.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about linear and quadratic discriminant analysis.

Linear and Quadratic Discriminant Analysis Quiz

Question 1 of 80 of 8 completed
What is the central modeling difference between LDA and QDA in this chapter?
Join the discussionAsk a question or share what you learned.

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!

A different angle
All writing
Machine Learning

Naive Bayes

Learn how naive Bayes classifiers work: Bayes decision rule, Gaussian and multinomial event models, smoothing, log-sum-exp, and NumPy implementation.

64 min read
Machine Learning

k-Nearest Neighbors

Build kNN from scratch, covering distance metrics, majority voting, local averaging, feature scaling, distance weighting, and the curse of dimensionality.

56 min read
Machine Learning

Support Vector Machines and Support Vector Regression

Learn how support vector machines and support vector regression work, from margins and hinge loss to kernels, dual coefficients, epsilon tubes, and scikit-learn implementation.

50 min read

About the author

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2026linearquadratic, author = {Michael Brenndoerfer}, title = {Linear and Quadratic Discriminant Analysis}, year = {2026}, url = {https://mbrenndoerfer.com/writing/discriminant-analysis-from-scratch}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-10-28} }
APAAcademic
Michael Brenndoerfer (2026). Linear and Quadratic Discriminant Analysis. Retrieved from https://mbrenndoerfer.com/writing/discriminant-analysis-from-scratch
MLAAcademic
Michael Brenndoerfer. "Linear and Quadratic Discriminant Analysis." 2026. Web. October 28, 2026. <https://mbrenndoerfer.com/writing/discriminant-analysis-from-scratch>.
CHICAGOAcademic
Michael Brenndoerfer. "Linear and Quadratic Discriminant Analysis." Accessed October 28, 2026. https://mbrenndoerfer.com/writing/discriminant-analysis-from-scratch.
HARVARDAcademic
Michael Brenndoerfer (2026) 'Linear and Quadratic Discriminant Analysis'. Available at: https://mbrenndoerfer.com/writing/discriminant-analysis-from-scratch (Accessed: October 28, 2026).
SimpleBasic
Michael Brenndoerfer (2026). Linear and Quadratic Discriminant Analysis. https://mbrenndoerfer.com/writing/discriminant-analysis-from-scratch
Newsletter

Stay up to date

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

Unsubscribe at any time. Privacy policy