use crate::{UtilsError, UtilsResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, Axis};
use scirs2_core::numeric::Float;
use std::cmp::Ordering;
use std::collections::HashMap;
#[inline]
fn compare_floats<T: Float>(a: &T, b: &T) -> Ordering {
match a.partial_cmp(b) {
Some(ord) => ord,
None => {
if a.is_nan() && b.is_nan() {
Ordering::Equal
} else if a.is_nan() {
Ordering::Greater
} else {
Ordering::Less
}
}
}
}
#[inline]
fn usize_to_float<T: Float>(value: usize) -> UtilsResult<T> {
T::from(value).ok_or_else(|| {
UtilsError::InvalidParameter(format!("Failed to convert usize {} to float type", value))
})
}
#[inline]
fn const_to_float<T: Float>(value: f64) -> UtilsResult<T> {
T::from(value).ok_or_else(|| {
UtilsError::InvalidParameter(format!(
"Failed to convert constant {} to float type",
value
))
})
}
pub struct DataCleaner;
impl DataCleaner {
pub fn drop_missing_rows<T>(data: &Array2<T>) -> UtilsResult<Array2<T>>
where
T: Float + Clone + std::iter::Sum,
{
let mut valid_rows = Vec::new();
for (row_idx, row) in data.axis_iter(Axis(0)).enumerate() {
if !row.iter().any(|&x| x.is_nan()) {
valid_rows.push(row_idx);
}
}
if valid_rows.is_empty() {
return Err(UtilsError::EmptyInput);
}
let mut result = Array2::zeros((valid_rows.len(), data.ncols()));
for (new_idx, &old_idx) in valid_rows.iter().enumerate() {
result.row_mut(new_idx).assign(&data.row(old_idx));
}
Ok(result)
}
pub fn fill_missing<T>(data: &mut Array2<T>, fill_value: T)
where
T: Float + Clone + std::iter::Sum,
{
data.mapv_inplace(|x| if x.is_nan() { fill_value } else { x });
}
pub fn fill_with_mean<T>(data: &mut Array2<T>) -> UtilsResult<()>
where
T: Float + Clone + std::iter::Sum,
{
for col_idx in 0..data.ncols() {
let col = data.column(col_idx);
let valid_values: Vec<T> = col.iter().cloned().filter(|x| !x.is_nan()).collect();
if !valid_values.is_empty() {
let mean = valid_values.iter().cloned().sum::<T>()
/ usize_to_float::<T>(valid_values.len())?;
for row_idx in 0..data.nrows() {
if data[[row_idx, col_idx]].is_nan() {
data[[row_idx, col_idx]] = mean;
}
}
}
}
Ok(())
}
pub fn fill_with_median<T>(data: &mut Array2<T>) -> UtilsResult<()>
where
T: Float + Clone + PartialOrd,
{
for col_idx in 0..data.ncols() {
let col = data.column(col_idx);
let mut valid_values: Vec<T> = col.iter().cloned().filter(|x| !x.is_nan()).collect();
if !valid_values.is_empty() {
valid_values.sort_by(compare_floats);
let median = if valid_values.len() % 2 == 0 {
let mid = valid_values.len() / 2;
(valid_values[mid - 1] + valid_values[mid]) / const_to_float::<T>(2.0)?
} else {
valid_values[valid_values.len() / 2]
};
for row_idx in 0..data.nrows() {
if data[[row_idx, col_idx]].is_nan() {
data[[row_idx, col_idx]] = median;
}
}
}
}
Ok(())
}
}
pub struct OutlierDetector;
impl OutlierDetector {
pub fn zscore_outliers<T>(data: &ArrayView1<T>, threshold: T) -> Vec<usize>
where
T: Float + Clone + std::iter::Sum,
{
let Ok(len_float) = usize_to_float::<T>(data.len()) else {
return Vec::new();
};
let mean = data.iter().cloned().sum::<T>() / len_float;
let variance = data.iter().map(|&x| (x - mean).powi(2)).sum::<T>() / len_float;
let std_dev = variance.sqrt();
if std_dev == T::zero() {
return Vec::new();
}
data.iter()
.enumerate()
.filter_map(|(idx, &value)| {
let z_score = (value - mean).abs() / std_dev;
if z_score > threshold {
Some(idx)
} else {
None
}
})
.collect()
}
pub fn iqr_outliers<T>(data: &ArrayView1<T>, multiplier: T) -> Vec<usize>
where
T: Float + Clone + PartialOrd,
{
let mut sorted_data: Vec<T> = data.iter().cloned().collect();
sorted_data.sort_by(compare_floats);
let n = sorted_data.len();
if n < 4 {
return Vec::new();
}
let q1_idx = n / 4;
let q3_idx = 3 * n / 4;
let q1 = sorted_data[q1_idx];
let q3 = sorted_data[q3_idx];
let iqr = q3 - q1;
let lower_bound = q1 - multiplier * iqr;
let upper_bound = q3 + multiplier * iqr;
data.iter()
.enumerate()
.filter_map(|(idx, &value)| {
if value < lower_bound || value > upper_bound {
Some(idx)
} else {
None
}
})
.collect()
}
pub fn modified_zscore_outliers<T>(data: &ArrayView1<T>, threshold: T) -> Vec<usize>
where
T: Float + Clone + PartialOrd,
{
let mut sorted_data: Vec<T> = data.iter().cloned().collect();
sorted_data.sort_by(compare_floats);
let n = sorted_data.len();
if n == 0 {
return Vec::new();
}
let Ok(two) = const_to_float::<T>(2.0) else {
return Vec::new();
};
let median = if n % 2 == 0 {
(sorted_data[n / 2 - 1] + sorted_data[n / 2]) / two
} else {
sorted_data[n / 2]
};
let mut deviations: Vec<T> = data.iter().map(|&x| (x - median).abs()).collect();
deviations.sort_by(compare_floats);
let mad = if deviations.len() % 2 == 0 {
let mid = deviations.len() / 2;
(deviations[mid - 1] + deviations[mid]) / two
} else {
deviations[deviations.len() / 2]
};
if mad == T::zero() {
return Vec::new();
}
let Ok(scale_factor) = const_to_float::<T>(1.4826) else {
return Vec::new();
};
let Ok(modified_z_factor) = const_to_float::<T>(0.6745) else {
return Vec::new();
};
let mad_scaled = mad * scale_factor;
data.iter()
.enumerate()
.filter_map(|(idx, &value)| {
let modified_z = modified_z_factor * (value - median).abs() / mad_scaled;
if modified_z > threshold {
Some(idx)
} else {
None
}
})
.collect()
}
}
pub struct FeatureScaler;
impl FeatureScaler {
pub fn standard_scale<T>(data: &Array2<T>) -> UtilsResult<(Array2<T>, Array1<T>, Array1<T>)>
where
T: Float + Clone + std::iter::Sum,
{
let mut scaled_data = data.clone();
let mut means = Array1::zeros(data.ncols());
let mut stds = Array1::zeros(data.ncols());
for col_idx in 0..data.ncols() {
let col = data.column(col_idx);
let col_len = usize_to_float::<T>(col.len())?;
let mean = col.iter().cloned().sum::<T>() / col_len;
let variance = col.iter().map(|&x| (x - mean).powi(2)).sum::<T>() / col_len;
let std_dev = variance.sqrt();
means[col_idx] = mean;
stds[col_idx] = std_dev;
if std_dev != T::zero() {
for row_idx in 0..data.nrows() {
scaled_data[[row_idx, col_idx]] = (data[[row_idx, col_idx]] - mean) / std_dev;
}
}
}
Ok((scaled_data, means, stds))
}
pub fn minmax_scale<T>(data: &Array2<T>) -> UtilsResult<(Array2<T>, Array1<T>, Array1<T>)>
where
T: Float + Clone + PartialOrd,
{
let mut scaled_data = data.clone();
let mut mins = Array1::zeros(data.ncols());
let mut maxs = Array1::zeros(data.ncols());
for col_idx in 0..data.ncols() {
let col = data.column(col_idx);
let min_val = col
.iter()
.cloned()
.fold(col[0], |acc, x| if x < acc { x } else { acc });
let max_val = col
.iter()
.cloned()
.fold(col[0], |acc, x| if x > acc { x } else { acc });
mins[col_idx] = min_val;
maxs[col_idx] = max_val;
let range = max_val - min_val;
if range != T::zero() {
for row_idx in 0..data.nrows() {
scaled_data[[row_idx, col_idx]] = (data[[row_idx, col_idx]] - min_val) / range;
}
}
}
Ok((scaled_data, mins, maxs))
}
pub fn robust_scale<T>(data: &Array2<T>) -> UtilsResult<(Array2<T>, Array1<T>, Array1<T>)>
where
T: Float + Clone + PartialOrd,
{
let mut scaled_data = data.clone();
let mut medians = Array1::zeros(data.ncols());
let mut iqrs = Array1::zeros(data.ncols());
for col_idx in 0..data.ncols() {
let col = data.column(col_idx);
let mut sorted_col: Vec<T> = col.iter().cloned().collect();
sorted_col.sort_by(compare_floats);
let n = sorted_col.len();
let median = if n % 2 == 0 {
(sorted_col[n / 2 - 1] + sorted_col[n / 2]) / const_to_float::<T>(2.0)?
} else {
sorted_col[n / 2]
};
let q1_idx = n / 4;
let q3_idx = 3 * n / 4;
let q1 = sorted_col[q1_idx];
let q3 = sorted_col[q3_idx];
let iqr = q3 - q1;
medians[col_idx] = median;
iqrs[col_idx] = iqr;
if iqr != T::zero() {
for row_idx in 0..data.nrows() {
scaled_data[[row_idx, col_idx]] = (data[[row_idx, col_idx]] - median) / iqr;
}
}
}
Ok((scaled_data, medians, iqrs))
}
}
pub struct DataQualityAssessor;
impl DataQualityAssessor {
pub fn missing_value_stats<T>(data: &Array2<T>) -> HashMap<String, f64>
where
T: Float,
{
let total_cells = data.len() as f64;
let mut missing_count = 0;
let mut missing_per_column = Vec::new();
let mut missing_per_row = Vec::new();
for col_idx in 0..data.ncols() {
let col_missing = data.column(col_idx).iter().filter(|&&x| x.is_nan()).count();
missing_per_column.push(col_missing as f64 / data.nrows() as f64);
missing_count += col_missing;
}
for row_idx in 0..data.nrows() {
let row_missing = data.row(row_idx).iter().filter(|&&x| x.is_nan()).count();
missing_per_row.push(row_missing as f64 / data.ncols() as f64);
}
let mut stats = HashMap::new();
stats.insert(
"total_missing_ratio".to_string(),
missing_count as f64 / total_cells,
);
stats.insert(
"max_column_missing_ratio".to_string(),
missing_per_column.iter().cloned().fold(0.0, f64::max),
);
stats.insert(
"max_row_missing_ratio".to_string(),
missing_per_row.iter().cloned().fold(0.0, f64::max),
);
stats.insert(
"columns_with_missing".to_string(),
missing_per_column.iter().filter(|&&x| x > 0.0).count() as f64,
);
stats.insert(
"rows_with_missing".to_string(),
missing_per_row.iter().filter(|&&x| x > 0.0).count() as f64,
);
stats
}
pub fn quality_metrics<T>(data: &Array2<T>) -> HashMap<String, f64>
where
T: Float + PartialOrd + std::iter::Sum + std::fmt::Display,
{
let mut metrics = HashMap::new();
let total_cells = data.len() as f64;
let missing_count = data.iter().filter(|&&x| x.is_nan()).count() as f64;
metrics.insert(
"completeness".to_string(),
1.0 - (missing_count / total_cells),
);
let mut unique_counts = Vec::new();
for col_idx in 0..data.ncols() {
let col = data.column(col_idx);
let mut unique_values = std::collections::HashSet::new();
for &value in col.iter() {
if !value.is_nan() {
unique_values.insert(format!("{value:.6}"));
}
}
let uniqueness = unique_values.len() as f64 / col.len() as f64;
unique_counts.push(uniqueness);
}
let avg_uniqueness = unique_counts.iter().sum::<f64>() / unique_counts.len() as f64;
metrics.insert("uniqueness".to_string(), avg_uniqueness);
let mut cv_values = Vec::new();
for col_idx in 0..data.ncols() {
let col = data.column(col_idx);
let valid_values: Vec<T> = col.iter().cloned().filter(|x| !x.is_nan()).collect();
if valid_values.len() > 1 {
if let Ok(valid_len) = usize_to_float::<T>(valid_values.len()) {
let mean = valid_values.iter().cloned().sum::<T>() / valid_len;
let variance =
valid_values.iter().map(|&x| (x - mean).powi(2)).sum::<T>() / valid_len;
let std_dev = variance.sqrt();
if mean != T::zero() {
if let Some(cv_f64) = (std_dev / mean.abs()).to_f64() {
cv_values.push(cv_f64);
}
}
}
}
}
if !cv_values.is_empty() {
let avg_cv = cv_values.iter().sum::<f64>() / cv_values.len() as f64;
metrics.insert("consistency".to_string(), 1.0 / (1.0 + avg_cv)); }
metrics
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_drop_missing_rows() {
let data = array![
[1.0, 2.0, 3.0],
[4.0, f64::NAN, 6.0],
[7.0, 8.0, 9.0],
[f64::NAN, 11.0, 12.0]
];
let cleaned = DataCleaner::drop_missing_rows(&data)
.expect("drop_missing_rows should succeed with valid data");
assert_eq!(cleaned.nrows(), 2);
assert_eq!(cleaned.row(0), array![1.0, 2.0, 3.0]);
assert_eq!(cleaned.row(1), array![7.0, 8.0, 9.0]);
}
#[test]
fn test_fill_missing_with_value() {
let mut data = array![[1.0, 2.0], [f64::NAN, 4.0], [5.0, f64::NAN]];
DataCleaner::fill_missing(&mut data, 0.0);
assert_eq!(data, array![[1.0, 2.0], [0.0, 4.0], [5.0, 0.0]]);
}
#[test]
fn test_fill_with_mean() {
let mut data = array![[1.0, 2.0], [f64::NAN, 4.0], [5.0, f64::NAN]];
DataCleaner::fill_with_mean(&mut data)
.expect("fill_with_mean should succeed with valid data");
assert_abs_diff_eq!(data[[1, 0]], 3.0, epsilon = 1e-10);
assert_abs_diff_eq!(data[[2, 1]], 3.0, epsilon = 1e-10);
}
#[test]
fn test_zscore_outliers() {
let data = array![1.0, 2.0, 3.0, 4.0, 100.0]; let outliers = OutlierDetector::zscore_outliers(&data.view(), 1.5);
assert_eq!(outliers, vec![4]);
}
#[test]
fn test_iqr_outliers() {
let data = array![1.0, 2.0, 3.0, 4.0, 5.0, 100.0]; let outliers = OutlierDetector::iqr_outliers(&data.view(), 1.5);
assert_eq!(outliers, vec![5]);
}
#[test]
fn test_standard_scaling() {
let data = array![[1.0, 10.0], [2.0, 20.0], [3.0, 30.0]];
let (scaled, _means, _stds) = FeatureScaler::standard_scale(&data)
.expect("standard_scale should succeed with valid data");
for col_idx in 0..scaled.ncols() {
let col = scaled.column(col_idx);
let mean = col.iter().sum::<f64>() / col.len() as f64;
assert_abs_diff_eq!(mean, 0.0, epsilon = 1e-10);
}
}
#[test]
fn test_minmax_scaling() {
let data = array![[1.0, 10.0], [2.0, 20.0], [3.0, 30.0]];
let (scaled, _mins, _maxs) = FeatureScaler::minmax_scale(&data)
.expect("minmax_scale should succeed with valid data");
for col_idx in 0..scaled.ncols() {
let col = scaled.column(col_idx);
let min_val = col.iter().cloned().fold(col[0], f64::min);
let max_val = col.iter().cloned().fold(col[0], f64::max);
assert_abs_diff_eq!(min_val, 0.0, epsilon = 1e-10);
assert_abs_diff_eq!(max_val, 1.0, epsilon = 1e-10);
}
}
#[test]
fn test_missing_value_stats() {
let data = array![[1.0, 2.0, 3.0], [f64::NAN, 5.0, 6.0], [7.0, f64::NAN, 9.0]];
let stats = DataQualityAssessor::missing_value_stats(&data);
assert_abs_diff_eq!(stats["total_missing_ratio"], 2.0 / 9.0, epsilon = 1e-10);
assert_eq!(stats["columns_with_missing"], 2.0);
assert_eq!(stats["rows_with_missing"], 2.0);
}
#[test]
fn test_quality_metrics() {
let data = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
let metrics = DataQualityAssessor::quality_metrics(&data);
assert_abs_diff_eq!(metrics["completeness"], 1.0, epsilon = 1e-10);
assert!(metrics.contains_key("uniqueness"));
assert!(metrics.contains_key("consistency"));
}
}