RedEnginePress logo
RedEnginePress
AlgorithmsLanguagesPlaygroundAbout

Naive Bayes

P
H
S
/// Naive Bayes classifier for classification tasks.
/// This implementation uses Gaussian Naive Bayes, which assumes that
/// features follow a normal (Gaussian) distribution.
/// The algorithm calculates class priors and feature statistics (mean and variance)
/// for each class, then uses Bayes' theorem to predict class probabilities.

pub struct ClassStatistics {
    pub class_label: f64,
    pub prior: f64,
    pub feature_means: Vec<f64>,
    pub feature_variances: Vec<f64>,
}

fn calculate_class_statistics(
    training_data: &[(Vec<f64>, f64)],
    class_label: f64,
    num_features: usize,
) -> Option<ClassStatistics> {
    let class_samples: Vec<&(Vec<f64>, f64)> = training_data
        .iter()
        .filter(|(_, label)| (*label - class_label).abs() < 1e-10)
        .collect();

    if class_samples.is_empty() {
        return None;
    }

    let prior = class_samples.len() as f64 / training_data.len() as f64;

    let mut feature_means = vec![0.0; num_features];
    let mut feature_variances = vec![0.0; num_features];

    // Calculate means
    for (features, _) in &class_samples {
        for (i, &feature) in features.iter().enumerate() {
            if i < num_features {
                feature_means[i] += feature;
            }
        }
    }

    let n = class_samples.len() as f64;
    for mean in &mut feature_means {
        *mean /= n;
    }

    // Calculate variances
    for (features, _) in &class_samples {
        for (i, &feature) in features.iter().enumerate() {
            if i < num_features {
                let diff = feature - feature_means[i];
                feature_variances[i] += diff * diff;
            }
        }
    }

    let epsilon = 1e-9;
    for variance in &mut feature_variances {
        *variance = (*variance / n).max(epsilon);
    }

    Some(ClassStatistics {
        class_label,
        prior,
        feature_means,
        feature_variances,
    })
}

fn gaussian_log_pdf(x: f64, mean: f64, variance: f64) -> f64 {
    let diff = x - mean;
    let exponent_term = -(diff * diff) / (2.0 * variance);
    let log_coefficient = -0.5 * (2.0 * std::f64::consts::PI * variance).ln();
    log_coefficient + exponent_term
}

pub fn train_naive_bayes(training_data: Vec<(Vec<f64>, f64)>) -> Option<Vec<ClassStatistics>> {
    if training_data.is_empty() {
        return None;
    }

    let num_features = training_data[0].0.len();
    if num_features == 0 {
        return None;
    }

    // Verify all samples have the same number of features
    if !training_data
        .iter()
        .all(|(features, _)| features.len() == num_features)
    {
        return None;
    }

    // Get unique class labels
    let mut unique_classes = Vec::new();
    for (_, label) in &training_data {
        if !unique_classes
            .iter()
            .any(|&c: &f64| (c - *label).abs() < 1e-10)
        {
            unique_classes.push(*label);
        }
    }

    let mut class_stats = Vec::new();

    for class_label in unique_classes {
        if let Some(mut stats) =
            calculate_class_statistics(&training_data, class_label, num_features)
        {
            stats.class_label = class_label;
            class_stats.push(stats);
        }
    }

    if class_stats.is_empty() {
        return None;
    }

    Some(class_stats)
}

pub fn predict_naive_bayes(model: &[ClassStatistics], test_point: &[f64]) -> Option<f64> {
    if model.is_empty() || test_point.is_empty() {
        return None;
    }

    // Get number of features from the first class statistics
    let num_features = model[0].feature_means.len();
    if test_point.len() != num_features {
        return None;
    }

    let mut best_class = None;
    let mut best_log_prob = f64::NEG_INFINITY;

    for stats in model {
        // Calculate log probability to avoid underflow
        let mut log_prob = stats.prior.ln();

        for (i, &feature) in test_point.iter().enumerate() {
            if i < stats.feature_means.len() && i < stats.feature_variances.len() {
                // Use log PDF directly to avoid numerical underflow
                log_prob +=
                    gaussian_log_pdf(feature, stats.feature_means[i], stats.feature_variances[i]);
            }
        }

        if log_prob > best_log_prob {
            best_log_prob = log_prob;
            best_class = Some(stats.class_label);
        }
    }

    best_class
}

