Naive Bayes
Learn how naive Bayes classifiers work: Bayes decision rule, Gaussian and multinomial event models, smoothing, log-sum-exp, and NumPy implementation.
Machine Learning from ScratchAll chapters Naive BayesFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Naive Bayes
Two measurements can carry nearly the same information. A classifier that multiplies their class-conditional likelihoods can then count that information twice: the predicted class may stay useful while its reported probability becomes too extreme. Naive Bayes makes this tradeoff explicit. It models each class with simple feature distributions and combines them using a conditional-independence assumption. For text counts, the corresponding multinomial model instead assumes independent token draws given the class and document length.
We will fit both Gaussian measurements and multinomial word counts, compare the primitive calculations with scikit-learn, and inspect a seeded example with nearly duplicated measurements. Training uses class counts and feature summaries; prediction adds one contribution per feature or observed token count. These simple operations make it possible to trace a prediction back to its inputs.
The model is generative rather than discriminative. Logistic regression, which we studied in Logistic Regression, models directly. Naive Bayes models and , then combines them with Bayes' theorem. This difference matters: a generative model can marginalize unobserved features under an appropriate observation model, but neither class implemented here silently accepts missing entries. Its probability estimates still depend on its distributional assumptions.
This chapter derives the Bayes decision rule, explains the conditional independence assumption, develops the Gaussian and multinomial event models, shows how smoothing and log-sum-exp keep computations stable, and examines what happens when independence fails. We will implement Gaussian and multinomial naive Bayes from NumPy primitives, compare them with scikit-learn, and study calibration and class imbalance.
Advantages
The first advantage is speed. Training a Gaussian naive Bayes classifier requires only class counts, per-class feature means, and per-class feature variances. That is work for observations and features, with no iterative optimization. Multinomial naive Bayes is similarly cheap: it accumulates word counts per class and then normalizes them. Prediction is also fast because each class score is a sum of terms. For high-dimensional text data, where the number of features can be tens or hundreds of thousands, this matters.
The second advantage is behavior with small samples and many features. A full multivariate Gaussian model would need to estimate a covariance matrix for each class, whose unregularized sample estimate is singular when a class has at most observations. Regularized full-covariance models remain possible. Naive Bayes replaces that matrix with separate variances per class. The number of parameters grows linearly in , not quadratically. This is why naive Bayes often works as a strong baseline for document classification, spam filtering, and other sparse count problems.
The third advantage is interpretability. Each class score is a sum of contributions from the prior and from individual features. We can inspect which words or measurements push an observation toward a class. The model also handles multiclass problems without modification: we compute one score per class and normalize.
Finally, naive Bayes is a natural online learner. With count data, we can update class counts and feature counts as new observations arrive. With Gaussian data, we can update means and variances incrementally. This makes the model useful in streaming settings where retraining a more complex classifier would be expensive.
Disadvantages
The central weakness is the conditional independence assumption. Naive Bayes treats features as independent once the class is known. When features remain dependent within a class, multiplying their marginal likelihoods can count overlapping evidence multiple times. A spam classifier may treat "free" and "win" as independent pieces of evidence. If those words remain dependent within spam, multiplying their marginal likelihoods discards that dependence. The result is a classifier that can rank observations well but produce probabilities that are too extreme.
The second weakness is the distributional assumption. Gaussian naive Bayes assumes each feature is normally distributed within each class. If a feature is skewed, multimodal, or bounded, a single Gaussian may be a poor description. Multinomial naive Bayes assumes that word counts follow a multinomial distribution with class-specific word probabilities. That is a reasonable approximation for some text data, but it ignores word order, grammar, and longer-range dependencies.
The third weakness is the zero-frequency problem. If a word never appears in the training documents of a class, the unsmoothed multinomial estimate for that word is zero. A test document containing that word then receives zero likelihood for that class, regardless of all other evidence. Smoothing fixes the numerical problem, but it does not make the model correct.
Class imbalance is another practical concern. The prior can dominate the score when one class is much more frequent than another. A model that predicts the majority class most of the time can have high accuracy but poor recall for the minority class. We need to evaluate with class-specific metrics and consider prior adjustment or threshold tuning.
Finally, naive Bayes cannot capture feature interactions. If the class depends on a combination of features rather than on each feature separately, naive Bayes may fail even with unlimited data. This is a bias problem, not a variance problem. More data will not fix a model that cannot represent the true decision boundary.
Formula
This section derives the Bayes decision rule and the log score, then develops the Gaussian and multinomial event models that the implementation uses.
Bayes decision rule and conditional independence
Every probabilistic classifier has to answer the same question: given a vector of measurements, which class is most plausible? Naive Bayes answers it by describing what data from each class look like, weighting those descriptions by how common each class is, and letting Bayes' theorem combine the two. The argument below builds that answer in stages: first the decision rule, then the obstacle that makes the rule impractical, then the assumption that removes the obstacle, and finally the numerical form a program evaluates.
We have a feature vector and a class label . Under equal-cost zero-one loss, the Bayes decision rule returns a class with the largest posterior probability , using a specified tie convention. Unequal error costs can require a different decision. Bayes' theorem expresses this posterior as
Each of the three factors has a distinct job:
- is the prior. It says how common class is before we look at any features at all.
- is the class-conditional likelihood. It says how plausible this particular measurement vector is if the class really is . This is where the model's description of the data lives.
- is the evidence. It is the marginal mass or density of the feature vector, with the class averaged out. It has no in it.
Only the first two factors carry information about which class we are in. The evidence can always be recovered afterwards by summing the numerator over classes:
Because that denominator is one number shared by every class, it rescales all posteriors by the same amount. Rescaling cannot change which class is largest, so the decision rule needs to look only at the numerator:
Here means the value of that makes the expression as large as possible. We are not yet comparing calibrated posterior probabilities; we are only asking which class produces the largest unnormalized score. The normalized probabilities come back later, and the ranking established here survives that normalization unchanged.
At this point the decision rule is settled, and the real difficulty becomes visible. The likelihood is a joint density over dimensions. Describing it without any structure means modelling how every feature varies with every other feature inside each class. For an unrestricted discrete joint table with possible values per feature, there are cells. Other structured families have different costs: a full Gaussian covariance, for example, has a quadratic rather than exponential parameter count. Naive Bayes reduces the feature model to class-conditional marginals.
The naive Bayes assumption is
This says that, conditional on the class, the features are independent. It does not imply that they are independent overall: mixing classes can create marginal association even when features are independent within every class. Within spam, the factorization says that seeing "free" supplies no further information about whether "win" appears once the class is known. Whether that holds depends on the class-specific data-generating process; Bayes' theorem alone does not supply the assumption. But it is far cheaper to estimate: the joint likelihood collapses from one complicated -dimensional object into a product of one-dimensional densities, and each of those can be estimated from a single column of the data.
Substituting the product into Bayes' theorem gives
where is the prior. The class score before normalization is
Taking logarithms is not cosmetic. Products of many small probability masses or densities can underflow floating-point arithmetic. A density can also exceed one, so very large density products can overflow. The logarithm is strictly increasing, so it does not change which class has the largest score. It converts the product into a sum, avoiding a direct product of small factors. The score still requires finite inputs and valid distribution parameters. It also exposes the structure of the model: each feature contributes one additive term, so a score can be decomposed into the prior plus a list of per-feature contributions that we can read off and interpret.
Once we have the scores, the normalized posterior is
For a batch of observations, we can store the scores in an matrix , where . Each column corresponds to a class, and each row corresponds to an observation. In code, we compute this matrix with broadcasting rather than writing nested loops.
One ingredient remains: the prior itself. It is usually estimated by its maximum likelihood value,
where is the number of training observations in class and is the total number of observations. If a class is rare, we may want to smooth the prior or set it explicitly. With Laplace smoothing, the estimate becomes
For , the extra pseudo-counts keep every prior strictly positive. This is the same idea we use for word probabilities later.
Gaussian and multinomial event models
The score above is a sum of terms of the form . Nothing in the derivation so far has said what that quantity is. Specifying it is choosing an event model, and the choice is where the modelling happens: it decides what kind of data the classifier can describe well. The two choices developed here are Gaussian for continuous features and multinomial for count features.
For Gaussian naive Bayes, we assume that feature within class follows a normal distribution:
where:
- : the value of feature for a given observation
- : the class label
- : the mean of feature within class
- : the variance of feature within class
- : the normal distribution with mean and variance
The density (probability density function) is
where:
- : the conditional density of feature given class
- : the mean of feature within class
- : the variance of feature within class
- : the constant
The factor normalizes the density so that it integrates to one. The exponent measures the squared distance between and the class mean, measured in units of the class standard deviation. A point far from the mean gets a small density. The variance controls how wide the distribution is: a large variance makes distant points less penalized.
When a class has observations with positive within-class variation for a feature, its Gaussian maximum likelihood estimates are the sample mean and the variance with divisor :
where:
- : the estimated mean of feature within class
- : the estimated variance of feature within class
- : the number of training observations in class
- : the value of feature for observation
- : the class label of observation , with the sum restricted to observations satisfying
With positive within-class variation, dividing by gives the variance MLE; for , dividing by gives the unbiased sample variance. If every value of the feature in a class is identical, the formula gives zero, but no maximum is attained over strictly positive Gaussian variances: the likelihood grows without bound as the variance approaches zero. The positive floor used below keeps the fitted density away from that zero-variance boundary. The relative difference between the two variance estimates decreases as grows. This is the same normal maximum likelihood calculation we saw in Maximum Likelihood Estimation.
The implementation uses the log density:
where:
- : the log density of feature given class
- : the mean of feature within class
- : the variance of feature within class
- : the observed value of feature
Let us derive this slowly. Start with the density:
Take the logarithm of the whole expression. The logarithm of a product is the sum of logarithms:
The first term is . The second term is just the exponent because . So we obtain the expression above. The constant term depends on the class through , so it is not the same for every class and must be included.
In practice, a class may have zero variance for a feature if all its training values are identical. Then the log density contains and the squared-distance term divides by zero. We fix this by adding a small positive constant to every variance:
Here is a small number such as , and is the variance of feature over the entire training set. The maximum is taken across features. This is the convention used by scikit-learn's GaussianNB through its var_smoothing parameter. With and at least one feature having positive global training variance, this gives a positive smoothing constant. If every global variance is zero, the formula also gives zero; our implementation rejects any remaining nonpositive class variance instead of claiming smoothing repaired it.
For multinomial naive Bayes, the data are nonnegative integer counts rather than continuous measurements. Conditional on the class and document length, tokens are independent categorical draws. The resulting count components are multinomial and are generally dependent when the length is fixed. The classifier below conditions on length, omitting a separate class-dependent length model. Each observation is a count vector , where is the number of times word occurs in a document, and is the vocabulary size. The document length is . The multinomial probability of the document given class is
where:
- : the document's count vector, with the number of times word occurs
- : the document length,
- : the factorial of the count of word , used in the multinomial coefficient
- : the probability of any particular ordering of the word tokens with those counts
- : the probability that a randomly chosen token from a document of class is word
- : the vocabulary size
The fraction is the multinomial coefficient. It counts the number of distinct orderings of the word tokens when there are tokens of each word. The product gives the probability of each token under the class-specific word distribution. The parameter is the probability that a randomly chosen token from a document of class is word , constrained so that the class-specific word probabilities sum to one.
The multinomial coefficient depends only on the document, not on the class. When we compare classes, it appears in every numerator and in the denominator, so it cancels. Therefore, for classification the likelihood is proportional to the product of class-specific word probabilities, with the proportionality factor independent of the class. This gives
Taking logs gives
where:
- : the log multinomial coefficient , which depends only on the document and is the same for every class
- : the vocabulary size
- : the count of word in the document
- : the probability that a randomly chosen token from a document of class is word
The constant is the same for all classes and does not affect the decision or the normalized posterior. The implementation below therefore stores log word probabilities and omits the class-independent multinomial coefficient from its class scores.
The maximum likelihood estimate of is the fraction of word tokens in class that are word :
where:
- : the total count of word in class
- : the total number of word tokens in class
The unsmoothed estimate requires a positive class token total . With smoothing parameter , even a class with zero observed tokens receives a defined uniform word distribution. We add pseudo-counts:
The numerator adds to every word count. The denominator adds so that the probabilities still sum to one. When , this is Laplace smoothing. When , it is sometimes called Lidstone smoothing. When , we recover the unsmoothed maximum likelihood estimate, which can be zero and cause the zero-frequency problem.
A related model is the Bernoulli event model. It represents a document by binary presence indicators rather than counts. The likelihood, over the fixed vocabulary of size , is
Here is the probability that word appears at least once in a document of class . The Bernoulli model explicitly penalizes words that are absent, because each absent word contributes the factor , while the multinomial model only looks at words that are present: a zero count contributes the factor and therefore no evidence. The smoothed estimate is
where is the number of documents in class that contain word , and is the number of documents in class . The denominator has because each word contributes two possible outcomes: present or absent.
Smoothing and stable log-probability computation
Two of the formulas above can fail on real data, and both failures are worth understanding rather than patching over. Smoothing solves the statistical failure; log-sum-exp solves the numerical one. They are separate problems that happen to appear in the same chapter.
Smoothing addresses a statistical issue: with limited data, a word or feature value may be missing from a class sample by chance. The unsmoothed estimate sets its probability to zero, which is an extreme claim: it asserts that the event is impossible, based on the absence of evidence rather than on evidence of absence. It also creates a numerical problem, because is negative infinity and a single such term wipes out every other piece of evidence in the score. Adding pseudo-counts keeps every probability strictly positive and regularizes the estimate toward a uniform distribution.
For multinomial naive Bayes, the effect of is easy to see. A larger pulls every toward . A smaller lets the observed counts dominate. If we set too small, rare words still get very small probabilities and the model can be unstable. As tends to infinity with the observed counts fixed, every class word distribution tends to uniform and predictions tend toward the prior. In practice, is a reasonable default, and we can tune it with cross-validation. The same principle applies to the Gaussian variance smoothing parameter : it should be small enough not to distort real variance differences, but large enough to prevent division by zero.
Even with strictly positive probabilities, multiplying many numbers can underflow. Suppose a document has 500 words and each word has probability . The product is , which is far below the smallest representable positive double. The base-10 logarithm of the product is , so the natural logarithm is , which is a perfectly manageable floating-point number. This is why we compute scores in log space.
After computing the log scores , we need to normalize them into probabilities. A direct computation of can overflow if some scores are large, or underflow if all scores are very negative. For finite class scores, log-sum-exp avoids exponentiating their common large scale. Let
Then
The subtraction makes the largest exponent zero, so the largest term in the sum is . The sum is therefore between and , which is numerically safe. The normalized posterior is
For finite scores this is mathematically identical to the original formula. Individual extremely small posterior probabilities may still round to zero, while their log probabilities remain available. The reordering protects the normalization without changing the model. In code, scipy.special.logsumexp implements this calculation.
For Gaussian naive Bayes, the same log-sum-exp step is used after we compute the joint log-likelihood for each class. Positive variances make the Gaussian log-density formula well defined. Extreme finite measurements can still overflow squared residuals; these teaching classes reject nonfinite computed scores. Variance smoothing and score normalization address different numerical problems.
Mathematical properties
With the formulas in place, it is worth asking what kind of model we have built and where its behaviour comes from. Three properties matter for practice: the model is generative, its decision boundary has a specific geometric shape, and its parameter count is linear in the dimension.
Naive Bayes is a generative classifier. It specifies a full probability model for the features and the label, then uses Bayes' theorem to classify. This is different from a discriminative classifier such as logistic regression, which models the conditional probability of the label directly. Generative models can be trained with unlabeled data in some settings, can handle missing features by marginalizing over them, and can generate synthetic observations. Discriminative models learn the label conditional or decision rule directly and can be more accurate when the generative assumptions are wrong; the comparison still depends on the model family and available training data.
For Gaussian naive Bayes, the decision boundary has a specific shape. The difference between the log scores of two classes and is
The squared terms make this a quadratic function of when the class variances differ. The boundary is therefore a quadratic surface. If the variances are shared across classes, so that for every feature , the quadratic terms cancel and the boundary becomes linear. This connects naive Bayes to linear and quadratic discriminant analysis. With a shared diagonal covariance matrix, Gaussian naive Bayes is a diagonal LDA. With class-specific diagonal covariance matrices, it is a diagonal QDA. We will study the full-covariance versions in the next chapter.
The parameter count is another important property. Gaussian naive Bayes has priors, means, and variances, for parameters. Multinomial naive Bayes has priors and word probabilities, for parameters. A full multivariate Gaussian with class-specific covariance matrices would have parameters. The linear parameter count is what makes naive Bayes usable in high dimensions.
These counts include all stored prior probabilities, although their sum-to-one constraint leaves only free prior parameters. Multinomial word probabilities likewise have free parameters per class. The full-covariance count below includes only the distinct entries of each symmetric covariance matrix.
The asymptotic notation becomes clearer when plotted. The next figure compares linear and quadratic parameter growth over a range of feature dimensions.
PythonIn [3]Expand
import numpy as np
K_params = 3
d_params = np.arange(1, 81)
nb_params = K_params + 2 * K_params * d_params
full_cov_params = (
K_params + K_params * d_params + K_params * d_params * (d_params + 1) / 2
)VisualizationOut [4]Expand

