TAI BUI
Back to curriculum
BuildPython

Naive Bayes

The "naive" assumption is wrong, and it works anyway. That's the beauty of it.

TIME: ~75 minutesPREREQUISITES: Phase 2, Lessons 01-07 (classification, Bayes' theorem)

Learning Objectives

  • Implement Multinomial Naive Bayes from scratch with Laplace smoothing for text classification
  • Explain why the naive independence assumption is mathematically wrong but produces correct class rankings in practice
  • Compare Multinomial, Bernoulli, and Gaussian Naive Bayes variants and select the right one for a given feature type
  • Evaluate Naive Bayes against logistic regression on high-dimensional sparse data and explain the bias-variance tradeoff at work

The Problem

You need to classify text. Emails into spam or not-spam. Customer reviews into positive or negative. Support tickets into categories. You have thousands of features (one per word) and limited training data.

Most classifiers choke here. Logistic regression needs enough samples to estimate thousands of weights reliably. Decision trees split on one word at a time and overfit wildly. KNN in 10,000 dimensions is meaningless because every point is equally far from every other point.

Naive Bayes handles this. It makes a mathematically wrong assumption (that every feature is independent of every other feature given the class), and it still outperforms "smarter" models on text classification, especially with small training sets. It trains in a single pass through the data. It scales to millions of features. It produces probability estimates (though often poorly calibrated due to the independence assumption).

Understanding why a wrong assumption leads to good predictions teaches a fundamental lesson about machine learning: winning is about the bias-variance tradeoff for your data, not about which model's assumptions are most accurate.

Warm-up0/3
1. 'Rainy weather' and 'people carrying umbrellas' tend to happen together in real life. If a simple prediction rule assumed those two facts were completely unrelated to each other, would that assumption be accurate?Warm-up
Eight days listed as chips, each with its weather and whether people carried umbrellas, above a box describing a rule that treats the two facts as unrelated, with a question about its accuracy.
Eight days typed for illustration, not real data.
2. You flip a coin 5 times and get 0 heads. Is it reasonable to conclude the coin's true probability of landing heads is exactly 0%?Warm-up
Five coins each showing tails, with a note that 0 heads were seen, and a question mark for the true chance of heads.
Illustrative flips, typed.
3. In probability notation, what does P(A | B) mean?Warm-up
The expression P(A | B) in large type with labels for A on the left of the bar and B on the right, and a question mark for how the bar is read.
Notation only, no values.
Check: The Problem0/2
1. Why does logistic regression struggle on text with thousands of word features and little training data?Explain
A grid with few rows labelled documents and a very large number of columns labelled word features, with a question about why logistic regression struggles.
Schematic; sizes are not to scale.
2. You have 200 labeled support tickets and a 20,000-word vocabulary, and need a strong, quick baseline. Which choice fits best?Design
A wide rectangle of sparse dots standing for 200 labeled tickets by 20,000 word features, with a note that you need a strong, quick baseline.
Schematic; dots are illustrative, not data.

The Concept

Bayes' Theorem (Quick Review)

Bayes' theorem flips conditional probabilities:

P(class | features) = P(features | class) * P(class) / P(features)

We want P(class | features): the probability that a document belongs to a class, given the words in it. We get there from three pieces:

  • P(features | class): the likelihood of seeing these words in documents of this class
  • P(class): the prior probability of the class (how common is spam in general?)
  • P(features): the evidence. It's the same for every class, so we can ignore it when comparing them.

The class with the highest P(class | features) wins.

The Naive Independence Assumption

The naive assumption: every feature is conditionally independent of every other feature, given the class.

P(w1, w2, ..., wn | class) = P(w1 | class) * P(w2 | class) * ... * P(wn | class)

Why settle for something this blunt? Because the alternative is intractable. Computing P(features | class) exactly means estimating the joint probability of all features together, and with a vocabulary of 10,000 words that's a distribution over 2^10,000 possible combinations. Impossible. The naive assumption trades that one impossible joint distribution for n simple per-feature distributions, each of which needs only a count.

This assumption is obviously wrong. The words "machine" and "learning" are not independent in any document. But classification only cares about rankings: which class scores highest. Correct probability estimates were never the point. The independence assumption introduces systematic errors, but those errors hit all classes about equally, so the ranking survives.

Why It Still Works

Three reasons:

  1. Ranking beats calibration. Classification only needs the top-ranked class to be correct. Even if P(spam) = 0.99999 when the true probability is 0.7, the classifier still picks spam. Winning the ranking is the job; getting the exact probability right never was.

  2. High bias, low variance. The independence assumption is a strong prior. It constrains the model heavily, which prevents overfitting. With limited training data, a model that's slightly wrong but stable beats a model that's theoretically right but wildly unstable. This is the bias-variance tradeoff in action.

  3. Feature redundancy cancels out. Correlated features provide redundant evidence. The classifier double-counts this evidence, but it double-counts it for the correct class too. If "machine" and "learning" always appear together, both provide evidence for the "tech" class. NB counts them twice, but it counts them twice for the right class.

A fourth, practical reason: Naive Bayes is extremely fast. Training is a single pass through the data counting frequencies. Prediction is a matrix multiplication. You can train on a million documents in seconds, which means you can iterate faster, try more feature sets, and run more experiments than with slower models.

The Math Step by Step

Let's trace through a concrete example. Suppose we have two classes: spam and not-spam. Our vocabulary has three words: "free", "money", "meeting".

Training data:

  • Spam emails mention "free" 80 times, "money" 60 times, "meeting" 10 times (150 total words)
  • Not-spam emails mention "free" 5 times, "money" 10 times, "meeting" 100 times (115 total words)
  • 40% of emails are spam, 60% are not-spam

With Laplace smoothing (alpha=1):

P(free | spam)    = (80 + 1) / (150 + 3) = 81/153 = 0.529
P(money | spam)   = (60 + 1) / (150 + 3) = 61/153 = 0.399
P(meeting | spam) = (10 + 1) / (150 + 3) = 11/153 = 0.072

P(free | not-spam)    = (5 + 1) / (115 + 3) = 6/118 = 0.051
P(money | not-spam)   = (10 + 1) / (115 + 3) = 11/118 = 0.093
P(meeting | not-spam) = (100 + 1) / (115 + 3) = 101/118 = 0.856

New email contains: "free" (2 times), "money" (1 time), "meeting" (0 times).

log P(spam | email) = log(0.4) + 2*log(0.529) + 1*log(0.399) + 0*log(0.072)
                    = -0.916 + 2*(-0.637) + (-0.919) + 0
                    = -3.109

log P(not-spam | email) = log(0.6) + 2*log(0.051) + 1*log(0.093) + 0*log(0.856)
                        = -0.511 + 2*(-2.976) + (-2.375) + 0
                        = -8.838

Spam wins by a large margin. The word "free" appearing twice is strong evidence for spam. Note that "meeting" not appearing contributes zero to both log sums (0 * log(P)): in Multinomial NB, absent words have no effect. Bernoulli NB is the variant that explicitly models word absence.

Three Variants

Naive Bayes comes in three flavors. Each models P(feature | class) differently.

Multinomial Naive Bayes

Models each feature as a count. Best for text data where features are word frequencies or TF-IDF values.

P(word_i | class) = (count of word_i in class + alpha) / (total words in class + alpha * vocab_size)

The alpha is Laplace smoothing (explained below). This variant is the workhorse for text classification.

Gaussian Naive Bayes

Models each feature as a normal distribution. Best for continuous features.

P(x_i | class) = (1 / sqrt(2 * pi * var)) * exp(-(x_i - mean)^2 / (2 * var))

Each class gets its own mean and variance per feature. This works well when features genuinely follow a bell curve within each class.

Bernoulli Naive Bayes

Models each feature as binary (present or absent). Best for short text or binary feature vectors.

P(word_i | class) = (docs in class containing word_i + alpha) / (total docs in class + 2 * alpha)

Unlike Multinomial, Bernoulli explicitly penalizes the absence of a word. If "free" typically appears in spam but is absent from this email, Bernoulli counts that as evidence against spam.

When to Use Each Variant

VariantFeature TypeBest ForExample
MultinomialCounts or frequenciesText classification, bag-of-wordsEmail spam, topic classification
GaussianContinuous valuesTabular data with normal-ish featuresIris classification, sensor data
BernoulliBinary (0/1)Short text, binary feature vectorsSMS spam, presence/absence features

Laplace Smoothing

Laplace smoothing adds a small count alpha (usually 1) to every feature count, so a word that never showed up in training doesn't zero out the whole prediction:

P(word_i | class) = (count(word_i, class) + alpha) / (total_words_in_class + alpha * vocab_size)

Here's why that matters. Without smoothing, P(word | class) = 0/N = 0 for any word that never appeared in a class's training data. One zero multiplied through the entire product makes P(class | features) = 0, regardless of all other evidence. A single unseen word destroys the entire prediction, no matter how much other evidence supports it.

With alpha=1, every word gets at least a tiny probability. The word "discombobulate" appearing in a test email no longer kills the spam probability. The smoothing has a Bayesian interpretation: it's equivalent to placing a uniform Dirichlet prior on the word distributions.

Higher alpha means stronger smoothing (more uniform distributions). Lower alpha means the model trusts the data more. Alpha is a hyperparameter you tune.

The effect of alpha:

AlphaEffectWhen to use
0.001Almost no smoothing, trust the dataVery large training set, no unseen features expected
0.1Light smoothingLarge training set
1.0Standard Laplace smoothingDefault starting point
10.0Heavy smoothing, flattens distributionsVery small training set, many unseen features expected

Log-Space Computation

Working in log space means replacing probability multiplication with addition of logarithms:

log P(class | features) = log P(class) + sum_i log P(feature_i | class)

The reason: multiplying hundreds of probabilities, each less than 1, causes floating-point underflow. The product becomes zero in floating point even though the true value is a very small positive number, just too small to represent. Adding logarithms sidesteps the problem entirely.

This turns the prediction into a dot product:

log_scores = X @ log_feature_probs.T + log_class_priors
prediction = argmax(log_scores)

Matrix multiplication. That's why Naive Bayes prediction is so fast: it's the same operation as a single-layer linear model.

Naive Bayes vs Logistic Regression

Both are linear classifiers for text. The difference is in what they model.

AspectNaive BayesLogistic Regression
TypeGenerative (models P(X|Y))Discriminative (models P(Y|X))
TrainingCount frequenciesOptimize loss function
Small dataBetter (strong prior helps)Worse (not enough to estimate weights)
Large dataWorse (wrong assumption hurts)Better (flexible boundary)
FeaturesAssumes independenceHandles correlations
SpeedSingle pass, very fastIterative optimization
CalibrationPoor probabilitiesBetter probabilities

Rule of thumb: start with Naive Bayes. If you have enough data and NB plateaus, switch to logistic regression.

Classification Pipeline

Mermaid diagram

This is the same log-space computation from above, laid out as a pipeline: sum the log prior and the log likelihoods for each class, then take the argmax.

log P(class | features) = log P(class) + sum_i log P(feature_i | class)
Check: The Concept0/24
1. What is Naive Bayes?Define
A box of word counts for one email feeds a box named Naive Bayes that outputs a spam or not-spam label, with a question about what happens inside the middle box.
Typed example email counts.
2. What does it mean for two features to be conditionally independent given the class?Define
A tech class label next to two word chips, machine and learning, with text asking what conditional independence given the class says about them.
Illustrative words from the lesson.
3. In a spam filter built with Bayes' theorem, what is the prior P(class)?Define
Bayes' theorem for classes given features with the term P(class) highlighted and a question mark asking its name.
Formula from the lesson.
4. What is the likelihood P(features | class) in Naive Bayes?Define
Bayes' theorem for classes given features with the term P(features | class) highlighted and a question mark asking its name.
Formula from the lesson.
5. What is the posterior in Naive Bayes classification?Define
Bayes' theorem for classes given features with the term P(class | features) highlighted and a question mark asking its name.
Formula from the lesson.
6. What does Laplace smoothing do in Naive Bayes?Define
A table of word counts in the spam training data where the word discombobulate has count 0, and a product of three word probabilities for a test email containing it, ending in a question mark.
Counts from the lesson's example, with its 'discombobulate' word added at 0.
7. What makes Naive Bayes a generative model?Define
A scatter plot of two classes of points over two features X1 and X2, with a legend for Y equal to 0 and 1 and a question about what a generative model learns from them.
Toy points drawn with numpy (seed 5).
8. What is a discriminative model, such as logistic regression?Define
Scatter plot of two classes of points with a straight line that separates them, a legend for the classes and a question about the type of model.
Toy points drawn with numpy (seed 5); line from logistic regression written in numpy.
9. What does it mean to work with log probability in Naive Bayes?Define
A table of three words from the spam class with their probabilities and the natural log of each probability.
Values computed from the lesson's worked example.
10. What kind of features does Multinomial Naive Bayes model?Define
An email with the text free money free feeding three vocabulary columns, free, money and meeting, each with an empty question mark cell.
Typed example email.
11. How does Bernoulli Naive Bayes model each feature, and when is it the best choice?Define
A short text message, Free entry. Reply WIN now, with an arrow to a feature for the word free whose value is a question mark.
Typed example message.
12. The naive independence assumption is clearly wrong for text. Why does Naive Bayes still classify well?Explain
The independence assumption written as a product for the words machine and learning, with a note that they appear together in real documents and a question about why the classifier still works.
Formula from the lesson.
13. Why does Naive Bayes assume feature independence instead of modeling P(features | class) exactly?Explain
A row of binary cells for words w1 to w10000 showing which words one document contains, with a question about why the exact likelihood is not modeled.
Typed 0/1 pattern for illustration.
14. Why can Naive Bayes ignore P(features) when comparing classes for one document?Explain
Bayes' theorem with the term P(features) highlighted and a question mark, above a note that one email is scored against the classes spam and not-spam.
Formula from the lesson.
15. The lesson gives 'feature redundancy cancels out' as a reason NB still works despite correlated features. Concretely, what does that mean?Explain
Four tech documents each containing the words machine and learning together, beside four sports documents containing neither word, and a question about what the redundancy cancelling out means.
Typed illustration of the lesson's machine and learning example.
16. With only 50 labeled documents, why can Naive Bayes beat a more flexible classifier?Explain
A grid of 50 small squares standing for labeled documents beside two model boxes, Naive Bayes and a more flexible classifier, with a question about which can come out ahead and why.
Schematic.
17. A word typical of spam is absent from an email. How do Multinomial and Bernoulli NB treat that absence?Explain
A short email mentioning meeting and money but not free, with a table of word counts for the email and the spam word probabilities, where the word free has count 0 and probability 0.529.
Typed email; probabilities from the lesson's worked example.
18. When should you use Multinomial NB versus Gaussian NB?Explain
Two boxes, Dataset A with word counts free 2, money 1, meeting 0 and Dataset B with four decimal measurements, and a question about which Naive Bayes variant fits each.
Typed example rows.
19. As the training set grows very large, how do Naive Bayes and logistic regression compare?Explain
Empty axes of accuracy against training set size from small to very large, with a question mark in the plot and a legend listing Naive Bayes and logistic regression.
Schematic, no data.
20. When is a high smoothing value such as alpha = 10.0 the right choice?Explain
Three cells holding the counts 3, 0 and 1 for one class, a note that alpha is 10.0, and a question about when that value is the right choice.
Typed counts.
21. An email contains 'free' twice and 'money' once. In Multinomial NB with log probabilities, how is the spam score computed?Apply
An email with free twice and money once, and a table giving P(spam) 0.4, P(free | spam) 0.529 and P(money | spam) 0.399, with the log spam score left as a question mark.
Values from the lesson's worked example.
22. Not-spam emails mention 'meeting' 100 times out of 115 total words, the vocabulary has 3 words, and alpha = 1. What is P(meeting | not-spam)?Apply
A table of not-spam word counts, free 5, money 10 and meeting 100, with total words 115, vocabulary size 3 and alpha 1, and the probability of meeting given not-spam as a question mark.
Counts from the lesson's worked example.
23. A word appears in 8 of the 20 spam documents. With alpha = 1, what is P(word | spam) under Bernoulli NB?Apply
Twenty document tiles in two rows with eight highlighted as containing a word, and a question mark for the Bernoulli probability of the word given spam with alpha 1.
Schematic of the stem's counts.
24. A spam email full of typical spam words gets P(spam | features) = 0. It contains one word that never appeared in any spam training email. Which ingredient was missing?Analyze
A table of word counts in the spam class where free is 80, money is 60 and an unseen word is 0, with the posterior P(spam | features) equal to 0 and a question about the missing ingredient.
Counts of free and money from the lesson; the unseen word is the stem's.

Build It

The code in code/naive_bayes.py implements both MultinomialNB and GaussianNB from scratch.

MultinomialNB

The from-scratch implementation:

  1. fit(X, y): For each class, count the frequency of each feature, add Laplace smoothing, compute log probabilities, and store the class priors (log of class frequencies).

  2. predict_log_proba(X): For each sample, compute log P(class) + sum of log P(feature_i | class) for all classes. This is a matrix multiplication: X @ log_probs.T + log_priors.

  3. predict(X): Return the class with the highest log probability.

python
class MultinomialNB:
    def __init__(self, alpha=1.0):
        self.alpha = alpha
 
    def fit(self, X, y):
        classes = np.unique(y)
        n_classes = len(classes)
        n_features = X.shape[1]
 
        self.classes_ = classes
        self.class_log_prior_ = np.zeros(n_classes)
        self.feature_log_prob_ = np.zeros((n_classes, n_features))
 
        for i, c in enumerate(classes):
            X_c = X[y == c]
            self.class_log_prior_[i] = np.log(X_c.shape[0] / X.shape[0])
            counts = X_c.sum(axis=0) + self.alpha
            self.feature_log_prob_[i] = np.log(counts / counts.sum())
 
        return self

The key insight: once fitting is done, prediction is just matrix multiplication plus a bias. That's why Naive Bayes is so fast.

GaussianNB

For continuous features, we estimate mean and variance per class per feature:

python
class GaussianNB:
    def __init__(self):
        pass
 
    def fit(self, X, y):
        classes = np.unique(y)
        self.classes_ = classes
        self.means_ = np.zeros((len(classes), X.shape[1]))
        self.vars_ = np.zeros((len(classes), X.shape[1]))
        self.priors_ = np.zeros(len(classes))
 
        for i, c in enumerate(classes):
            X_c = X[y == c]
            self.means_[i] = X_c.mean(axis=0)
            self.vars_[i] = X_c.var(axis=0) + 1e-9
            self.priors_[i] = X_c.shape[0] / X.shape[0]
 
        return self

Prediction uses the Gaussian PDF per feature, multiplied across features (added in log space).

Demo: Text Classification

The code generates synthetic bag-of-words data simulating two classes (tech articles vs sports articles). Each class has a different word frequency distribution, and MultinomialNB classifies them using word counts.

The synthetic data works like this: we create 200 "words" (feature columns). Words 0-39 have high frequency in tech articles and low in sports. Words 80-119 have high frequency in sports and low in tech. Words 40-79 are medium frequency in both. This creates a realistic scenario where some words are strong class indicators and others are noise.

Demo: Continuous Features

The code generates Iris-like data (3 classes, 4 features, Gaussian clusters). GaussianNB classifies using per-class mean and variance. Each class has a different center (mean vector) and different spread (variance), mimicking real-world data where measurements differ systematically between categories.

The code also demonstrates:

  • Smoothing comparison: training MultinomialNB with different alpha values to show the effect of smoothing strength on accuracy.
  • Training size experiment: how NB accuracy improves as training data grows from 20 to 1600 samples. NB reaches decent accuracy even with very few samples, which is its main advantage.
  • Confusion matrix: per-class precision, recall, and F1 score to show where NB makes mistakes.

Prediction Speed

Naive Bayes prediction is a matrix multiplication. For n samples with d features and k classes:

  • MultinomialNB: one matrix multiply (n x d) @ (d x k) = O(n * d * k)
  • GaussianNB: n * k Gaussian PDF evaluations, each over d features = O(n * d * k)

Both are linear in every dimension. Compare that to KNN, which needs a distance computation against every training point, or an SVM with an RBF kernel, which needs a kernel evaluation against every support vector. NB is faster by orders of magnitude at prediction time.

Check: Build It0/5
1. Once fit has run, why is MultinomialNB prediction so fast?Explain
A fitted MultinomialNB box, a new document count vector feeding it, and an output class, with a question about what the prediction step computes.
Schematic.
2. The labels y contain ten samples, four with class 0 and six with class 1. What does fit store in class_log_prior_?Apply
The label array y with ten entries, four zeros and six ones shown as chips, and a call to fit followed by a question mark for class_log_prior_.
Typed labels (a real fit uses them below).
3. You call fit on the from-scratch MultinomialNB with a feature matrix containing negative values. What happens?Apply
Code that builds a four by three matrix X containing negative numbers and a label array, then calls fit on the lesson's MultinomialNB, with a question about what happens.
Typed matrix (the run result is shown with the explanation).
4. A model trained on 2 classes and 200 features gets X of shape (5, 200). What shape does predict_log_proba return?Apply
A grid labelled X.shape equal to (5, 200), a box for a model fitted on 2 classes and 200 features, and the call predict_log_proba with a question mark for the output shape.
Schematic.
5. One class has feature counts [3, 0, 1] summed over its documents and alpha = 1. What does feature_log_prob_ hold for that class?Apply
Three cells with feature counts 3, 0 and 1 for one class, alpha equal to 1.0, and a question mark for the stored feature_log_prob_ row.
Counts from the stem.

Use It

With sklearn, both variants are one-liners:

python
from sklearn.naive_bayes import GaussianNB, MultinomialNB
 
gnb = GaussianNB()
gnb.fit(X_train, y_train)
print(f"GaussianNB accuracy: {gnb.score(X_test, y_test):.3f}")
 
mnb = MultinomialNB(alpha=1.0)
mnb.fit(X_train_counts, y_train)
print(f"MultinomialNB accuracy: {mnb.score(X_test_counts, y_test):.3f}")

For text classification with sklearn:

python
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
 
text_clf = Pipeline([
    ("vectorizer", CountVectorizer()),
    ("classifier", MultinomialNB(alpha=1.0)),
])
 
text_clf.fit(train_texts, train_labels)
accuracy = text_clf.score(test_texts, test_labels)

The code in naive_bayes.py compares the from-scratch implementations against sklearn on the same data to verify correctness.

TF-IDF with Naive Bayes

Raw word counts give every word equal weight per occurrence. But common words like "the" and "is" appear frequently in every class, so they carry no information. TF-IDF (Term Frequency - Inverse Document Frequency) downweights common words and upweights rare, discriminative words.

python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
 
text_clf = Pipeline([
    ("tfidf", TfidfVectorizer()),
    ("classifier", MultinomialNB(alpha=0.1)),
])

TF-IDF values are non-negative, so they work with MultinomialNB. The combination of TF-IDF and MultinomialNB is one of the strongest baselines for text classification. It frequently beats more complex models on datasets with fewer than 10,000 training samples.

BernoulliNB for Short Text

For short text (tweets, SMS, chat messages), BernoulliNB can outperform MultinomialNB. Short texts have low word counts, so the frequency information that MultinomialNB relies on is noisy. BernoulliNB only cares about presence or absence, which is more reliable with short text.

python
from sklearn.naive_bayes import BernoulliNB
from sklearn.feature_extraction.text import CountVectorizer
 
text_clf = Pipeline([
    ("vectorizer", CountVectorizer(binary=True)),
    ("classifier", BernoulliNB(alpha=1.0)),
])

The binary=True flag in CountVectorizer converts all counts to 0/1. Without it, BernoulliNB still works, but it's seeing counts it wasn't designed for.

Calibrating NB Probabilities

NB probabilities are poorly calibrated. When NB says P(spam) = 0.95, the true probability might be 0.7. If you need reliable probability estimates (for example, to set a threshold or to combine with other models), use sklearn's CalibratedClassifierCV:

python
from sklearn.calibration import CalibratedClassifierCV
 
calibrated_nb = CalibratedClassifierCV(MultinomialNB(), cv=5, method="sigmoid")
calibrated_nb.fit(X_train, y_train)
proba = calibrated_nb.predict_proba(X_test)

This fits a logistic regression on top of NB's raw scores using cross-validation. The resulting probabilities are much closer to the true class frequencies.

Common Gotchas

  1. Negative feature values. MultinomialNB requires non-negative features. If you have negative values (like TF-IDF with certain settings or standardized features), use GaussianNB instead, or shift the features to be positive.

  2. Zero variance features. GaussianNB divides by variance. If a feature has zero variance for a class (all values identical), the probability computation breaks. The code adds a small smoothing term (1e-9) to all variances to prevent this.

  3. Class imbalance. If 99% of emails are not-spam, the prior P(not-spam) = 0.99 is so strong that it overwhelms the likelihood evidence. You can set class priors manually or use the class_prior parameter in sklearn.

  4. Feature scaling. MultinomialNB doesn't need scaling (it works on counts). GaussianNB doesn't need scaling either (it estimates per-feature statistics). This is an advantage over logistic regression and SVM, both of which are sensitive to feature scales.

Check: Use It0/6
1. Why pair TF-IDF features with MultinomialNB instead of raw word counts?Explain
Four words of one email with raw counts 6 for the, 4 for is, 2 for free and 1 for prize, above a row of dashed empty cells for the weights that would be used instead.
Typed illustrative counts.
2. Why would you wrap MultinomialNB in sklearn's CalibratedClassifierCV?Explain
Lesson code that wraps MultinomialNB in CalibratedClassifierCV with 5 folds and a sigmoid method, drawn as a flow from MultinomialNB through the wrapper to probabilities, with a question about why.
Code from the lesson.
3. 99% of the training emails are not-spam, and the model labels nearly every email not-spam, even ones full of spam words. What explains this, and what is a fix?Analyze
A bar showing 99 percent of training emails as not-spam and 1 percent as spam, and a spam-looking email that the model labels not-spam, with a question about the explanation and a fix.
Typed email; proportions from the stem.
4. After standardizing your features to zero mean, MultinomialNB fails on the new data. What is the problem, and what does the lesson suggest?Analyze
A flow from word-count features through a standardize step that sets each column mean to 0 into a MultinomialNB fit step marked as failing, with a question about the problem and the suggestion.
Schematic.
5. You must classify short chat messages and plan to use presence/absence of words as features. Which setup fits best?Design
Three short chat messages beside a 0/1 table showing which of six words each message contains, and a question about which setup fits best.
Typed messages; table shows presence only.
6. A pipeline must flag emails when P(spam) exceeds 0.9 and combine that value with other models' probabilities, using Naive Bayes scores. What design fits?Design
Emails feeding Naive Bayes scores, which feed two uses: flagging when P(spam) exceeds 0.9 and combining with other models' probabilities, with a question about the design.
Schematic.

Ship It

This lesson produces:

  • outputs/skill-naive-bayes-chooser.md, a decision skill for picking the right NB variant
  • code/naive_bayes.py, MultinomialNB and GaussianNB from scratch, with a sklearn comparison

When Naive Bayes Fails

NB fails when the independence assumption causes incorrect rankings, not just incorrect probabilities. That happens when:

  1. Strong feature interactions. If the class depends on the combination of two features but not either alone (XOR-like patterns), NB misses it entirely. Each feature alone provides no evidence, and NB can't combine them nonlinearly.

  2. Highly correlated features with opposing evidence. If feature A says "spam" and feature B says "not-spam," but A and B are perfectly correlated (they always agree in reality), NB sees conflicting evidence where there is none.

  3. Very large training sets. With enough data, discriminative models like logistic regression learn the true decision boundary and outperform NB. The independence assumption that helped with small data now holds the model back.

In practice, these failure modes are rare for text classification: text features are numerous, individually weak, and the independence assumption's errors tend to cancel out. For tabular data with a few strongly correlated features, reach for logistic regression or tree-based models first.

Check: Ship It0/2
1. The class depends on the combination of two features, but neither feature alone gives any evidence (an XOR-like pattern). What does Naive Bayes do?Analyze
A scatter plot of two binary-valued features with four clusters at the corners, class 0 at bottom-left and top-right and class 1 at the other two corners.
Synthetic points with Gaussian jitter, numpy (seed 3).
2. A Naive Bayes text classifier plateaus while logistic regression keeps improving as the training set grows enormous. Which explanation fits?Analyze
Accuracy against training-set size for Naive Bayes and logistic regression, with Naive Bayes flattening while logistic regression keeps rising.
Synthetic bag-of-words data with a block of copied features, numpy.

Exercises

  1. Smoothing experiment. Train MultinomialNB on text data with alpha values of 0.01, 0.1, 1.0, 10.0, and 100.0. Plot accuracy vs alpha. Where does performance peak? Why does very high alpha hurt?

  2. Feature independence test. Take a real text dataset. Pick two words that are obviously correlated ("machine" and "learning"). Compute P(word1 | class) * P(word2 | class) and compare to P(word1 AND word2 | class). How wrong is the independence assumption? Does it affect classification accuracy?

  3. Bernoulli implementation. Extend the code with a BernoulliNB class. Convert bag-of-words to binary (present/absent) and compare accuracy against MultinomialNB on text data. When does Bernoulli win?

  4. NB vs Logistic Regression. Train both on text data. Start with 100 training samples and increase to 10,000. Plot accuracy vs training set size for both. At what point does Logistic Regression overtake Naive Bayes?

  5. Spam filter. Build a complete spam classifier: tokenize raw email text, build vocabulary, create bag-of-words features, train MultinomialNB, evaluate with precision and recall (not just accuracy — why?).

Key Terms

TermWhat people sayWhat it actually means
Naive Bayes"Simple probabilistic classifier"A classifier that applies Bayes' theorem with the assumption that features are conditionally independent given the class
Conditional independence"Features don't affect each other"P(A, B | C) = P(A | C) * P(B | C): knowing B tells you nothing new about A once you know C
Laplace smoothing"Add-one smoothing"Adding a small count to every feature to prevent zero probabilities from dominating the prediction
Prior"What you believed before seeing data"P(class): the probability of each class before observing any features
Likelihood"How well the data fits"P(features | class): the probability of observing these features if the class is known
Posterior"What you believe after seeing data"P(class | features): the updated probability of the class after observing the features
Generative model"Models how data is generated"A model that learns P(X | Y) and P(Y), then uses Bayes' theorem to get P(Y | X)
Discriminative model"Models the decision boundary"A model that directly learns P(Y | X) without modeling how X is generated
Log probability"Avoid underflow"Working with log P instead of P to prevent the product of many small numbers from becoming zero in floating point

Further Reading

Code

Ships With