pub fn naive_bayes(training_data: Vec<(Vec<f64>, f64)>, test_point: Vec<f64>) -> Option<f64> {
    let model = train_naive_bayes(training_data)?;
    predict_naive_bayes(&model, &test_point)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_naive_bayes_simple_classification() {
        let training_data = vec![
            (vec![1.0, 1.0], 0.0),
            (vec![1.1, 1.0], 0.0),
            (vec![1.0, 1.1], 0.0),
            (vec![5.0, 5.0], 1.0),
            (vec![5.1, 5.0], 1.0),
            (vec![5.0, 5.1], 1.0),
        ];

        // Test point closer to class 0
        let test_point = vec![1.05, 1.05];
        let result = naive_bayes(training_data.clone(), test_point);
        assert_eq!(result, Some(0.0));

        // Test point closer to class 1
        let test_point = vec![5.05, 5.05];
        let result = naive_bayes(training_data, test_point);
        assert_eq!(result, Some(1.0));
    }

    #[test]
    fn test_naive_bayes_one_dimensional() {
        let training_data = vec![
            (vec![1.0], 0.0),
            (vec![1.1], 0.0),
            (vec![1.2], 0.0),
            (vec![5.0], 1.0),
            (vec![5.1], 1.0),
            (vec![5.2], 1.0),
        ];

        let test_point = vec![1.15];
        let result = naive_bayes(training_data.clone(), test_point);
        assert_eq!(result, Some(0.0));

        let test_point = vec![5.15];
        let result = naive_bayes(training_data, test_point);
        assert_eq!(result, Some(1.0));
    }

    #[test]
    fn test_naive_bayes_empty_training_data() {
        let training_data = vec![];
        let test_point = vec![1.0, 2.0];
        let result = naive_bayes(training_data, test_point);
        assert_eq!(result, None);
    }

    #[test]
    fn test_naive_bayes_empty_test_point() {
        let training_data = vec![(vec![1.0, 2.0], 0.0)];
        let test_point = vec![];
        let result = naive_bayes(training_data, test_point);
        assert_eq!(result, None);
    }

    #[test]
    fn test_naive_bayes_dimension_mismatch() {
        let training_data = vec![(vec![1.0, 2.0], 0.0), (vec![3.0, 4.0], 1.0)];
        let test_point = vec![1.0]; // Wrong dimension
        let result = naive_bayes(training_data, test_point);
        assert_eq!(result, None);
    }

    #[test]
    fn test_naive_bayes_inconsistent_feature_dimensions() {
        let training_data = vec![
            (vec![1.0, 2.0], 0.0),
            (vec![3.0], 1.0), // Different dimension
        ];
        let test_point = vec![1.0, 2.0];
        let result = naive_bayes(training_data, test_point);
        assert_eq!(result, None);
    }

    #[test]
    fn test_naive_bayes_multiple_classes() {
        let training_data = vec![
            (vec![1.0, 1.0], 0.0),
            (vec![1.1, 1.0], 0.0),
            (vec![5.0, 5.0], 1.0),
            (vec![5.1, 5.0], 1.0),
            (vec![9.0, 9.0], 2.0),
            (vec![9.1, 9.0], 2.0),
        ];

        let test_point = vec![1.05, 1.05];
        let result = naive_bayes(training_data.clone(), test_point);
        assert_eq!(result, Some(0.0));

        let test_point = vec![5.05, 5.05];
        let result = naive_bayes(training_data.clone(), test_point);
        assert_eq!(result, Some(1.0));

        let test_point = vec![9.05, 9.05];
        let result = naive_bayes(training_data, test_point);
        assert_eq!(result, Some(2.0));
    }

    #[test]
    fn test_train_and_predict_separately() {
        let training_data = vec![
            (vec![1.0, 1.0], 0.0),
            (vec![1.1, 1.0], 0.0),
            (vec![5.0, 5.0], 1.0),
            (vec![5.1, 5.0], 1.0),
        ];

        let model = train_naive_bayes(training_data);
        assert!(model.is_some());

        let model = model.unwrap();
        assert_eq!(model.len(), 2);

        let test_point = vec![1.05, 1.05];
        let result = predict_naive_bayes(&model, &test_point);
        assert_eq!(result, Some(0.0));
    }
}

About this Algorithm

from sklearn import datasets
import pandas as pd
iris = datasets.load_iris()
df = pd.DataFrame(iris.data)
df.columns = ["sl", "sw", 'pl', 'pw']
def abc(k, *val):
    if k &lt; val[0]:
        return 0
    else:
        return 1
df.sl.apply(abc, args=(5,))
0      1
1      0
2      0
3      0
4      1
5      1
6      0
7      1
8      0
9      0
10     1
11     0
12     0
13     0
14     1
15     1
16     1
17     1
18     1
19     1
20     1
21     1
22     0
23     1
24     0
25     1
26     1
27     1
28     1
29     0
      ..