For fixed feature dimension and positive class probabilities, correctly specified Gaussian or multinomial class models admit consistent parameter estimates under the usual sampling and nondegeneracy conditions. Regularization must become negligible: Gaussian variance additions should tend to zero, while multinomial pseudo-counts can remain fixed because their ratio to growing class token totals then vanishes. With these conditions, the induced rule approaches Bayes risk. A misspecified independence model need not recover the true posterior, although its decision rule can still be useful.
Finally, good classification does not imply good probability estimates. Naive Bayes often ranks observations correctly even when its posterior probabilities are overconfident. If we need calibrated probabilities for risk assessment or decision making, we must check calibration separately. We will return to this point in the practical implications.
Visualizing Naive Bayes
The first figure shows decision regions of a two-dimensional Gaussian naive Bayes classifier. Each class gets its own mean and variance for each feature. The background color is the predicted probability of class 1. The decision boundary is where the two class scores tie. Because the class variances differ, the boundary can curve. The model is not using the correlation between the two features; it fits two separate one-dimensional Gaussians per class.
PythonIn [5]Expand
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.special import logsumexp
from sklearn.calibration import calibration_curve
from sklearn.datasets import make_classification
from sklearn.metrics import accuracy_score, brier_score_loss, log_loss
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB, MultinomialNB
# Generate a two-feature, two-class problem for the decision-region figure.
X_vis, y_vis = make_classification(
n_samples=300,
n_features=2,
n_informative=2,
n_redundant=0,
n_classes=2,
n_clusters_per_class=1,
class_sep=1.2,
flip_y=0.02,
random_state=42,
)
clf_vis = GaussianNB().fit(X_vis, y_vis)
# Build a grid over the feature space and compute the predicted probability
# of class 1 at each grid point for the decision-region figure.
x_min, x_max = X_vis[:, 0].min() - 0.5, X_vis[:, 0].max() + 0.5
y_min, y_max = X_vis[:, 1].min() - 0.5, X_vis[:, 1].max() + 0.5
xx, yy = np.meshgrid(
np.linspace(x_min, x_max, 300),
np.linspace(y_min, y_max, 300),
)
grid = np.column_stack([xx.ravel(), yy.ravel()])
Z = clf_vis.predict_proba(grid)[:, 1].reshape(xx.shape)VisualizationOut [6]Expand

