๐ŸŽฒ Naive Bayes

Bayes rule, conditional independence, and what the naive assumption really costs
Instance & Probabilistic Beginner
Contents

    Every classifier so far has drawn a boundary directly. Naive Bayes takes the opposite route: it models how each class generates data, then applies Bayes' rule to invert that into a prediction.

    $$P(y \mid \mathbf{x}) = \frac{P(\mathbf{x} \mid y)\,P(y)}{P(\mathbf{x})} \;\;\propto\;\; P(\mathbf{x}\mid y)\,P(y)$$

    The obstacle is $P(\mathbf{x}\mid y)$: a joint distribution over all $p$ features needs exponentially much data to estimate. The "naive" assumption cuts it down to size by declaring the features conditionally independent given the class:

    $$P(\mathbf{x}\mid y) = \prod_{j=1}^{p} P(x_j \mid y)$$

    That turns one $p$-dimensional density into $p$ one-dimensional ones. The assumption is usually false — here the two EEG features correlate above 0.9 — and the classifier often works anyway. Section 6 shows precisely why.

    Display helpers ready: banner, step, check, expand, show_matrix, show_matmul, show_elementwise.

    1. The Data

    The same 20 EEG epochs as the Logistic Regression, KNN and SVM walkthroughs, so all four decision boundaries can be compared on identical points.

    20 epochs x 2 features, classes [10, 10]
    
        id   spike_rate   line_length   class         label
    -------------------------------------------------------
      ep00          1.0          12.0       0    no seizure
      ep01          1.6          15.0       0    no seizure
      ep02          0.7          11.0       0    no seizure
      ep03          2.1          18.0       0    no seizure
      ep04          1.3          14.0       0    no seizure
      ep05          2.6          21.0       0    no seizure
      ep06          0.9          16.0       0    no seizure
      ep07          1.9          13.0       0    no seizure
      ep08          3.1          24.0       0    no seizure
      ep09          4.4          33.0       0    no seizure
      ep10          6.2          41.0       1       seizure
      ep11          7.1          47.0       1       seizure
      ep12          5.4          38.0       1       seizure
      ep13          8.0          52.0       1       seizure
      ep14          6.7          44.0       1       seizure
      ep15          5.9          49.0       1       seizure
      ep16          7.6          39.0       1       seizure
      ep17          6.0          35.0       1       seizure
      ep18          3.6          27.0       1       seizure
      ep19          8.3          46.0       1       seizure
    -------------------------------------------------------
    
    feature means : [ 4.22 29.75]
    feature stds  : [ 2.564 13.794]
    
    Note the scales: line_length spans ~41 while spike_rate spans ~8.
    Any algorithm that measures DISTANCE will be dominated by the larger one
    unless the features are scaled - that matters for KNN and SVM below.
    correlation between the two features: +0.9621
       within class 0: +0.9489
       within class 1: +0.7524
    
    Naive Bayes will assume these are ZERO. They are not. Keep that in mind
    for section 6, where the consequence is measured.

    2. Priors

    $$P(y = c) = \frac{n_c}{n}$$

    The prior is what the model believes before seeing any features — the base rate. On badly imbalanced data it dominates: if a condition affects 1 in 1000, the evidence must be strong to overcome a prior of 0.001.

    class    count      prior    log prior
    ----------------------------------------------
        no seizure       10   0.500000    -0.693147
           seizure       10   0.500000    -0.693147
    ----------------------------------------------
             total       20   1.000000
    
    Balanced here, so the prior will not decide anything. The formulas below
    keep it explicit anyway, because on imbalanced data it does the deciding.

    3. Per-Feature Likelihoods

    For continuous features, Gaussian Naive Bayes fits one normal distribution per (feature, class) pair:

    $$P(x_j \mid y=c) = \frac{1}{\sqrt{2\pi\sigma_{jc}^2}} \exp\!\left(-\frac{(x_j - \mu_{jc})^2}{2\sigma_{jc}^2}\right)$$

    With $p$ features and $k$ classes that is $2pk$ numbers — here $2 \times 2 \times 2 = 8$. Modelling the full joint would instead require a $2\times2$ covariance matrix per class, and in higher dimensions the gap becomes the whole argument for the method.

    Note the variance is the maximum-likelihood one (divide by $n_c$, not $n_c - 1$), which is what scikit-learn uses.

    fitted parameters (standardised features)
    
             class       feature           mu      sigma^2       sigma
    ----------------------------------------------------------------
        no seizure    spike_rate    -0.881536     0.180811    0.425219
        no seizure   line_length    -0.873538     0.214465    0.463104
           seizure    spike_rate     0.881536     0.264979    0.514761
           seizure   line_length     0.873538     0.259397    0.509310
    ----------------------------------------------------------------
    
    by hand, mu for class 0 / spike_rate:
       (-1.256 + -1.022 + -1.373 + -0.827 + -1.139 + ...) / 10
       = -8.815356 / 10 = -0.881536
       from the table: -0.881536   MATCH: True
    
       sigma^2 = sum (x - mu)^2 / n_c
               = (0.140 + 0.020 + 0.242 + 0.003 + 0.066 + ...) / 10
               = 1.808112 / 10 = 0.180811
       from the table: 0.180811   MATCH: True
    
    
    total parameters: 2 classes x 2 features x (mu, sigma^2) = 8, plus 2 priors
    a full joint Gaussian would need 2 classes x (2 means + 3 covariance terms) = 10
    at p = 100 features: naive needs 400 numbers, full joint needs 10,300
    Each panel is one row of the parameter table above. Naive Bayes multiplies
    these two panels together and never asks whether that is legitimate.

    4. Classifying One Point, By Hand

    Work in log space. Multiplying many small probabilities underflows to zero in floating point; adding logs does not, and the arg max is unchanged:

    $$\log P(y=c\mid\mathbf{x}) = \log P(y=c) + \sum_j \log P(x_j\mid y=c) - \log P(\mathbf{x})$$

    The last term is the same for every class, so it can be ignored for the decision — but it is needed to turn the scores back into probabilities.

    classifying ep09: spike_rate_z = +0.0702, line_length_z = +0.2356   (true class 0)
    
    --- class 0 (no seizure) ---
       log P(y=0)                 = log(0.5000) = -0.693147
       P(spike_rate=+0.070 | y=0)
          = exp(-(+0.0702 - -0.8815)^2 / (2*0.1808)) / sqrt(2*pi*0.1808)
          = 0.07663754      log = -2.568668
       P(line_length=+0.236 | y=0)
          = exp(-(+0.2356 - -0.8735)^2 / (2*0.2145)) / sqrt(2*pi*0.2145)
          = 0.04893898      log = -3.017181
       log posterior (unnormalised) = -6.278997
    
    --- class 1 (seizure) ---
       log P(y=1)                 = log(0.5000) = -0.693147
       P(spike_rate=+0.070 | y=1)
          = exp(-(+0.0702 - +0.8815)^2 / (2*0.2650)) / sqrt(2*pi*0.2650)
          = 0.22380903      log = -1.496962
       P(line_length=+0.236 | y=1)
          = exp(-(+0.2356 - +0.8735)^2 / (2*0.2594)) / sqrt(2*pi*0.2594)
          = 0.35747798      log = -1.028682
       log posterior (unnormalised) = -3.218791
    
    normalising with the log-sum-exp trick (max = -3.218791):
       class 0: exp(-6.278997 - (-3.218791)) = 0.04687805
       class 1: exp(-3.218791 - (-3.218791)) = 1.00000000
       sum = 1.04687805
       P(y=0 | x) = 0.04477891
       P(y=1 | x) = 0.95522109
    
    prediction: class 1   true: 0   WRONG
    id    log P(.|0)    log P(.|1)    P(y=1|x)   pred   true    ok
    ----------------------------------------------------------------
      ep00      -1.69188     -18.80931    0.000000      0      0   yes
      ep01      -1.04991     -15.30473    0.000001      0      0   yes
      ep02      -2.12401     -20.39297    0.000000      0      0   yes
      ep03      -0.91542     -12.43781    0.000010      0      0   yes
      ep04      -1.25707     -16.72426    0.000000      0      0   yes
      ep05      -1.21182      -9.89677    0.000169      0      0   yes
      ep06      -1.41421     -16.87400    0.000000      0      0   yes
      ep07      -1.17823     -15.61638    0.000001      0      0   yes
      ep08      -1.93913      -7.68161    0.003197      0      0   yes
      ep09      -6.27900      -3.21879    0.955221      1      0    NO
      ep10     -15.12129      -1.22126    0.999999      1      1   yes
      ep11     -22.53982      -1.57654    1.000000      1      1   yes
      ep12     -10.93375      -1.67341    0.999905      1      1   yes
      ep13     -30.66935      -2.90946    1.000000      1      1   yes
      ep14     -18.83346      -1.25520    1.000000      1      1   yes
      ep15     -19.44043      -1.81397    1.000000      1      1   yes
      ep16     -19.84801      -1.63182    1.000000      1      1   yes
      ep17     -11.43999      -1.72682    0.999940      1      1   yes
      ep18      -3.09734      -5.79233    0.063270      0      1    NO
      ep19     -27.63014      -2.32193    1.000000      1      1   yes
    ----------------------------------------------------------------
    
    accuracy: 0.9000  (18/20)
    misclassified: ['ep09', 'ep18']

    5. The Decision Boundary

    With equal variances across classes the log-posterior difference is linear in $\mathbf{x}$ and the boundary is a straight line. When the variances differ, the quadratic terms survive and the boundary becomes a conic — a parabola, ellipse or hyperbola.

    So Gaussian Naive Bayes is a quadratic classifier in general, and reduces to a linear one in a special case.

    variances per class:
       class 0: [0.1808 0.2145]   equal across features
       class 1: [0.265  0.2594]   equal across features
    
    class variances identical across classes? False  -> boundary is quadratic
    
    Right panel: the contours are ellipses aligned with the AXES. They can
    never tilt, because a diagonal covariance has no off-diagonal term. The
    real data is tilted, and that mismatch is the naive assumption made visible.

    6. What the Naive Assumption Costs

    The features here correlate above 0.9, so the independence assumption is plainly wrong. What does that cost?

    The key insight: the assumption corrupts the probabilities much more than it corrupts the decision. Getting the arg max right only requires the ranking of the classes to survive, and that is far more robust than the magnitudes.

    Below, Gaussian NB (diagonal covariance) is compared with QDA, which fits the full covariance and therefore makes no independence assumption.

    model   train acc     LOO-CV   mean max prob
    --------------------------------------------------------------------
       Gaussian NB (independent)      0.9000     0.9000        0.994421
           QDA (full covariance)      0.9000     0.9000        0.972994
    --------------------------------------------------------------------
    
    agreement between the two on which class wins: 100.0%
    mean |difference| in P(y=1|x)                : 0.0214
    max  |difference| in P(y=1|x)                : 0.2443
    
        id    NB P(y=1)    QDA P(y=1)       diff   same call?
    --------------------------------------------------------
      ep09     0.955221      0.710928     0.2443         True
      ep18     0.063270      0.151356     0.0881         True
      ep08     0.003197      0.041183     0.0380         True
      ep12     0.999905      0.974879     0.0250         True
      ep17     0.999940      0.986978     0.0130         True
      ep05     0.000169      0.010498     0.0103         True
      ep10     0.999999      0.996773     0.0032         True
      ep03     0.000010      0.002681     0.0027         True
    --------------------------------------------------------
    
    predictions more extreme than 1% / 99%:
       Gaussian NB : 90.0%
       QDA         : 70.0%
    
    NB is over-confident: multiplying correlated features counts the SAME
    evidence twice, so the log-posteriors are pushed further apart than the
    data justifies. The class that wins is usually still the right one, which
    is why the accuracy holds up while the probabilities should not be trusted.

    Duplicating a feature makes the failure explicit

    Copy one column so a feature appears twice. Nothing new has been measured, but Naive Bayes counts the evidence twice over — and its confidence rises for no reason at all.

    copies of feature 0   accuracy    mean max P   # above 99%
    ------------------------------------------------------------
                        1     0.9000    0.99442089            18
                        2     0.9000    0.99841434            18
                        3     0.9000    0.99953763            20
                        5     0.9000    0.99995719            20
                        8     0.9000    0.99999857            20
    ------------------------------------------------------------
    
    Accuracy barely moves; confidence marches to certainty. The extra columns
    carry zero new information - they are literally the same numbers - but the
    independence assumption has no way to notice.
    
    This is the practical rule: use Naive Bayes for the DECISION, and do not
    read its probabilities as calibrated unless you have checked them.

    7. Other Likelihoods, and Zero Counts

    Gaussian is only one choice. The framework is identical; only $P(x_j\mid y)$ changes:

    Variant $P(x_j\mid y)$ Used for
    Gaussian normal density continuous features
    Multinomial $\propto \theta_{jc}^{x_j}$ word counts
    Bernoulli $\theta_{jc}^{x_j}(1-\theta_{jc})^{1-x_j}$ binary presence/absence

    For discrete variants a word never seen with a class gives $P = 0$, which annihilates the entire product regardless of other evidence. Laplace smoothing prevents that:

    $$P(x_j \mid y=c) = \frac{N_{jc} + \alpha}{N_c + \alpha\,d}$$

    word counts per report (a toy text problem)
    
      doc      spike      sharp      alpha   artifact      burst   class
    --------------------------------------------------------------
       d0          3          2          0          0          2       1
       d1          4          1          0          1          3       1
       d2          2          3          1          0          1       1
       d3          0          0          4          2          0       0
       d4          1          0          3          3          0       0
       d5          0          1          5          1          0       0
    --------------------------------------------------------------
    
    word totals per class (note the zeros):
    
     class      spike      sharp      alpha   artifact      burst    total
    --------------------------------------------------------------
         0          1          1         12          6          0       20
         1          9          6          1          1          6       23
    --------------------------------------------------------------
    
    'alpha' never appears in class 1. Without smoothing:
       P(alpha | y=1) = 0/23 = 0
       -> any report containing 'alpha' gets P(y=1) = 0 exactly,
          no matter how many spike/burst words it also contains.
    
       alpha     P(spike|1)     P(sharp|1)     P(alpha|1)     P(artif|1)     P(burst|1)
    ------------------------------------------------------------------------------
        0.00     0.39130435     0.26086957     0.04347826     0.04347826     0.26086957
        0.01     0.39088937     0.26073753     0.04381779     0.04381779     0.26073753
        0.50     0.37254902     0.25490196     0.05882353     0.05882353     0.25490196
        1.00     0.35714286     0.25000000     0.07142857     0.07142857     0.25000000
        5.00     0.29166667     0.22916667     0.12500000     0.12500000     0.22916667
    ------------------------------------------------------------------------------
    
    a new report with counts [5, 2, 1, 0, 3] (contains 'alpha' once):
    
       alpha = 1e-10   : P(y=0) = 0.00000000, P(y=1) = 1.00000000 -> class 1
       alpha = 0.01    : P(y=0) = 0.00000000, P(y=1) = 1.00000000 -> class 1
       alpha = 1.0     : P(y=0) = 0.00000172, P(y=1) = 0.99999828 -> class 1
    
    With almost no smoothing the single 'alpha' nearly vetoes class 1 despite
    five 'spike' counts. Smoothing restores the balance of evidence.

    8. Checking Against scikit-learn

    priors ours    : [0.5 0.5]
    priors sklearn : [0.5 0.5]
    MATCH: True
    
    means ours    :
    [[-0.8815 -0.8735]
     [ 0.8815  0.8735]]
    means sklearn :
    [[-0.8815 -0.8735]
     [ 0.8815  0.8735]]
    MATCH: True
    
    variances ours    : [[0.1808112467, 0.2144649543], [0.2649788516, 0.2593969651]]
    variances sklearn : [[0.1808112477, 0.2144649553], [0.2649788526, 0.2593969661]]
    sklearn's epsilon : 1.000e-09
    MATCH (allowing for epsilon): True
    
    log-posterior DIFFERENCE (class1 - class0) MATCH: True
       max discrepancy: 1.443e-07
       (the absolute values differ by log P(x), which we never computed)
    probabilities MATCH  : True
    predictions MATCH    : True
    
    the single hand-computed point from section 4 (ep09):
       ours    P(y=1|x) = 0.9552210949
       sklearn P(y=1|x) = 0.9552210941
       MATCH: True

    9. Summary

    Stage Formula
    Bayes' rule $P(y\mid\mathbf{x}) \propto P(\mathbf{x}\mid y)P(y)$
    Naive assumption $P(\mathbf{x}\mid y) = \prod_j P(x_j\mid y)$
    Gaussian likelihood $\mathcal{N}(x_j;\,\mu_{jc},\,\sigma_{jc}^2)$
    Decision $\arg\max_c \big[\log P(y{=}c) + \sum_j \log P(x_j\mid c)\big]$
    Smoothing $(N_{jc}+\alpha)/(N_c+\alpha d)$
    Parameters $2pk$ — linear in features, not exponential
    Training cost one pass, $O(np)$

    The one idea: replace a joint distribution nobody can estimate with a product of marginals anyone can. The assumption is almost always false, and the classifier is useful anyway — because the arg max survives distortions that the probabilities do not.

    The honest caveat: treat the output as a ranking, not a probability. The feature-duplication experiment in section 6 shows confidence climbing toward certainty while accuracy does not move at all.

    Where it still wins: very high dimensions with little data (spam and text classification), because it needs $O(p)$ parameters instead of $O(p^2)$, trains in a single pass, and updates incrementally.

    What is deliberately left out: Complement NB for imbalanced text, TF-IDF weighting, the formal relationship to logistic regression as a generative/discriminative pair, and LDA/QDA, which relax exactly the assumption examined here.

    References

    1. On the Optimality of the Simple Bayesian Classifier under Zero-One Loss โ€” Domingos & Pazzani, 1997 (Machine Learning 29)