120    1
121    1
122    1
123    1
124    1
125    1
126    1
127    1
128    1
129    1
130    1
131    1
132    1
133    1
134    1
135    1
136    1
137    1
138    1
139    1
140    1
141    1
142    1
143    1
144    1
145    1
146    1
147    1
148    1
149    1
Name: sl, dtype: int64
def label(val, *boundaries):
    if (val &lt; boundaries[0]):
        return 'a'
    elif (val &lt; boundaries[1]):
        return 'b'
    elif (val &lt; boundaries[2]):
        return 'c'
    else:
        return 'd'

def toLabel(df, old_feature_name):
    second = df[old_feature_name].mean()
    minimum = df[old_feature_name].min()
    first = (minimum + second)/2
    maximum = df[old_feature_name].max()
    third = (maximum + second)/2
    return df[old_feature_name].apply(label, args= (first, second, third))
df['sl_labeled'] = toLabel(df, 'sl')
df['sw_labeled'] = toLabel(df, 'sw')
df['pl_labeled'] = toLabel(df, 'pl')
df['pw_labeled'] = toLabel(df, 'pw')
df
sl sw pl pw sl_labeled sw_labeled pl_labeled pw_labeled
0 5.1 3.5 1.4 0.2 b c a a
1 4.9 3.0 1.4 0.2 a b a a
2 4.7 3.2 1.3 0.2 a c a a
3 4.6 3.1 1.5 0.2 a c a a
4 5.0 3.6 1.4 0.2 a c a a
5 5.4 3.9 1.7 0.4 b d a a
6 4.6 3.4 1.4 0.3 a c a a
7 5.0 3.4 1.5 0.2 a c a a
8 4.4 2.9 1.4 0.2 a b a a
9 4.9 3.1 1.5 0.1 a c a a
10 5.4 3.7 1.5 0.2 b c a a
11 4.8 3.4 1.6 0.2 a c a a
12 4.8 3.0 1.4 0.1 a b a a
13 4.3 3.0 1.1 0.1 a b a a
14 5.8 4.0 1.2 0.2 b d a a
15 5.7 4.4 1.5 0.4 b d a a
16 5.4 3.9 1.3 0.4 b d a a
17 5.1 3.5 1.4 0.3 b c a a
18 5.7 3.8 1.7 0.3 b d a a
19 5.1 3.8 1.5 0.3 b d a a
20 5.4 3.4 1.7 0.2 b c a a
21 5.1 3.7 1.5 0.4 b c a a
22 4.6 3.6 1.0 0.2 a c a a
23 5.1 3.3 1.7 0.5 b c a a
24 4.8 3.4 1.9 0.2 a c a a
25 5.0 3.0 1.6 0.2 a b a a
26 5.0 3.4 1.6 0.4 a c a a
27 5.2 3.5 1.5 0.2 b c a a
28 5.2 3.4 1.4 0.2 b c a a
29 4.7 3.2 1.6 0.2 a c a a
... ... ... ... ... ... ... ... ...
120 6.9 3.2 5.7 2.3 d c d d
121 5.6 2.8 4.9 2.0 b b c d
122 7.7 2.8 6.7 2.0 d b d d
123 6.3 2.7 4.9 1.8 c b c c
124 6.7 3.3 5.7 2.1 c c d d
125 7.2 3.2 6.0 1.8 d c d c
126 6.2 2.8 4.8 1.8 c b c c
127 6.1 3.0 4.9 1.8 c b c c
128 6.4 2.8 5.6 2.1 c b d d
129 7.2 3.0 5.8 1.6 d b d c
130 7.4 2.8 6.1 1.9 d b d d
131 7.9 3.8 6.4 2.0 d d d d
132 6.4 2.8 5.6 2.2 c b d d
133 6.3 2.8 5.1 1.5 c b c c
134 6.1 2.6 5.6 1.4 c b d c
135 7.7 3.0 6.1 2.3 d b d d
136 6.3 3.4 5.6 2.4 c c d d
137 6.4 3.1 5.5 1.8 c c d c
138 6.0 3.0 4.8 1.8 c b c c
139 6.9 3.1 5.4 2.1 d c d d
140 6.7 3.1 5.6 2.4 c c d d
141 6.9 3.1 5.1 2.3 d c c d
142 5.8 2.7 5.1 1.9 b b c d
143 6.8 3.2 5.9 2.3 c c d d
144 6.7 3.3 5.7 2.5 c c d d
145 6.7 3.0 5.2 2.3 c b c d
146 6.3 2.5 5.0 1.9 c a c d
147 6.5 3.0 5.2 2.0 c b c d
148 6.2 3.4 5.4 2.3 c c d d
149 5.9 3.0 5.1 1.8 c b c c