Subtracting the two Gaussian log scores gives a quadratic expression without feature cross-products. Equal corresponding class variances cancel the quadratic terms, leaving a linear score difference. Unequal variances can produce curved boundaries, as in this fitted example. The shape is restricted, however: the model cannot represent a boundary that depends on a nonlinear interaction between the features beyond what the squared terms allow. If the true classes were arranged in a checkerboard pattern, this model would struggle.
The second figure examines calibration. A classifier is calibrated if, among all observations assigned a predicted probability of , about actually belong to the positive class. Naive Bayes is not guaranteed to satisfy this property. When features are correlated, the model treats them as independent evidence and can become overconfident. The simulation below creates two features that carry nearly the same information. The model treats these strongly overlapping measurements as separate evidence.
PythonIn [7]Expand
# Simulate a latent score, its binary label, and two nearly collinear features.
rng = np.random.default_rng(7)
n_cal = 5000
z = rng.normal(size=n_cal)
p_true = 1.0 / (1.0 + np.exp(-2.0 * z))
y_cal = rng.binomial(1, p_true)
# Feature 2 is almost a copy of feature 1, so the independence assumption fails.
x1 = z + 0.45 * rng.normal(size=n_cal)
x2 = x1 + 0.08 * rng.normal(size=n_cal)
X_cal = np.column_stack([x1, x2])
X_train_cal, X_test_cal, y_train_cal, y_test_cal = train_test_split(
X_cal, y_cal, test_size=0.5, random_state=7, stratify=y_cal
)
clf_cal = GaussianNB().fit(X_train_cal, y_train_cal)
# Predict on the held-out split and collect calibration diagnostics.
p_hat_cal = clf_cal.predict_proba(X_test_cal)[:, 1]
prob_true, prob_pred = calibration_curve(
y_test_cal, p_hat_cal, n_bins=10, strategy="quantile"
)
brier_cal = brier_score_loss(y_test_cal, p_hat_cal)ConsoleOut [8]Expand
Brier score on the simulated test set: 0.184
VisualizationOut [9]Expand

