use crate::validators::{ValidationReport, ValidationResult, Validator};
use polars::prelude::*;
pub struct OutlierSigmaValidator {
pub sigma: f64,
}
impl Validator for OutlierSigmaValidator {
fn name(&self) -> &'static str {
"OutlierSigma"
}
fn validate(&self, df: &DataFrame, column_name: &str) -> ValidationResult<ValidationReport> {
let series = df.column(column_name)?;
let f64_series = match series.strict_cast(&DataType::Float64) {
Ok(s) => s,
Err(_) => {
return Ok(ValidationReport {
status: "skipped",
details: Some("column could not be cast to a numeric type".to_string()),
});
}
};
let values = f64_series.f64()?;
if let (Some(mean), Some(std_dev)) = (values.mean(), values.std(1)) {
if std_dev == 0.0 {
return Ok(ValidationReport {
status: "pass",
details: Some("standard deviation is zero; no outliers possible".to_string()),
});
}
let deviation = (values - mean).into_series();
let threshold = self.sigma * std_dev;
let mask = abs(&deviation)?.gt(threshold);
let outlier_count = mask?.sum().unwrap_or(0);
if outlier_count > 0 {
Ok(ValidationReport {
status: "fail",
details: Some(format!("outliers={}, sigma={}", outlier_count, self.sigma)),
})
} else {
Ok(ValidationReport {
status: "pass",
details: None,
})
}
} else {
Ok(ValidationReport {
status: "skipped",
details: Some("mean or standard deviation could not be calculated".to_string()),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_f64_df(values: &[Option<f64>]) -> DataFrame {
let s = Series::new("col".into(), values.to_vec());
DataFrame::new(vec![s.into()]).unwrap()
}
fn make_str_df(values: &[Option<&str>]) -> DataFrame {
let s = Series::new("col".into(), values.to_vec());
DataFrame::new(vec![s.into()]).unwrap()
}
#[test]
fn passes_when_no_outliers() {
let df = make_f64_df(&[Some(4.0), Some(5.0), Some(6.0)]);
let validator = OutlierSigmaValidator { sigma: 2.0 };
let report = validator.validate(&df, "col").unwrap();
assert_eq!(report.status, "pass");
}
#[test]
fn fails_when_outliers_present() {
let df = make_f64_df(&[
Some(10.0),
Some(10.0),
Some(10.0),
Some(10.0),
Some(10.0),
Some(10.0),
Some(10.0),
Some(10.0),
Some(100.0),
]);
let validator = OutlierSigmaValidator { sigma: 2.0 };
let report = validator.validate(&df, "col").unwrap();
assert_eq!(report.status, "fail");
assert!(report.details.unwrap().contains("outliers="));
}
#[test]
fn passes_when_std_dev_zero() {
let df = make_f64_df(&[Some(5.0), Some(5.0), Some(5.0)]);
let validator = OutlierSigmaValidator { sigma: 2.0 };
let report = validator.validate(&df, "col").unwrap();
assert_eq!(report.status, "pass");
assert!(
report
.details
.unwrap()
.contains("standard deviation is zero")
);
}
#[test]
fn skips_when_column_not_numeric() {
let df = make_str_df(&[Some("a"), Some("b")]);
let validator = OutlierSigmaValidator { sigma: 2.0 };
let report = validator.validate(&df, "col").unwrap();
assert_eq!(report.status, "skipped");
assert!(report.details.unwrap().contains("could not be cast"));
}
#[test]
fn skips_when_all_nulls() {
let df = make_f64_df(&[None, None, None]);
let validator = OutlierSigmaValidator { sigma: 2.0 };
let report = validator.validate(&df, "col").unwrap();
assert_eq!(report.status, "skipped");
assert!(
report
.details
.unwrap()
.contains("mean or standard deviation")
);
}
#[test]
fn skips_when_empty_column() {
let s: Series = Series::new("col".into(), Vec::<Option<f64>>::new());
let df = DataFrame::new(vec![s.into()]).unwrap();
let validator = OutlierSigmaValidator { sigma: 2.0 };
let report = validator.validate(&df, "col").unwrap();
assert_eq!(report.status, "skipped");
assert!(
report
.details
.unwrap()
.contains("mean or standard deviation")
);
}
}