Naive Bayes: Principles, Examples, and Python Implementation

Naive Bayes : Guide Complet — Principes, Exemples et Implémentation Python

Naive Bayes: Naive Bayesian Classification — Complete Guide

Summary

Naive Bayes (or naive Bayesian classification) is a probabilistic classification algorithm based on Bayes’ theorem. Its originality lies in a simplifying assumption: all predictor variables are considered conditionally independent given the target class. This approximation, although often unrealistic, gives rise to an extremely fast, robust, and surprisingly effective algorithm in many real-world contexts, especially natural language processing.

Despite its conceptual simplicity, Naive Bayes remains an essential benchmark for text classification, spam filtering, and any problem where the number of features is high. In this guide, we will explore in detail the mathematical principles, the different variants, the implementation in Python, and the concrete use cases of the naive Bayesian approach.

Mathematical Principle of Naive Bayes

Bayes’ Theorem

The core of the algorithm rests on Bayes’ theorem, formulated in the 18th century by mathematician Reverend Thomas Bayes. This theorem allows computing the posterior probability of a class given observations:

P(C|X) = P(X|C) · P(C) / P(X)

Where:

  • P(C|X) is the posterior probability of class C given observation X.
  • P(X|C) is the likelihood, i.e., the probability of observing X given class C.
  • P(C) is the prior probability of class C.
  • P(X) is the marginal probability of the observation (constant for all classes).

The Conditional Independence Assumption

The central idea of Naive Bayes is the conditional independence assumption. We assume that features x_1, x_2, …, x_n are independent of each other given class C. This allows factorizing the likelihood:

P(X|C) = P(x_1, x_2, ..., x_n | C) = Π P(x_i | C)

That is, the joint probability of the entire set of features given the class equals the product of the individual probabilities of each feature given the class. This assumption is rarely verified in practice — words in a text or characteristics of a patient are often correlated — but it considerably simplifies the calculations.

Prediction Formula

To classify a new observation X, we seek the class that maximizes the posterior probability:

C* = argmax_c P(C=c) · Π P(x_i | C=c)

There is no need to calculate P(X) since it is identical for all classes. The decision therefore relies on the product of the prior probability of each class by the product of the individual likelihoods.

Laplace Smoothing

A major practical problem arises when a feature never appears in the training data for a given class. In this case, P(x_i|C) = 0, and since the probabilities are multiplied, the entire product becomes zero — even if other features strongly point to that class.

The solution is Laplace smoothing (or additive smoothing): we add a small constant α (usually 1) to the numerator and α × K to the denominator (where K is the number of possible values):

P(x_i | C) = (count(x_i, C) + α) / (count(C) + α × K)

This technique ensures that no probability will ever be exactly zero, while remaining faithful to the proportions observed in the data.

Intuition: The Probabilistic Detective

Imagine a detective who needs to determine whether an email is spam or not. They examine each word present in the message: “free,” “urgency,” “won,” “meeting,” etc. For each word, they ask: “What is the probability of finding this word in a spam? In a legitimate email?”

The naive detective of Naive Bayes evaluates each word separately, as if it were independent of the others. In reality, the words “win” and “lottery” often appear together — they are not independent. But the detective ignores this correlation and simply multiplies all the individual probabilities.

One might think that this approximation is too crude to be useful. Yet, it works surprisingly well. Why? Because the goal is not to estimate perfect probabilities, but to compare the classes against each other. Even if individual probabilities are imperfect, their combination retains enough information to make the right decision in the majority of cases.

This is the beauty of Naive Bayes: a naive assumption that produces remarkably solid results. This probabilistic detective, with his simplistic method, often beats much more sophisticated models, especially when the data is noisy or the training set is limited.

Python Implementation of Naive Bayes

The Three Main Variants

Scikit-learn offers three implementations of Naive Bayes, each suited to a different type of data:

  1. GaussianNB: assumes that continuous features follow a normal (Gaussian) distribution.
  2. MultinomialNB: designed for discrete data (counts), ideal for text classification.
  3. BernoulliNB: suited for binary (presence/absence) features.

1. GaussianNB — Continuous Data Classification

GaussianNB models each feature by a Gaussian distribution specific to each class. For each class c and each feature i, we estimate the mean μ{i,c} and the variance σ²{i,c}, then compute:

