use crate::error::{DatarustError, Result};
use crate::matrix::Matrix;
use crate::stats;
use crate::traits::{default_input_names, FeatureNames};
use crate::Transformer;
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ImputeStrategy {
#[default]
Mean,
Median,
MostFrequent,
Constant(f64),
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SimpleImputer {
strategy: ImputeStrategy,
fill_values: Vec<f64>,
fitted: bool,
}
impl SimpleImputer {
pub fn new(strategy: ImputeStrategy) -> Self {
Self {
strategy,
fill_values: vec![],
fitted: false,
}
}
pub fn strategy(&self) -> &ImputeStrategy {
&self.strategy
}
pub fn fill_values(&self) -> &[f64] {
&self.fill_values
}
#[allow(clippy::needless_range_loop)]
fn compute_fill(x: &Matrix, strategy: &ImputeStrategy) -> Result<Vec<f64>> {
let data = x.rows_ref();
let cols = x.ncols();
let mut fills = Vec::with_capacity(cols);
for j in 0..cols {
let col: Vec<f64> = (0..x.nrows())
.filter_map(|i| {
let v = data[i][j];
if v.is_nan() {
None
} else {
Some(v)
}
})
.collect();
let fill = match strategy {
ImputeStrategy::Mean => {
if col.is_empty() {
return Err(DatarustError::AllMissing(format!("column {}", j)));
}
let s: f64 = col.iter().sum();
s / col.len() as f64
}
ImputeStrategy::Median => {
if col.is_empty() {
return Err(DatarustError::AllMissing(format!("column {}", j)));
}
let mut c = col.clone();
c.sort_by(|a, b| a.total_cmp(b));
stats::median_sorted(&c).expect("column non-empty (checked above)")
}
ImputeStrategy::MostFrequent => {
if col.is_empty() {
return Err(DatarustError::AllMissing(format!("column {}", j)));
}
let mut c = col.clone();
c.sort_by(|a, b| a.total_cmp(b));
let single: Vec<Vec<f64>> = c.into_iter().map(|v| vec![v]).collect();
stats::mode_column(&single)[0]
}
ImputeStrategy::Constant(v) => *v,
};
fills.push(fill);
}
Ok(fills)
}
}
impl Default for SimpleImputer {
fn default() -> Self {
Self::new(ImputeStrategy::Mean)
}
}
impl FeatureNames for SimpleImputer {
fn feature_names_out(&self, input_features: Option<&[String]>) -> Vec<String> {
match input_features {
Some(fs) => fs.to_vec(),
None => default_input_names(self.fill_values.len()),
}
}
}
impl Transformer for SimpleImputer {
fn name(&self) -> &'static str {
"SimpleImputer"
}
fn fit(&mut self, x: &Matrix) -> Result<()> {
self.fill_values = Self::compute_fill(x, &self.strategy)?;
self.fitted = true;
Ok(())
}
fn transform(&self, x: &Matrix) -> Result<Matrix> {
if !self.fitted {
return Err(DatarustError::NotFitted("SimpleImputer".into()));
}
if self.fill_values.len() != x.ncols() {
return Err(DatarustError::ShapeMismatch {
expected: format!("{} features", self.fill_values.len()),
actual: format!("{} features", x.ncols()),
});
}
let mut out = x.clone();
for i in 0..out.nrows() {
for j in 0..out.ncols() {
if out.get(i, j).is_nan() {
out.set(i, j, self.fill_values[j]);
}
}
}
Ok(out)
}
fn is_fitted(&self) -> bool {
self.fitted
}
}
#[cfg(test)]
mod tests {
use super::*;
fn nan() -> f64 {
f64::NAN
}
fn m_missing() -> Matrix {
Matrix::new(vec![
vec![1.0, 10.0, nan()],
vec![2.0, nan(), 5.0],
vec![3.0, 30.0, 5.0],
vec![4.0, 40.0, 5.0],
])
.unwrap()
}
#[test]
fn mean_strategy() {
let mut imp = SimpleImputer::new(ImputeStrategy::Mean);
let out = imp.fit_transform(&m_missing()).unwrap();
assert!((imp.fill_values()[1] - (80.0 / 3.0)).abs() < 1e-9);
assert!((out.get(1, 1) - (80.0 / 3.0)).abs() < 1e-9);
assert!((out.get(0, 2) - 5.0).abs() < 1e-9);
}
#[test]
fn median_strategy() {
let mut imp = SimpleImputer::new(ImputeStrategy::Median);
let out = imp.fit_transform(&m_missing()).unwrap();
assert!((imp.fill_values()[1] - 30.0).abs() < 1e-9);
assert!((out.get(1, 1) - 30.0).abs() < 1e-9);
}
#[test]
fn most_frequent_strategy() {
let x = Matrix::new(vec![
vec![nan(), 5.0],
vec![1.0, 5.0],
vec![2.0, 9.0],
vec![2.0, 5.0],
])
.unwrap();
let mut imp = SimpleImputer::new(ImputeStrategy::MostFrequent);
let out = imp.fit_transform(&x).unwrap();
assert!((imp.fill_values()[0] - 2.0).abs() < 1e-9);
assert!((out.get(0, 0) - 2.0).abs() < 1e-9);
assert!((imp.fill_values()[1] - 5.0).abs() < 1e-9);
}
#[test]
fn most_frequent_tie_smallest() {
let x = Matrix::new(vec![vec![nan()], vec![1.0], vec![2.0]]).unwrap();
let mut imp = SimpleImputer::new(ImputeStrategy::MostFrequent);
imp.fit(&x).unwrap();
assert!((imp.fill_values()[0] - 1.0).abs() < 1e-9);
}
#[test]
fn constant_strategy() {
let mut imp = SimpleImputer::new(ImputeStrategy::Constant(-99.0));
let out = imp.fit_transform(&m_missing()).unwrap();
assert!((out.get(0, 2) - (-99.0)).abs() < 1e-9);
assert!((out.get(1, 1) - (-99.0)).abs() < 1e-9);
assert!((imp.fill_values()[0] - (-99.0)).abs() < 1e-9);
}
#[test]
fn no_missing_unchanged() {
let x = Matrix::new(vec![vec![1.0, 2.0], vec![3.0, 4.0]]).unwrap();
let mut imp = SimpleImputer::new(ImputeStrategy::Mean);
let out = imp.fit_transform(&x).unwrap();
assert_eq!(out.rows_ref(), x.rows_ref());
}
#[test]
fn all_missing_column_errors() {
let x = Matrix::new(vec![vec![nan(), 1.0], vec![nan(), 2.0]]).unwrap();
let mut imp = SimpleImputer::new(ImputeStrategy::Mean);
let err = imp.fit(&x).unwrap_err();
assert!(matches!(err, DatarustError::AllMissing(_)));
}
#[test]
fn all_missing_median_errors() {
let x = Matrix::new(vec![vec![nan()], vec![nan()]]).unwrap();
let mut imp = SimpleImputer::new(ImputeStrategy::Median);
assert!(imp.fit(&x).is_err());
}
#[test]
fn constant_works_with_all_missing() {
let x = Matrix::new(vec![vec![nan()], vec![nan()]]).unwrap();
let mut imp = SimpleImputer::new(ImputeStrategy::Constant(0.0));
let out = imp.fit_transform(&x).unwrap();
for i in 0..2 {
assert!((out.get(i, 0) - 0.0).abs() < 1e-9);
}
}
#[test]
fn transform_before_fit_errors() {
let imp = SimpleImputer::new(ImputeStrategy::Mean);
assert!(matches!(
imp.transform(&m_missing()),
Err(DatarustError::NotFitted(_))
));
}
#[test]
fn transform_new_data_uses_fitted() {
let mut imp = SimpleImputer::new(ImputeStrategy::Mean);
imp.fit(&m_missing()).unwrap();
let new = Matrix::new(vec![vec![nan(), nan(), nan()]]).unwrap();
let out = imp.transform(&new).unwrap();
assert!((out.get(0, 1) - (80.0 / 3.0)).abs() < 1e-9);
}
}