150 rows × 8 columns

df.drop(['sl', 'sw', 'pl', 'pw'], axis = 1, inplace = True)
set(df['sl_labeled'])
{&#x27;a&#x27;, &#x27;b&#x27;, &#x27;c&#x27;, &#x27;d&#x27;}
df["output"] = iris.target
df
sl_labeled sw_labeled pl_labeled pw_labeled output
0 b c a a 0
1 a b a a 0
2 a c a a 0
3 a c a a 0
4 a c a a 0
5 b d a a 0
6 a c a a 0
7 a c a a 0
8 a b a a 0
9 a c a a 0
10 b c a a 0
11 a c a a 0
12 a b a a 0
13 a b a a 0
14 b d a a 0
15 b d a a 0
16 b d a a 0
17 b c a a 0
18 b d a a 0
19 b d a a 0
20 b c a a 0
21 b c a a 0
22 a c a a 0
23 b c a a 0
24 a c a a 0
25 a b a a 0
26 a c a a 0
27 b c a a 0
28 b c a a 0
29 a c a a 0
... ... ... ... ... ...
120 d c d d 2
121 b b c d 2
122 d b d d 2
123 c b c c 2
124 c c d d 2
125 d c d c 2
126 c b c c 2
127 c b c c 2
128 c b d d 2
129 d b d c 2
130 d b d d 2
131 d d d d 2
132 c b d d 2
133 c b c c 2
134 c b d c 2
135 d b d d 2
136 c c d d 2
137 c c d c 2
138 c b c c 2
139 d c d d 2
140 c c d d 2
141 d c c d 2
142 b b c d 2
143 c c d d 2
144 c c d d 2
145 c b c d 2
146 c a c d 2
147 c b c d 2
148 c c d d 2
149 c b c c 2

150 rows × 5 columns

def fit(data):
    output_name = data.columns[-1]
    features = data.columns[0:-1]
    counts = {}
    possible_outputs = set(data[output_name])
    for output in possible_outputs:
        counts[output] = {}
        smallData = data[data[output_name] == output]
        counts[output]["total_count"] = len(smallData)
        for f in features:
            counts[output][f] = {}
            possible_values = set(smallData[f])
            for value in possible_values:
                val_count = len(smallData[smallData[f] == value])
                counts[output][f][value] = val_count
    return counts
fit(df)
{0: {&#x27;pl_labeled&#x27;: {&#x27;a&#x27;: 50},
  &#x27;pw_labeled&#x27;: {&#x27;a&#x27;: 50},
  &#x27;sl_labeled&#x27;: {&#x27;a&#x27;: 28, &#x27;b&#x27;: 22},
  &#x27;sw_labeled&#x27;: {&#x27;a&#x27;: 1, &#x27;b&#x27;: 7, &#x27;c&#x27;: 32, &#x27;d&#x27;: 10},
  &#x27;total_count&#x27;: 50},
 1: {&#x27;pl_labeled&#x27;: {&#x27;b&#x27;: 7, &#x27;c&#x27;: 43},
  &#x27;pw_labeled&#x27;: {&#x27;b&#x27;: 10, &#x27;c&#x27;: 40},
  &#x27;sl_labeled&#x27;: {&#x27;a&#x27;: 3, &#x27;b&#x27;: 21, &#x27;c&#x27;: 24, &#x27;d&#x27;: 2},
  &#x27;sw_labeled&#x27;: {&#x27;a&#x27;: 13, &#x27;b&#x27;: 29, &#x27;c&#x27;: 8},
  &#x27;total_count&#x27;: 50},
 2: {&#x27;pl_labeled&#x27;: {&#x27;c&#x27;: 20, &#x27;d&#x27;: 30},
  &#x27;pw_labeled&#x27;: {&#x27;c&#x27;: 16, &#x27;d&#x27;: 34},
  &#x27;sl_labeled&#x27;: {&#x27;a&#x27;: 1, &#x27;b&#x27;: 5, &#x27;c&#x27;: 29, &#x27;d&#x27;: 15},
  &#x27;sw_labeled&#x27;: {&#x27;a&#x27;: 5, &#x27;b&#x27;: 28, &#x27;c&#x27;: 15, &#x27;d&#x27;: 2},
  &#x27;total_count&#x27;: 50}}