P(x_i | C=c) = (1 / √(2πσ²_{i,c})) · exp(-(x_i - μ_{i,c})² / (2σ²_{i,c}))
from sklearn.naive_bayes import GaussianNB
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report

# Load data
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.3, random_state=42
)

# Train the classifier
gnb = GaussianNB()
gnb.fit(X_train, y_train)

# Predictions and evaluation
y_pred = gnb.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.2%}")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

# Posterior probabilities for a new observation
probas = gnb.predict_proba(X_test[:3])
print("\nProbabilities for the first 3 predictions:")
for i, proba in enumerate(probas):
    print(f"  Observation {i}: {proba}")

On the Iris dataset, GaussianNB typically achieves accuracy above 95%, demonstrating that even with the independence assumption, the results are excellent when the data is clean and well-separated.

2. MultinomialNB — Text Classification

MultinomialNB is the most widely used variant in natural language processing. It works on word (or token) counts and assumes that the frequency of each word follows a multinomial distribution.

from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split

# Training corpus
texts = [
    "gagnez de l argent facilement cliquez ici maintenant",
    "offre exceptionelle prix imbattable livraison gratuite",
    "réunion d équipe demain à neuf heures salle trois",
    "le rapport trimestriel sera présenté vendredi prochain",
    "promotion limitee cinquante pourcent de reduction aujourd hui",
    "merci de confirmer votre présence pour la conférence",
    "félicitations vous êtes le gagnant cliquez pour réclamer",
    "bonjour je souhaite réserver une chambre pour deux nuits",
]
labels = ["spam", "spam", "ham", "ham", "spam", "ham", "spam", "ham"]

# Vectorization: converting texts to count matrices
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(texts)

# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
    X, labels, test_size=0.25, random_state=42
)

# Train the multinomial classifier
mnb = MultinomialNB(alpha=1.0)
mnb.fit(X_train, y_train)

# Predict on new messages
new_messages = [
    "réduction exceptionelle offre limitee",
    "le directeur demande votre rapport avant vendredi",
]
X_new = vectorizer.transform(new_messages)
predictions = mnb.predict(X_new)

for msg, pred in zip(new_messages, predictions):
    print(f"  \"{msg}\" → {pred}")

# Per-class probability analysis
log_proba = mnb.predict_log_proba(X_new)
for i, (msg, lp) in enumerate(zip(new_messages, log_proba)):
    print(f"  \"{msg}\" : spam={lp[0]:.2f}, ham={lp[1]:.2f}")

Laplace smoothing (α) here is crucial: it prevents unknown words in the training set from completely canceling the probability of a class.

3. BernoulliNB — Binary Features

BernoulliNB is designed for strictly binary features. In a text context, it is used with a binary representation (word presence/absence) rather than counts.

from sklearn.naive_bayes import BernoulliNB
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split

# Same data as above
texts = [
    "gagnez de l argent facilement cliquez ici maintenant",
    "offre exceptionelle prix imbattable livraison gratuite",
    "réunion d équipe demain à neuf heures salle trois",
    "le rapport trimestriel sera présenté vendredi prochain",
    "promotion limitee cinquante pourcent de reduction aujourd hui",
    "merci de confirmer votre présence pour la conférence",
    "félicitations vous êtes le gagnant cliquez pour réclamer",
    "bonjour je souhaite réserver une chambre pour deux nuits",
]
labels = ["spam", "spam", "ham", "ham", "spam", "ham", "spam", "ham"]

# Binary encoding: we don't count occurrences, just note presence
vectorizer = CountVectorizer(binary=True)
X = vectorizer.fit_transform(textes)

# Training
bnb = BernoulliNB(alpha=1.0)
bnb.fit(X, labels)

# Prediction
new = ["gagnez cliquez maintenant pour réclamer", "confirmez votre rendez-vous médical"]
X_new = vectorizer.transform(new)
for msg, pred in zip(new, bnb.predict(X_new)):
    print(f"  \"{msg}\" → {pred}")

BernoulliNB can outperform MultinomialNB when the presence of a word is more informative than its frequency. In spam filtering, the word “winner” typically appears only once in an email, so the binary version is more than sufficient.

4. GaussianNB From-Scratch Implementation

To truly understand Naive Bayes, nothing beats implementing it from scratch:

import numpy as np