The reliability diagram is a diagnostic, not a proof. In this simulation, the two features are nearly collinear, so the independence assumption is badly violated. The class-conditional marginals are not guaranteed to be Gaussian either, so this single curve does not isolate the effect of dependence. The model can still separate the classes, but its probabilities are not trustworthy as estimates of risk. If we needed calibrated probabilities, we would apply a post-hoc calibration method such as Platt scaling or isotonic regression, which we will discuss in Part XII: Calibration and Prediction Uncertainty.
Example: A Two-Feature Gaussian Calculation
The formulas are compact, but compact formulas hide their arithmetic. Before writing any code, we work one small Gaussian naive Bayes problem by hand, in full, so that every symbol in the score has a concrete number attached to it. For the Gaussian parameter estimates in this worked example, subscripts give the class first and the feature second; the general Gaussian formulas above use feature first and class second. The training data have two features and two classes, with five observations per class:
| Class | Feature 1 | Feature 2 |
|---|---|---|
| 0 | 1 | 2 |
| 0 | 2 | 3 |
| 0 | 3 | 2 |
| 0 | 1 | 1 |
| 0 | 2 | 2 |
| 1 | 6 | 5 |
| 1 | 7 | 6 |
| 1 | 8 | 7 |
| 1 | 7 | 6 |
| 1 | 8 | 5 |
The first thing to check is whether anything needs smoothing. Each class has five observations, no feature is constant within a class, and no variance is zero, so the unsmoothed maximum likelihood estimates are safe here and we can use them directly.
The priors are equal because each class has five observations: . We want to classify the new point , which sits in the gap between the two clusters but is closer to class 1.
We begin with the sufficient statistics. For class 0, the feature means are
The variances use division by :
For class 1, the means are
The variances use division by in the same way:
where:
- : the class 1 values of feature 1 used in the mean
- : the class 1 values of feature 2 used in the mean
- : the sum of the class 1 values of feature 1
- : the sum of the class 1 values of feature 2
- : the class 1 mean for feature 1,
- : the class 1 mean for feature 2,
- : the sum of squared deviations for feature 1 and, separately, for feature 2
- : the number of class 1 observations,
With the statistics in hand, we evaluate one log density at a time. Each density has the same two-part structure: a normalizing constant that depends only on the variance, and a negative penalty that grows with the squared distance from the mean. For class 0 and feature 1, with :
where:
- : the value of feature 1 for the new point
- : the class 0 mean for feature 1,
- : the class 0 variance for feature 1,
- : the log-density normalizing contribution from the variance
- : the squared distance from the mean measured in units of the variance
The first term is . The squared distance is , and the denominator is , so the second term is . The squared-distance term dominates the normalizing constant here. The new point is far from the class 0 mean, and in this calculation the distance penalty () is much larger than the variance normalizing constant (). Thus
For class 0 and feature 2, with :
where:
- : the value of feature 2 for the new point
- : the class 0 mean for feature 2,
- : the class 0 variance for feature 2,
The first term is . The squared distance is , and the denominator is , so the second term is . Thus
The total log score for class 0 is the log prior plus the two log densities, evaluated before rounding:
Now we repeat the calculation for class 1. For feature 1 the arithmetic is symmetric, because the distance from to is also and the variance is the same:
For class 1 and feature 2, with and mean :
where:
- : the class 1 mean for feature 2,
- : the class 1 variance for feature 2,
The squared distance is , so the second term is . The first term is . Thus
This is the decisive term. Feature 1 contributes identically to both classes, so the entire difference between the two scores comes from feature 2, where the new point is much closer to the class 1 mean. The total log score for class 1, again evaluated before rounding, is
Class 1 has the larger score by . To convert the scores into a posterior probability, we use the logistic form for two classes:
where:
- : the unnormalized log score for class 0, computed above as
- : the unnormalized log score for class 1, computed above as
- : the score difference, which is the only quantity that matters after normalization
Substituting gives
So we classify the point as class 1 with a predicted probability of about . Notice how the log-sum-exp normalization works here in a two-class setting: the difference of scores is the only quantity that matters, and the sigmoid turns that difference into a probability without ever computing the raw likelihoods.
Within class 0, the sample correlation between the two features is about . Within class 1, it is about . These sample correlations are a diagnostic to investigate; five observations per class cannot establish population conditional dependence. The fitted model nevertheless multiplies the two marginal likelihoods. It favors class 1, while the calibration of its probability remains a separate question.
We can verify the arithmetic with a short computation:
PythonIn [10]Expand
X_ex = np.array(
[
[1, 2],
[2, 3],
[3, 2],
[1, 1],
[2, 2],
[6, 5],
[7, 6],
[8, 7],
[7, 6],
[8, 5],
],
dtype=float,
)
y_ex = np.array([0] * 5 + [1] * 5)
x_new = np.array([4.5, 4.0])
def normal_log_pdf(x, mu, var):
return -0.5 * np.log(2 * np.pi * var) - 0.5 * (x - mu) ** 2 / var
example_rows = []
for k in [0, 1]:
Xk = X_ex[y_ex == k]
mu = Xk.mean(axis=0)
var = Xk.var(axis=0)
log_score = np.log(0.5)
for j in range(2):
log_score += normal_log_pdf(x_new[j], mu[j], var[j])
example_rows.append(
{
"class": k,
"mean feature 1": mu[0],
"mean feature 2": mu[1],
"variance feature 1": var[0],
"variance feature 2": var[1],
"log score": log_score,
}
)
example_table = pd.DataFrame(example_rows)ConsoleOut [11]Expand
class mean feature 1 mean feature 2 variance feature 1 variance feature 2 log score
0 1.8 2.0 0.56 0.40 -13.2919
1 7.2 5.8 0.56 0.56 -11.3530
Posterior probability of class 1: 0.8742
Within-class correlation for class 0: 0.4226
Within-class correlation for class 1: 0.4286The printed means, variances, and scores match the hand calculation at the displayed precision. The posterior from the printed scores comes out at approximately , matching the earlier sigmoid calculation. The arithmetic agrees with the formulas. The sample correlations motivate checking the class-specific dependence and calibration on more data; they do not establish population dependence from five observations per class.
The printed scores come from a product of one-dimensional densities. The next figure shows those densities directly for feature 2, with the new observation marked.
PythonIn [12]Expand
# Compute per-feature Gaussian densities for feature 2 from the worked example.
feature_index = 1
x_grid_density = np.linspace(
X_ex[:, feature_index].min() - 1.0,
X_ex[:, feature_index].max() + 1.0,
300,
)
density_by_class = {}
for k in [0, 1]:
Xk = X_ex[y_ex == k]
mu = Xk[:, feature_index].mean()
var = Xk[:, feature_index].var()
density_by_class[k] = np.exp(normal_log_pdf(x_grid_density, mu, var))VisualizationOut [13]Expand

