Linear Discriminant Analysis


Theoretical Background

Linear Discriminant Analysis (LDA) is a supervised classification and dimensionality reduction technique introduced by Fisher (1936). Given \(K\) classes and \(p\) predictors, LDA finds a set of linear combinations of the predictors — called discriminant functions — that maximise the ratio of between-class variance to within-class variance.

Formally, the \(k\)-th discriminant function is:

\[z_k = \mathbf{a}_k^\top \mathbf{x}\]

where \(\mathbf{a}_k\) is the vector of discriminant coefficients (the scaling matrix returned by MASS::lda) and \(\mathbf{x}\) is the vector of predictor values for an observation. For \(K\) classes there are at most \(\min(K-1, p)\) discriminant functions.

Classification is performed by assigning each observation to the class whose centroid (in the discriminant space) is closest, weighted by the prior class probabilities:

\[\hat{y} = \arg\max_k \left[ \log \pi_k - \tfrac{1}{2} \delta_k^2(\mathbf{x}) \right]\]

where \(\pi_k\) is the prior probability of class \(k\) and \(\delta_k^2(\mathbf{x})\) is the squared Mahalanobis distance from \(\mathbf{x}\) to the \(k\)-th class centroid.

Key assumptions:

Assumption Description
Multivariate normality Predictors follow a multivariate normal distribution within each class
Homogeneity of covariance All classes share the same covariance matrix \(\boldsymbol{\Sigma}\) (relaxed in QDA)
No perfect multicollinearity Predictors must not be linearly dependent
Adequate sample size The smallest class should comfortably exceed the number of predictors \(p\)

The singular values (SVD) returned by MASS::lda quantify the importance of each discriminant function: larger values indicate greater between-class separation relative to within-class scatter.


Dataset — infert

The infert dataset (available in base R) is drawn from a matched case-control study on secondary infertility. It contains data from 248 women and examines whether a history of induced or spontaneous abortions is associated with infertility.

Variable Description
case Outcome — 1 = infertile case, 0 = fertile control
education Educational attainment (0–5 yrs, 6–11 yrs, 12+ yrs)
age Age at time of interview
parity Number of prior pregnancies carried to term
induced Number of prior induced abortions (0, 1, 2+)
spontaneous Number of prior spontaneous abortions (0, 1, 2+)
stratum Matching stratum identifier
pooled.stratum Pooled stratum for stratified analysis

The classification goal is to correctly assign each woman to the infertile or fertile group based on her reproductive history and demographics.

Code
data(infert)

# Class balance
cat("Class distribution (case):\n")
Class distribution (case):
Code
print(table(infert$case))

  0   1 
165  83 
Code
cat(sprintf("\nPrevalence: %.1f%% cases\n", 100 * mean(infert$case)))

Prevalence: 33.5% cases
Code
# Visual overview: induced and spontaneous abortions by case status
p1 <- ggplot(infert, aes(x=factor(induced), fill=factor(case))) +
  geom_bar(position="dodge") +
  scale_fill_manual(values=c("#2166ac","#d6604d"), labels=c("Control","Case")) +
  theme_bw() +
  labs(title="Induced abortions by case status",
       x="Number of induced abortions", y="Count", fill=NULL)

p2 <- ggplot(infert, aes(x=factor(spontaneous), fill=factor(case))) +
  geom_bar(position="dodge") +
  scale_fill_manual(values=c("#2166ac","#d6604d"), labels=c("Control","Case")) +
  theme_bw() +
  labs(title="Spontaneous abortions by case status",
       x="Number of spontaneous abortions", y="Count", fill=NULL)

plot_multiplot(p1, p2, cols=2)

[[1]]

Model Specification

We fit LDA using all available predictors to discriminate infertile cases from controls. Because there are only two classes (\(K = 2\)), LDA produces a single discriminant function (\(K - 1 = 1\)).

Note that case ~ . also enters the matching identifiers stratum and pooled.stratum as if they were numeric predictors, although their values are arbitrary labels. Likewise, age and parity were matching variables, so by design they carry almost no discriminating information. In a real analysis the identifiers should be dropped (e.g. case ~ education + age + parity + induced + spontaneous), and matched data are better analysed with conditional logistic regression (survival::clogit). The education dummies and the 0/1/2 abortion counts also depart from the multivariate normality assumption. They are kept here to illustrate the mechanics of LDA.

Code
model <- MASS::lda(case ~ ., data=infert)

Results

Code
result <- report_lda(model=model)

Prior Probabilities and Class Counts

Prior probabilities reflect the proportion of each class in the training data. LDA uses these priors to penalise or favour predictions toward a class based on its base rate. Because each infertile case was matched with two fertile controls, the priors are about 2:1 (0.665 for controls, 0.335 for cases), which pushes borderline observations toward the control class.

