The Flea Beetle Problem

The Flea Beetle Problem#

This notebook is part of Bite Size Bayes, an introduction to probability and Bayesian statistics using Python.

Click here to run this notebook on Colab

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

Introduction#

This notebook introduces Bayesian classification. As an example, I use data from three species of flea beetle to classify a hypothetical new specimen.

This problem is based on this data story on DASL, which is based on Lubischew, A.A. (1962), “On the use of discriminant functions in taxonomy”. Biometrics, 18, 455-477.

Different species of flea beetle can be distinguished by the width and angle of the aedeagus. The data below includes measurements and known species classification for 74 specimens.

Suppose you discover a new specimen under conditions where it is equally likely to be any of the three species. You measure the aedeagus and find width 140 microns and angle 15 (in multiples of 7.5 degrees). What is the probability that it belongs to each species?

Data#

Description: Data were collected on the genus of flea beetle Chaetocnema, which contains three species: concinna (Con), heikertingeri (Hei), and heptapotamica (Hep). Measurements were made on the width and angle of the aedeagus of each beetle. The goal of the original study was to form a classification rule to distinguish the three species.

Number of cases: 74

Variable Names:

Width: The maximal width of aedeagus in the forpart (in microns)

Angle: The front angle of the aedeagus (1 unit = 7.5 degrees)

Species: Species of flea beetle from the genus Chaetocnema

We can read the data from this file:

# Load the data file
import os

if not os.path.exists('flea_beetles.csv'):
    !wget https://github.com/AllenDowney/BiteSizeBayes/raw/master/flea_beetles.csv
df = pd.read_csv('flea_beetles.csv', delimiter='\t')
df.head()
Width Angle Species
0 150 15 Con
1 147 13 Con
2 144 14 Con
3 144 16 Con
4 153 13 Con

Here’s what the distributions of width look like.

def plot_cdfs(df, col):
    for name, group in df.groupby('Species'):
        cdf = Cdf(group[col], label=name)
        thinkplot.Cdf(cdf)
    
    thinkplot.decorate(xlabel=col, 
                       ylabel='CDF',
                       loc='lower right')
from scipy.stats import gaussian_kde

def plot_kdes(df, col):
    xs = np.linspace(df[col].min(), df[col].max())
    
    for name, group in df.groupby('Species'):
        kde = gaussian_kde(group[col])
        ys = kde.evaluate(xs)
        plt.plot(xs, ys, label=name)
        
    plt.xlabel('Measurement')
    plt.ylabel('Probability density')
    plt.legend()
plot_kdes(df, 'Width')
_images/14c7f0052bb11e2fdfbb0d6e652a72661468eac33e567ef603e2119598920fe2.png

And the distributions of angle.

plot_kdes(df, 'Angle')
_images/5ee582be0011f8121faffa97e06873c35cec5dbed945c330a87cd01942d03e51.png

I’ll group the data by species and compute summary statistics.

grouped = df.groupby('Species')

Here are the means.

means = grouped.mean()
means
Width Angle
Species
Con 146.190476 14.095238
Hei 124.645161 14.290323
Hep 138.272727 10.090909

And the standard deviations.

stddevs = grouped.std()
stddevs
Width Angle
Species
Con 5.626891 0.889087
Hei 4.622758 1.101319
Hep 4.142484 0.971454

And the correlations.

for name, group in grouped:
    corr = group.Width.corr(group.Angle)
    print(name, corr)
Con -0.193701151757636
Hei -0.06420611481268004
Hep -0.12478515405529579

Those correlations are small enough that we can get an acceptable approximation by ignoring them, but we might want to come back later and write a complete solution that takes them into account.

Computing likelihoods#

from scipy.stats import norm

dist_width = {}
dist_angle = {}
for name, group in grouped:
    dist_width[name] = norm(group.Width.mean(), group.Width.std())
    dist_angle[name] = norm(group.Angle.mean(), group.Angle.std())
table = pd.DataFrame()
table['hypo'] = ['Con', 'Hei', 'Hep']
table['prior'] = 1/3
table
hypo prior
0 Con 0.333333
1 Hei 0.333333
2 Hep 0.333333
table['likelihood'] = [dist_width[hypo].pdf(140) 
                       for hypo in table['hypo']]
table
hypo prior likelihood
0 Con 0.333333 0.038709
1 Hei 0.333333 0.000347
2 Hep 0.333333 0.088287

We’ll start with equal priors

table['unnorm'] = table['prior'] * table['likelihood']
prob_data = table['unnorm'].sum()
table['posterior'] = table['unnorm'] / prob_data
table
hypo prior likelihood unnorm posterior
0 Con 0.333333 0.038709 0.012903 0.303977
1 Hei 0.333333 0.000347 0.000116 0.002724
2 Hep 0.333333 0.088287 0.029429 0.693298
table2 = pd.DataFrame()
table2['hypo'] = table['hypo']
table2['prior'] = table['posterior']
table2['likelihood'] = [dist_angle[hypo].pdf(15) 
                       for hypo in table['hypo']]
table2
hypo prior likelihood
0 Con 0.303977 0.267359
1 Hei 0.002724 0.294327
2 Hep 0.693298 0.000001
table2['unnorm'] = table2['prior'] * table2['likelihood']
prob_data = table2['unnorm'].sum()
table2['posterior'] = table2['unnorm'] / prob_data
table2
hypo prior likelihood unnorm posterior
0 Con 0.303977 0.267359 8.127101e-02 0.99022
1 Hei 0.002724 0.294327 8.018754e-04 0.00977
2 Hep 0.693298 0.000001 8.114748e-07 0.00001

Based on these measurements, the specimen is very likely to be an example of Chaetocnema concinna.

Copyright 2020–2026 Allen Downey

Code license: MIT License

Text license: CC BY-NC-SA 4.0