Welcome to Week 5! Over the past few weeks, we've been building our classification toolkit. We started with linear regression for predicting continuous values, then moved to logistic regression for classification problems. Today, we're going to explore a completely different way of thinking about classification - one that might surprise you with its elegance and power.
Last week, we learned about logistic regression, which directly models the probability that a data point belongs to a particular class given its features. This is called a discriminative approach because we're learning to discriminate directly between classes.
Key characteristics of logistic regression:
Logistic regression works well for many problems, but today we're going to ask a different question entirely.
Here's the key insight for today: What if we first project our high-dimensional data onto a lower dimension where the classes are naturally well-separated, and then classify?
Think about it like this: Imagine you're looking at a complex 3D sculpture. From some angles, it looks like a confusing mess. But from just the right angle, you can clearly see what it represents. Linear Discriminant Analysis (LDA) is all about finding that "right angle" - the best direction to project your data so that different classes become easy to distinguish.
This approach was pioneered by Ronald Fisher in 1936 (yes, the same Fisher who gave us many statistical concepts!), and it's still widely used today because it's:
By the end of this lecture, you should be able to:
Core Outcomes (All Students):
LinearDiscriminantAnalysis and QuadraticDiscriminantAnalysis from sklearnAdvanced Outcomes (MPS439 Students):
Let's dive in!
Real-world datasets often have many features. For example:
When we have data in high dimensions (say, 50 or 100 features), it becomes:
The question is: Can we somehow reduce the dimensionality while preserving - or even enhancing - our ability to separate classes?
Here's where projection comes in. Let's start with a simple 2D example to build intuition.

Figure 1: The left panel shows two classes (blue and red) in 2D space. They overlap quite a bit when we look at them in the original feature space. But look at the right panel! When we project all the points onto a carefully chosen line (shown as the diagonal on the left), the two classes become much more separated in this 1D projected space.
This is powerful! We've:
The key question: How do we find this "magic line" - the optimal projection direction?
Not all projection directions are created equal. Look at the comparison below:

Figure 2: These plots show the same 2D data projected onto two different directions. The left shows a "bad" projection where the classes heavily overlap after projection. The right shows a "good" projection where the classes are clearly separated. The difference is dramatic!
What makes a projection "good"? Intuitively, we want:
Fisher's Linear Discriminant gives us a mathematical way to find the projection that optimally balances these two goals.
Linear Discriminant Analysis seeks to find the direction vector such that when we project our data onto this direction:
The resulting 1D values maximize the separation between classes while minimizing the spread within each class.
Once we have this optimal projection, classification becomes simple: project a new point and compare its projected value to a threshold.
Now let's get into the mathematics - but don't worry, we'll build this up step by step with clear intuition at each stage.
We have training data:
For simplicity, let's start with binary classification: (two classes)
Our goal: Find a direction vector (also -dimensional) such that projecting our data onto this direction gives maximum class separation.
When we project a point onto direction , we get a scalar value:
This is just the dot product - it tells us "how much" of lies along the direction .
For example, if and , then (we're projecting onto the horizontal axis).
After projecting all our training points, we get two sets of 1D values:
After projection, each class has a mean in the 1D projected space:
Using the linearity of the dot product, we can write this more compactly:
where and are the mean vectors of the two classes in the original -dimensional space.
Between-class variance measures how far apart these projected means are:
We want this to be LARGE - the farther apart the class means are in the projected space, the better!
But wait! We can make the between-class variance arbitrarily large just by scaling to be huge. That's cheating - we need another constraint.
The key insight: We also want the points within each class to be tightly clustered after projection. We measure this with within-class variance:
Total within-class variance:
We want this to be SMALL - tight clusters are easier to separate!

Figure 3: This illustration shows the two components we're trying to balance. The horizontal axis represents the projected space. The two bell curves show the distributions of the two classes after projection. The "between-class variance" is the distance between the peaks (large is good!). The "within-class variance" is the spread of each curve (small is good!).
Now we can state Fisher's brilliant idea: Find the projection that maximizes the ratio of between-class variance to within-class variance.
This is called Fisher's criterion or the Fisher discriminant. It's a beautiful formulation because:
To solve for the optimal , we need to express Fisher's criterion in terms of matrices. This allows us to use linear algebra tools.
Define the within-class scatter matrix:
This measures the total scatter (spread) of points within each class in the original -dimensional space. It's a matrix that captures the covariance structure of each class.
Using this matrix, we can show (through some algebra) that:
Similarly, the between-class separation can be written as:
where is called the between-class scatter matrix.
Fisher's criterion becomes:
Through calculus (taking derivatives and setting to zero), the optimal direction that maximizes is:
The symbol means "proportional to" - the exact scale of doesn't matter, only its direction.
Intuitive interpretation:
Important note: Don't worry about computing by hand - sklearn does all of this for us! The important thing is understanding what the algorithm is doing conceptually.
Let's visualize what this means:

Figure 4: This shows 2D data with two classes. The thick arrow shows the projection direction found by LDA - this is Fisher's optimal direction. The black line is the decision boundary, which is perpendicular to the projection direction. The class means ( and ) are marked with stars.
Notice:
Now that we have the optimal projection direction , how do we actually classify new points?
The process is beautifully simple:
Step 1: Project the new point onto the direction :
Step 2: Compare to a threshold :
That's it! The hard work was finding - classification is now trivial.
How do we choose the threshold ? There are several reasonable approaches:
Option 1: Midpoint (simplest)
This places the decision boundary exactly halfway between the projected class means.
Option 2: Weighted by class proportions
If one class is much more common than the other, we might want to adjust the threshold to reflect this. If Class 0 appears with probability and Class 1 with probability in the training data, we can incorporate this into our threshold choice.
sklearn handles this automatically based on the training data, so you typically don't need to worry about it.
Here's something cool: Even though we're making decisions in the 1D projected space, this corresponds to a decision boundary in the original -dimensional space.
The decision rule defines a hyperplane in dimensions:
This hyperplane is perpendicular to the projection direction . All points on one side of the hyperplane are classified as Class 0, all points on the other side as Class 1.
Key insight: LDA produces linear decision boundaries, just like logistic regression! But the way we arrive at these boundaries is completely different.
So far we've focused on binary classification (two classes). But LDA naturally extends to classes!
For multiple classes:
Example: With 3 classes, we find 2 projection directions. We can then visualize all our data in this 2D projected space, which often gives beautiful, interpretable visualizations.
For this lecture, we'll stick with the binary case for simplicity, but know that sklearn handles multiple classes automatically.
Enough theory - let's see how easy this is to use in practice!
sklearn provides a simple interface for LDA through the LinearDiscriminantAnalysis class. It works just like other classifiers we've seen (logistic regression, etc.).
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import numpy as np
import matplotlib.pyplot as plt
# Load iris dataset (we'll use only 2 classes for simplicity)
iris = load_iris()
X = iris.data[:100, :2] # First 100 samples (setosa and versicolor), first 2 features
y = iris.target[:100] # Binary labels: 0 and 1
# Split into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
Training is just one line:
# Create and train the LDA model
lda = LinearDiscriminantAnalysis()
lda.fit(X_train, y_train)
That's it! Behind the scenes, sklearn:
Prediction is equally simple:
# Predict on test set
y_pred = lda.predict(X_test)
# Calculate accuracy
accuracy = lda.score(X_test, y_test)
print(f"Test accuracy: {accuracy:.3f}")
You can also get probability estimates:
# Get probability predictions
y_prob = lda.predict_proba(X_test)
print(y_prob[:5]) # First 5 predictions
For 2D data, we can visualize the decision boundary:
# Create a mesh to plot decision boundary
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),
np.linspace(y_min, y_max, 200))
# Predict for each point in the mesh
Z = lda.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# Plot
plt.contourf(xx, yy, Z, alpha=0.3, cmap='RdBu')
plt.scatter(X_train[:, 0], X_train[:, 1], c=y_train, cmap='RdBu', edgecolors='black')
plt.xlabel('Sepal length')
plt.ylabel('Sepal width')
plt.title('LDA Decision Boundary')
plt.show()