class NaiveBayesFromScratch:
    """Gaussian Naive Bayes implemented from scratch."""

    def __init__(self):
        self.classes = None
        self.means = None
        self.variances = None
        self.priors = None

    def fit(self, X, y):
        """Estimates Gaussian parameters and prior probabilities."""
        self.classes = np.unique(y)
        n_samples, n_features = X.shape

        self.means = np.zeros((len(self.classes), n_features))
        self.variances = np.zeros((len(self.classes), n_features))
        self.priors = np.zeros(len(self.classes))

        for idx, c in enumerate(self.classes):
            X_c = X[y == c]
            # Mean and variance for each feature
            self.means[idx, :] = X_c.mean(axis=0)
            self.variances[idx, :] = X_c.var(axis=0)
            # Prior probability of the class
            self.priors[idx] = X_c.shape[0] / n_samples

    def _gaussian_density(self, x, mean, var):
        """Computes Gaussian density."""
        coefficient = 1.0 / np.sqrt(2.0 * np.pi * var)
        exponent = np.exp(-(x - mean) ** 2 / (2.0 * var))
        return coefficient * exponent

    def predict_proba(self, X):
        """Computes posterior probabilities for each class."""
        n_samples = X.shape[0]
        posteriors = np.zeros((n_samples, len(self.classes)))

        for idx, c in enumerate(self.classes):
            # Log prior probability
            prior = np.log(self.priors[idx])

            # Log-likelihood: sum of Gaussian density logs
            likelihood = 0
            for feature in range(X.shape[1]):
                likelihood += np.log(
                    self._gaussian_density(X[:, feature], self.means[idx, feature],
                                           self.variances[idx, feature]) + 1e-9
                )

            # Log posterior probability
            posteriors[:, idx] = prior + likelihood

        # Normalization to get valid probabilities
        log_max = np.max(posteriors, axis=1, keepdims=True)
        posteriors = np.exp(posteriors - log_max)
        posteriors /= posteriors.sum(axis=1, keepdims=True)

        return posteriors

    def predict(self, X):
        """Returns the most probable class."""
        probas = self.predict_proba(X)
        return self.classes[np.argmax(probas, axis=1)]

# Test with Iris data
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris.data, iris.target, test_size=0.3, random_state=42
)

nb = NaiveBayesFromScratch()
nb.fit(X_train, y_train)
predictions = nb.predict(X_test)
accuracy = np.mean(predictions == y_test)
print(f"Accuracy (from scratch): {accuracy:.2%}")
print(f"Classes: {nb.classes}")
print(f"Priors: {nb.priors}")

This manual implementation reveals the beauty of the algorithm: all it takes is estimating a few descriptive statistics (means and variances per class) to build a complete classifier. The use of logarithms for combining probabilities avoids floating-point underflow issues.

Naive Bayes Hyperparameters

Alpha (Smoothing)

This is the most important hyperparameter for MultinomialNB and BernoulliNB:

  • alpha=1.0 (default): standard Laplace smoothing, suitable in the majority of cases.
  • alpha < 1.0 (e.g., 0.1): weaker smoothing, the model sticks more closely to the observed data. Risk of overfitting if certain categories are underrepresented.
  • alpha > 1.0 (e.g., 2.0): stronger smoothing, the model is more regular but may lose accuracy.
# Search for the optimal hyperparameter
from sklearn.model_selection import GridSearchCV

param_grid = {"alpha": [0.01, 0.1, 0.5, 1.0, 2.0, 5.0]}
grid = GridSearchCV(MultinomialNB(), param_grid, cv=5, scoring="accuracy")
grid.fit(X_train, y_train)
print(f"Best alpha: {grid.best_params_['alpha']}")

Var_smoothing (GaussianNB)

This parameter controls the fraction of the largest variance added to variances for numerical stability:

  • var_smoothing=1e-9 (default): generally sufficient.
  • Increase this value if the data contains features with very low (or zero) variance, which could cause divisions by zero.

Advantages and Limitations of Naive Bayes

Advantages

  • Training speed: Simple computation of descriptive statistics (means, variances, counts). No complex iteration required.
  • Efficiency with little data: Works well even with just a few hundred examples. Where a neural network would need thousands of observations, Naive Bayes already gives convincing results.
  • Noise handling: The independence assumption acts as an implicit form of regularization. The model does not latch onto spurious correlations in the training data.
  • No excessive overfitting: The model is inherently simple and rarely too complex for the data.
  • Calibrated probabilities: The probabilities produced are often well calibrated (though not perfect).
  • Ideal for text: Word counts align naturally with the Bayesian approach.

