Skip to main content

sklears_feature_extraction/
engineering.rs

1//! Feature engineering utilities
2
3use 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
9// Re-export basic feature engineering methods
10pub use crate::basic_features::*;
11
12// Re-export RBF and kernel approximation methods
13pub use crate::rbf_methods::*;
14
15// Re-export temporal feature extraction methods
16pub use crate::temporal_features::*;
17
18// Re-export statistical feature extraction methods
19pub use crate::statistical_features::*;
20
21// Re-export sketching and sampling methods
22pub use crate::sketching_sampling::*;
23
24// =============================================================================
25// Re-exports from time_series_features for backward compatibility
26// =============================================================================
27
28// Time series wavelet transform components have been moved to time_series_features.rs
29pub use crate::time_series_features::{
30    TimeSeriesWaveletExtractor, TimeSeriesWaveletType, WaveletFeatureType,
31};
32
33// Time-frequency feature extractor has been moved to time_series_features.rs
34pub use crate::time_series_features::TimeFrequencyExtractor;
35
36// =============================================================================
37// Re-exports from topological_features for backward compatibility
38// =============================================================================
39
40// Topological feature extractors have been moved to topological_features.rs
41pub 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        // Create a simple 2D dataset forming a triangle
54        let data = Array2::from_shape_vec(
55            (3, 2),
56            vec![
57                0.0, 0.0, // Point 0
58                1.0, 0.0, // Point 1
59                0.5, 0.8, // Point 2
60            ],
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        // Should have features for face counts (3 dimensions: 0, 1, 2) + euler characteristic
75        assert_eq!(features.len(), 4);
76
77        // All features should be finite
78        for &feature in &features {
79            assert!(feature.is_finite(), "Feature should be finite: {}", feature);
80        }
81
82        // Should have 3 vertices
83        assert_eq!(features[0], 3.0);
84
85        // Should have 3 edges (triangle)
86        assert_eq!(features[1], 3.0);
87
88        // Should have 1 triangle
89        assert_eq!(features[2], 1.0);
90
91        // Euler characteristic for triangle should be 1 (3 - 3 + 1 = 1)
92        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, // Point 0
101                1.0, 0.0, // Point 1
102                0.5, 0.8, // Point 2
103                0.5, 0.2, // Point 3
104            ],
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        // Should have depth measures: max_dim, mean_depth, max_depth, min_depth, complexity
120        assert_eq!(features.len(), 5);
121
122        // All features should be finite
123        for &feature in &features {
124            assert!(feature.is_finite(), "Feature should be finite: {}", feature);
125        }
126
127        // Max dimension should be <= 2
128        assert!(features[0] <= 2.0);
129
130        // Mean, max, min depths should be positive
131        assert!(features[1] > 0.0);
132        assert!(features[2] > 0.0);
133        assert!(features[3] > 0.0);
134
135        // Complexity should be between 0 and 1
136        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, // Point 0
145                1.0, 0.0, // Point 1
146                0.0, 1.0, // Point 2
147                1.0, 1.0, // Point 3
148            ],
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        // Should have boundary properties for dimensions 1 and 2
164        // For each dimension: faces_dim, faces_dim_minus_1, density
165        assert_eq!(features.len(), 6);
166
167        // All features should be finite
168        for &feature in &features {
169            assert!(feature.is_finite(), "Feature should be finite: {}", feature);
170        }
171
172        // All counts should be non-negative
173        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, // Point 0
188                1.0, 0.0, // Point 1
189                0.5, 0.8, // Point 2
190                0.0, 1.0, // Point 3
191                1.0, 1.0, // Point 4
192            ],
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        // Should have face counts normalized by number of vertices
208        assert_eq!(features.len(), 3);
209
210        // All features should be finite
211        for &feature in &features {
212            assert!(feature.is_finite(), "Feature should be finite: {}", feature);
213        }
214
215        // Vertices count should be 1.0 (normalized)
216        assert_eq!(features[0], 1.0);
217
218        // Other counts should be between 0 and some reasonable upper bound
219        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, // Point 0
230                1.0, 0.0, // Point 1
231                0.0, 1.0, // Point 2
232            ],
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        // Should have face counts + euler characteristic
248        assert_eq!(features.len(), 4);
249
250        // All features should be finite
251        for &feature in &features {
252            assert!(feature.is_finite(), "Feature should be finite: {}", feature);
253        }
254
255        // Should still form a valid simplicial complex
256        assert_eq!(features[0], 3.0); // vertices
257        assert!(features[1] > 0.0); // edges
258        assert!(features[3].is_finite()); // euler characteristic
259    }
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        // Should have face counts + euler characteristic
304        assert_eq!(features.len(), 4);
305
306        // Should have 1 vertex, 0 edges, 0 triangles
307        assert_eq!(features[0], 1.0);
308        assert_eq!(features[1], 0.0);
309        assert_eq!(features[2], 0.0);
310
311        // Euler characteristic should be 1
312        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        // Should have statistical features: 9 features total
352        assert_eq!(features.len(), 9);
353
354        // All features should be finite
355        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        // Should have distance matrix features: mean, std, min, max
376        assert_eq!(features.len(), 4);
377
378        // All features should be finite and non-negative
379        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        // Mean distance should be > 0 for non-identical points
389        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        // Should have correlation features: mean, std, max_abs
408        assert_eq!(features.len(), 3);
409
410        // All features should be finite
411        for &feature in &features {
412            assert!(feature.is_finite(), "Feature should be finite: {}", feature);
413        }
414
415        // Max absolute correlation should be between 0 and 1
416        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        // Should have 9 + 4 + 3 = 16 features
439        assert_eq!(features.len(), 16);
440
441        // All features should be finite
442        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        // Test Manhattan distance
453        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        // Test Cosine distance
464        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        // Features should be different for different metrics
475        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        // Should extract features successfully with custom thread count
493        assert_eq!(features.len(), 9);
494
495        // All features should be finite
496        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        // Should handle small datasets gracefully
520        assert_eq!(features.len(), 16);
521
522        // All features should be finite
523        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        // Should handle single sample (distance matrix features will be zeros)
542        assert_eq!(features.len(), 13); // 9 statistical + 4 distance (zeros)
543
544        // Distance features should be zero for single sample
545        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) // Larger than data size
604            .include_statistical_features(true);
605
606        let features = extractor
607            .extract_features(&data.view())
608            .expect("operation should succeed");
609
610        // Should handle large chunk sizes gracefully
611        assert_eq!(features.len(), 9);
612
613        // All features should be finite
614        for &feature in &features {
615            assert!(feature.is_finite(), "Feature should be finite: {}", feature);
616        }
617    }
618}
619
620/// Parallel Feature Extractor for High-Performance Computing
621///
622/// This extractor provides parallel computation of various feature types across data chunks,
623/// optimized for large datasets and multi-core systems.
624///
625/// Features extracted include:
626/// - Statistical features computed in parallel across columns and rows
627/// - Distance matrix features with parallel distance computation
628/// - Correlation features with parallel correlation computation
629/// - Configurable chunk size and thread count for optimal performance
630///
631/// # Parameters
632///
633/// * `chunk_size` - Size of data chunks for parallel processing
634/// * `num_threads` - Number of threads to use (None for automatic)
635/// * `include_distance_matrix` - Whether to compute distance matrix features
636/// * `include_statistical_features` - Whether to compute statistical features
637/// * `include_correlation_features` - Whether to compute correlation features
638/// * `distance_metric` - Distance metric for distance matrix computation
639///
640/// # Examples
641///
642/// ```
643/// # use sklears_feature_extraction::engineering::ParallelFeatureExtractor;
644/// # use scirs2_core::ndarray::Array2;
645/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
646/// let data = Array2::from_shape_vec((1000, 50), (0..50000).map(|x| x as f64).collect())?;
647///
648/// let extractor = ParallelFeatureExtractor::new()
649///     .chunk_size(100)
650///     .num_threads(4)
651///     .include_statistical_features(true)
652///     .include_distance_matrix(true);
653///
654/// let features = extractor.extract_features(&data.view())?;
655/// assert!(features.len() > 0);
656/// # Ok(())
657/// # }
658/// ```
659#[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    /// Create a new parallel feature extractor
671    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    /// Set the chunk size for parallel processing
683    pub fn chunk_size(mut self, size: usize) -> Self {
684        self.chunk_size = size.max(1);
685        self
686    }
687
688    /// Set the number of threads to use
689    pub fn num_threads(mut self, threads: usize) -> Self {
690        self.num_threads = Some(threads.max(1));
691        self
692    }
693
694    /// Set whether to compute distance matrix features
695    pub fn include_distance_matrix(mut self, include: bool) -> Self {
696        self.include_distance_matrix = include;
697        self
698    }
699
700    /// Set whether to compute statistical features
701    pub fn include_statistical_features(mut self, include: bool) -> Self {
702        self.include_statistical_features = include;
703        self
704    }
705
706    /// Set whether to compute correlation features
707    pub fn include_correlation_features(mut self, include: bool) -> Self {
708        self.include_correlation_features = include;
709        self
710    }
711
712    /// Set the distance metric for distance matrix computation
713    pub fn distance_metric(mut self, metric: &str) -> Self {
714        self.distance_metric = metric.to_string();
715        self
716    }
717
718    /// Extract features using parallel processing
719    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        // Set up thread pool if specified
727        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    /// Internal implementation of feature extraction
743    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        // Verify all features are finite
765        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    /// Compute statistical features in parallel across data chunks
777    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        // Process columns in parallel
785        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        // Aggregate statistics across all columns
794        let mut features = Vec::new();
795
796        // Mean of column means
797        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        // Mean of column standard deviations
801        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        // Mean of column mins and maxs
805        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        // Global statistics
811        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        // Parallel computation of row-wise statistics
825        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        // Aggregate row statistics
846        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    /// Compute statistics for a single column
862    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    /// Compute distance matrix features in parallel
874    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]); // mean, std, min, max distances
882        }
883
884        // Compute distance matrix in parallel using chunks
885        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        // Flatten and aggregate distances
906        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    /// Compute correlation features in parallel
938    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]); // mean, std, max absolute correlation
946        }
947
948        // Compute column means in parallel
949        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        // Compute correlations in parallel
958        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        // Aggregate correlations
982        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    /// Compute distance between two points using the specified metric
1005    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) // Maximum cosine distance
1031                } 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    /// Compute Pearson correlation between two columns
1043    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/// Mixed-type feature extractor for handling heterogeneous data
1080///
1081/// This extractor can handle different types of features (numerical, categorical, binary, ordinal)
1082/// and apply appropriate transformations to each type, creating a unified feature matrix.
1083#[derive(Debug, Clone)]
1084pub struct MixedTypeFeatureExtractor {
1085    /// Types of features for each column
1086    pub feature_types: Vec<FeatureType>,
1087    /// Whether to normalize numerical features
1088    pub normalize_numerical: bool,
1089    /// Whether to handle missing values
1090    pub handle_missing_values: bool,
1091    /// Strategy for handling missing values
1092    pub missing_value_strategy: MissingValueStrategy,
1093    /// Categorical encoding strategy
1094    pub encoding_strategy: CategoricalEncoding,
1095    /// Whether to include interaction features
1096    pub include_interaction_features: bool,
1097    /// Random state for reproducible results
1098    pub random_state: Option<u64>,
1099}
1100
1101/// Types of features that can be handled
1102#[derive(Debug, Clone, Copy, PartialEq)]
1103pub enum FeatureType {
1104    /// Continuous numerical features
1105    Numerical,
1106    /// Categorical features with no inherent order
1107    Categorical,
1108    /// Binary features (0/1)
1109    Binary,
1110    /// Ordinal features with inherent order
1111    Ordinal,
1112    /// Date/time features
1113    DateTime,
1114}
1115
1116/// Strategies for handling missing values
1117#[derive(Debug, Clone, Copy)]
1118pub enum MissingValueStrategy {
1119    /// Replace with mean (for numerical) or mode (for categorical)
1120    Mean,
1121    /// Replace with median (for numerical) or mode (for categorical)
1122    Median,
1123    /// Replace with mode (most frequent value)
1124    Mode,
1125    /// Replace with zero
1126    Zero,
1127    /// Forward fill
1128    ForwardFill,
1129    /// Backward fill
1130    BackwardFill,
1131}
1132
1133/// Categorical encoding strategies
1134#[derive(Debug, Clone, Copy)]
1135pub enum CategoricalEncoding {
1136    /// One-hot encoding
1137    OneHot,
1138    /// Label encoding (ordinal integers)
1139    LabelEncoding,
1140    /// Target encoding (mean of target for each category)
1141    TargetEncoding,
1142    /// Binary encoding
1143    BinaryEncoding,
1144    /// Frequency encoding
1145    FrequencyEncoding,
1146}
1147
1148impl MixedTypeFeatureExtractor {
1149    /// Create a new mixed-type feature extractor
1150    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    /// Set feature types for each column
1163    pub fn feature_types(mut self, types: Vec<FeatureType>) -> Self {
1164        self.feature_types = types;
1165        self
1166    }
1167
1168    /// Set whether to normalize numerical features
1169    pub fn normalize_numerical(mut self, normalize: bool) -> Self {
1170        self.normalize_numerical = normalize;
1171        self
1172    }
1173
1174    /// Set whether to handle missing values
1175    pub fn handle_missing_values(mut self, handle: bool) -> Self {
1176        self.handle_missing_values = handle;
1177        self
1178    }
1179
1180    /// Set missing value strategy
1181    pub fn missing_value_strategy(mut self, strategy: MissingValueStrategy) -> Self {
1182        self.missing_value_strategy = strategy;
1183        self
1184    }
1185
1186    /// Set categorical encoding strategy
1187    pub fn encoding_strategy(mut self, strategy: CategoricalEncoding) -> Self {
1188        self.encoding_strategy = strategy;
1189        self
1190    }
1191
1192    /// Set whether to include interaction features
1193    pub fn include_interaction_features(mut self, include: bool) -> Self {
1194        self.include_interaction_features = include;
1195        self
1196    }
1197
1198    /// Set random state
1199    pub fn random_state(mut self, seed: u64) -> Self {
1200        self.random_state = Some(seed);
1201        self
1202    }
1203
1204    /// Handle missing values in data
1205    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        // Find non-missing values
1215        let valid_values: Vec<f64> = column.iter().filter(|&&x| x.is_finite()).copied().collect();
1216
1217        if valid_values.is_empty() {
1218            return result; // All zeros if no valid values
1219        }
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                // Find most frequent value
1237                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                // Use first valid value
1250                valid_values[0]
1251            }
1252            (MissingValueStrategy::BackwardFill, _) => {
1253                // Use last valid value
1254                valid_values[valid_values.len() - 1]
1255            }
1256            _ => valid_values.iter().sum::<f64>() / valid_values.len() as f64, // Default to mean
1257        };
1258
1259        // Replace missing values
1260        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    /// Normalize numerical features
1272    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    /// Encode categorical features
1285    fn encode_categorical(&self, column: &Array1<f64>) -> Array2<f64> {
1286        match self.encoding_strategy {
1287            CategoricalEncoding::OneHot => {
1288                // Find unique values
1289                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                // Map each unique value to an integer
1310                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                // For simplicity, use the value itself (would normally use target mean)
1329                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                // Default to label encoding
1337                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    /// Process a single feature column
1347    fn process_feature(
1348        &self,
1349        data: &Array2<f64>,
1350        col_idx: usize,
1351        feature_type: FeatureType,
1352    ) -> Array2<f64> {
1353        // Handle missing values
1354        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                // Ensure binary values are 0 or 1
1372                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                // Treat as numerical but preserve order
1377                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                // For simplicity, treat as numerical
1386                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    /// Transform the data according to feature types
1397    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        // Process each feature according to its type
1413        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        // Concatenate all processed features
1419        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
1439/// Fitted mixed-type feature extractor
1440pub struct FittedMixedTypeFeatureExtractor {
1441    extractor: MixedTypeFeatureExtractor,
1442    transformation_stats: std::collections::HashMap<String, f64>,
1443}
1444
1445impl FittedMixedTypeFeatureExtractor {
1446    /// Get transformation statistics
1447    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)] // X follows sklearn/math convention for feature matrix
1463impl 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        // Set default feature types if not specified
1472        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        // Calculate transformation statistics
1479        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        // Estimate output feature count (simplified)
1505        let estimated_output_features = match self.encoding_strategy {
1506            CategoricalEncoding::OneHot => {
1507                // Estimate expanded size for one-hot encoding
1508                let categorical_expansion = n_categorical * 3; // Assume avg 3 categories
1509                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)] // X follows sklearn/math convention for feature matrix
1532impl 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, // numerical: 1,2,3  categorical: 1,0  ordinal: 5
1557                4.0, 5.0, 6.0, 2.0, 1.0,
1558                3.0, // numerical: 4,5,6  categorical: 2,1  ordinal: 3
1559                7.0, 8.0, 9.0, 1.0, 0.0,
1560                4.0, // numerical: 7,8,9  categorical: 1,0  ordinal: 4
1561                2.0, 3.0, 4.0, 3.0, 1.0,
1562                2.0, // numerical: 2,3,4  categorical: 3,1  ordinal: 2
1563            ],
1564        )
1565        .expect("operation should succeed");
1566
1567        let feature_types = vec![
1568            FeatureType::Numerical,   // col 0
1569            FeatureType::Numerical,   // col 1
1570            FeatureType::Numerical,   // col 2
1571            FeatureType::Categorical, // col 3
1572            FeatureType::Binary,      // col 4
1573            FeatureType::Ordinal,     // col 5
1574        ];
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); // Should be expanded due to one-hot encoding
1586
1587        // Check that all features are finite
1588        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            // Check that all features are finite
1626            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        // Check that configuration was applied
1668        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            // Check that no NaN values remain
1721            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        // Results should be identical
1754        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
1761// Temporal-spatial feature extractor has been moved to time_series_features.rs
1762pub use crate::time_series_features::TemporalSpatialExtractor;