Implementation in NumPy and scikit-learn
We now implement both event models from primitives. The goal is to see exactly where the formulas enter the code, then compare with scikit-learn to confirm that the implementation is correct. We use NumPy for the linear algebra and scikit-learn only as a reference.
Gaussian Naive Bayes from scratch
The class below stores the class priors, per-class feature means, and per-class feature variances. The fit method computes the maximum likelihood estimates. The _joint_log_likelihood method evaluates the Gaussian log density for every class and every observation. The predict_log_proba method applies the log-sum-exp normalization.
PythonIn [14]Expand
class GaussianNaiveBayes:
"""Gaussian naive Bayes classifier with variance smoothing."""
def __init__(self, var_smoothing=1e-9):
if not np.isfinite(var_smoothing) or var_smoothing < 0:
raise ValueError("var_smoothing must be finite and nonnegative.")
self.var_smoothing = var_smoothing
def fit(self, X, y):
X = np.asarray(X, dtype=float)
y = np.asarray(y)
if (
X.ndim != 2
or X.shape[0] == 0
or X.shape[1] == 0
or y.ndim != 1
or len(y) != len(X)
):
raise ValueError(
"Use a nonempty dense matrix and one label per row."
)
if not np.all(np.isfinite(X)):
raise ValueError(
"Features must be finite; handle missing values explicitly."
)
self.n_features_in_ = X.shape[1]
self.classes_, counts = np.unique(y, return_counts=True)
self.priors_ = counts / counts.sum()
self.means_ = np.zeros((len(self.classes_), X.shape[1]))
self.vars_ = np.zeros_like(self.means_)
for k, label in enumerate(self.classes_):
Xk = X[y == label]
self.means_[k] = Xk.mean(axis=0)
self.vars_[k] = Xk.var(axis=0)
epsilon = self.var_smoothing * X.var(axis=0).max()
self.vars_ += epsilon
if not np.all(np.isfinite(self.vars_)) or np.any(self.vars_ <= 0):
raise ValueError(
"Positive finite class variances are required; remove constant features or choose valid smoothing."
)
self.epsilon_ = epsilon
return self
def _joint_log_likelihood(self, X):
X = np.asarray(X, dtype=float)
if (
X.ndim != 2
or X.shape[1] != self.n_features_in_
or not np.all(np.isfinite(X))
):
raise ValueError(
"Prediction requires a finite matrix with the fitted feature count."
)
jll = np.empty((X.shape[0], len(self.classes_)))
for k in range(len(self.classes_)):
mu = self.means_[k]
var = self.vars_[k]
jll[:, k] = (
np.log(self.priors_[k])
- 0.5 * np.sum(np.log(2.0 * np.pi * var))
- 0.5 * np.sum((X - mu) ** 2 / var, axis=1)
)
if not np.all(np.isfinite(jll)):
raise ValueError(
"Computed scores are nonfinite; inspect feature magnitudes and variances."
)
return jll
def predict_log_proba(self, X):
jll = self._joint_log_likelihood(X)
centered = jll - np.max(jll, axis=1, keepdims=True)
return centered - logsumexp(centered, axis=1, keepdims=True)
def predict_proba(self, X):
return np.exp(self.predict_log_proba(X))
def predict(self, X):
return self.classes_[np.argmax(self._joint_log_likelihood(X), axis=1)]The teaching classes accept finite dense feature matrices with matching labels. Use nonmissing class labels of one consistent string or numeric type; these small classes do not validate missing or mixed-type labels. The Gaussian class requires positive fitted variances; the multinomial class requires nonnegative values and alpha>0. Integer counts match the multinomial sampling interpretation, while nonnegative fractional weights use its score as an approximation.
Each line of _joint_log_likelihood maps directly onto a piece of the log-score formula. np.log(self.priors_[k]) is the log prior. The term -0.5 * np.sum(np.log(2.0 * np.pi * var)) collects the normalizing constants across features. The term -0.5 * np.sum((X - mu) ** 2 / var, axis=1) is the summed squared-distance penalty, broadcast over every observation at once.
The fit method uses np.var, which divides by the number of observations. That matches the maximum likelihood estimate. The smoothing constant is a fraction of the largest feature variance in the full training set, which matches scikit-learn's convention. The _joint_log_likelihood method uses broadcasting: (X - mu) ** 2 / var has shape , and summing over axis=1 produces one score per observation.
Numerical comparison with scikit-learn
We generate a three-class problem with six features, two of which are redundant. Redundant features are linear combinations of informative features, so they introduce correlation. This is a useful stress test for the independence assumption. We fit both our implementation and scikit-learn's GaussianNB on the same training split and compare accuracy, log loss, and probabilities.
PythonIn [15]Expand
# Generate a three-class problem with two redundant, correlated features.
X_num, y_num = make_classification(
n_samples=600,
n_features=6,
n_informative=4,
n_redundant=2,
n_classes=3,
n_clusters_per_class=1,
class_sep=1.0,
flip_y=0.05,
random_state=123,
)
X_train_num, X_test_num, y_train_num, y_test_num = train_test_split(
X_num, y_num, test_size=0.3, random_state=123, stratify=y_num
)
custom_gnb = GaussianNaiveBayes().fit(X_train_num, y_train_num)
sk_gnb = GaussianNB().fit(X_train_num, y_train_num)
# Predict and collect probabilities on the shared held-out split.
custom_pred = custom_gnb.predict(X_test_num)
sk_pred = sk_gnb.predict(X_test_num)
custom_proba = custom_gnb.predict_proba(X_test_num)
sk_proba = sk_gnb.predict_proba(X_test_num)ConsoleOut [16]Expand
model accuracy log loss from scratch 0.85 0.5086 scikit-learn 0.85 0.5086 Max absolute probability difference: 1.89e-15
The two implementations agree to floating-point precision. The accuracy and log loss are identical up to rounding. This confirms that the from-scratch implementation follows the same Gaussian event model as scikit-learn. The log loss is a proper scoring rule: it rewards confident correct predictions and penalizes confident wrong ones. It is often more informative than accuracy when we care about probability quality.
The maximum absolute probability difference is worth reading literally. It is a numerical check, not a performance measure: a value near machine epsilon says that two independently written programs computed the same numbers. If it were large, the discrepancy would point to a difference in how means, variances, or smoothing constants were computed, and we would go back to the formulas to find which convention differed.
Multinomial Naive Bayes from scratch
For count data, we accumulate word counts per class, add the smoothing parameter, and normalize. The _joint_log_likelihood method is a matrix multiplication between the document count matrix and the transposed matrix of log word probabilities. This is one of the reasons multinomial naive Bayes is so fast for text.
PythonIn [17]Expand
class MultinomialNaiveBayes:
"""Multinomial naive Bayes classifier with additive smoothing."""
def __init__(self, alpha=1.0, fit_prior=True):
if not np.isfinite(alpha) or alpha <= 0:
raise ValueError("This implementation requires a finite alpha > 0.")
self.alpha = alpha
self.fit_prior = fit_prior
def fit(self, X, y):
X = np.asarray(X, dtype=float)
y = np.asarray(y)
if (
X.ndim != 2
or X.shape[0] == 0
or X.shape[1] == 0
or y.ndim != 1
or len(y) != len(X)
):
raise ValueError(
"Use a nonempty dense matrix and one label per row."
)
if not np.all(np.isfinite(X)):
raise ValueError(
"Features must be finite; handle missing values explicitly."
)
self.n_features_in_ = X.shape[1]
self.classes_, counts = np.unique(y, return_counts=True)
if self.fit_prior:
self.priors_ = counts / counts.sum()
else:
self.priors_ = np.full(len(self.classes_), 1.0 / len(self.classes_))
if np.any(X < 0):
raise ValueError("Multinomial features must be nonnegative.")
self.feature_count_ = np.zeros((len(self.classes_), X.shape[1]))
for k, label in enumerate(self.classes_):
self.feature_count_[k] = X[y == label].sum(axis=0)
self.class_count_ = counts.astype(float)
self.token_count_ = self.feature_count_.sum(axis=1)
smoothed = self.feature_count_ + self.alpha
normalizers = smoothed.sum(axis=1, keepdims=True)
if not np.all(np.isfinite(smoothed)) or not np.all(
np.isfinite(normalizers)
):
raise ValueError(
"Aggregated counts or smoothing overflow; rescale input magnitudes."
)
self.feature_log_prob_ = np.log(smoothed) - np.log(normalizers)
return self
def _joint_log_likelihood(self, X):
X = np.asarray(X, dtype=float)
if (
X.ndim != 2
or X.shape[1] != self.n_features_in_
or not np.all(np.isfinite(X))
or np.any(X < 0)
):
raise ValueError(
"Prediction requires finite nonnegative features with the fitted width."
)
jll = X @ self.feature_log_prob_.T + np.log(self.priors_)
if not np.all(np.isfinite(jll)):
raise ValueError(
"Computed scores are nonfinite; inspect input magnitudes."
)
return jll
def predict_log_proba(self, X):
jll = self._joint_log_likelihood(X)
centered = jll - np.max(jll, axis=1, keepdims=True)
return centered - logsumexp(centered, axis=1, keepdims=True)
def predict_proba(self, X):
return np.exp(self.predict_log_proba(X))
def predict(self, X):
return self.classes_[np.argmax(self._joint_log_likelihood(X), axis=1)]The smoothing shows up in two places. self.feature_count_ + self.alpha is the numerator of the smoothed word probability. The row-wise normalization by normalizers plays the role of in the denominator. With finite positive smoothed counts and normalizers, the probabilities for each class sum to one; the guard rejects aggregate overflow. Storing feature_log_prob_ rather than the probabilities themselves means prediction never multiplies small numbers: X @ feature_log_prob_.T evaluates in a single matrix product.
We now use a tiny document-count example. The vocabulary has four words: "free", "win", "meeting", and "offer". The training set has three spam documents and three ham documents, represented by individual document rows; fitting then aggregates counts within each class. The first test document contains "free" twice and "win" once. The second test document adds one occurrence of "offer". The word "offer" never appears in the ham training counts, so without smoothing the ham likelihood would be zero.
PythonIn [18]Expand
vocab = ["free", "win", "meeting", "offer"]
X_counts = np.array(
[
[4, 5, 1, 3],
[5, 4, 2, 2],
[3, 6, 0, 4],
[1, 1, 8, 0],
[2, 0, 7, 0],
[1, 2, 9, 0],
],
dtype=float,
)
y_counts = np.array(["spam"] * 3 + ["ham"] * 3)
X_counts_test = np.array(
[
[2, 1, 0, 0],
[2, 1, 0, 1],
],
dtype=float,
)
# Fit both the from-scratch and scikit-learn multinomial models with Laplace smoothing.
custom_mnb = MultinomialNaiveBayes(alpha=1.0).fit(X_counts, y_counts)
sk_mnb = MultinomialNB(alpha=1.0).fit(X_counts, y_counts)ConsoleOut [19]Expand
Smoothed word probabilities (alpha = 1):
free win meeting offer
ham 0.1429 0.1143 0.7143 0.0286
spam 0.3023 0.3721 0.0930 0.2326
Predicted spam probabilities:
document P(spam) from scratch P(spam) scikit-learn
free free win 0.9358 0.9358
free free win offer 0.9916 0.9916The three spam rows total , so adding one per word gives over a total of . The ham rows total , giving over a total of after smoothing. The pseudocount of one gives "offer" a nonzero probability in ham even though it never appeared in the ham training counts. Without smoothing, that probability would be zero and the second test document would have zero ham likelihood.
The counts above produce two class-specific word distributions. The figure compares those distributions side by side.
PythonIn [20]Expand
spam_idx_theta = list(custom_mnb.classes_).index("spam")
ham_idx_theta = list(custom_mnb.classes_).index("ham")
theta_spam = np.exp(custom_mnb.feature_log_prob_[spam_idx_theta])
theta_ham = np.exp(custom_mnb.feature_log_prob_[ham_idx_theta])
word_positions = np.arange(len(vocab))VisualizationOut [21]Expand