Limitations

  • Unrealistic independence assumption: Features are rarely independent in practice. Words like “machine” and “learning” always appear together in technical documents.
  • Correlation problem: If two highly correlated features are present, their influence is counted twice, which can bias the prediction.
  • GaussianNB assumes normality: If the data follows a very different distribution (exponential, uniform, multimodal), performance drops significantly.
  • Sensitive to unseen categories: Even with smoothing, categories absent from training are poorly handled and can reduce prediction quality.
  • Imperfect probabilities: The absolute values of the probabilities are not reliable because of the assumed independence. You can trust the class ranking, but not the probabilities themselves.
  • Less powerful than complex models: For problems with complex interactions between features, algorithms like Random Forest, XGBoost, or neural networks are significantly superior.

4 Concrete Use Cases of Naive Bayes

1. Spam and Unwanted Email Filtering

This is the historical application of Naive Bayes, popularized by Paul Graham in his essay “A Plan for Spam” in 2002. The principle is simple and elegant:

  • Each word in the message contributes to the final decision.
  • Words like “lottery,” “urgency,” “click here” increase the probability of spam.
  • Words like “meeting,” “budget,” “report” decrease that probability.
  • Laplace smoothing handles words never encountered before.
# Concrete spam filtering example
spam_emails = [
    "URGENT Votre compte sera fermé cliquez ici immédiatement",
    "GAGNEZ 1000 euros gratuitement offre limitée aujourd'hui",
    "Médicament miracle perte de poids garantie sans ordonnance",
]
legit_emails = [
    "Bonjour, la réunion de projet est reportée à jeudi prochain.",
    "Veuillez trouver ci-joint le rapport mensuel des ventes.",
    "Merci de confirmer votre présence pour le séminaire d'entreprise.",
]

# Training and evaluation
all_texts = spam_emails + legit_emails
all_labels = ["spam"] * len(spam_emails) + ["ham"] * len(legit_emails)

vec = CountVectorizer()
X = vec.fit_transform(all_texts)

mnb = MultinomialNB(alpha=0.5)
mnb.fit(X, all_labels)

# Test on new messages
test_messages = [
    "Votre colis sera livré demain avant midi",
    "Félicitations vous avez été sélectionné pour recevoir un prix",
]
X_test = vec.transform(test_messages)
for msg, pred in zip(test_messages, mnb.predict(X_test)):
    print(f"  {pred.upper()}: \"{msg}\"")

2. Sentiment Analysis on Customer Reviews

Naive Bayes is commonly used to determine whether a customer review is positive, negative, or neutral. Each word carries a sentiment:

  • “excellent,” “fantastic,” “recommended” → positive
  • “disappointed,” “catastrophic,” “stay away” → negative
  • “adequate,” “average,” “so-so” → neutral

The main advantage is speed: you can analyze millions of reviews in a few seconds, which is crucial for real-time brand reputation monitoring on the internet. E-commerce companies use it to automatically sort reviews and identify recurring problems reported by customers.

3. Assisted Medical Diagnosis

In the medical field, Naive Bayes can help assess the probability of a disease based on the symptoms presented by a patient:

  • Each symptom contributes independently to the probabilistic diagnosis.
  • The prior probability of each disease can be calibrated based on regional epidemiological statistics.
  • The result is a list of possible diagnoses ranked by probability, which the doctor can use as a decision aid.

While the independence assumption is questionable in medicine (symptoms are often related), Naive Bayes remains a fast and interpretable initial screening tool. Its transparency — each symptom has a clear weight — is a major asset in a field where explainability is crucial.

4. Banking Fraud Detection

Financial institutions use Naive Bayes to detect suspicious transactions in real time:

  • Each transaction feature (amount, location, time, type of merchant) is evaluated independently.
  • A high-amount transaction made abroad at 3 a.m. triggers a probabilistic alert.
  • Classification speed (a few milliseconds) is essential to block a transaction before it is validated.

The algorithm produces a fraud probability that can be compared against a configurable threshold. This approach is particularly useful as a complement to other detection systems, forming an ensemble of models that reinforce each other for maximum security.

See Also