Code
result$prior_counts
      prior counts mean.education6.11yrs mean.education12..yrs mean.age mean.parity mean.induced mean.spontaneous mean.stratum mean.pooled.stratum
0 0.6653226    165             0.4848485             0.4666667 31.49091    2.084848    0.5636364        0.3878788     41.80606            33.52121
1 0.3346774     83             0.4819277             0.4698795 31.53012    2.108434    0.5903614        0.9518072     42.00000            33.69880

Class Means (Group Centroids)

The group means show the average value of each predictor in each class. Large differences between class means on a predictor indicate that it is likely to be a strong discriminator. Here, only spontaneous shows a clearly higher mean in the case group (0.95 vs 0.39). induced differs only slightly (0.59 vs 0.56), and age and parity are almost identical because they were matching variables.

Code
round(result$means, 3)
  education6-11yrs education12+ yrs    age parity induced spontaneous stratum pooled.stratum
0            0.485            0.467 31.491  2.085   0.564       0.388  41.806         33.521
1            0.482            0.470 31.530  2.108   0.590       0.952  42.000         33.699

Discriminant Function Coefficients

The coefficients (the scaling matrix) define the linear combination of predictors that maximises class separation. They are raw (unstandardised) weights, scaled so that the discriminant scores have unit within-group variance. Their size therefore depends on each predictor’s units (e.g. age is per year), so to compare predictors multiply each coefficient by that predictor’s pooled within-group standard deviation, or use structure correlations.

Code
round(result$coeficients, 4)
                     LD1
education6-11yrs  0.7502
education12+ yrs  2.0488
age               0.0630
parity           -0.4574
induced           1.1616
spontaneous       1.9802
stratum          -0.0019
pooled.stratum   -0.0521

Positive coefficients push scores toward the infertile case class; negative coefficients push toward the control class (the case centroid lies on the positive side of LD1 in this fit; the sign of a discriminant function is arbitrary). education12+ yrs (2.05) and spontaneous (1.98) carry the largest positive raw weights, followed by induced (1.16). Because raw coefficients depend on units, these weights alone do not rank the predictors’ importance; a standardised comparison is needed for that.

Model Description — Singular Values

MASS::lda reports the singular value (SVD) of the single discriminant function as the ratio of between- to within-group standard deviation on that function. The between-group part is weighted by group size, so the value grows with the sample size and is not an effect size. A more interpretable measure of separation is the distance between the two class centroids on LD1, which is expressed in within-class standard deviation units because the discriminant scores have unit within-group variance.

Code
result$model_description
  Observations      SDV
1          248 8.428289
Code
cat(sprintf("\nVariance explained by LD1: 100%% (only one function for K=2)\n"))

Variance explained by LD1: 100% (only one function for K=2)
Code
cat(sprintf("Between/within-class SD ratio: %.4f\n", result$model_description$SDV))
Between/within-class SD ratio: 8.4283
Code
centroids<-tapply(predict(model)$x[,1],infert$case,mean)
cat(sprintf("LD1 centroids: control = %.3f, case = %.3f (distance = %.3f within-class SD)\n",centroids["0"],centroids["1"],diff(centroids)))
LD1 centroids: control = -0.380, case = 0.755 (distance = 1.134 within-class SD)

The two class centroids are only about 1.1 within-class standard deviations apart, a moderate separation, even though the singular value (8.43) looks large.

Discriminant Scores

We can project all observations onto the single discriminant axis and visualise the overlap between classes. Good discrimination appears as clearly separated distributions.

Code
scores <- predict(model)$x
df_scores <- data.frame(LD1=scores[,1], case=factor(infert$case, labels=c("Control","Case")))
# LD1 score where P(case)=0.5: midpoint of centroids shifted by the log prior ratio
boundary<-mean(centroids)+log(model$prior["0"]/model$prior["1"])/diff(centroids)

ggplot(df_scores, aes(x=LD1, fill=case)) +
  geom_density(alpha=0.55) +
  scale_fill_manual(values=c("#2166ac","#d6604d")) +
  geom_vline(xintercept=boundary, linetype="dashed", colour="grey40") +
  theme_bw() +
  labs(title="Distribution of discriminant scores (LD1)",
       subtitle=sprintf("Dashed line = LD1 score where P(case) = 0.5 (%.2f)",boundary),
       x="Linear Discriminant 1", y="Density", fill=NULL)

The degree of overlap between the two distributions reflects classification uncertainty. A clear bimodal separation would indicate near-perfect discrimination; substantial overlap signals limited discriminating power.


Classification Performance

Confusion Matrix (Resubstitution)