Look at the two documents side by side. Adding a single "offer" token adds a log word-probability term for each class. Here the smoothed offer probability is larger in spam, so the extra token increases the spam score relative to ham. A zero count contributed no term before; multinomial naive Bayes does not separately penalize word absence.
Effect of smoothing
The smoothing parameter controls the trade-off between the observed counts and a uniform distribution. A very small approaches the unsmoothed maximum likelihood estimate. A large pulls the word probabilities toward and weakens the effect of the data. The table below shows the predicted spam probability for both test documents as varies.
PythonIn [22]Expand
# Sweep the smoothing parameter and record spam probabilities for both documents.
alphas = [0.01, 0.1, 1.0, 10.0]
alpha_rows = []
for alpha in alphas:
model = MultinomialNaiveBayes(alpha=alpha).fit(X_counts, y_counts)
spam_idx = list(model.classes_).index("spam")
probs = model.predict_proba(X_counts_test)[:, spam_idx]
alpha_rows.append(
{
"alpha": alpha,
"P(spam | no offer)": probs[0],
"P(spam | offer)": probs[1],
}
)
alpha_results = pd.DataFrame(alpha_rows)ConsoleOut [23]Expand
alpha P(spam | no offer) P(spam | offer) 0.01 0.9574 0.9999 0.10 0.9555 0.9994 1.00 0.9358 0.9916 10.00 0.7751 0.8548
With , the model is close to the raw counts. The word "offer" still has a tiny ham probability, so the document containing "offer" is strongly but not absolutely classified as spam. As increases, each class token distribution moves toward the uniform distribution; both converge to the same uniform limit. At , the four pseudo-counts total 40 per class, comparable with the observed class token totals of 39 and 31, and the predictions move toward the prior. In practice, we choose by cross-validation rather than by inspecting a single test document.
Over this grid, the document without "offer" moves from about 0.9574 to 0.7751, while the document with "offer" moves from about 0.9999 to 0.8548. The absolute probability change is therefore larger for the first document. The unseen ham token creates a zero-frequency problem without smoothing, but the normalized probability change also depends on the other word ratios and the starting probability.
The four-point table hides the shape of the change. A finer sweep shows how both documents respond as smoothing increases.
PythonIn [24]Expand
alpha_grid = np.logspace(-2, 1.2, 80)
alpha_curve_rows = []
for alpha_value in alpha_grid:
model_curve = MultinomialNaiveBayes(alpha=alpha_value).fit(
X_counts, y_counts
)
spam_idx_curve = list(model_curve.classes_).index("spam")
probs_curve = model_curve.predict_proba(X_counts_test)[:, spam_idx_curve]
alpha_curve_rows.append(
{
"alpha": alpha_value,
"without offer": probs_curve[0],
"with offer": probs_curve[1],
}
)
alpha_curve = pd.DataFrame(alpha_curve_rows)VisualizationOut [25]Expand

