Linear and Quadratic Discriminant Analysis
Build LDA and QDA classifiers from Gaussian class models and Bayes' rule, with covariance estimation, shrinkage, and NumPy implementations.
Machine Learning from ScratchAll chapters 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
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 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 classes when it makes the mathematics clearer. Notation is standard: is the feature vector, the class label, the prior probability of class , the class-conditional mean, and the class-conditional covariance matrix. We assume 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 covariance matrix, mean vectors, and priors. For features and two classes (), the covariance contributes distinct entries, the means contribute , and the priors contribute , for exactly free generative parameters. Ordinary binary logistic regression has coefficients including its intercept; fitting two separate one-vs-rest models would store 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 full covariance estimates, each with 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 observations once class subsets are available costs . QDA then factors covariance matrices at a cost of and scores one new observation in . LDA factors a single shared covariance and can precompute its class weights: the additional work is , after which a prediction costs . 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, can rival the number of observations per class. A centered class sample has rank at most . For , its empirical covariance is singular when ; 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 , 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 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 covariance parameters rather than LDA's . 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 tend to look like?", we ask "how plausible is this particular 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
where denotes the multivariate normal density
where:
- : the mean vector of the Gaussian, which locates the center of the ellipsoidal cloud
- : the covariance matrix, which sets the shape and orientation of the cloud
- : the covariance determinant; its square root is proportional to the volume of an ellipsoid with fixed Mahalanobis radius
- : the squared Mahalanobis distance from to
- : the number of feature dimensions
The mean locates the center of a class cloud, and the covariance determines its shape and orientation. For ellipsoids with the same Mahalanobis radius, volume is proportional to . A larger determinant therefore means a larger such ellipsoid and a lower Gaussian peak density. The quadratic form 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:
where:
- : the posterior probability that the observation belongs to class
- : the class-conditional density of given class
- : the prior probability of class
- : the marginal density of 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 ." 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
where:
- : the feature vector being classified
- : the mean vector of class
- : the covariance matrix of class
- : the prior probability of class
- : the number of features
- : the determinant of the class covariance
The first term is identical for every and drops out of any comparison. Dropping it, we obtain the discriminant function
where:
- : the discriminant score for class at point
- : the feature vector
- : the mean vector of class
- : the covariance matrix of class
- : the prior probability of class
- : the squared Mahalanobis distance from to class 's mean, in units of class 's own covariance
- : the determinant of the class covariance, penalizing diffuse class clouds
The Bayes decision rule is then .
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 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 , 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:
where:
- : the two classes being compared
- : the discriminant scores for classes and at point , each defined up to the same additive constant
- : the posterior probabilities of classes and given
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 and 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: for all . 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 term is the same for all classes and drops out:
where:
- : the discriminant score for class at point
- : the feature vector being classified
- : the mean vector of class
- : the shared covariance matrix common to all classes
- : the prior probability of class
- : the squared Mahalanobis distance from to class 's mean in the shared metric
We now expand the quadratic form. In matrix notation,
After multiplying the expansion by , the term 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
This is a linear function of . Define
where:
- : the covariance-adjusted weight vector for class
- : the class- bias (intercept), combining the mean-quadratic term and the log prior
- : the shared covariance, class- mean, and class- prior, as defined earlier
so that is a linear score. For two classes, the decision boundary between them is , or
with
where:
- : the covariance-adjusted normal to the boundary, pointing toward increasing class-0-versus-class-1 score
- : the bias term that shifts the boundary based on the class means and priors
- : the shared covariance, class means, and class priors, as defined earlier
The sign convention matters: equals , so a positive value favors class 0 after accounting for the priors. In general, is not parallel to . 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 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 and the within-class scatter is , so the ratio is
where:
- : the direction onto which the data are projected
- : the difference between the class means, setting the between-class separation
- : the shared (within-class) covariance, setting the within-class spread
When the class means differ, a maximizing nonzero direction is proportional to . Its sign and nonzero scale do not change the variance ratio. If the means coincide, 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 mean parameters and shared symmetric-covariance parameters. Estimating the class priors adds free parameters under , giving in total. For fixed , the covariance term grows quadratically in , while the mean term grows linearly. In the multiclass setting, LDA needs at most discriminant directions. The centered class means have at most 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 , where 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 . 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
where:
- : the discriminant score for class at point
- : the feature vector being classified
- : the mean vector of class
- : the class-specific covariance matrix for class
- : the prior probability of class
- : the squared Mahalanobis distance from to class 's mean, in class 's own metric
- : the determinant of the class covariance, penalizing diffuse class clouds
When varies with , the 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 , 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 ( and ), the boundary is
which is the equation written with the class-0 terms leading.
where:
- : the feature vector at which the boundary is evaluated
- : the covariance matrices of classes 0 and 1 (both assumed invertible)
- : the mean vectors of classes 0 and 1
- : the constant term, defined explicitly below
The constant collects the terms that do not depend on . Writing out in full (the form matching the displayed boundary equation) and keeping only the terms without gives
where:
- : the feature vector
- : the covariance matrices of classes 0 and 1
- : the class mean vectors
- : the class prior probabilities
- : the constant term collecting all contributions that do not depend on (the mean-quadratic and log-determinant terms plus the log prior ratio)
The boundary equation has the form of a general quadratic in : the symmetric matrix coefficient of the second-order term is , the linear coefficient is , and 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 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 term by term, consistent with the sign convention used above. The -independent contribution is
matching above, while the -dependent part yields the quadratic and linear coefficients already stated.
The number of free parameters in QDA is (means) plus (priors) plus (covariances), so its covariance term grows 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]Expand

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, , where counts observations in class and . The maximum-likelihood class means are . For covariance estimates, this chapter uses the unbiased sample convention rather than the Gaussian maximum-likelihood convention:
where:
- : the estimated covariance matrix of class
- : the number of observations in class
- : the empirical mean of class
- : the -th feature vector, associated with label
The denominator gives the unbiased class covariance estimator for . When the centered class scatter is positive definite, the Gaussian covariance maximum-likelihood estimator divides that same scatter by . 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 grows. For LDA, the unbiased pooled covariance estimator is
where:
- : the pooled estimate of the shared covariance matrix used by LDA
- : the total number of observations across all classes
- : the number of classes
- : the number of observations in class
- : the empirical mean of class
- : the -th feature vector, associated with label
This pools the within-class scatter and divides by the residual degrees of freedom . When the pooled scatter is positive definite, the shared-covariance Gaussian maximum-likelihood estimator instead divides that same scatter by . 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 residual degrees of freedom rather than one class's . For a class with , 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 . If class covariances differ, pooling instead estimates a compromise matrix.
For a centered class sample with , . Thus forces singularity: for and , there are at most 49 nonzero eigenvalues and the inverse does not exist. Having is necessary, but not sufficient, for nonsingularity. LDA's pooled scatter similarly has rank at most . 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:
where:
- : the shrunk (regularized) covariance estimate for class at mixing level
- : the mixing (shrinkage) parameter in controlling the bias-variance tradeoff; recovers the empirical covariance and yields the pure target
- : the empirical covariance of class
- : the shrinkage target, for example the identity matrix or a scaled version
At , the estimate is the empirical covariance; at , it is the target . If the empirical covariance is positive semidefinite and the target is positive definite, any 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 with invertible , then and . Mahalanobis distances are unchanged, while . The full Gaussian discriminants therefore change by , common to every class. Reduced LDA scores, after dropping their shared quadratic term, can acquire a different common term depending on , 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):
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 . In the earlier sign convention, , so , where . Equivalently, its class-1 logit has coefficient . 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 , so holds with , a quadratic function of . 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 . 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]Expand
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]Expand
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]Expand
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]Expand

