use nalgebra::{DMatrix, DVector};
use std::f64::consts::E;
#[derive(Debug, Clone)]
pub struct LogisticRegression {
pub coefficients: DVector<f64>,
}
impl LogisticRegression {
pub fn new() -> Self {
Self {
coefficients: DVector::zeros(0),
}
}
pub fn fit(
&mut self,
x: &DMatrix<f64>,
y: &DVector<f64>,
max_iter: usize,
tol: f64,
) -> Result<(), String> {
let n_features = x.ncols();
let n_samples = x.nrows();
if n_samples != y.len() {
return Err("Number of samples in X and y must match".to_string());
}
let mut beta = DVector::zeros(n_features);
for _iter in 0..max_iter {
let xb = x * β
let p: DVector<f64> = xb.map(|val| 1.0 / (1.0 + E.powf(-val)));
let error = y - &p;
let gradient = x.transpose() * &error;
let w_vec: DVector<f64> = p.map(|val| val * (1.0 - val));
let mut x_weighted = x.clone();
for i in 0..n_samples {
let w = w_vec[i];
let mut row = x_weighted.row_mut(i);
row *= w;
}
let mut hessian = x.transpose() * x_weighted;
for i in 0..n_features {
hessian[(i, i)] += 1e-6;
}
let delta = hessian.lu().solve(&gradient).ok_or("Hessian is singular")?;
beta += δ
if delta.norm() < tol {
break;
}
}
self.coefficients = beta;
Ok(())
}
pub fn predict_proba(&self, x: &DMatrix<f64>) -> DVector<f64> {
let xb = x * &self.coefficients;
xb.map(|val| 1.0 / (1.0 + E.powf(-val)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logistic_regression() {
let x = DMatrix::from_row_slice(
4,
3,
&[1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0],
);
let y = DVector::from_vec(vec![0.0, 1.0, 1.0, 1.0]);
let mut model = LogisticRegression::new();
model.fit(&x, &y, 100, 1e-6).unwrap();
let preds = model.predict_proba(&x);
assert!(preds[0] < 0.5);
assert!(preds[1] > 0.5);
assert!(preds[2] > 0.5);
assert!(preds[3] > 0.5);
}
}