Key Parameters
The parameter alpha controls additive smoothing. The scikit-learn default alpha=1.0 is Laplace smoothing. Compare it with smaller values such as or inside validation. Larger values add more pseudo-count mass relative to the observed token counts, pulling the fitted probabilities further toward uniform; whether that improves the intended prediction needs validation. A value of zero reintroduces the zero-frequency problem when a token is absent from a class sample.
The parameter fit_prior determines whether the class priors are learned from the training labels. If it is True, the prior is the observed class frequency. If it is False, the prior is uniform. We can also pass an explicit class_prior array in scikit-learn when we have external information about class frequencies or when we want to correct for class imbalance. For example, in a rare-disease screening problem, we might set the prior to the known population prevalence rather than the prevalence observed in a case-control sample.
For Gaussian naive Bayes, the main parameter is var_smoothing. It adds a small fraction of the largest feature variance to every class-feature variance. The default is . Increasing it stabilizes the model when some features have zero or near-zero variance within a class. Decreasing it makes the model closer to the unsmoothed maximum likelihood estimate. The parameter priors can be used to set class priors explicitly, just as with the multinomial model.
A practical note about scaling: The unsmoothed Gaussian naive Bayes decision rule is invariant to separate invertible affine transformations of each feature. If we replace by , the mean and variance scale accordingly, and the log-density changes by a constant that is the same for every class. The decision is unchanged. However, var_smoothing is computed from the largest feature variance, so features with very different scales can receive uneven effective smoothing. Standardizing features, as discussed in Standardization, makes the smoothing parameter easier to interpret and can improve numerical behavior.
Key Methods
The fit method estimates the model parameters. For Gaussian naive Bayes, it computes class priors, per-class means, and per-class variances. For multinomial naive Bayes, it accumulates word counts and computes smoothed log word probabilities. Both methods return the fitted model, which allows chaining. Both primitive classifiers break an exact score tie by choosing the first class in the sorted np.unique class order.
The predict method returns the most likely class for each observation. It is equivalent to taking the argmax of the joint log-likelihood. The predict_proba method returns the normalized posterior probabilities. The predict_log_proba method returns the logarithms of those probabilities. For extremely small posterior probabilities, predict_log_proba can preserve distinctions across observations that probability outputs lose to underflow.
The scikit-learn implementations also provide partial_fit, which updates the model with a batch of observations without retraining from scratch. This is useful for streaming data and for datasets that do not fit in memory. Our from-scratch classes do not implement partial_fit, but the update equations are straightforward: for Gaussian naive Bayes, we update running means and variances; for multinomial naive Bayes, we add the new counts to the existing count matrix.
The scikit-learn score method returns the mean accuracy on the given data. Accuracy is a useful summary when the classes are balanced, but it can be misleading when they are not. We should pair it with precision, recall, F1, and the confusion matrix, as discussed in Classification Metrics and Decision Thresholds.
Practical Implications
A naive Bayes model can serve as a baseline when its event model matches the feature representation and its held-out performance meets the intended decision. Its class counts and per-feature sufficient statistics can be accumulated in one pass, supporting text classification, spam filtering, topic assignment, streaming updates, and initial screening models. The dense teaching classes here use per-class masks and separate summary operations, rather than implementing that streaming algorithm. It is less appropriate when the decision depends on feature interactions, when calibrated risk scores are required without post-hoc correction, or, for the Gaussian variant, when the within-class feature distributions are poorly represented by Gaussian densities.
The numerical example in this chapter compares held-out predictions from two implementations of the same Gaussian event model. It establishes numerical agreement, not general superiority. The tiny word-count example checks multinomial arithmetic; six documents cannot establish real-world classification performance. A useful baseline must be evaluated on data that represent its intended deployment, with a metric that matches the decision.
The event model should follow the meaning of the inputs. Gaussian densities describe continuous measurements; multinomial probabilities describe token counts conditional on document length; Bernoulli indicators describe presence or absence over a fixed vocabulary. Choosing among them is a modelling decision about the data-generating process, not a tuning detail, and it determines which preprocessing steps are coherent.
Data requirements and preprocessing
Gaussian naive Bayes assumes each feature is approximately normal within each class. The assumption applies to one column at a time, so it can be checked with per-class histograms, density plots, skewness summaries, and outlier checks. Categorical and binary variables are poorly represented by a Gaussian summary; the Bernoulli event model is the natural alternative for presence indicators. Skewed positive measurements may be logged or otherwise transformed when the transformation follows the measurement process, but the change should be validated rather than chosen from test performance.
The multinomial sampling model describes integer token counts. The implementations here allow finite nonnegative inputs with noninteger weights, subject to input-shape and numerical checks. Raw term frequencies match the multinomial sampling story; TF-IDF weights are nonnegative but not counts, so the likelihood no longer describes how the data were generated. The model can still work as a linear scorer over log-probability weights, but that use should be treated as an approximation. The same caution applies to rates, normalized intensities, and other nonnegative continuous inputs.
Missing values require explicit handling. The Gaussian implementations here require finite observed values, and mean imputation changes the estimated means and variances; the result depends on the imputation rule and missingness pattern. If missingness is informative, an indicator column or a model that marginalizes over the missing entry is preferable to silent imputation. Multinomial naive Bayes treats an absent count as zero, which is appropriate only for structural zeros rather than unrecorded values. Feature scaling is not required for the unsmoothed Gaussian decision rule, but var_smoothing is derived from the largest training variance, so columns on very different scales receive uneven effective smoothing; standardizing nonconstant columns can make the parameter easier to interpret and improve numerical conditioning.
Best Practices
Treat the default settings as starting points. For multinomial naive Bayes, begin with alpha=1.0 and compare a small grid such as {0.01, 0.1, 1, 10} using cross-validation. For Gaussian naive Bayes, var_smoothing=1e-9 is a starting value to assess within validation. Inspect constant or near-constant class-feature columns, and tune smoothing using training folds. Tune the event model, smoothing, and any feature transformation inside a pipeline so that vocabulary construction and scaling are fitted on training folds only. The final test set should remain untouched until the model and decision threshold are fixed.
Choose the evaluation metric before tuning. If the application needs a hard label or a ranking, accuracy, precision, recall, F1, or the area under the precision-recall curve may be more relevant than log loss. If the application needs probability estimates, log loss and a reliability diagram should accompany the classification metrics. Class priors should reflect the deployment population when it is known; otherwise, learn them from training data and check whether the training sample was collected under a different sampling scheme.
Keep the model as simple as the evidence supports. The count and moment summaries avoid iterative model fitting. Choose a validation scheme that respects the sampling design and a computational budget measured on the intended data. If a more flexible model is considered, compare it against the naive Bayes baseline on the same folds and with the same metric. Improvements in accuracy alone are not sufficient when the application depends on minority-class recall or calibrated risk.
Dependence violations, class imbalance, and calibration
The independence assumption is the first diagnostic. Duplicated or nearly duplicated features cause the model to count the same evidence more than once. Accuracy can remain acceptable while probabilities move toward zero and one, so a reliability diagram is more informative than a confusion matrix alone. Within-class correlation can be checked directly, and highly correlated columns can be removed or combined before fitting when the redundancy is not intentional.
Class imbalance affects the prior and the decision threshold. A majority-class prior can dominate the score, producing high accuracy with poor minority-class recall. Class-specific precision, recall, F1, and the confusion matrix should be reported alongside accuracy. If the prior is known to differ from the training sample, set it explicitly or adjust the threshold after validation; changing the prior changes the scores, so the effect should be evaluated rather than assumed.
Calibration should be checked separately from discrimination. A model can rank observations well and still assign probabilities that are too extreme. Violations of the conditional independence assumption can make naive Bayes probabilities overconfident. If calibrated probabilities are required, fit Platt scaling or isotonic regression on held-out data and verify the correction on a separate split. The calibrator should not be fitted on the same data used to fit the classifier.
Diagnostics and failure cases
Two failure modes deserve explicit checks. In an XOR-like pattern, each feature is uninformative on its own and the class depends on their combination. Naive Bayes multiplies marginal likelihoods and can perform near chance regardless of sample size because the limitation is representational rather than statistical. In text data, the zero-frequency problem is the analogue: a word absent from a class's training documents receives zero unsmoothed probability, and a test document containing that word can eliminate the class. Smoothing repairs the arithmetic, but it does not create evidence about the word.
The XOR failure is representational, not numerical. The next figure shows the decision regions a Gaussian naive Bayes model produces on such data.
PythonIn [26]Expand
# XOR data: labels depend on the sign product of two features.
rng_xor = np.random.default_rng(2024)
n_xor = 400
X_xor = rng_xor.uniform(-1.0, 1.0, size=(n_xor, 2))
y_xor = ((X_xor[:, 0] * X_xor[:, 1]) > 0).astype(int)
clf_xor = GaussianNB().fit(X_xor, y_xor)
x_min_xor, x_max_xor = X_xor[:, 0].min() - 0.1, X_xor[:, 0].max() + 0.1
y_min_xor, y_max_xor = X_xor[:, 1].min() - 0.1, X_xor[:, 1].max() + 0.1
xx_xor, yy_xor = np.meshgrid(
np.linspace(x_min_xor, x_max_xor, 220),
np.linspace(y_min_xor, y_max_xor, 220),
)
grid_xor = np.column_stack([xx_xor.ravel(), yy_xor.ravel()])
Z_xor = clf_xor.predict_proba(grid_xor)[:, 1].reshape(xx_xor.shape)VisualizationOut [27]Expand

