use super::{argmax, classification_result_from, normalize_log, sorted_unique, validate_dims};
use crate::algorithms::classification::ClassificationResult;
use crate::algorithms::count_to_f64;
use crate::error::{Error, Result};
const LN_2PI: f64 = 1.837_877_066_409_345_5;
const VAR_SMOOTHING: f64 = 1e-9;
#[derive(Debug, Clone)]
pub struct GaussianNbModel {
classes: Vec<usize>,
log_priors: Vec<f64>,
means: Vec<Vec<f64>>,
variances: Vec<Vec<f64>>,
n_features: usize,
}
fn feature_variances(x: &[Vec<f64>], n_features: usize) -> Vec<f64> {
let n = count_to_f64(x.len());
let mut mean = vec![0.0_f64; n_features];
for row in x {
for (m, &v) in mean.iter_mut().zip(row.iter()) {
*m += v;
}
}
for m in &mut mean {
*m /= n;
}
let mut var = vec![0.0_f64; n_features];
for row in x {
for ((acc, &v), &m) in var.iter_mut().zip(row.iter()).zip(mean.iter()) {
let diff = v - m;
*acc = diff.mul_add(diff, *acc);
}
}
for acc in &mut var {
*acc /= n;
}
var
}
pub fn gaussian_nb_fit(x: &[Vec<f64>], y: &[usize]) -> Result<GaussianNbModel> {
let n_features = validate_dims(x, y)?;
let classes = sorted_unique(y);
if classes.len() < 2 {
return Err(Error::InsufficientData);
}
let n_total = count_to_f64(y.len());
let global_var = feature_variances(x, n_features);
let max_var = global_var.iter().copied().fold(0.0_f64, f64::max);
let epsilon = VAR_SMOOTHING * max_var;
let mut log_priors = Vec::with_capacity(classes.len());
let mut means = Vec::with_capacity(classes.len());
let mut variances = Vec::with_capacity(classes.len());
for &cls in &classes {
let rows: Vec<&Vec<f64>> = x
.iter()
.zip(y)
.filter_map(|(row, &label)| (label == cls).then_some(row))
.collect();
let n_c = count_to_f64(rows.len());
let mut mean = vec![0.0_f64; n_features];
for row in &rows {
for (m, &v) in mean.iter_mut().zip(row.iter()) {
*m += v;
}
}
for m in &mut mean {
*m /= n_c;
}
let mut var = vec![0.0_f64; n_features];
for row in &rows {
for ((acc, &v), &m) in var.iter_mut().zip(row.iter()).zip(mean.iter()) {
let diff = v - m;
*acc = diff.mul_add(diff, *acc);
}
}
for acc in &mut var {
*acc = (*acc / n_c).max(0.0) + epsilon;
}
log_priors.push((n_c / n_total).ln());
means.push(mean);
variances.push(var);
}
Ok(GaussianNbModel {
classes,
log_priors,
means,
variances,
n_features,
})
}
impl GaussianNbModel {
#[must_use]
pub fn classes(&self) -> &[usize] {
&self.classes
}
fn joint_log_likelihoods(&self, x: &[Vec<f64>]) -> Result<Vec<Vec<f64>>> {
let mut out = Vec::with_capacity(x.len());
for row in x {
if row.len() != self.n_features {
return Err(Error::InvalidInput(
"sample feature count differs from the fitted model".to_owned(),
));
}
let mut scores = Vec::with_capacity(self.classes.len());
for ((&log_prior, mean), var) in self
.log_priors
.iter()
.zip(self.means.iter())
.zip(self.variances.iter())
{
let mut ll = log_prior;
for ((&value, &m), &v) in row.iter().zip(mean.iter()).zip(var.iter()) {
let diff = value - m;
let quad = diff.mul_add(diff, 0.0) / (2.0 * v);
ll += (-0.5_f64).mul_add(LN_2PI + v.ln(), -quad);
}
scores.push(ll);
}
out.push(scores);
}
Ok(out)
}
pub fn predict(&self, x: &[Vec<f64>]) -> Result<Vec<usize>> {
let joints = self.joint_log_likelihoods(x)?;
Ok(joints
.iter()
.map(|row| self.classes.get(argmax(row)).copied().unwrap_or(0))
.collect())
}
pub fn predict_log_proba(&self, x: &[Vec<f64>]) -> Result<Vec<Vec<f64>>> {
let joints = self.joint_log_likelihoods(x)?;
Ok(joints.iter().map(|row| normalize_log(row)).collect())
}
pub fn classification_result(
&self,
x: &[Vec<f64>],
y_true: &[usize],
) -> Result<ClassificationResult> {
let predictions = self.predict(x)?;
classification_result_from(&self.classes, &predictions, y_true, "Gaussian Naive Bayes")
}
}