The confusion matrix below shows how well the model classifies the training observations. Because this is a resubstitution estimate (fitted and evaluated on the same data), it is optimistically biased — it overestimates true generalisability.

Code
result$cmatrix
         0     1    sum    p
0   146.00 42.00 188.00 0.78
1    19.00 41.00  60.00 0.68
sum 165.00 83.00 248.00 1.00
p     0.88  0.49   1.00 0.75

Rows give the predicted class and columns the observed class, and each cell is a count of observations; the diagonal holds the correct classifications. The p column is the precision of each predicted class, the p row is the recall of each observed class (0.88 for controls but only 0.49 for cases), and the bottom-right cell is the overall accuracy (0.75).

Code
# Overall accuracy from the confusion matrix
pred_class <- predict(model)$class
obs_class  <- factor(infert$case)
acc <- mean(pred_class == obs_class)
cat(sprintf("Overall resubstitution accuracy: %.1f%%\n", 100 * acc))
Overall resubstitution accuracy: 75.4%
Code
cat(sprintf("Baseline (majority class):        %.1f%%\n", 100 * max(prop.table(table(infert$case)))))
Baseline (majority class):        66.5%

Posterior Probabilities

LDA also returns posterior class probabilities for each observation — the probability of belonging to each class given the discriminant score. Values close to 0 or 1 indicate high-confidence assignments; values near 0.5 indicate ambiguity.

Code
post <- predict(model)$posterior
df_post <- data.frame(
  prob_case = post[, "1"],
  case      = factor(infert$case, labels=c("Control","Case"))
)

ggplot(df_post, aes(x=prob_case, fill=case)) +
  geom_histogram(bins=30, position="identity", alpha=0.6) +
  scale_fill_manual(values=c("#2166ac","#d6604d")) +
  geom_vline(xintercept=0.5, linetype="dashed", colour="grey40") +
  theme_bw() +
  labs(title="Posterior probability of being a case",
       subtitle="Dashed line = 0.5 decision threshold",
       x="P(case = 1 | x)", y="Count", fill=NULL)


Second Example — Iris Dataset

The iris dataset provides a three-class problem (\(K = 3\), \(\min(K-1, p) = 2\) discriminant functions) that illustrates multi-class LDA and the interpretation of a two-dimensional discriminant space.

Code
model_iris <- MASS::lda(Species ~ ., data=iris)
result_iris <- report_lda(model=model_iris)

Proportion of Trace

With two discriminant functions, the proportion of trace tells us how much of the total between-class variance each function captures.

Code
prop_trace <- model_iris$svd^2 / sum(model_iris$svd^2)
cat("LD1 proportion of trace:", round(prop_trace[1], 4), "\n")
LD1 proportion of trace: 0.9912 
Code
cat("LD2 proportion of trace:", round(prop_trace[2], 4), "\n")
LD2 proportion of trace: 0.0088 

LD1 captures almost all of the separation (99.1% of the trace): setosa lies far from the other two species, and versicolor and virginica are also largely separated along LD1. LD2 adds only a small extra contrast between versicolor and virginica.

Discriminant Space Plot

Code
scores_iris <- as.data.frame(predict(model_iris)$x)
scores_iris$Species <- iris$Species

ggplot(scores_iris, aes(x=LD1, y=LD2, colour=Species)) +
  geom_point(alpha=0.7, size=2) +
  stat_ellipse(level=0.95, linewidth=0.8) +
  scale_colour_manual(values=c("#4dac26","#2166ac","#d6604d")) +
  theme_bw() +
  labs(title="Iris species in the LDA discriminant space",
       subtitle="95% concentration ellipses per class",
       x="Linear Discriminant 1", y="Linear Discriminant 2")

Classification Performance

Code
result_iris$cmatrix
           setosa versicolor virginica    sum    p
setosa      50.00       0.00      0.00  50.00 1.00
versicolor   0.00      48.00      1.00  49.00 0.98
virginica    0.00       2.00     49.00  51.00 0.96
sum         50.00      50.00     50.00 150.00 1.00
p            1.00       0.96      0.98   1.00 0.98
Code
pred_iris <- predict(model_iris)$class
cat(sprintf("Overall resubstitution accuracy: %.1f%%\n",
            100 * mean(pred_iris == iris$Species)))
Overall resubstitution accuracy: 98.0%

References

Fisher, R. A. (1936). The use of multiple measurements in taxonomic problems. Annals of Eugenics, 7(2), 179–188.

McLachlan, G. J. (2004). Discriminant Analysis and Statistical Pattern Recognition. Wiley.

Venables, W. N., & Ripley, B. D. (2002). Modern Applied Statistics with S (4th ed.). Springer.