VisualizationOut [9]Expand

The first figure shows LDA's straight boundary, whose normal uses the pooled estimate and the class means, . 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 full 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:
Both classes have four observations, so . 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 , and the unbiased variance is
For class 1, the mean is , and the unbiased variance is
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 . The numerator is the sum of the within-class squared deviations: class 0 contributes and class 1 contributes , for a total of . Thus
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 , the one-dimensional linear discriminant is
where:
- : the estimated mean of class
- : the pooled variance shared by both classes
- : the estimated prior probability of class
- : the (scalar) feature value being classified
Working through the two classes:
At a test point :
The larger score is , so LDA predicts class 0. At a test point :
Here LDA predicts class 1. The boundary between the two classes is where , i.e., . Solving: subtract from both sides to get ; add to both sides to get ; divide by to get . As expected, this is the midpoint of the two class means, . 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
For class 0:
where:
- : the estimated mean of class 0
- : the unbiased estimated variance of class 0
- : the estimated prior probability of class 0
- : the test point at which the class-0 QDA score is evaluated
For class 1:
At :
The quadratic contributions are exactly for class 0 and for class 1. The combined log-variance and log-prior constants are approximately and , 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 .
QDA also predicts class 0 at because . At , where the exact LDA scores tie:
QDA favors class 1 at , 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 and . Class 1 pays the larger log-variance penalty, approximately rather than , 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 gives a quadratic equation. We keep the exact variances and logarithms until the numerical evaluation:
The equal log-prior terms cancel. Multiplying by and collecting the log-variance difference gives
The left-hand side expands to . Define the boundary polynomial
Here is the exact quadratic whose zeros are the QDA ties, and is the natural logarithm. Since , the constant term is approximately . The quadratic formula gives
To determine the winning regions, subtract the exact class-0 score from the class-1 score:
The multiplier is positive, so the score difference has the same sign as . Its positive leading coefficient makes 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]Expand
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]Expand

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 and a one-dimensional label array y of length . 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 . It solves once, where the rows of are the class means. The rows of are the weights from the derivation, and the length- intercept vector contains . Prediction evaluates X @ coef.T + intercept, an score matrix for 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 , satisfying , plus its log-determinant. To evaluate the Mahalanobis distance, the scoring routine solves by forward substitution. Here has shape ; summing its squared entries down each column yields the squared distances. This triangular solve avoids an explicit inverse and reuses the factors fitted once per class.
PythonIn [12]Expand
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 scoresBoth 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]Expand
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]Expand
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]Expand
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 within-class normalization as our LDA. The default unregularized QDA SVD path uses the same 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 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 defaultstore_covariance=False, the'svd'path avoids forming the covariance explicitly; it cannot be combined withshrinkage. Use'lsqr'or'eigen'when shrinkage is needed.reg_param(QDA, scikit-learn's default'svd'solver): regularization strength in (default:0.0). It blends the retained empirical covariance eigenvalues, the squared singular values divided by , toward one. When the SVD contains a full feature-space basis, this corresponds to blending the class covariance toward identity. With , 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 suitableshrinkagevalue 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 . 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 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 , 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 , meaning its standard deviation over repeated samples divided by that feature's true variance. This is approximately at and at . 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, , 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 for LDA or for dense QDA. For observations, total scoring work grows as or , 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, means, and priors, for generative parameters. QDA estimates class-specific covariances, the same number of means, and the same number of priors, for parameters. Here is the feature dimension and the class count. QDA increases the covariance parameter count by a factor of , rather than multiplying every parameter count by . 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
Join the discussionAsk a question or share what you learned.
Keep exploring
Reference
Citation details
Cite or share this article.

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!