Figure 5: This shows the result of applying LDA to real data (Iris dataset). The colored regions show the classification areas, the decision boundary is where the colors meet, and the points show training data (circles) and test data (triangles). Notice the clean linear separation!
LDA gives us access to several useful attributes:
# The projection direction (coefficients)
print("Projection direction:", lda.coef_)
# Class means in the original space
print("Class means:", lda.means_)
# Prior probabilities (class proportions)
print("Class priors:", lda.priors_)
The coef_ attribute gives us the direction vector . This tells us which features are most important for discrimination:
Beyond simple classification, LDA is useful for:
1. Dimensionality Reduction
# Use LDA to project to 1D
lda = LinearDiscriminantAnalysis(n_components=1)
X_transformed = lda.fit_transform(X_train, y_train)
print(X_transformed.shape) # Now 1D!
This is like PCA but supervised - it finds low-dimensional representations that preserve class separability.
2. Feature Importance
The projection direction tells us which features matter most for classification.
3. Data Visualization
For multi-class problems, projecting to 2D or 3D using LDA often gives beautiful, interpretable visualizations.
LDA makes a strong assumption: all classes have the same "spread" or covariance structure. But what if this isn't true?
Remember that LDA assumes all classes share the same within-class scatter. In mathematical terms, we assume:
where is the covariance matrix.
Reality check: Often different classes have different spreads!
Examples:
When the equal covariance assumption is violated, LDA might perform poorly.
Quadratic Discriminant Analysis (QDA) relaxes this assumption. Instead of assuming one shared covariance matrix, QDA allows each class to have its own:
This added flexibility comes at a cost:
The difference is dramatic when classes have different spreads:

Figure 6: This comparison shows the same data with two classes that have different covariance structures - one class is circular, the other is elliptical. The left panel shows LDA forcing a linear boundary, which isn't optimal. The right panel shows QDA finding a curved boundary that better respects the different shapes of the classes.
Use LDA when:
Use QDA when:
Rule of thumb: For features and classes:
QDA needs a lot more data to estimate all those parameters reliably!
The good news: using QDA is almost identical to LDA!
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
# Create and train QDA model
qda = QuadraticDiscriminantAnalysis()
qda.fit(X_train, y_train)
# Make predictions
y_pred_qda = qda.predict(X_test)
# Calculate accuracy
accuracy_qda = qda.score(X_test, y_test)
print(f"QDA test accuracy: {accuracy_qda:.3f}")
The API is the same - only the underlying algorithm differs!
Let's compare both on the same dataset:
# Train both models
lda = LinearDiscriminantAnalysis()
qda = QuadraticDiscriminantAnalysis()
lda.fit(X_train, y_train)
qda.fit(X_train, y_train)
# Compare performance
lda_acc = lda.score(X_test, y_test)
qda_acc = qda.score(X_test, y_test)
print(f"LDA accuracy: {lda_acc:.3f}")
print(f"QDA accuracy: {qda_acc:.3f}")
Try this on different datasets! Sometimes LDA wins (simpler model, less overfitting), sometimes QDA wins (more flexible when classes really are different). It depends on your data.
We now have two methods for linear classification: LDA and logistic regression. Both produce linear decision boundaries. So what's the difference?
The fundamental difference is in their approach:
Logistic Regression (Discriminative):
LDA (Generative - see appendix):
Despite these different philosophies, they often produce similar results in practice!
Let's see them side by side:

