1use crate::{Float, SklResult};
4use rayon::prelude::*;
5use scirs2_core::ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, Axis};
6use sklears_core::prelude::{Fit, SklearsError, Transform};
7use sklears_core::traits::{Estimator, Untrained};
8
9pub use crate::basic_features::*;
11
12pub use crate::rbf_methods::*;
14
15pub use crate::temporal_features::*;
17
18pub use crate::statistical_features::*;
20
21pub use crate::sketching_sampling::*;
23
24pub use crate::time_series_features::{
30 TimeSeriesWaveletExtractor, TimeSeriesWaveletType, WaveletFeatureType,
31};
32
33pub use crate::time_series_features::TimeFrequencyExtractor;
35
36pub use crate::topological_features::{
42 MapperExtractor, PersistentHomologyExtractor, SimplicialComplexExtractor,
43};
44
45#[allow(non_snake_case)]
46#[cfg(test)]
47mod simplicial_complex_tests {
48 use super::*;
49 use scirs2_core::ndarray::Array2;
50
51 #[test]
52 fn test_simplicial_complex_extractor_basic() {
53 let data = Array2::from_shape_vec(
55 (3, 2),
56 vec![
57 0.0, 0.0, 1.0, 0.0, 0.5, 0.8, ],
61 )
62 .expect("operation should succeed");
63
64 let extractor = SimplicialComplexExtractor::new()
65 .max_dimension(2)
66 .distance_threshold(1.5)
67 .include_face_counts(true)
68 .include_euler_characteristic(true);
69
70 let features = extractor
71 .extract_features(&data.view())
72 .expect("operation should succeed");
73
74 assert_eq!(features.len(), 4);
76
77 for &feature in &features {
79 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
80 }
81
82 assert_eq!(features[0], 3.0);
84
85 assert_eq!(features[1], 3.0);
87
88 assert_eq!(features[2], 1.0);
90
91 assert_eq!(features[3], 1.0);
93 }
94
95 #[test]
96 fn test_simplicial_complex_extractor_depth_measures() {
97 let data = Array2::from_shape_vec(
98 (4, 2),
99 vec![
100 0.0, 0.0, 1.0, 0.0, 0.5, 0.8, 0.5, 0.2, ],
105 )
106 .expect("operation should succeed");
107
108 let extractor = SimplicialComplexExtractor::new()
109 .max_dimension(2)
110 .distance_threshold(1.0)
111 .include_face_counts(false)
112 .include_euler_characteristic(false)
113 .include_depth_measures(true);
114
115 let features = extractor
116 .extract_features(&data.view())
117 .expect("operation should succeed");
118
119 assert_eq!(features.len(), 5);
121
122 for &feature in &features {
124 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
125 }
126
127 assert!(features[0] <= 2.0);
129
130 assert!(features[1] > 0.0);
132 assert!(features[2] > 0.0);
133 assert!(features[3] > 0.0);
134
135 assert!(features[4] >= 0.0 && features[4] <= 1.0);
137 }
138
139 #[test]
140 fn test_simplicial_complex_extractor_boundary_properties() {
141 let data = Array2::from_shape_vec(
142 (4, 2),
143 vec![
144 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, ],
149 )
150 .expect("operation should succeed");
151
152 let extractor = SimplicialComplexExtractor::new()
153 .max_dimension(2)
154 .distance_threshold(1.5)
155 .include_face_counts(false)
156 .include_euler_characteristic(false)
157 .include_boundary_properties(true);
158
159 let features = extractor
160 .extract_features(&data.view())
161 .expect("operation should succeed");
162
163 assert_eq!(features.len(), 6);
166
167 for &feature in &features {
169 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
170 }
171
172 for &feature in &features {
174 assert!(
175 feature >= 0.0,
176 "Feature should be non-negative: {}",
177 feature
178 );
179 }
180 }
181
182 #[test]
183 fn test_simplicial_complex_extractor_normalization() {
184 let data = Array2::from_shape_vec(
185 (5, 2),
186 vec![
187 0.0, 0.0, 1.0, 0.0, 0.5, 0.8, 0.0, 1.0, 1.0, 1.0, ],
193 )
194 .expect("operation should succeed");
195
196 let extractor = SimplicialComplexExtractor::new()
197 .max_dimension(2)
198 .distance_threshold(1.5)
199 .include_face_counts(true)
200 .include_euler_characteristic(false)
201 .normalize_by_vertices(true);
202
203 let features = extractor
204 .extract_features(&data.view())
205 .expect("operation should succeed");
206
207 assert_eq!(features.len(), 3);
209
210 for &feature in &features {
212 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
213 }
214
215 assert_eq!(features[0], 1.0);
217
218 for i in 1..features.len() {
220 assert!(features[i] >= 0.0 && features[i] <= 10.0);
221 }
222 }
223
224 #[test]
225 fn test_simplicial_complex_extractor_manhattan_distance() {
226 let data = Array2::from_shape_vec(
227 (3, 2),
228 vec![
229 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, ],
233 )
234 .expect("operation should succeed");
235
236 let extractor = SimplicialComplexExtractor::new()
237 .max_dimension(2)
238 .distance_threshold(1.5)
239 .distance_metric("manhattan")
240 .include_face_counts(true)
241 .include_euler_characteristic(true);
242
243 let features = extractor
244 .extract_features(&data.view())
245 .expect("operation should succeed");
246
247 assert_eq!(features.len(), 4);
249
250 for &feature in &features {
252 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
253 }
254
255 assert_eq!(features[0], 3.0); assert!(features[1] > 0.0); assert!(features[3].is_finite()); }
260
261 #[test]
262 fn test_simplicial_complex_extractor_empty_data() {
263 let data = Array2::zeros((0, 2));
264 let extractor = SimplicialComplexExtractor::new();
265
266 let result = extractor.extract_features(&data.view());
267 assert!(result.is_err(), "Should return error for empty data");
268 }
269
270 #[test]
271 fn test_simplicial_complex_extractor_no_features_selected() {
272 let data = Array2::from_shape_vec((3, 2), vec![0.0, 0.0, 1.0, 0.0, 0.5, 0.8])
273 .expect("operation should succeed");
274
275 let extractor = SimplicialComplexExtractor::new()
276 .include_face_counts(false)
277 .include_euler_characteristic(false)
278 .include_boundary_properties(false)
279 .include_depth_measures(false);
280
281 let result = extractor.extract_features(&data.view());
282 assert!(
283 result.is_err(),
284 "Should return error when no features are selected"
285 );
286 }
287
288 #[test]
289 fn test_simplicial_complex_extractor_single_point() {
290 let data =
291 Array2::from_shape_vec((1, 2), vec![0.0, 0.0]).expect("operation should succeed");
292
293 let extractor = SimplicialComplexExtractor::new()
294 .max_dimension(2)
295 .distance_threshold(1.0)
296 .include_face_counts(true)
297 .include_euler_characteristic(true);
298
299 let features = extractor
300 .extract_features(&data.view())
301 .expect("operation should succeed");
302
303 assert_eq!(features.len(), 4);
305
306 assert_eq!(features[0], 1.0);
308 assert_eq!(features[1], 0.0);
309 assert_eq!(features[2], 0.0);
310
311 assert_eq!(features[3], 1.0);
313 }
314
315 #[test]
316 fn test_simplicial_complex_extractor_invalid_distance_metric() {
317 let data = Array2::from_shape_vec((3, 2), vec![0.0, 0.0, 1.0, 0.0, 0.5, 0.8])
318 .expect("operation should succeed");
319
320 let extractor = SimplicialComplexExtractor::new().distance_metric("invalid");
321
322 let result = extractor.extract_features(&data.view());
323 assert!(
324 result.is_err(),
325 "Should return error for invalid distance metric"
326 );
327 }
328}
329
330#[allow(non_snake_case)]
331#[cfg(test)]
332mod parallel_feature_tests {
333 use super::*;
334 use scirs2_core::ndarray::Array2;
335
336 #[test]
337 fn test_parallel_feature_extractor_basic() {
338 let data = Array2::from_shape_vec((100, 5), (0..500).map(|x| x as f64).collect())
339 .expect("operation should succeed");
340
341 let extractor = ParallelFeatureExtractor::new()
342 .chunk_size(20)
343 .include_statistical_features(true)
344 .include_distance_matrix(false)
345 .include_correlation_features(false);
346
347 let features = extractor
348 .extract_features(&data.view())
349 .expect("operation should succeed");
350
351 assert_eq!(features.len(), 9);
353
354 for &feature in &features {
356 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
357 }
358 }
359
360 #[test]
361 fn test_parallel_feature_extractor_distance_matrix() {
362 let data = Array2::from_shape_vec((50, 3), (0..150).map(|x| (x as f64) * 0.1).collect())
363 .expect("operation should succeed");
364
365 let extractor = ParallelFeatureExtractor::new()
366 .chunk_size(10)
367 .include_statistical_features(false)
368 .include_distance_matrix(true)
369 .distance_metric("euclidean");
370
371 let features = extractor
372 .extract_features(&data.view())
373 .expect("operation should succeed");
374
375 assert_eq!(features.len(), 4);
377
378 for &feature in &features {
380 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
381 assert!(
382 feature >= 0.0,
383 "Distance feature should be non-negative: {}",
384 feature
385 );
386 }
387
388 assert!(features[0] > 0.0, "Mean distance should be positive");
390 }
391
392 #[test]
393 fn test_parallel_feature_extractor_correlation() {
394 let data = Array2::from_shape_vec((80, 4), (0..320).map(|x| (x as f64) * 0.01).collect())
395 .expect("operation should succeed");
396
397 let extractor = ParallelFeatureExtractor::new()
398 .chunk_size(15)
399 .include_statistical_features(false)
400 .include_distance_matrix(false)
401 .include_correlation_features(true);
402
403 let features = extractor
404 .extract_features(&data.view())
405 .expect("operation should succeed");
406
407 assert_eq!(features.len(), 3);
409
410 for &feature in &features {
412 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
413 }
414
415 assert!(
417 features[2] >= 0.0 && features[2] <= 1.0,
418 "Max absolute correlation should be in [0,1]: {}",
419 features[2]
420 );
421 }
422
423 #[test]
424 fn test_parallel_feature_extractor_all_features() {
425 let data = Array2::from_shape_vec((60, 3), (0..180).map(|x| (x as f64) * 0.05).collect())
426 .expect("operation should succeed");
427
428 let extractor = ParallelFeatureExtractor::new()
429 .chunk_size(12)
430 .include_statistical_features(true)
431 .include_distance_matrix(true)
432 .include_correlation_features(true);
433
434 let features = extractor
435 .extract_features(&data.view())
436 .expect("operation should succeed");
437
438 assert_eq!(features.len(), 16);
440
441 for &feature in &features {
443 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
444 }
445 }
446
447 #[test]
448 fn test_parallel_feature_extractor_different_metrics() {
449 let data = Array2::from_shape_vec((30, 2), (0..60).map(|x| x as f64).collect())
450 .expect("operation should succeed");
451
452 let extractor_manhattan = ParallelFeatureExtractor::new()
454 .include_statistical_features(false)
455 .include_distance_matrix(true)
456 .distance_metric("manhattan");
457
458 let features_manhattan = extractor_manhattan
459 .extract_features(&data.view())
460 .expect("operation should succeed");
461 assert_eq!(features_manhattan.len(), 4);
462
463 let extractor_cosine = ParallelFeatureExtractor::new()
465 .include_statistical_features(false)
466 .include_distance_matrix(true)
467 .distance_metric("cosine");
468
469 let features_cosine = extractor_cosine
470 .extract_features(&data.view())
471 .expect("operation should succeed");
472 assert_eq!(features_cosine.len(), 4);
473
474 assert_ne!(features_manhattan[0], features_cosine[0]);
476 }
477
478 #[test]
479 fn test_parallel_feature_extractor_custom_threads() {
480 let data = Array2::from_shape_vec((40, 3), (0..120).map(|x| x as f64).collect())
481 .expect("operation should succeed");
482
483 let extractor = ParallelFeatureExtractor::new()
484 .chunk_size(8)
485 .num_threads(2)
486 .include_statistical_features(true);
487
488 let features = extractor
489 .extract_features(&data.view())
490 .expect("operation should succeed");
491
492 assert_eq!(features.len(), 9);
494
495 for &feature in &features {
497 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
498 }
499 }
500
501 #[test]
502 fn test_parallel_feature_extractor_small_data() {
503 let data = Array2::from_shape_vec(
504 (5, 2),
505 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],
506 )
507 .expect("operation should succeed");
508
509 let extractor = ParallelFeatureExtractor::new()
510 .chunk_size(2)
511 .include_statistical_features(true)
512 .include_distance_matrix(true)
513 .include_correlation_features(true);
514
515 let features = extractor
516 .extract_features(&data.view())
517 .expect("operation should succeed");
518
519 assert_eq!(features.len(), 16);
521
522 for &feature in &features {
524 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
525 }
526 }
527
528 #[test]
529 fn test_parallel_feature_extractor_single_sample() {
530 let data =
531 Array2::from_shape_vec((1, 3), vec![1.0, 2.0, 3.0]).expect("operation should succeed");
532
533 let extractor = ParallelFeatureExtractor::new()
534 .include_statistical_features(true)
535 .include_distance_matrix(true);
536
537 let features = extractor
538 .extract_features(&data.view())
539 .expect("operation should succeed");
540
541 assert_eq!(features.len(), 13); for i in 9..13 {
546 assert_eq!(
547 features[i], 0.0,
548 "Distance feature should be zero for single sample"
549 );
550 }
551 }
552
553 #[test]
554 fn test_parallel_feature_extractor_empty_data() {
555 let data = Array2::zeros((0, 3));
556 let extractor = ParallelFeatureExtractor::new();
557
558 let result = extractor.extract_features(&data.view());
559 assert!(result.is_err(), "Should return error for empty data");
560 }
561
562 #[test]
563 fn test_parallel_feature_extractor_no_features_selected() {
564 let data = Array2::from_shape_vec((10, 2), (0..20).map(|x| x as f64).collect())
565 .expect("operation should succeed");
566
567 let extractor = ParallelFeatureExtractor::new()
568 .include_statistical_features(false)
569 .include_distance_matrix(false)
570 .include_correlation_features(false);
571
572 let result = extractor.extract_features(&data.view());
573 assert!(
574 result.is_err(),
575 "Should return error when no features are selected"
576 );
577 }
578
579 #[test]
580 fn test_parallel_feature_extractor_invalid_distance_metric() {
581 let data = Array2::from_shape_vec((10, 2), (0..20).map(|x| x as f64).collect())
582 .expect("operation should succeed");
583
584 let extractor = ParallelFeatureExtractor::new()
585 .include_statistical_features(false)
586 .include_distance_matrix(true)
587 .include_correlation_features(false)
588 .distance_metric("invalid");
589
590 let result = extractor.extract_features(&data.view());
591 assert!(
592 result.is_err(),
593 "Should return error for invalid distance metric"
594 );
595 }
596
597 #[test]
598 fn test_parallel_feature_extractor_large_chunk_size() {
599 let data = Array2::from_shape_vec((20, 3), (0..60).map(|x| x as f64).collect())
600 .expect("operation should succeed");
601
602 let extractor = ParallelFeatureExtractor::new()
603 .chunk_size(100) .include_statistical_features(true);
605
606 let features = extractor
607 .extract_features(&data.view())
608 .expect("operation should succeed");
609
610 assert_eq!(features.len(), 9);
612
613 for &feature in &features {
615 assert!(feature.is_finite(), "Feature should be finite: {}", feature);
616 }
617 }
618}
619
620#[derive(Debug, Clone)]
660pub struct ParallelFeatureExtractor {
661 chunk_size: usize,
662 num_threads: Option<usize>,
663 include_distance_matrix: bool,
664 include_statistical_features: bool,
665 include_correlation_features: bool,
666 distance_metric: String,
667}
668
669impl ParallelFeatureExtractor {
670 pub fn new() -> Self {
672 Self {
673 chunk_size: 1000,
674 num_threads: None,
675 include_distance_matrix: false,
676 include_statistical_features: true,
677 include_correlation_features: false,
678 distance_metric: "euclidean".to_string(),
679 }
680 }
681
682 pub fn chunk_size(mut self, size: usize) -> Self {
684 self.chunk_size = size.max(1);
685 self
686 }
687
688 pub fn num_threads(mut self, threads: usize) -> Self {
690 self.num_threads = Some(threads.max(1));
691 self
692 }
693
694 pub fn include_distance_matrix(mut self, include: bool) -> Self {
696 self.include_distance_matrix = include;
697 self
698 }
699
700 pub fn include_statistical_features(mut self, include: bool) -> Self {
702 self.include_statistical_features = include;
703 self
704 }
705
706 pub fn include_correlation_features(mut self, include: bool) -> Self {
708 self.include_correlation_features = include;
709 self
710 }
711
712 pub fn distance_metric(mut self, metric: &str) -> Self {
714 self.distance_metric = metric.to_string();
715 self
716 }
717
718 pub fn extract_features(&self, data: &ArrayView2<Float>) -> SklResult<Array1<Float>> {
720 if data.nrows() == 0 || data.ncols() == 0 {
721 return Err(SklearsError::InvalidInput(
722 "Input data cannot be empty".to_string(),
723 ));
724 }
725
726 let result = if let Some(threads) = self.num_threads {
728 rayon::ThreadPoolBuilder::new()
729 .num_threads(threads)
730 .build()
731 .map_err(|e| {
732 SklearsError::InvalidInput(format!("Failed to create thread pool: {}", e))
733 })?
734 .install(|| self.extract_features_impl(data))
735 } else {
736 self.extract_features_impl(data)
737 };
738
739 result
740 }
741
742 fn extract_features_impl(&self, data: &ArrayView2<Float>) -> SklResult<Array1<Float>> {
744 let mut features = Vec::new();
745
746 if self.include_statistical_features {
747 features.extend(self.compute_parallel_statistical_features(data)?);
748 }
749
750 if self.include_distance_matrix {
751 features.extend(self.compute_parallel_distance_matrix_features(data)?);
752 }
753
754 if self.include_correlation_features {
755 features.extend(self.compute_parallel_correlation_features(data)?);
756 }
757
758 if features.is_empty() {
759 return Err(SklearsError::InvalidInput(
760 "No features selected for extraction".to_string(),
761 ));
762 }
763
764 for &feature in &features {
766 if !feature.is_finite() {
767 return Err(SklearsError::InvalidInput(
768 "Non-finite feature detected".to_string(),
769 ));
770 }
771 }
772
773 Ok(Array1::from_vec(features))
774 }
775
776 fn compute_parallel_statistical_features(
778 &self,
779 data: &ArrayView2<Float>,
780 ) -> SklResult<Vec<Float>> {
781 let n_samples = data.nrows();
782 let n_features = data.ncols();
783
784 let column_stats: Vec<Vec<Float>> = (0..n_features)
786 .into_par_iter()
787 .map(|col_idx| {
788 let column = data.column(col_idx);
789 self.compute_column_statistics(&column)
790 })
791 .collect();
792
793 let mut features = Vec::new();
795
796 let column_means: Vec<Float> = column_stats.iter().map(|stats| stats[0]).collect();
798 features.push(column_means.iter().sum::<Float>() / n_features as Float);
799
800 let column_stds: Vec<Float> = column_stats.iter().map(|stats| stats[1]).collect();
802 features.push(column_stds.iter().sum::<Float>() / n_features as Float);
803
804 let column_mins: Vec<Float> = column_stats.iter().map(|stats| stats[2]).collect();
806 let column_maxs: Vec<Float> = column_stats.iter().map(|stats| stats[3]).collect();
807 features.push(column_mins.iter().sum::<Float>() / n_features as Float);
808 features.push(column_maxs.iter().sum::<Float>() / n_features as Float);
809
810 features.push(
812 *column_mins
813 .iter()
814 .min_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
815 .expect("operation should succeed"),
816 );
817 features.push(
818 *column_maxs
819 .iter()
820 .max_by(|a, b| a.partial_cmp(b).expect("operation should succeed"))
821 .expect("operation should succeed"),
822 );
823
824 let row_stats: Vec<Vec<Float>> = (0..n_samples)
826 .into_par_iter()
827 .step_by(self.chunk_size)
828 .map(|start_idx| {
829 let end_idx = (start_idx + self.chunk_size).min(n_samples);
830 let mut chunk_stats = Vec::new();
831
832 for row_idx in start_idx..end_idx {
833 let row = data.row(row_idx);
834 let row_mean = row.iter().sum::<Float>() / n_features as Float;
835 let row_var = row.iter().map(|&x| (x - row_mean).powi(2)).sum::<Float>()
836 / n_features as Float;
837 chunk_stats.push(row_mean);
838 chunk_stats.push(row_var.sqrt());
839 }
840
841 chunk_stats
842 })
843 .collect();
844
845 let all_row_stats: Vec<Float> = row_stats.into_iter().flatten().collect();
847 if !all_row_stats.is_empty() {
848 features.push(all_row_stats.iter().sum::<Float>() / all_row_stats.len() as Float);
849 features.push(all_row_stats.iter().cloned().fold(0.0, Float::max));
850 features.push(
851 all_row_stats
852 .iter()
853 .cloned()
854 .fold(Float::INFINITY, Float::min),
855 );
856 }
857
858 Ok(features)
859 }
860
861 fn compute_column_statistics(&self, column: &ArrayView1<Float>) -> Vec<Float> {
863 let n = column.len() as Float;
864 let mean = column.iter().sum::<Float>() / n;
865 let variance = column.iter().map(|&x| (x - mean).powi(2)).sum::<Float>() / n;
866 let std_dev = variance.sqrt();
867 let min_val = column.iter().cloned().fold(Float::INFINITY, Float::min);
868 let max_val = column.iter().cloned().fold(Float::NEG_INFINITY, Float::max);
869
870 vec![mean, std_dev, min_val, max_val]
871 }
872
873 fn compute_parallel_distance_matrix_features(
875 &self,
876 data: &ArrayView2<Float>,
877 ) -> SklResult<Vec<Float>> {
878 let n_samples = data.nrows();
879
880 if n_samples < 2 {
881 return Ok(vec![0.0, 0.0, 0.0, 0.0]); }
883
884 let distance_chunks: Result<Vec<Vec<Float>>, SklearsError> = (0..n_samples)
886 .into_par_iter()
887 .step_by(self.chunk_size)
888 .map(|start_i| -> SklResult<Vec<Float>> {
889 let end_i = (start_i + self.chunk_size).min(n_samples);
890 let mut chunk_distances = Vec::new();
891
892 for i in start_i..end_i {
893 for j in (i + 1)..n_samples {
894 let dist = self.compute_distance(&data.row(i), &data.row(j))?;
895 chunk_distances.push(dist);
896 }
897 }
898
899 Ok(chunk_distances)
900 })
901 .collect();
902
903 let distance_chunks = distance_chunks?;
904
905 let all_distances: Vec<Float> = distance_chunks.into_iter().flatten().collect();
907
908 if all_distances.is_empty() {
909 return Ok(vec![0.0, 0.0, 0.0, 0.0]);
910 }
911
912 let n_distances = all_distances.len() as Float;
913 let mean_distance = all_distances.iter().sum::<Float>() / n_distances;
914 let variance = all_distances
915 .iter()
916 .map(|&d| (d - mean_distance).powi(2))
917 .sum::<Float>()
918 / n_distances;
919 let std_distance = variance.sqrt();
920 let min_distance = all_distances
921 .iter()
922 .cloned()
923 .fold(Float::INFINITY, Float::min);
924 let max_distance = all_distances
925 .iter()
926 .cloned()
927 .fold(Float::NEG_INFINITY, Float::max);
928
929 Ok(vec![
930 mean_distance,
931 std_distance,
932 min_distance,
933 max_distance,
934 ])
935 }
936
937 fn compute_parallel_correlation_features(
939 &self,
940 data: &ArrayView2<Float>,
941 ) -> SklResult<Vec<Float>> {
942 let n_features = data.ncols();
943
944 if n_features < 2 {
945 return Ok(vec![0.0, 0.0, 0.0]); }
947
948 let column_means: Vec<Float> = (0..n_features)
950 .into_par_iter()
951 .map(|col_idx| {
952 let column = data.column(col_idx);
953 column.iter().sum::<Float>() / data.nrows() as Float
954 })
955 .collect();
956
957 let correlation_chunks: Vec<Vec<Float>> = (0..n_features)
959 .into_par_iter()
960 .step_by(self.chunk_size)
961 .map(|start_i| {
962 let end_i = (start_i + self.chunk_size).min(n_features);
963 let mut chunk_correlations = Vec::new();
964
965 for i in start_i..end_i {
966 for j in (i + 1)..n_features {
967 let corr = self.compute_correlation(
968 &data.column(i),
969 &data.column(j),
970 column_means[i],
971 column_means[j],
972 );
973 chunk_correlations.push(corr);
974 }
975 }
976
977 chunk_correlations
978 })
979 .collect();
980
981 let all_correlations: Vec<Float> = correlation_chunks.into_iter().flatten().collect();
983
984 if all_correlations.is_empty() {
985 return Ok(vec![0.0, 0.0, 0.0]);
986 }
987
988 let n_correlations = all_correlations.len() as Float;
989 let mean_correlation = all_correlations.iter().sum::<Float>() / n_correlations;
990 let variance = all_correlations
991 .iter()
992 .map(|&c| (c - mean_correlation).powi(2))
993 .sum::<Float>()
994 / n_correlations;
995 let std_correlation = variance.sqrt();
996 let max_abs_correlation = all_correlations
997 .iter()
998 .map(|&c| c.abs())
999 .fold(0.0, Float::max);
1000
1001 Ok(vec![mean_correlation, std_correlation, max_abs_correlation])
1002 }
1003
1004 fn compute_distance(&self, p1: &ArrayView1<Float>, p2: &ArrayView1<Float>) -> SklResult<Float> {
1006 match self.distance_metric.as_str() {
1007 "euclidean" => {
1008 let dist = p1
1009 .iter()
1010 .zip(p2.iter())
1011 .map(|(a, b)| (a - b).powi(2))
1012 .sum::<Float>()
1013 .sqrt();
1014 Ok(dist)
1015 }
1016 "manhattan" => {
1017 let dist = p1
1018 .iter()
1019 .zip(p2.iter())
1020 .map(|(a, b)| (a - b).abs())
1021 .sum::<Float>();
1022 Ok(dist)
1023 }
1024 "cosine" => {
1025 let dot_product = p1.iter().zip(p2.iter()).map(|(a, b)| a * b).sum::<Float>();
1026 let norm1 = p1.iter().map(|&a| a.powi(2)).sum::<Float>().sqrt();
1027 let norm2 = p2.iter().map(|&a| a.powi(2)).sum::<Float>().sqrt();
1028
1029 if norm1 == 0.0 || norm2 == 0.0 {
1030 Ok(1.0) } else {
1032 Ok(1.0 - dot_product / (norm1 * norm2))
1033 }
1034 }
1035 _ => Err(SklearsError::InvalidInput(format!(
1036 "Unsupported distance metric: {}",
1037 self.distance_metric
1038 ))),
1039 }
1040 }
1041
1042 fn compute_correlation(
1044 &self,
1045 col1: &ArrayView1<Float>,
1046 col2: &ArrayView1<Float>,
1047 mean1: Float,
1048 mean2: Float,
1049 ) -> Float {
1050 let n = col1.len() as Float;
1051
1052 let covariance = col1
1053 .iter()
1054 .zip(col2.iter())
1055 .map(|(&x, &y)| (x - mean1) * (y - mean2))
1056 .sum::<Float>()
1057 / n;
1058
1059 let var1 = col1.iter().map(|&x| (x - mean1).powi(2)).sum::<Float>() / n;
1060 let var2 = col2.iter().map(|&x| (x - mean2).powi(2)).sum::<Float>() / n;
1061
1062 let std1 = var1.sqrt();
1063 let std2 = var2.sqrt();
1064
1065 if std1 == 0.0 || std2 == 0.0 {
1066 0.0
1067 } else {
1068 covariance / (std1 * std2)
1069 }
1070 }
1071}
1072
1073impl Default for ParallelFeatureExtractor {
1074 fn default() -> Self {
1075 Self::new()
1076 }
1077}
1078
1079#[derive(Debug, Clone)]
1084pub struct MixedTypeFeatureExtractor {
1085 pub feature_types: Vec<FeatureType>,
1087 pub normalize_numerical: bool,
1089 pub handle_missing_values: bool,
1091 pub missing_value_strategy: MissingValueStrategy,
1093 pub encoding_strategy: CategoricalEncoding,
1095 pub include_interaction_features: bool,
1097 pub random_state: Option<u64>,
1099}
1100
1101#[derive(Debug, Clone, Copy, PartialEq)]
1103pub enum FeatureType {
1104 Numerical,
1106 Categorical,
1108 Binary,
1110 Ordinal,
1112 DateTime,
1114}
1115
1116#[derive(Debug, Clone, Copy)]
1118pub enum MissingValueStrategy {
1119 Mean,
1121 Median,
1123 Mode,
1125 Zero,
1127 ForwardFill,
1129 BackwardFill,
1131}
1132
1133#[derive(Debug, Clone, Copy)]
1135pub enum CategoricalEncoding {
1136 OneHot,
1138 LabelEncoding,
1140 TargetEncoding,
1142 BinaryEncoding,
1144 FrequencyEncoding,
1146}
1147
1148impl MixedTypeFeatureExtractor {
1149 pub fn new() -> Self {
1151 Self {
1152 feature_types: Vec::new(),
1153 normalize_numerical: true,
1154 handle_missing_values: true,
1155 missing_value_strategy: MissingValueStrategy::Mean,
1156 encoding_strategy: CategoricalEncoding::OneHot,
1157 include_interaction_features: false,
1158 random_state: None,
1159 }
1160 }
1161
1162 pub fn feature_types(mut self, types: Vec<FeatureType>) -> Self {
1164 self.feature_types = types;
1165 self
1166 }
1167
1168 pub fn normalize_numerical(mut self, normalize: bool) -> Self {
1170 self.normalize_numerical = normalize;
1171 self
1172 }
1173
1174 pub fn handle_missing_values(mut self, handle: bool) -> Self {
1176 self.handle_missing_values = handle;
1177 self
1178 }
1179
1180 pub fn missing_value_strategy(mut self, strategy: MissingValueStrategy) -> Self {
1182 self.missing_value_strategy = strategy;
1183 self
1184 }
1185
1186 pub fn encoding_strategy(mut self, strategy: CategoricalEncoding) -> Self {
1188 self.encoding_strategy = strategy;
1189 self
1190 }
1191
1192 pub fn include_interaction_features(mut self, include: bool) -> Self {
1194 self.include_interaction_features = include;
1195 self
1196 }
1197
1198 pub fn random_state(mut self, seed: u64) -> Self {
1200 self.random_state = Some(seed);
1201 self
1202 }
1203
1204 fn handle_missing(
1206 &self,
1207 data: &Array2<f64>,
1208 col_idx: usize,
1209 feature_type: FeatureType,
1210 ) -> Array1<f64> {
1211 let column = data.column(col_idx);
1212 let mut result = Array1::zeros(column.len());
1213
1214 let valid_values: Vec<f64> = column.iter().filter(|&&x| x.is_finite()).copied().collect();
1216
1217 if valid_values.is_empty() {
1218 return result; }
1220
1221 let replacement_value = match (self.missing_value_strategy, feature_type) {
1222 (MissingValueStrategy::Mean, FeatureType::Numerical) => {
1223 valid_values.iter().sum::<f64>() / valid_values.len() as f64
1224 }
1225 (MissingValueStrategy::Median, FeatureType::Numerical) => {
1226 let mut sorted = valid_values.clone();
1227 sorted.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
1228 let mid = sorted.len() / 2;
1229 if sorted.len().is_multiple_of(2) {
1230 (sorted[mid - 1] + sorted[mid]) / 2.0
1231 } else {
1232 sorted[mid]
1233 }
1234 }
1235 (MissingValueStrategy::Mode, _) => {
1236 let mut counts = std::collections::HashMap::new();
1238 for &val in &valid_values {
1239 *counts.entry(val as i64).or_insert(0) += 1;
1240 }
1241 counts
1242 .into_iter()
1243 .max_by_key(|(_, count)| *count)
1244 .map(|(val, _)| val as f64)
1245 .unwrap_or(0.0)
1246 }
1247 (MissingValueStrategy::Zero, _) => 0.0,
1248 (MissingValueStrategy::ForwardFill, _) => {
1249 valid_values[0]
1251 }
1252 (MissingValueStrategy::BackwardFill, _) => {
1253 valid_values[valid_values.len() - 1]
1255 }
1256 _ => valid_values.iter().sum::<f64>() / valid_values.len() as f64, };
1258
1259 for (i, &value) in column.iter().enumerate() {
1261 result[i] = if value.is_finite() {
1262 value
1263 } else {
1264 replacement_value
1265 };
1266 }
1267
1268 result
1269 }
1270
1271 fn normalize_column(&self, column: &Array1<f64>) -> Array1<f64> {
1273 let mean = column.mean().unwrap_or(0.0);
1274 let std = column.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / column.len() as f64;
1275 let std = std.sqrt();
1276
1277 if std == 0.0 {
1278 Array1::zeros(column.len())
1279 } else {
1280 column.mapv(|x| (x - mean) / std)
1281 }
1282 }
1283
1284 fn encode_categorical(&self, column: &Array1<f64>) -> Array2<f64> {
1286 match self.encoding_strategy {
1287 CategoricalEncoding::OneHot => {
1288 let mut unique_values: Vec<i64> = column
1290 .iter()
1291 .map(|&x| x as i64)
1292 .collect::<std::collections::HashSet<_>>()
1293 .into_iter()
1294 .collect();
1295 unique_values.sort();
1296
1297 let n_categories = unique_values.len();
1298 let mut encoded = Array2::zeros((column.len(), n_categories));
1299
1300 for (row_idx, &value) in column.iter().enumerate() {
1301 if let Some(cat_idx) = unique_values.iter().position(|&x| x == value as i64) {
1302 encoded[[row_idx, cat_idx]] = 1.0;
1303 }
1304 }
1305
1306 encoded
1307 }
1308 CategoricalEncoding::LabelEncoding => {
1309 let mut unique_values: Vec<i64> = column
1311 .iter()
1312 .map(|&x| x as i64)
1313 .collect::<std::collections::HashSet<_>>()
1314 .into_iter()
1315 .collect();
1316 unique_values.sort();
1317
1318 let mut encoded = Array2::zeros((column.len(), 1));
1319 for (row_idx, &value) in column.iter().enumerate() {
1320 if let Some(label) = unique_values.iter().position(|&x| x == value as i64) {
1321 encoded[[row_idx, 0]] = label as f64;
1322 }
1323 }
1324
1325 encoded
1326 }
1327 CategoricalEncoding::TargetEncoding => {
1328 let mut encoded = Array2::zeros((column.len(), 1));
1330 for (row_idx, &value) in column.iter().enumerate() {
1331 encoded[[row_idx, 0]] = value;
1332 }
1333 encoded
1334 }
1335 _ => {
1336 let mut encoded = Array2::zeros((column.len(), 1));
1338 for (row_idx, &value) in column.iter().enumerate() {
1339 encoded[[row_idx, 0]] = value;
1340 }
1341 encoded
1342 }
1343 }
1344 }
1345
1346 fn process_feature(
1348 &self,
1349 data: &Array2<f64>,
1350 col_idx: usize,
1351 feature_type: FeatureType,
1352 ) -> Array2<f64> {
1353 let column = if self.handle_missing_values {
1355 self.handle_missing(data, col_idx, feature_type)
1356 } else {
1357 data.column(col_idx).to_owned()
1358 };
1359
1360 match feature_type {
1361 FeatureType::Numerical => {
1362 let processed = if self.normalize_numerical {
1363 self.normalize_column(&column)
1364 } else {
1365 column
1366 };
1367 processed.insert_axis(Axis(1))
1368 }
1369 FeatureType::Categorical => self.encode_categorical(&column),
1370 FeatureType::Binary => {
1371 let binary = column.mapv(|x| if x > 0.5 { 1.0 } else { 0.0 });
1373 binary.insert_axis(Axis(1))
1374 }
1375 FeatureType::Ordinal => {
1376 let processed = if self.normalize_numerical {
1378 self.normalize_column(&column)
1379 } else {
1380 column
1381 };
1382 processed.insert_axis(Axis(1))
1383 }
1384 FeatureType::DateTime => {
1385 let processed = if self.normalize_numerical {
1387 self.normalize_column(&column)
1388 } else {
1389 column
1390 };
1391 processed.insert_axis(Axis(1))
1392 }
1393 }
1394 }
1395
1396 fn transform_data(&self, data: &Array2<f64>) -> SklResult<Array2<f64>> {
1398 if data.is_empty() {
1399 return Err(SklearsError::InvalidInput("Empty input data".to_string()));
1400 }
1401
1402 if self.feature_types.len() != data.ncols() {
1403 return Err(SklearsError::InvalidInput(format!(
1404 "Number of feature types ({}) doesn't match number of columns ({})",
1405 self.feature_types.len(),
1406 data.ncols()
1407 )));
1408 }
1409
1410 let mut processed_features = Vec::new();
1411
1412 for (col_idx, &feature_type) in self.feature_types.iter().enumerate() {
1414 let processed = self.process_feature(data, col_idx, feature_type);
1415 processed_features.push(processed);
1416 }
1417
1418 if processed_features.is_empty() {
1420 return Ok(Array2::zeros((data.nrows(), 0)));
1421 }
1422
1423 let total_cols: usize = processed_features.iter().map(|f| f.ncols()).sum();
1424 let mut result = Array2::zeros((data.nrows(), total_cols));
1425
1426 let mut col_offset = 0;
1427 for feature_matrix in processed_features {
1428 let n_cols = feature_matrix.ncols();
1429 result
1430 .slice_mut(s![.., col_offset..col_offset + n_cols])
1431 .assign(&feature_matrix);
1432 col_offset += n_cols;
1433 }
1434
1435 Ok(result)
1436 }
1437}
1438
1439pub struct FittedMixedTypeFeatureExtractor {
1441 extractor: MixedTypeFeatureExtractor,
1442 transformation_stats: std::collections::HashMap<String, f64>,
1443}
1444
1445impl FittedMixedTypeFeatureExtractor {
1446 pub fn transformation_statistics(&self) -> &std::collections::HashMap<String, f64> {
1448 &self.transformation_stats
1449 }
1450}
1451
1452impl Estimator<Untrained> for MixedTypeFeatureExtractor {
1453 type Config = ();
1454 type Error = SklearsError;
1455 type Float = Float;
1456
1457 fn config(&self) -> &Self::Config {
1458 &()
1459 }
1460}
1461
1462#[allow(non_snake_case)] impl Fit<Array2<f64>, ()> for MixedTypeFeatureExtractor {
1464 type Fitted = FittedMixedTypeFeatureExtractor;
1465
1466 fn fit(self, X: &Array2<f64>, _y: &()) -> SklResult<Self::Fitted> {
1467 if X.is_empty() {
1468 return Err(SklearsError::InvalidInput("Empty input data".to_string()));
1469 }
1470
1471 let feature_types = if self.feature_types.is_empty() {
1473 vec![FeatureType::Numerical; X.ncols()]
1474 } else {
1475 self.feature_types.clone()
1476 };
1477
1478 let mut stats = std::collections::HashMap::new();
1480
1481 let n_numerical = feature_types
1482 .iter()
1483 .filter(|&&t| t == FeatureType::Numerical)
1484 .count();
1485 let n_categorical = feature_types
1486 .iter()
1487 .filter(|&&t| t == FeatureType::Categorical)
1488 .count();
1489 let n_binary = feature_types
1490 .iter()
1491 .filter(|&&t| t == FeatureType::Binary)
1492 .count();
1493 let n_ordinal = feature_types
1494 .iter()
1495 .filter(|&&t| t == FeatureType::Ordinal)
1496 .count();
1497
1498 stats.insert("n_numerical_features".to_string(), n_numerical as f64);
1499 stats.insert("n_categorical_features".to_string(), n_categorical as f64);
1500 stats.insert("n_binary_features".to_string(), n_binary as f64);
1501 stats.insert("n_ordinal_features".to_string(), n_ordinal as f64);
1502 stats.insert("total_input_features".to_string(), X.ncols() as f64);
1503
1504 let estimated_output_features = match self.encoding_strategy {
1506 CategoricalEncoding::OneHot => {
1507 let categorical_expansion = n_categorical * 3; n_numerical + n_binary + n_ordinal + categorical_expansion
1510 }
1511 _ => feature_types.len(),
1512 };
1513
1514 stats.insert(
1515 "total_output_features".to_string(),
1516 estimated_output_features as f64,
1517 );
1518
1519 let updated_extractor = MixedTypeFeatureExtractor {
1520 feature_types,
1521 ..self
1522 };
1523
1524 Ok(FittedMixedTypeFeatureExtractor {
1525 extractor: updated_extractor,
1526 transformation_stats: stats,
1527 })
1528 }
1529}
1530
1531#[allow(non_snake_case)] impl Transform<Array2<f64>, Array2<f64>> for FittedMixedTypeFeatureExtractor {
1533 fn transform(&self, X: &Array2<f64>) -> SklResult<Array2<f64>> {
1534 self.extractor.transform_data(X)
1535 }
1536}
1537
1538impl Default for MixedTypeFeatureExtractor {
1539 fn default() -> Self {
1540 Self::new()
1541 }
1542}
1543
1544#[allow(non_snake_case)]
1545#[cfg(test)]
1546mod sketching_tests {
1547 use super::*;
1548 use scirs2_core::ndarray::Array2;
1549
1550 #[test]
1551 fn test_mixed_type_feature_extractor_basic() {
1552 let data = Array2::from_shape_vec(
1553 (4, 6),
1554 vec![
1555 1.0, 2.0, 3.0, 1.0, 0.0,
1556 5.0, 4.0, 5.0, 6.0, 2.0, 1.0,
1558 3.0, 7.0, 8.0, 9.0, 1.0, 0.0,
1560 4.0, 2.0, 3.0, 4.0, 3.0, 1.0,
1562 2.0, ],
1564 )
1565 .expect("operation should succeed");
1566
1567 let feature_types = vec![
1568 FeatureType::Numerical, FeatureType::Numerical, FeatureType::Numerical, FeatureType::Categorical, FeatureType::Binary, FeatureType::Ordinal, ];
1575
1576 let extractor = MixedTypeFeatureExtractor::new()
1577 .feature_types(feature_types)
1578 .normalize_numerical(true)
1579 .encoding_strategy(CategoricalEncoding::OneHot);
1580
1581 let fitted = extractor.fit(&data, &()).expect("operation should succeed");
1582 let features = fitted.transform(&data).expect("operation should succeed");
1583
1584 assert_eq!(features.nrows(), 4);
1585 assert!(features.ncols() > 6); for &value in features.iter() {
1589 assert!(value.is_finite());
1590 }
1591 }
1592
1593 #[test]
1594 fn test_mixed_type_feature_extractor_different_encodings() {
1595 let data = Array2::from_shape_vec(
1596 (3, 4),
1597 vec![1.0, 1.0, 0.0, 5.0, 2.0, 2.0, 1.0, 3.0, 3.0, 1.0, 0.0, 4.0],
1598 )
1599 .expect("operation should succeed");
1600
1601 let feature_types = vec![
1602 FeatureType::Numerical,
1603 FeatureType::Categorical,
1604 FeatureType::Binary,
1605 FeatureType::Ordinal,
1606 ];
1607
1608 let encodings = vec![
1609 CategoricalEncoding::OneHot,
1610 CategoricalEncoding::LabelEncoding,
1611 CategoricalEncoding::TargetEncoding,
1612 ];
1613
1614 for encoding in encodings {
1615 let extractor = MixedTypeFeatureExtractor::new()
1616 .feature_types(feature_types.clone())
1617 .encoding_strategy(encoding);
1618
1619 let fitted = extractor.fit(&data, &()).expect("operation should succeed");
1620 let features = fitted.transform(&data).expect("operation should succeed");
1621
1622 assert_eq!(features.nrows(), 3);
1623 assert!(features.ncols() > 0);
1624
1625 for &value in features.iter() {
1627 assert!(value.is_finite());
1628 }
1629 }
1630 }
1631
1632 #[test]
1633 fn test_mixed_type_feature_extractor_empty_data() {
1634 let data = Array2::zeros((0, 0));
1635 let extractor = MixedTypeFeatureExtractor::new();
1636
1637 let result = extractor.fit(&data, &());
1638 assert!(result.is_err());
1639 }
1640
1641 #[test]
1642 fn test_mixed_type_feature_extractor_configuration() {
1643 let data =
1644 Array2::from_shape_vec((3, 3), vec![1.0, 1.0, 0.0, 2.0, 2.0, 1.0, 3.0, 1.0, 0.0])
1645 .expect("operation should succeed");
1646
1647 let feature_types = vec![
1648 FeatureType::Numerical,
1649 FeatureType::Categorical,
1650 FeatureType::Binary,
1651 ];
1652
1653 let extractor = MixedTypeFeatureExtractor::new()
1654 .feature_types(feature_types)
1655 .normalize_numerical(true)
1656 .handle_missing_values(true)
1657 .missing_value_strategy(MissingValueStrategy::Mean)
1658 .encoding_strategy(CategoricalEncoding::OneHot)
1659 .include_interaction_features(true)
1660 .random_state(42);
1661
1662 let fitted = extractor.fit(&data, &()).expect("operation should succeed");
1663 let features = fitted.transform(&data).expect("operation should succeed");
1664
1665 assert_eq!(features.nrows(), 3);
1666
1667 let stats = fitted.transformation_statistics();
1669 assert!(stats.contains_key("n_numerical_features"));
1670 assert!(stats.contains_key("n_categorical_features"));
1671 assert!(stats.contains_key("n_binary_features"));
1672 assert!(stats.contains_key("total_output_features"));
1673 }
1674
1675 #[test]
1676 fn test_mixed_type_feature_extractor_missing_values() {
1677 let data = Array2::from_shape_vec(
1678 (4, 3),
1679 vec![
1680 1.0,
1681 1.0,
1682 f64::NAN,
1683 2.0,
1684 f64::NAN,
1685 1.0,
1686 f64::NAN,
1687 2.0,
1688 0.0,
1689 3.0,
1690 1.0,
1691 1.0,
1692 ],
1693 )
1694 .expect("operation should succeed");
1695
1696 let feature_types = vec![
1697 FeatureType::Numerical,
1698 FeatureType::Categorical,
1699 FeatureType::Binary,
1700 ];
1701
1702 let strategies = vec![
1703 MissingValueStrategy::Mean,
1704 MissingValueStrategy::Median,
1705 MissingValueStrategy::Mode,
1706 MissingValueStrategy::Zero,
1707 ];
1708
1709 for strategy in strategies {
1710 let extractor = MixedTypeFeatureExtractor::new()
1711 .feature_types(feature_types.clone())
1712 .handle_missing_values(true)
1713 .missing_value_strategy(strategy);
1714
1715 let fitted = extractor.fit(&data, &()).expect("operation should succeed");
1716 let features = fitted.transform(&data).expect("operation should succeed");
1717
1718 assert_eq!(features.nrows(), 4);
1719
1720 for &value in features.iter() {
1722 assert!(
1723 value.is_finite(),
1724 "All values should be finite after missing value handling"
1725 );
1726 }
1727 }
1728 }
1729
1730 #[test]
1731 fn test_mixed_type_feature_extractor_consistency() {
1732 let data = Array2::from_shape_vec(
1733 (3, 4),
1734 vec![1.0, 1.0, 0.0, 5.0, 2.0, 2.0, 1.0, 3.0, 3.0, 1.0, 0.0, 4.0],
1735 )
1736 .expect("operation should succeed");
1737
1738 let feature_types = vec![
1739 FeatureType::Numerical,
1740 FeatureType::Categorical,
1741 FeatureType::Binary,
1742 FeatureType::Ordinal,
1743 ];
1744
1745 let extractor = MixedTypeFeatureExtractor::new()
1746 .feature_types(feature_types)
1747 .random_state(42);
1748
1749 let fitted = extractor.fit(&data, &()).expect("operation should succeed");
1750 let features1 = fitted.transform(&data).expect("operation should succeed");
1751 let features2 = fitted.transform(&data).expect("operation should succeed");
1752
1753 assert_eq!(features1.shape(), features2.shape());
1755 for (a, b) in features1.iter().zip(features2.iter()) {
1756 assert_eq!(a, b);
1757 }
1758 }
1759}
1760
1761pub use crate::time_series_features::TemporalSpatialExtractor;