use crate::s;
use scirs2_core::ndarray::{Array1, Array2};
use serde::{Deserialize, Serialize};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
pub trait DataFrameInterface {
fn nrows(&self) -> usize;
fn ncols(&self) -> usize;
fn column_names(&self) -> Vec<String>;
fn to_array2(&self) -> Result<Array2<Float>>;
fn column(&self, name: &str) -> Result<Array1<Float>>;
fn select(&self, columns: &[String]) -> Result<Box<dyn DataFrameInterface>>;
fn shape(&self) -> (usize, usize) {
(self.nrows(), self.ncols())
}
}
#[derive(Debug, Clone)]
pub struct SimpleDataFrame {
data: Array2<Float>,
column_names: Vec<String>,
}
impl SimpleDataFrame {
pub fn new(data: Array2<Float>, column_names: Vec<String>) -> Result<Self> {
if data.ncols() != column_names.len() {
return Err(SklearsError::InvalidInput(format!(
"Column count mismatch: {} columns but {} names",
data.ncols(),
column_names.len()
)));
}
Ok(Self { data, column_names })
}
pub fn from_array(data: Array2<Float>) -> Self {
let ncols = data.ncols();
let column_names = (0..ncols).map(|i| format!("col_{}", i)).collect();
Self { data, column_names }
}
pub fn as_array(&self) -> &Array2<Float> {
&self.data
}
pub fn into_array(self) -> Array2<Float> {
self.data
}
}
impl DataFrameInterface for SimpleDataFrame {
fn nrows(&self) -> usize {
self.data.nrows()
}
fn ncols(&self) -> usize {
self.data.ncols()
}
fn column_names(&self) -> Vec<String> {
self.column_names.clone()
}
fn to_array2(&self) -> Result<Array2<Float>> {
Ok(self.data.clone())
}
fn column(&self, name: &str) -> Result<Array1<Float>> {
let idx = self
.column_names
.iter()
.position(|n| n == name)
.ok_or_else(|| SklearsError::InvalidInput(format!("Column '{}' not found", name)))?;
Ok(self.data.column(idx).to_owned())
}
fn select(&self, columns: &[String]) -> Result<Box<dyn DataFrameInterface>> {
let indices: Vec<usize> = columns
.iter()
.map(|name| {
self.column_names
.iter()
.position(|n| n == name)
.ok_or_else(|| {
SklearsError::InvalidInput(format!("Column '{}' not found", name))
})
})
.collect::<Result<Vec<_>>>()?;
let selected_data = self.data.select(scirs2_core::ndarray::Axis(1), &indices);
let selected_names = columns.to_vec();
Ok(Box::new(SimpleDataFrame::new(
selected_data,
selected_names,
)?))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SparseMatrix {
pub row_indices: Vec<usize>,
pub col_indices: Vec<usize>,
pub values: Vec<Float>,
pub shape: (usize, usize),
pub nnz: usize,
}
impl SparseMatrix {
pub fn new(
row_indices: Vec<usize>,
col_indices: Vec<usize>,
values: Vec<Float>,
shape: (usize, usize),
) -> Result<Self> {
if row_indices.len() != col_indices.len() || row_indices.len() != values.len() {
return Err(SklearsError::InvalidInput(
"Sparse matrix indices and values must have same length".to_string(),
));
}
let nnz = values.len();
for &row_idx in &row_indices {
if row_idx >= shape.0 {
return Err(SklearsError::InvalidInput(format!(
"Row index {} out of bounds for shape {:?}",
row_idx, shape
)));
}
}
for &col_idx in &col_indices {
if col_idx >= shape.1 {
return Err(SklearsError::InvalidInput(format!(
"Column index {} out of bounds for shape {:?}",
col_idx, shape
)));
}
}
Ok(Self {
row_indices,
col_indices,
values,
shape,
nnz,
})
}
pub fn to_dense(&self) -> Array2<Float> {
let mut dense = Array2::zeros(self.shape);
for i in 0..self.nnz {
let row = self.row_indices[i];
let col = self.col_indices[i];
let val = self.values[i];
dense[[row, col]] = val;
}
dense
}
pub fn from_dense(array: &Array2<Float>, threshold: Float) -> Self {
let mut row_indices = Vec::new();
let mut col_indices = Vec::new();
let mut values = Vec::new();
let shape = array.dim();
for i in 0..shape.0 {
for j in 0..shape.1 {
let val = array[[i, j]];
if val.abs() > threshold {
row_indices.push(i);
col_indices.push(j);
values.push(val);
}
}
}
let nnz = values.len();
Self {
row_indices,
col_indices,
values,
shape,
nnz,
}
}
pub fn sparsity(&self) -> Float {
let total_elements = self.shape.0 * self.shape.1;
1.0 - (self.nnz as Float / total_elements as Float)
}
pub fn transpose(&self) -> Self {
Self {
row_indices: self.col_indices.clone(),
col_indices: self.row_indices.clone(),
values: self.values.clone(),
shape: (self.shape.1, self.shape.0),
nnz: self.nnz,
}
}
}
pub struct BatchProcessor {
batch_size: usize,
overlap: usize,
}
impl BatchProcessor {
pub fn new(batch_size: usize, overlap: usize) -> Self {
Self {
batch_size,
overlap,
}
}
pub fn split(&self, data: &Array2<Float>) -> Vec<Array2<Float>> {
let n_samples = data.nrows();
let mut batches = Vec::new();
let stride = self.batch_size - self.overlap;
let mut start = 0;
while start < n_samples {
let end = (start + self.batch_size).min(n_samples);
let batch = data.slice(s![start..end, ..]).to_owned();
batches.push(batch);
if end >= n_samples {
break;
}
start += stride;
}
batches
}
pub fn process_batches<F, R>(&self, data: &Array2<Float>, mut processor: F) -> Vec<R>
where
F: FnMut(&Array2<Float>, usize) -> R,
{
let batches = self.split(data);
batches
.iter()
.enumerate()
.map(|(idx, batch)| processor(batch, idx))
.collect()
}
}
pub struct DataConverter;
impl DataConverter {
pub fn standardize(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
let mean = data
.mean_axis(scirs2_core::ndarray::Axis(0))
.expect("array should have elements for mean computation");
let centered = data - &mean;
let variance = centered
.mapv(|x| x.powi(2))
.mean_axis(scirs2_core::ndarray::Axis(0))
.expect("operation should succeed");
let std = variance.mapv(|v| v.sqrt().max(1e-8));
let standardized = ¢ered / &std;
(standardized, mean, std)
}
pub fn min_max_scale(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
let min_vals = data.fold_axis(scirs2_core::ndarray::Axis(0), Float::INFINITY, |&a, &b| {
a.min(b)
});
let max_vals = data.fold_axis(
scirs2_core::ndarray::Axis(0),
Float::NEG_INFINITY,
|&a, &b| a.max(b),
);
let range = &max_vals - &min_vals;
let safe_range = range.mapv(|r| r.max(1e-8));
let scaled = (data - &min_vals) / &safe_range;
(scaled, min_vals, max_vals)
}
pub fn robust_scale(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
let n_features = data.ncols();
let mut medians = Array1::zeros(n_features);
let mut iqrs = Array1::zeros(n_features);
for (i, col) in data.axis_iter(scirs2_core::ndarray::Axis(1)).enumerate() {
let mut sorted: Vec<Float> = col.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let n = sorted.len();
let median = sorted[n / 2];
let q1 = sorted[n / 4];
let q3 = sorted[3 * n / 4];
let iqr = (q3 - q1).max(1e-8);
medians[i] = median;
iqrs[i] = iqr;
}
let scaled = (data - &medians) / &iqrs;
(scaled, medians, iqrs)
}
pub fn to_nonnegative(data: &Array2<Float>) -> Array2<Float> {
data.mapv(|x| x.max(0.0))
}
pub fn log_transform(data: &Array2<Float>, offset: Float) -> Array2<Float> {
data.mapv(|x| (x + offset).ln())
}
}
const SUPPORTED_DTYPES_F32: &str = "float32";
const SUPPORTED_DTYPES_F64: &str = "float64";
#[derive(Debug)]
pub struct MemoryMappedArray {
shape: (usize, usize),
dtype: String,
path: String,
}
impl MemoryMappedArray {
pub fn new(path: String, shape: (usize, usize), dtype: String) -> Self {
Self { shape, dtype, path }
}
pub fn shape(&self) -> (usize, usize) {
self.shape
}
pub fn dtype(&self) -> &str {
&self.dtype
}
pub fn path(&self) -> &str {
&self.path
}
pub fn validate_dtype(&self) -> Result<()> {
let float_size = std::mem::size_of::<Float>();
let expected_dtype = if float_size == 4 {
SUPPORTED_DTYPES_F32
} else {
SUPPORTED_DTYPES_F64
};
if self.dtype != expected_dtype {
return Err(SklearsError::InvalidInput(format!(
"dtype mismatch for file '{}': stored dtype is '{}' but the native Float type \
requires '{}' (size {} bytes)",
self.path, self.dtype, expected_dtype, float_size
)));
}
Ok(())
}
pub fn to_array(&self) -> Result<Array2<Float>> {
self.validate_dtype()?;
if !std::path::Path::new(&self.path).exists() {
return Err(SklearsError::InvalidInput(format!(
"Cannot reopen memory-mapped file '{}': path does not exist",
self.path
)));
}
Err(SklearsError::InvalidInput(format!(
"Memory-mapped array loading not yet implemented for file '{}' - use memmap2 crate",
self.path
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::random::thread_rng;
#[test]
fn test_simple_dataframe() {
let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.expect("shape and data length should match");
let columns = vec!["A".to_string(), "B".to_string()];
let df = SimpleDataFrame::new(data, columns).expect("matrix indexing should be valid");
assert_eq!(df.nrows(), 3);
assert_eq!(df.ncols(), 2);
assert_eq!(df.column_names(), vec!["A", "B"]);
}
#[test]
fn test_dataframe_from_array() {
let data = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.expect("shape and data length should match");
let df = SimpleDataFrame::from_array(data);
assert_eq!(df.nrows(), 2);
assert_eq!(df.ncols(), 3);
}
#[test]
fn test_dataframe_column() {
let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.expect("shape and data length should match");
let columns = vec!["A".to_string(), "B".to_string()];
let df = SimpleDataFrame::new(data, columns).expect("matrix indexing should be valid");
let col_a = df.column("A").expect("matrix indexing should be valid");
assert_eq!(col_a.len(), 3);
assert_eq!(col_a[0], 1.0);
}
#[test]
fn test_sparse_matrix_creation() {
let row_indices = vec![0, 1, 2];
let col_indices = vec![0, 1, 2];
let values = vec![1.0, 2.0, 3.0];
let shape = (3, 3);
let sparse = SparseMatrix::new(row_indices, col_indices, values, shape)
.expect("parsing should succeed");
assert_eq!(sparse.nnz, 3);
assert_eq!(sparse.shape, (3, 3));
}
#[test]
fn test_sparse_to_dense() {
let row_indices = vec![0, 1, 1];
let col_indices = vec![0, 0, 1];
let values = vec![1.0, 2.0, 3.0];
let shape = (2, 2);
let sparse = SparseMatrix::new(row_indices, col_indices, values, shape)
.expect("parsing should succeed");
let dense = sparse.to_dense();
assert_eq!(dense[[0, 0]], 1.0);
assert_eq!(dense[[1, 0]], 2.0);
assert_eq!(dense[[1, 1]], 3.0);
assert_eq!(dense[[0, 1]], 0.0);
}
#[test]
fn test_sparse_from_dense() {
let dense = Array2::from_shape_vec((2, 2), vec![1.0, 0.0, 0.0, 2.0])
.expect("shape and data length should match");
let sparse = SparseMatrix::from_dense(&dense, 0.5);
assert_eq!(sparse.nnz, 2);
assert!(sparse.sparsity() > 0.0);
}
#[test]
fn test_batch_processor() {
let data = Array2::from_shape_vec((10, 3), (0..30).map(|x| x as Float).collect())
.expect("shape and data length should match");
let processor = BatchProcessor::new(4, 1);
let batches = processor.split(&data);
assert!(batches.len() >= 3);
assert_eq!(batches[0].nrows(), 4);
}
#[test]
fn test_data_converter_standardize() {
let mut rng = thread_rng();
let data = Array2::from_shape_fn((20, 5), |_| rng.gen_range(0.0..10.0));
let (standardized, mean, std) = DataConverter::standardize(&data);
assert_eq!(standardized.dim(), data.dim());
assert_eq!(mean.len(), 5);
assert_eq!(std.len(), 5);
let new_mean = standardized
.mean_axis(scirs2_core::ndarray::Axis(0))
.expect("operation should succeed");
for &m in new_mean.iter() {
assert!(m.abs() < 0.1); }
}
#[test]
fn test_data_converter_min_max() {
let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.expect("shape and data length should match");
let (scaled, _min_vals, _max_vals) = DataConverter::min_max_scale(&data);
assert_eq!(scaled.dim(), data.dim());
let min_scaled =
scaled.fold_axis(scirs2_core::ndarray::Axis(0), Float::INFINITY, |&a, &b| {
a.min(b)
});
let max_scaled = scaled.fold_axis(
scirs2_core::ndarray::Axis(0),
Float::NEG_INFINITY,
|&a, &b| a.max(b),
);
for &val in min_scaled.iter() {
assert!(val >= -0.01); }
for &val in max_scaled.iter() {
assert!(val <= 1.01); }
}
#[test]
fn test_data_converter_to_nonnegative() {
let data = Array2::from_shape_vec((2, 2), vec![-1.0, 2.0, -3.0, 4.0])
.expect("shape and data length should match");
let nonneg = DataConverter::to_nonnegative(&data);
for &val in nonneg.iter() {
assert!(val >= 0.0);
}
}
#[test]
fn test_memory_mapped_array_dtype_valid() {
use std::mem::size_of;
let native_dtype = if size_of::<Float>() == 4 {
"float32"
} else {
"float64"
};
let mma = MemoryMappedArray::new(
std::env::temp_dir().join("test.npy").display().to_string(),
(10, 10),
native_dtype.to_string(),
);
assert!(
mma.validate_dtype().is_ok(),
"validate_dtype should succeed for matching dtype"
);
}
#[test]
fn test_memory_mapped_array_dtype_mismatch() {
use std::mem::size_of;
let wrong_dtype = if size_of::<Float>() == 4 {
"float64"
} else {
"float32"
};
let mma = MemoryMappedArray::new(
std::env::temp_dir().join("test.npy").display().to_string(),
(10, 10),
wrong_dtype.to_string(),
);
let err = mma.validate_dtype();
assert!(
err.is_err(),
"validate_dtype should fail for mismatching dtype"
);
let msg = format!("{:?}", err);
assert!(
msg.contains(wrong_dtype),
"error should mention the stored dtype"
);
}
#[test]
fn test_memory_mapped_array_path_in_error() {
use std::mem::size_of;
let native_dtype = if size_of::<Float>() == 4 {
"float32"
} else {
"float64"
};
let nonexistent_path = std::env::temp_dir()
.join("does_not_exist_for_sklears_test_12345.npy")
.display()
.to_string();
let mma =
MemoryMappedArray::new(nonexistent_path.clone(), (5, 5), native_dtype.to_string());
let err = mma.to_array();
assert!(err.is_err(), "to_array should fail for missing file");
let msg = format!("{:?}", err);
assert!(
msg.contains(nonexistent_path.as_str()),
"error message should include the file path"
);
}
#[test]
fn test_memory_mapped_array_accessors() {
let path = std::env::temp_dir().join("data.bin").display().to_string();
let dtype = "float64".to_string();
let mma = MemoryMappedArray::new(path.clone(), (3, 4), dtype.clone());
assert_eq!(mma.path(), path);
assert_eq!(mma.dtype(), dtype);
assert_eq!(mma.shape(), (3, 4));
}
}