use incremental_rs::{
IncrementalLinearRegression, IncrementalSupervisedEstimator, LearningRateSchedule,
};
use ndarray015::{Array1, Array2};
use crate::error::{Error, Result};
use crate::frame::{Dataset, Frame};
use crate::traits::{PartialFit, Predictor};
fn to_array2(frame: &Frame) -> Result<Array2<f64>> {
let (n, p) = frame.shape();
Array2::from_shape_vec((n, p), frame.buf().to_vec())
.map_err(|e| Error::Backend(format!("ndarray conversion failed: {e}")))
}
pub struct IncrementalLinear {
inner: IncrementalLinearRegression,
fitted: bool,
}
impl IncrementalLinear {
pub fn new() -> Self {
IncrementalLinear::with_rate(0.01, 0.0)
}
pub fn with_rate(learning_rate: f64, l2_penalty: f64) -> Self {
IncrementalLinear {
inner: IncrementalLinearRegression::new(
LearningRateSchedule::Constant {
initial_rate: learning_rate,
},
l2_penalty,
),
fitted: false,
}
}
}
impl Default for IncrementalLinear {
fn default() -> Self {
IncrementalLinear::new()
}
}
impl PartialFit for IncrementalLinear {
fn name(&self) -> &'static str {
"IncrementalLinear"
}
fn partial_fit(&mut self, batch: &Dataset) -> Result<()> {
let x = to_array2(batch.features())?;
let y = Array1::from(batch.target().to_vec());
self.inner
.partial_fit(&x, &y)
.map_err(|e| Error::Backend(format!("partial_fit: {e}")))?;
self.fitted = true;
Ok(())
}
}
impl Predictor for IncrementalLinear {
fn predict(&self, frame: &Frame) -> Result<Vec<f64>> {
if !self.fitted {
return Err(Error::NotFitted("IncrementalLinear::predict".into()));
}
let x = to_array2(frame)?;
let y = self
.inner
.predict(&x)
.map_err(|e| Error::Backend(format!("incremental predict: {e}")))?;
Ok(y.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn learns_a_line_over_streamed_batches() {
let mut model = IncrementalLinear::with_rate(0.05, 0.0);
for epoch in 0..200 {
let start = (epoch % 5) as f64;
let rows: Vec<Vec<f64>> = (0..4).map(|i| vec![start + i as f64 * 0.25]).collect();
let y: Vec<f64> = rows.iter().map(|r| 3.0 * r[0] + 2.0).collect();
let batch = Dataset::new(Frame::from_rows(rows, vec!["x".into()]).unwrap(), y).unwrap();
model.partial_fit(&batch).unwrap();
}
let probe = Frame::from_rows(vec![vec![2.0]], vec!["x".into()]).unwrap();
let pred = model.predict(&probe).unwrap()[0];
assert!((pred - 8.0).abs() < 1.0, "expected ~8.0, got {pred}");
}
#[test]
fn predict_before_fit_errors() {
let f = Frame::from_rows(vec![vec![1.0]], vec!["x".into()]).unwrap();
assert!(IncrementalLinear::new().predict(&f).is_err());
}
}