Vocabulary drift is another practical failure for text models. A high out-of-vocabulary rate at prediction time means the fitted word probabilities no longer cover the incoming documents. The rate should be monitored and compared with the rate observed during validation. Repeated tokens with a large class log-probability ratio can also dominate a document through accumulated log-score differences; this is a property of the model, not a numerical bug, and it should be examined with per-document score contributions when predictions are surprising.
Evaluation should match the decision. Accuracy is a weak summary under imbalance, where a majority-class predictor can score well while missing the minority class. Precision, recall, F1, and the area under the precision-recall curve give a clearer picture of minority-class behavior. Probability quality needs its own checks: log loss against a constant-prior baseline shows whether the scores add information beyond class frequencies, and a reliability diagram shows whether predicted frequencies agree with observed frequencies. Brier score combines calibration, discrimination, and outcome uncertainty, so it should not be read as a calibration-only measure.
Choosing naive Bayes over alternatives
Naive Bayes is a sensible first model when the feature dimension is large, the sample is moderate, and training speed matters. Bag-of-words text classification supplies a concrete use case for multinomial token probabilities, including spam filtering and topic assignment. It is also convenient for streaming data because the sufficient statistics are class counts, running moments, or accumulated word counts; scikit-learn implementations can be updated with partial_fit rather than retrained.
Relative to logistic regression, naive Bayes specifies stronger class-conditional distributional assumptions and estimates class counts and per-feature summaries without iterative optimization. Its data requirements and calibration depend on how well those assumptions match the problem. Logistic regression can use explicitly supplied interaction features. A fitted naive Bayes score uses class parameters rather than scanning all training records as a brute-force nearest-neighbor predictor does; compare measured latency as well as predictive quality for the intended data. Compared with LDA and QDA, Gaussian naive Bayes is the diagonal-covariance special case: it gives up the ability to model correlations in exchange for a much smaller parameter count.
The choice should follow the deployment constraint. If the application needs a quick baseline, a streaming update, or a model with interpretable per-feature contributions, naive Bayes is often appropriate. If the decision boundary depends on interactions or the application requires well-calibrated risk scores, a discriminative model or a post-hoc calibrator may be justified. The comparison should be made on held-out data with the metric that reflects the decision.
Computational Considerations
A Gaussian fit costs for observations and features, and prediction costs per observation for classes. An accumulated-statistics multinomial fit can process the nonzero count entries, and prediction can use a sparse matrix product when the input is sparse. Memory scales with the parameter count, for Gaussian and for multinomial, rather than with the number of training observations. Vocabulary size still increases parameter memory, and dense training-array storage must be budgeted separately.
The teaching classes convert inputs to dense NumPy arrays. Their per-class Boolean masks add grouping work, and np.unique sorts the labels. They are not sparse or streaming implementations, so their memory profile should not be read as the profile of a production text pipeline. The scikit-learn multinomial implementation can operate on sparse matrices and provides partial_fit, which changes the practical limits for large corpora.
The stated estimators use direct class-count, moment, and token-count summaries instead of iterative optimization. Refresh schedules should still follow measured runtime, the availability of new labels, and changes in the input distribution. An iterative alternative may require repeated passes over the feature matrix, so compare the actual algorithms and data representation.
Performance and Deployment Considerations
Deployment decisions should follow from the evaluation rather than from the model's simplicity. If the application needs only a ranking or a hard label, ranking quality and the chosen threshold matter more than calibration. If it needs risk scores, calibration should be verified on held-out data and corrected when it fails. Class priors should reflect the deployment population, particularly when the training data were collected under case-control sampling. A model that is accurate on a balanced test set can still be miscalibrated when deployed to a population with a different base rate.
Monitoring should track the input distribution, the out-of-vocabulary rate for text, and the observed positive rate against the predicted rate. Prior shift and vocabulary drift can degrade a model that was correct at training time, and neither is visible from a single accuracy score. For Gaussian models, feature means and variances can be compared between training and production batches; for multinomial models, token frequencies and document lengths serve the same purpose.
The practical sequence is to establish naive Bayes as a baseline, validate it with cross-validation and class-specific metrics, check calibration, and only then decide whether a model with explicit covariance or feature interactions is justified. The baseline should be retained as a reference even after a more complex model is deployed, because it provides a cheap check on whether added complexity is still earning its cost.
Summary
Naive Bayes is a generative classifier that applies Bayes' theorem. The Gaussian model factors the feature densities conditional on the class. The multinomial model instead treats token draws as independent given the class and document length; its count components share a fixed total and are generally dependent. The decision rule compares class priors times the corresponding event-model likelihoods. Taking logs turns the product into a sum, and log-sum-exp normalization converts the scores into stable posterior probabilities.
The two event models developed here are Gaussian and multinomial. Gaussian naive Bayes estimates a mean and variance for each feature within each class, making it suitable for continuous measurements. Multinomial naive Bayes estimates class-specific word probabilities from counts, making it suitable for text and other count data. Smoothing with pseudo-counts prevents zero probabilities, and positive effective variance smoothing can prevent division by zero; globally constant training features still require explicit handling.
The independence assumption is the model's main limitation. When features are correlated, naive Bayes can double-count evidence and produce overconfident probabilities. It can still classify well, but good classification does not imply good calibration. Class imbalance can also cause the prior to dominate, so we should evaluate with class-specific metrics and consider prior adjustment or threshold tuning.
In practice, naive Bayes is a fast, interpretable baseline for high-dimensional problems, especially text classification. It is not a universal replacement for models that represent feature interactions or full covariance structure. We should use it when its assumptions and computational profile fit the problem, and we should validate its probability estimates before relying on them for decisions.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about naive Bayes.
Naive Bayes Concepts
Join the discussionAsk a question or share what you learned.
Keep exploring
Reference
Citation details
Cite or share this article.

Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!