1use crate::s;
14use scirs2_core::ndarray::{Array1, Array2};
15use serde::{Deserialize, Serialize};
16use sklears_core::{
17 error::{Result, SklearsError},
18 types::Float,
19};
20
21pub trait DataFrameInterface {
23 fn nrows(&self) -> usize;
25
26 fn ncols(&self) -> usize;
28
29 fn column_names(&self) -> Vec<String>;
31
32 fn to_array2(&self) -> Result<Array2<Float>>;
34
35 fn column(&self, name: &str) -> Result<Array1<Float>>;
37
38 fn select(&self, columns: &[String]) -> Result<Box<dyn DataFrameInterface>>;
40
41 fn shape(&self) -> (usize, usize) {
43 (self.nrows(), self.ncols())
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct SimpleDataFrame {
50 data: Array2<Float>,
51 column_names: Vec<String>,
52}
53
54impl SimpleDataFrame {
55 pub fn new(data: Array2<Float>, column_names: Vec<String>) -> Result<Self> {
57 if data.ncols() != column_names.len() {
58 return Err(SklearsError::InvalidInput(format!(
59 "Column count mismatch: {} columns but {} names",
60 data.ncols(),
61 column_names.len()
62 )));
63 }
64
65 Ok(Self { data, column_names })
66 }
67
68 pub fn from_array(data: Array2<Float>) -> Self {
70 let ncols = data.ncols();
71 let column_names = (0..ncols).map(|i| format!("col_{}", i)).collect();
72
73 Self { data, column_names }
74 }
75
76 pub fn as_array(&self) -> &Array2<Float> {
78 &self.data
79 }
80
81 pub fn into_array(self) -> Array2<Float> {
83 self.data
84 }
85}
86
87impl DataFrameInterface for SimpleDataFrame {
88 fn nrows(&self) -> usize {
89 self.data.nrows()
90 }
91
92 fn ncols(&self) -> usize {
93 self.data.ncols()
94 }
95
96 fn column_names(&self) -> Vec<String> {
97 self.column_names.clone()
98 }
99
100 fn to_array2(&self) -> Result<Array2<Float>> {
101 Ok(self.data.clone())
102 }
103
104 fn column(&self, name: &str) -> Result<Array1<Float>> {
105 let idx = self
106 .column_names
107 .iter()
108 .position(|n| n == name)
109 .ok_or_else(|| SklearsError::InvalidInput(format!("Column '{}' not found", name)))?;
110
111 Ok(self.data.column(idx).to_owned())
112 }
113
114 fn select(&self, columns: &[String]) -> Result<Box<dyn DataFrameInterface>> {
115 let indices: Vec<usize> = columns
116 .iter()
117 .map(|name| {
118 self.column_names
119 .iter()
120 .position(|n| n == name)
121 .ok_or_else(|| {
122 SklearsError::InvalidInput(format!("Column '{}' not found", name))
123 })
124 })
125 .collect::<Result<Vec<_>>>()?;
126
127 let selected_data = self.data.select(scirs2_core::ndarray::Axis(1), &indices);
128 let selected_names = columns.to_vec();
129
130 Ok(Box::new(SimpleDataFrame::new(
131 selected_data,
132 selected_names,
133 )?))
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct SparseMatrix {
140 pub row_indices: Vec<usize>,
142
143 pub col_indices: Vec<usize>,
145
146 pub values: Vec<Float>,
148
149 pub shape: (usize, usize),
151
152 pub nnz: usize,
154}
155
156impl SparseMatrix {
157 pub fn new(
159 row_indices: Vec<usize>,
160 col_indices: Vec<usize>,
161 values: Vec<Float>,
162 shape: (usize, usize),
163 ) -> Result<Self> {
164 if row_indices.len() != col_indices.len() || row_indices.len() != values.len() {
165 return Err(SklearsError::InvalidInput(
166 "Sparse matrix indices and values must have same length".to_string(),
167 ));
168 }
169
170 let nnz = values.len();
171
172 for &row_idx in &row_indices {
174 if row_idx >= shape.0 {
175 return Err(SklearsError::InvalidInput(format!(
176 "Row index {} out of bounds for shape {:?}",
177 row_idx, shape
178 )));
179 }
180 }
181
182 for &col_idx in &col_indices {
183 if col_idx >= shape.1 {
184 return Err(SklearsError::InvalidInput(format!(
185 "Column index {} out of bounds for shape {:?}",
186 col_idx, shape
187 )));
188 }
189 }
190
191 Ok(Self {
192 row_indices,
193 col_indices,
194 values,
195 shape,
196 nnz,
197 })
198 }
199
200 pub fn to_dense(&self) -> Array2<Float> {
202 let mut dense = Array2::zeros(self.shape);
203
204 for i in 0..self.nnz {
205 let row = self.row_indices[i];
206 let col = self.col_indices[i];
207 let val = self.values[i];
208 dense[[row, col]] = val;
209 }
210
211 dense
212 }
213
214 pub fn from_dense(array: &Array2<Float>, threshold: Float) -> Self {
216 let mut row_indices = Vec::new();
217 let mut col_indices = Vec::new();
218 let mut values = Vec::new();
219
220 let shape = array.dim();
221
222 for i in 0..shape.0 {
223 for j in 0..shape.1 {
224 let val = array[[i, j]];
225 if val.abs() > threshold {
226 row_indices.push(i);
227 col_indices.push(j);
228 values.push(val);
229 }
230 }
231 }
232
233 let nnz = values.len();
234
235 Self {
236 row_indices,
237 col_indices,
238 values,
239 shape,
240 nnz,
241 }
242 }
243
244 pub fn sparsity(&self) -> Float {
246 let total_elements = self.shape.0 * self.shape.1;
247 1.0 - (self.nnz as Float / total_elements as Float)
248 }
249
250 pub fn transpose(&self) -> Self {
252 Self {
253 row_indices: self.col_indices.clone(),
254 col_indices: self.row_indices.clone(),
255 values: self.values.clone(),
256 shape: (self.shape.1, self.shape.0),
257 nnz: self.nnz,
258 }
259 }
260}
261
262pub struct BatchProcessor {
264 batch_size: usize,
265 overlap: usize,
266}
267
268impl BatchProcessor {
269 pub fn new(batch_size: usize, overlap: usize) -> Self {
271 Self {
272 batch_size,
273 overlap,
274 }
275 }
276
277 pub fn split(&self, data: &Array2<Float>) -> Vec<Array2<Float>> {
279 let n_samples = data.nrows();
280 let mut batches = Vec::new();
281
282 let stride = self.batch_size - self.overlap;
283 let mut start = 0;
284
285 while start < n_samples {
286 let end = (start + self.batch_size).min(n_samples);
287 let batch = data.slice(s![start..end, ..]).to_owned();
288 batches.push(batch);
289
290 if end >= n_samples {
291 break;
292 }
293
294 start += stride;
295 }
296
297 batches
298 }
299
300 pub fn process_batches<F, R>(&self, data: &Array2<Float>, mut processor: F) -> Vec<R>
302 where
303 F: FnMut(&Array2<Float>, usize) -> R,
304 {
305 let batches = self.split(data);
306 batches
307 .iter()
308 .enumerate()
309 .map(|(idx, batch)| processor(batch, idx))
310 .collect()
311 }
312}
313
314pub struct DataConverter;
316
317impl DataConverter {
318 pub fn standardize(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
320 let mean = data
321 .mean_axis(scirs2_core::ndarray::Axis(0))
322 .expect("array should have elements for mean computation");
323 let centered = data - &mean;
324
325 let variance = centered
326 .mapv(|x| x.powi(2))
327 .mean_axis(scirs2_core::ndarray::Axis(0))
328 .expect("operation should succeed");
329
330 let std = variance.mapv(|v| v.sqrt().max(1e-8));
331 let standardized = ¢ered / &std;
332
333 (standardized, mean, std)
334 }
335
336 pub fn min_max_scale(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
338 let min_vals = data.fold_axis(scirs2_core::ndarray::Axis(0), Float::INFINITY, |&a, &b| {
339 a.min(b)
340 });
341
342 let max_vals = data.fold_axis(
343 scirs2_core::ndarray::Axis(0),
344 Float::NEG_INFINITY,
345 |&a, &b| a.max(b),
346 );
347
348 let range = &max_vals - &min_vals;
349 let safe_range = range.mapv(|r| r.max(1e-8));
350
351 let scaled = (data - &min_vals) / &safe_range;
352
353 (scaled, min_vals, max_vals)
354 }
355
356 pub fn robust_scale(data: &Array2<Float>) -> (Array2<Float>, Array1<Float>, Array1<Float>) {
358 let n_features = data.ncols();
359 let mut medians = Array1::zeros(n_features);
360 let mut iqrs = Array1::zeros(n_features);
361
362 for (i, col) in data.axis_iter(scirs2_core::ndarray::Axis(1)).enumerate() {
363 let mut sorted: Vec<Float> = col.to_vec();
364 sorted.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
365
366 let n = sorted.len();
367 let median = sorted[n / 2];
368 let q1 = sorted[n / 4];
369 let q3 = sorted[3 * n / 4];
370 let iqr = (q3 - q1).max(1e-8);
371
372 medians[i] = median;
373 iqrs[i] = iqr;
374 }
375
376 let scaled = (data - &medians) / &iqrs;
377
378 (scaled, medians, iqrs)
379 }
380
381 pub fn to_nonnegative(data: &Array2<Float>) -> Array2<Float> {
383 data.mapv(|x| x.max(0.0))
384 }
385
386 pub fn log_transform(data: &Array2<Float>, offset: Float) -> Array2<Float> {
388 data.mapv(|x| (x + offset).ln())
389 }
390}
391
392const SUPPORTED_DTYPES_F32: &str = "float32";
394const SUPPORTED_DTYPES_F64: &str = "float64";
395
396#[derive(Debug)]
398pub struct MemoryMappedArray {
399 shape: (usize, usize),
400 dtype: String,
402 path: String,
404}
405
406impl MemoryMappedArray {
407 pub fn new(path: String, shape: (usize, usize), dtype: String) -> Self {
409 Self { shape, dtype, path }
410 }
411
412 pub fn shape(&self) -> (usize, usize) {
414 self.shape
415 }
416
417 pub fn dtype(&self) -> &str {
419 &self.dtype
420 }
421
422 pub fn path(&self) -> &str {
424 &self.path
425 }
426
427 pub fn validate_dtype(&self) -> Result<()> {
432 let float_size = std::mem::size_of::<Float>();
433 let expected_dtype = if float_size == 4 {
434 SUPPORTED_DTYPES_F32
435 } else {
436 SUPPORTED_DTYPES_F64
437 };
438
439 if self.dtype != expected_dtype {
440 return Err(SklearsError::InvalidInput(format!(
441 "dtype mismatch for file '{}': stored dtype is '{}' but the native Float type \
442 requires '{}' (size {} bytes)",
443 self.path, self.dtype, expected_dtype, float_size
444 )));
445 }
446 Ok(())
447 }
448
449 pub fn to_array(&self) -> Result<Array2<Float>> {
454 self.validate_dtype()?;
456
457 if !std::path::Path::new(&self.path).exists() {
459 return Err(SklearsError::InvalidInput(format!(
460 "Cannot reopen memory-mapped file '{}': path does not exist",
461 self.path
462 )));
463 }
464
465 Err(SklearsError::InvalidInput(format!(
466 "Memory-mapped array loading not yet implemented for file '{}' - use memmap2 crate",
467 self.path
468 )))
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use scirs2_core::random::thread_rng;
476
477 #[test]
478 fn test_simple_dataframe() {
479 let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
480 .expect("shape and data length should match");
481 let columns = vec!["A".to_string(), "B".to_string()];
482
483 let df = SimpleDataFrame::new(data, columns).expect("matrix indexing should be valid");
484
485 assert_eq!(df.nrows(), 3);
486 assert_eq!(df.ncols(), 2);
487 assert_eq!(df.column_names(), vec!["A", "B"]);
488 }
489
490 #[test]
491 fn test_dataframe_from_array() {
492 let data = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
493 .expect("shape and data length should match");
494 let df = SimpleDataFrame::from_array(data);
495
496 assert_eq!(df.nrows(), 2);
497 assert_eq!(df.ncols(), 3);
498 }
499
500 #[test]
501 fn test_dataframe_column() {
502 let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
503 .expect("shape and data length should match");
504 let columns = vec!["A".to_string(), "B".to_string()];
505 let df = SimpleDataFrame::new(data, columns).expect("matrix indexing should be valid");
506
507 let col_a = df.column("A").expect("matrix indexing should be valid");
508 assert_eq!(col_a.len(), 3);
509 assert_eq!(col_a[0], 1.0);
510 }
511
512 #[test]
513 fn test_sparse_matrix_creation() {
514 let row_indices = vec![0, 1, 2];
515 let col_indices = vec![0, 1, 2];
516 let values = vec![1.0, 2.0, 3.0];
517 let shape = (3, 3);
518
519 let sparse = SparseMatrix::new(row_indices, col_indices, values, shape)
520 .expect("parsing should succeed");
521
522 assert_eq!(sparse.nnz, 3);
523 assert_eq!(sparse.shape, (3, 3));
524 }
525
526 #[test]
527 fn test_sparse_to_dense() {
528 let row_indices = vec![0, 1, 1];
529 let col_indices = vec![0, 0, 1];
530 let values = vec![1.0, 2.0, 3.0];
531 let shape = (2, 2);
532
533 let sparse = SparseMatrix::new(row_indices, col_indices, values, shape)
534 .expect("parsing should succeed");
535 let dense = sparse.to_dense();
536
537 assert_eq!(dense[[0, 0]], 1.0);
538 assert_eq!(dense[[1, 0]], 2.0);
539 assert_eq!(dense[[1, 1]], 3.0);
540 assert_eq!(dense[[0, 1]], 0.0);
541 }
542
543 #[test]
544 fn test_sparse_from_dense() {
545 let dense = Array2::from_shape_vec((2, 2), vec![1.0, 0.0, 0.0, 2.0])
546 .expect("shape and data length should match");
547 let sparse = SparseMatrix::from_dense(&dense, 0.5);
548
549 assert_eq!(sparse.nnz, 2);
550 assert!(sparse.sparsity() > 0.0);
551 }
552
553 #[test]
554 fn test_batch_processor() {
555 let data = Array2::from_shape_vec((10, 3), (0..30).map(|x| x as Float).collect())
556 .expect("shape and data length should match");
557
558 let processor = BatchProcessor::new(4, 1);
559 let batches = processor.split(&data);
560
561 assert!(batches.len() >= 3);
562 assert_eq!(batches[0].nrows(), 4);
563 }
564
565 #[test]
566 fn test_data_converter_standardize() {
567 let mut rng = thread_rng();
568 let data = Array2::from_shape_fn((20, 5), |_| rng.gen_range(0.0..10.0));
569
570 let (standardized, mean, std) = DataConverter::standardize(&data);
571
572 assert_eq!(standardized.dim(), data.dim());
573 assert_eq!(mean.len(), 5);
574 assert_eq!(std.len(), 5);
575
576 let new_mean = standardized
578 .mean_axis(scirs2_core::ndarray::Axis(0))
579 .expect("operation should succeed");
580 for &m in new_mean.iter() {
581 assert!(m.abs() < 0.1); }
583 }
584
585 #[test]
586 fn test_data_converter_min_max() {
587 let data = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
588 .expect("shape and data length should match");
589
590 let (scaled, _min_vals, _max_vals) = DataConverter::min_max_scale(&data);
591
592 assert_eq!(scaled.dim(), data.dim());
593
594 let min_scaled =
596 scaled.fold_axis(scirs2_core::ndarray::Axis(0), Float::INFINITY, |&a, &b| {
597 a.min(b)
598 });
599 let max_scaled = scaled.fold_axis(
600 scirs2_core::ndarray::Axis(0),
601 Float::NEG_INFINITY,
602 |&a, &b| a.max(b),
603 );
604
605 for &val in min_scaled.iter() {
606 assert!(val >= -0.01); }
608 for &val in max_scaled.iter() {
609 assert!(val <= 1.01); }
611 }
612
613 #[test]
614 fn test_data_converter_to_nonnegative() {
615 let data = Array2::from_shape_vec((2, 2), vec![-1.0, 2.0, -3.0, 4.0])
616 .expect("shape and data length should match");
617 let nonneg = DataConverter::to_nonnegative(&data);
618
619 for &val in nonneg.iter() {
620 assert!(val >= 0.0);
621 }
622 }
623
624 #[test]
628 fn test_memory_mapped_array_dtype_valid() {
629 use std::mem::size_of;
630 let native_dtype = if size_of::<Float>() == 4 {
631 "float32"
632 } else {
633 "float64"
634 };
635 let mma = MemoryMappedArray::new(
636 std::env::temp_dir().join("test.npy").display().to_string(),
637 (10, 10),
638 native_dtype.to_string(),
639 );
640 assert!(
641 mma.validate_dtype().is_ok(),
642 "validate_dtype should succeed for matching dtype"
643 );
644 }
645
646 #[test]
648 fn test_memory_mapped_array_dtype_mismatch() {
649 use std::mem::size_of;
650 let wrong_dtype = if size_of::<Float>() == 4 {
652 "float64"
653 } else {
654 "float32"
655 };
656 let mma = MemoryMappedArray::new(
657 std::env::temp_dir().join("test.npy").display().to_string(),
658 (10, 10),
659 wrong_dtype.to_string(),
660 );
661 let err = mma.validate_dtype();
662 assert!(
663 err.is_err(),
664 "validate_dtype should fail for mismatching dtype"
665 );
666 let msg = format!("{:?}", err);
667 assert!(
668 msg.contains(wrong_dtype),
669 "error should mention the stored dtype"
670 );
671 }
672
673 #[test]
675 fn test_memory_mapped_array_path_in_error() {
676 use std::mem::size_of;
677 let native_dtype = if size_of::<Float>() == 4 {
678 "float32"
679 } else {
680 "float64"
681 };
682 let nonexistent_path = std::env::temp_dir()
683 .join("does_not_exist_for_sklears_test_12345.npy")
684 .display()
685 .to_string();
686 let mma =
687 MemoryMappedArray::new(nonexistent_path.clone(), (5, 5), native_dtype.to_string());
688 let err = mma.to_array();
689 assert!(err.is_err(), "to_array should fail for missing file");
690 let msg = format!("{:?}", err);
691 assert!(
692 msg.contains(nonexistent_path.as_str()),
693 "error message should include the file path"
694 );
695 }
696
697 #[test]
699 fn test_memory_mapped_array_accessors() {
700 let path = std::env::temp_dir().join("data.bin").display().to_string();
701 let dtype = "float64".to_string();
702 let mma = MemoryMappedArray::new(path.clone(), (3, 4), dtype.clone());
703 assert_eq!(mma.path(), path);
704 assert_eq!(mma.dtype(), dtype);
705 assert_eq!(mma.shape(), (3, 4));
706 }
707}