Figure 7: Both methods produce linear decision boundaries on this dataset, but they're slightly different. The boundaries are similar but not identical - they've been optimized according to different criteria. Both achieve good accuracy here.
LDA is better when:
Small sample sizes: LDA can be more stable with limited data because it makes stronger assumptions (Gaussian distributions with equal covariance). These assumptions, if true, provide extra information that helps with small samples.
Classes are well-separated: When classes don't overlap much, LDA's assumptions are often reasonable and it performs well.
Multi-class problems: LDA naturally handles multiple classes and provides nice dimensionality reduction. Logistic regression needs to be extended (one-vs-rest or softmax).
Dimensionality reduction needed: LDA simultaneously classifies and reduces dimensions, which can be very useful for visualization and subsequent processing.
Features are approximately Gaussian: When the underlying assumptions are met, LDA can be more efficient (statistically).
Logistic Regression is better when:
Non-Gaussian data: Logistic regression doesn't assume anything about feature distributions. If your features are highly non-Gaussian (e.g., binary features, skewed distributions), logistic regression is more robust.
Very different class sizes: When one class is much rarer than another, logistic regression handles the imbalance better.
Regularization is important: Logistic regression works seamlessly with Ridge (L2) and Lasso (L1) regularization. This is harder to implement with LDA.
Outliers present: Logistic regression is generally more robust to outliers because it doesn't estimate covariance matrices.
Online learning: Logistic regression can be updated incrementally as new data arrives. LDA requires recomputing covariance matrices.
Here's a reasonable approach when facing a new classification problem:
# Try both and compare!
from sklearn.linear_model import LogisticRegression
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.model_selection import cross_val_score
# Logistic Regression
logreg = LogisticRegression()
logreg_scores = cross_val_score(logreg, X, y, cv=5)
# LDA
lda = LinearDiscriminantAnalysis()
lda_scores = cross_val_score(lda, X, y, cv=5)
print(f"Logistic Regression CV accuracy: {logreg_scores.mean():.3f} (+/- {logreg_scores.std():.3f})")
print(f"LDA CV accuracy: {lda_scores.mean():.3f} (+/- {lda_scores.std():.3f})")
Don't overthink it: Try both and see which works better on your specific data!
| Aspect | LDA | Logistic Regression |
|---|---|---|
| Assumptions | Gaussian features, equal covariance | None on feature distribution |
| Decision boundary | Linear | Linear |
| Multi-class | Native support | Requires extension |
| Dimensionality reduction | Yes (automatic) | No |
| Small sample performance | Often better | Can overfit |
| Robustness to outliers | Less robust | More robust |
| Regularization | Difficult | Easy (L1/L2) |
| Interpretability | Projection direction | Coefficients |
LDA shines in these scenarios:
✅ Classes are approximately Gaussian distributed: When features within each class follow (roughly) normal distributions, LDA's assumptions are met and it performs optimally.
✅ Similar covariance structures: When all classes have roughly the same "shape" or spread, linear boundaries make sense.
✅ Small to moderate sample sizes: LDA's assumptions help stabilize estimates when data is limited.
✅ Need for dimensionality reduction: If you want both classification and dimension reduction in one step, LDA is perfect.
✅ Interpretability matters: The projection directions have clear geometric meaning.
✅ Multiple classes with good separation: LDA handles multi-class problems elegantly and can project to low dimensions for visualization.
LDA can perform poorly when:
❌ Highly non-Gaussian data: If features are binary, categorical, or have weird distributions (e.g., heavy-tailed, multimodal), LDA's Gaussian assumption is violated.
❌ Very different class spreads: If one class is very tight and another is very spread out, the equal covariance assumption fails (consider QDA instead).
❌ Outliers present: Outliers can severely distort the mean and covariance estimates, ruining the projection direction.
❌ Very small sample sizes: If you have fewer samples than features (), the covariance matrix can't be inverted reliably. LDA breaks down.
❌ Highly imbalanced classes: If one class is extremely rare, LDA can be unstable.
1. Standardize your features
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
lda = LinearDiscriminantAnalysis()
lda.fit(X_train_scaled, y_train)
Features on very different scales can cause problems. Standardization helps.
2. Check your assumptions
Before using LDA, do a quick sanity check:
3. Compare with simpler methods
Always compare LDA with logistic regression on your specific data:
# Quick comparison
from sklearn.model_selection import cross_val_score
lda_score = cross_val_score(lda, X, y, cv=5).mean()
logreg_score = cross_val_score(LogisticRegression(), X, y, cv=5).mean()
print(f"LDA: {lda_score:.3f}, LogReg: {logreg_score:.3f}")
4. Use cross-validation
Don't trust a single train-test split! Use k-fold cross-validation to get reliable performance estimates.
5. For high-dimensional data, consider shrinkage
sklearn offers a shrinkage parameter that can help with high-dimensional data:
lda = LinearDiscriminantAnalysis(shrinkage='auto', solver='lsqr')
This regularizes the covariance matrix estimate, preventing overfitting.
Let's recap what we've learned today:
🎯 Core Concept: LDA finds the optimal projection direction that maximizes class separation while minimizing within-class spread. This is Fisher's criterion.
🎯 Two Perspectives:
🎯 Implementation: sklearn makes it trivial with LinearDiscriminantAnalysis and QuadraticDiscriminantAnalysis
🎯 LDA vs QDA: LDA assumes equal covariance (→ linear boundaries), QDA allows different covariances (→ curved boundaries)
🎯 LDA vs Logistic Regression: Both give linear boundaries but via different routes. Try both on your data!
🎯 Practical wisdom:
Next week, we'll explore Decision Trees - a completely different approach to classification that:
Decision trees will give us our first taste of truly non-linear classification. Exciting stuff!
Note for Students: This section provides an alternative mathematical perspective on LDA. It's not required for using LDA effectively or for the core learning outcomes. However, if you're in MPS439 or are simply curious about the deeper theory, this section will give you a richer understanding of why LDA works. Feel free to skip this if it feels overwhelming!
So far, we've understood LDA through the lens of projection: find the direction that best separates classes. But there's a completely different way to derive the exact same algorithm using probability theory and Bayes' theorem. This is called the generative approach because we "generate" or model each class separately.
Instead of directly modeling (the probability of the class given features), the generative approach models:
Then we use Bayes' theorem to get what we want:
The denominator is the same for all classes, so we can ignore it for classification (we just compare the numerators for different classes).
Here's the key assumption of LDA from the generative perspective:
Each class follows a multivariate Gaussian distribution:
where:
The multivariate Gaussian density is:
Don't be intimidated by this formula! The key is that it's a bell-shaped distribution in multiple dimensions.
For binary classification (classes 0 and 1), we classify a point as Class 1 if:
Using Bayes' theorem:
The cancels out:
Taking the logarithm (which preserves the inequality):
Now substitute the Gaussian densities and simplify. After lots of algebra (the exponentials become quadratics, terms cancel because is the same), you end up with:
where and involves the prior probabilities and means.
This is a linear decision boundary! And notice: the direction is exactly what we got from Fisher's discriminant!
The crucial assumption is that both classes have the same covariance matrix . Here's why:
If we allowed different covariances ( and ), when we substitute into Bayes' theorem and simplify, the terms wouldn't cancel. We'd be left with:
This is a quadratic in , not linear! The decision boundary becomes a curve (or hyperbola, or ellipse) - this is exactly what QDA does.
So:
The beautiful thing is that the projection perspective (Fisher's discriminant) and the generative perspective (Gaussian models + Bayes' theorem) lead to exactly the same algorithm!
Note that (within-class scatter) is essentially our estimate of the pooled covariance !
This is one of those beautiful moments in mathematics where two completely different ways of thinking about a problem lead to the same answer. It suggests LDA is capturing something fundamental about the structure of classification problems.
Understanding the generative view provides:
If this perspective interests you:
But remember: you can use and understand LDA effectively without any of this! This is enrichment, not requirement.
Test your understanding with these questions:
Conceptual: Explain in your own words why LDA projects data onto a lower-dimensional space before classification. What advantage does this give?
Interpretation: You fit an LDA model and lda.coef_ gives you [0.8, -0.3]. What does this tell you about the two features?
Comparison: When would you choose QDA over LDA? Give a concrete example.
Code: Write sklearn code to train an LDA classifier, make predictions on test data, and print the accuracy.
Visual: Given a 2D scatter plot with two classes, sketch what you think the LDA decision boundary would look like and explain your reasoning.
Mathematical: Explain why Fisher's criterion uses a ratio (between-class variance / within-class variance) rather than just maximizing between-class variance.
Derivation: Show mathematically why equal covariance matrices lead to linear decision boundaries while different covariances lead to quadratic boundaries.
Implementation: Implement a simple LDA classifier from scratch for 2D binary classification. You'll need to compute class means, within-class scatter matrix, and the projection direction.
Theory: Explain the connection between the projection perspective and the generative perspective on LDA. Why do they give the same answer?
For more practice and deeper understanding:
Lab exercises (for Friday's session) will give you hands-on practice with:
End of Lecture Notes
Remember: The goal isn't to memorize formulas, but to understand the core ideas. LDA finds the best projection for separating classes. Everything else follows from that simple insight. See you in lab!