Skip to main content

sklears_feature_selection/
benchmark.rs

1//! Benchmarking framework for feature selection methods
2//!
3//! This module provides comprehensive benchmarking capabilities to evaluate and compare
4//! feature selection methods against reference implementations and standard datasets.
5//! All implementations follow the SciRS2 policy using scirs2-core for numerical computations.
6
7use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
8use scirs2_core::random::thread_rng;
9use sklears_core::error::{Result as SklResult, SklearsError};
10type Result<T> = SklResult<T>;
11use std::collections::HashMap;
12use std::time::{Duration, Instant};
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16/// BenchmarkError
17pub enum BenchmarkError {
18    #[error("Invalid dataset configuration")]
19    /// InvalidDataset
20    InvalidDataset,
21    #[error("Method execution failed: {0}")]
22    /// MethodExecutionFailed
23    MethodExecutionFailed(String),
24    #[error("Benchmark configuration error: {0}")]
25    /// ConfigurationError
26    ConfigurationError(String),
27    #[error("Statistical analysis failed")]
28    /// StatisticalAnalysisFailed
29    StatisticalAnalysisFailed,
30    #[error("Comparison baseline not found")]
31    /// BaselineNotFound
32    BaselineNotFound,
33}
34
35impl From<BenchmarkError> for SklearsError {
36    fn from(err: BenchmarkError) -> Self {
37        SklearsError::FitError(format!("Benchmark error: {}", err))
38    }
39}
40
41/// Comprehensive benchmarking suite for feature selection methods
42#[derive(Debug)]
43pub struct FeatureSelectionBenchmark {
44    datasets: Vec<BenchmarkDataset>,
45    methods: HashMap<String, Box<dyn BenchmarkableMethod>>,
46    config: BenchmarkConfig,
47    results: Vec<BenchmarkResult>,
48}
49
50impl Default for FeatureSelectionBenchmark {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl FeatureSelectionBenchmark {
57    /// Create a new benchmarking suite
58    pub fn new() -> Self {
59        Self {
60            datasets: Vec::new(),
61            methods: HashMap::new(),
62            config: BenchmarkConfig::default(),
63            results: Vec::new(),
64        }
65    }
66
67    /// Configure the benchmark suite
68    pub fn with_config(mut self, config: BenchmarkConfig) -> Self {
69        self.config = config;
70        self
71    }
72
73    /// Add a dataset to the benchmark suite
74    pub fn add_dataset(&mut self, dataset: BenchmarkDataset) -> Result<()> {
75        if dataset.X.is_empty() || dataset.y.is_empty() {
76            return Err(BenchmarkError::InvalidDataset.into());
77        }
78        self.datasets.push(dataset);
79        Ok(())
80    }
81
82    /// Add multiple standard datasets for benchmarking
83    pub fn add_standard_datasets(&mut self) -> Result<()> {
84        // Add synthetic datasets with known properties
85        self.add_dataset(self.create_synthetic_linear_dataset(500, 50, 10)?)?;
86        self.add_dataset(self.create_synthetic_nonlinear_dataset(300, 100, 15)?)?;
87        self.add_dataset(self.create_high_dimensional_dataset(200, 1000, 5)?)?;
88        self.add_dataset(self.create_correlated_features_dataset(400, 80, 20)?)?;
89        self.add_dataset(self.create_noisy_dataset(600, 60, 12)?)?;
90
91        Ok(())
92    }
93
94    /// Register a benchmarkable method
95    pub fn register_method(&mut self, name: String, method: Box<dyn BenchmarkableMethod>) {
96        self.methods.insert(name, method);
97    }
98
99    /// Run comprehensive benchmark suite
100    pub fn run_benchmark(&mut self) -> Result<BenchmarkSuiteResults> {
101        if self.datasets.is_empty() {
102            return Err(BenchmarkError::ConfigurationError("No datasets added".to_string()).into());
103        }
104
105        if self.methods.is_empty() {
106            return Err(
107                BenchmarkError::ConfigurationError("No methods registered".to_string()).into(),
108            );
109        }
110
111        let mut all_results = Vec::new();
112        let start_time = Instant::now();
113
114        println!(
115            "Starting benchmark suite with {} datasets and {} methods...",
116            self.datasets.len(),
117            self.methods.len()
118        );
119
120        // Run benchmarks for each dataset-method combination
121        for (dataset_idx, dataset) in self.datasets.iter().enumerate() {
122            println!(
123                "Benchmarking dataset {}/{}: {}",
124                dataset_idx + 1,
125                self.datasets.len(),
126                dataset.name
127            );
128
129            for (method_name, method) in self.methods.iter() {
130                println!("  Running method: {}", method_name);
131
132                // Run multiple iterations if configured
133                let mut method_results = Vec::new();
134
135                for iteration in 0..self.config.n_iterations {
136                    if self.config.verbose && iteration % 10 == 0 {
137                        println!(
138                            "    Iteration {}/{}",
139                            iteration + 1,
140                            self.config.n_iterations
141                        );
142                    }
143
144                    let result = self.run_single_benchmark(
145                        method.as_ref(),
146                        method_name,
147                        dataset,
148                        iteration,
149                    )?;
150
151                    method_results.push(result);
152                }
153
154                // Aggregate results for this method on this dataset
155                let aggregated_result = self.aggregate_method_results(&method_results)?;
156                all_results.push(aggregated_result);
157            }
158        }
159
160        let total_duration = start_time.elapsed();
161
162        // Perform statistical analysis
163        let statistical_analysis = self.perform_statistical_analysis(&all_results)?;
164
165        // Generate rankings
166        let rankings = self.generate_rankings(&all_results)?;
167
168        // Create comprehensive results
169        let suite_results = BenchmarkSuiteResults {
170            individual_results: all_results,
171            statistical_analysis,
172            rankings,
173            execution_summary: ExecutionSummary {
174                total_duration,
175                n_datasets: self.datasets.len(),
176                n_methods: self.methods.len(),
177                n_total_runs: self.datasets.len() * self.methods.len() * self.config.n_iterations,
178            },
179            configuration: self.config.clone(),
180        };
181
182        self.results
183            .extend(suite_results.individual_results.clone());
184
185        println!(
186            "Benchmark suite completed in {:.2}s",
187            total_duration.as_secs_f64()
188        );
189
190        Ok(suite_results)
191    }
192
193    /// Run a single benchmark iteration
194    #[allow(non_snake_case)]
195    fn run_single_benchmark(
196        &self,
197        method: &dyn BenchmarkableMethod,
198        method_name: &str,
199        dataset: &BenchmarkDataset,
200        iteration: usize,
201    ) -> Result<BenchmarkResult> {
202        let start_time = Instant::now();
203
204        // Split data if cross-validation is configured
205        let (train_indices, test_indices) = if self.config.use_cross_validation {
206            self.create_cv_split(dataset.X.nrows(), iteration % self.config.cv_folds)?
207        } else {
208            self.create_holdout_split(dataset.X.nrows())?
209        };
210
211        // Extract training and test data
212        let X_train = self.extract_samples(dataset.X.view(), &train_indices);
213        let y_train = self.extract_targets(dataset.y.view(), &train_indices);
214        let X_test = self.extract_samples(dataset.X.view(), &test_indices);
215        let y_test = self.extract_targets(dataset.y.view(), &test_indices);
216
217        // Measure memory usage before method execution
218        let memory_before = self.get_memory_usage();
219
220        // Execute feature selection method
221        let selected_features = match method.select_features(X_train.view(), y_train.view()) {
222            Ok(features) => features,
223            Err(e) => return Err(BenchmarkError::MethodExecutionFailed(format!("{:?}", e)).into()),
224        };
225
226        let selection_time = start_time.elapsed();
227
228        // Measure memory usage after method execution
229        let memory_after = self.get_memory_usage();
230        let memory_delta = memory_after.saturating_sub(memory_before);
231
232        // Evaluate selected features
233        let evaluation_start = Instant::now();
234        let evaluation_metrics = self.evaluate_feature_selection(
235            X_train.view(),
236            y_train.view(),
237            X_test.view(),
238            y_test.view(),
239            &selected_features,
240            &dataset.ground_truth,
241        )?;
242        let evaluation_time = evaluation_start.elapsed();
243
244        // Compute feature quality metrics
245        let feature_metrics =
246            self.compute_feature_metrics(dataset.X.view(), dataset.y.view(), &selected_features)?;
247
248        Ok(BenchmarkResult {
249            method_name: method_name.to_string(),
250            dataset_name: dataset.name.clone(),
251            iteration,
252            selected_features: selected_features.clone(),
253            n_features_selected: selected_features.len(),
254            n_features_total: dataset.X.ncols(),
255            selection_time,
256            evaluation_time,
257            memory_usage: memory_delta,
258            evaluation_metrics,
259            feature_metrics,
260            dataset_properties: dataset.properties.clone(),
261        })
262    }
263
264    /// Create synthetic linear dataset with known relevant features
265    fn create_synthetic_linear_dataset(
266        &self,
267        n_samples: usize,
268        n_features: usize,
269        n_relevant: usize,
270    ) -> Result<BenchmarkDataset> {
271        let mut X = Array2::zeros((n_samples, n_features));
272        let mut y = Array1::zeros(n_samples);
273
274        // Generate relevant features with linear relationship to target
275        for i in 0..n_samples {
276            let mut target = 0.0;
277
278            for j in 0..n_relevant {
279                let value = thread_rng().random::<f64>() * 10.0 - 5.0; // Range [-5, 5]
280                X[[i, j]] = value;
281                target += value * (j + 1) as f64; // Different coefficients for each feature
282            }
283
284            // Add noise features
285            for j in n_relevant..n_features {
286                X[[i, j]] = thread_rng().random::<f64>() * 10.0 - 5.0;
287            }
288
289            // Add noise to target
290            target += (thread_rng().random::<f64>() - 0.5) * 2.0;
291            y[i] = target;
292        }
293
294        let ground_truth = Some((0..n_relevant).collect());
295        let properties = DatasetProperties {
296            task_type: TaskType::Regression,
297            n_informative_features: n_relevant,
298            noise_level: 0.1,
299            correlation_structure: CorrelationStructure::Linear,
300            feature_types: vec![FeatureType::Continuous; n_features],
301        };
302
303        Ok(BenchmarkDataset {
304            name: format!("SyntheticLinear_{}x{}", n_samples, n_features),
305            X,
306            y,
307            ground_truth,
308            properties,
309        })
310    }
311
312    /// Create synthetic nonlinear dataset
313    fn create_synthetic_nonlinear_dataset(
314        &self,
315        n_samples: usize,
316        n_features: usize,
317        n_relevant: usize,
318    ) -> Result<BenchmarkDataset> {
319        let mut X = Array2::zeros((n_samples, n_features));
320        let mut y = Array1::zeros(n_samples);
321
322        for i in 0..n_samples {
323            let mut target = 0.0;
324
325            // Generate relevant features with nonlinear relationship
326            for j in 0..n_relevant {
327                let value = thread_rng().random::<f64>() * 6.0 - 3.0; // Range [-3, 3]
328                X[[i, j]] = value;
329
330                // Nonlinear relationships: quadratic, sine, exponential
331                match j % 3 {
332                    0 => target += value * value,
333                    1 => target += (value * std::f64::consts::PI / 2.0).sin() * 2.0,
334                    2 => target += (value.abs()).ln().max(-5.0),
335                    _ => unreachable!(),
336                }
337            }
338
339            // Noise features
340            for j in n_relevant..n_features {
341                X[[i, j]] = thread_rng().random::<f64>() * 6.0 - 3.0;
342            }
343
344            y[i] = target + (thread_rng().random::<f64>() - 0.5) * 0.5;
345        }
346
347        let ground_truth = Some((0..n_relevant).collect());
348        let properties = DatasetProperties {
349            task_type: TaskType::Regression,
350            n_informative_features: n_relevant,
351            noise_level: 0.15,
352            correlation_structure: CorrelationStructure::Nonlinear,
353            feature_types: vec![FeatureType::Continuous; n_features],
354        };
355
356        Ok(BenchmarkDataset {
357            name: format!("SyntheticNonlinear_{}x{}", n_samples, n_features),
358            X,
359            y,
360            ground_truth,
361            properties,
362        })
363    }
364
365    /// Create high-dimensional dataset (p >> n scenario)
366    fn create_high_dimensional_dataset(
367        &self,
368        n_samples: usize,
369        n_features: usize,
370        n_relevant: usize,
371    ) -> Result<BenchmarkDataset> {
372        let mut X = Array2::zeros((n_samples, n_features));
373        let mut y = Array1::zeros(n_samples);
374
375        for i in 0..n_samples {
376            let mut target = 0.0;
377
378            // Sparse relevant features
379            for j in 0..n_relevant {
380                let value = thread_rng().random::<f64>() * 4.0 - 2.0;
381                X[[i, j]] = value;
382                target += value * (1.0 / (j + 1) as f64); // Decreasing importance
383            }
384
385            // Many noise features
386            for j in n_relevant..n_features {
387                X[[i, j]] = thread_rng().random::<f64>() * 4.0 - 2.0;
388            }
389
390            y[i] = if target > 0.0 { 1.0 } else { 0.0 }; // Binary classification
391        }
392
393        let ground_truth = Some((0..n_relevant).collect());
394        let properties = DatasetProperties {
395            task_type: TaskType::BinaryClassification,
396            n_informative_features: n_relevant,
397            noise_level: 0.2,
398            correlation_structure: CorrelationStructure::Sparse,
399            feature_types: vec![FeatureType::Continuous; n_features],
400        };
401
402        Ok(BenchmarkDataset {
403            name: format!("HighDimensional_{}x{}", n_samples, n_features),
404            X,
405            y,
406            ground_truth,
407            properties,
408        })
409    }
410
411    /// Create dataset with correlated features
412    fn create_correlated_features_dataset(
413        &self,
414        n_samples: usize,
415        n_features: usize,
416        n_relevant: usize,
417    ) -> Result<BenchmarkDataset> {
418        let mut X = Array2::zeros((n_samples, n_features));
419        let mut y = Array1::zeros(n_samples);
420
421        for i in 0..n_samples {
422            let mut target = 0.0;
423
424            // Generate base relevant features
425            let base_features: Vec<f64> = (0..n_relevant)
426                .map(|_| thread_rng().random::<f64>() * 4.0 - 2.0)
427                .collect();
428
429            for j in 0..n_relevant {
430                X[[i, j]] = base_features[j];
431                target += base_features[j] * (j + 1) as f64;
432            }
433
434            // Add correlated copies and linear combinations
435            let mut feature_idx = n_relevant;
436            for j in 0..n_relevant.min((n_features - n_relevant) / 3) {
437                if feature_idx < n_features {
438                    // Highly correlated copy with noise
439                    X[[i, feature_idx]] =
440                        base_features[j] + (thread_rng().random::<f64>() - 0.5) * 0.2;
441                    feature_idx += 1;
442                }
443
444                if feature_idx < n_features && j + 1 < base_features.len() {
445                    // Linear combination of two features
446                    X[[i, feature_idx]] = 0.5 * base_features[j] + 0.3 * base_features[j + 1];
447                    feature_idx += 1;
448                }
449            }
450
451            // Fill remaining with noise
452            for j in feature_idx..n_features {
453                X[[i, j]] = thread_rng().random::<f64>() * 4.0 - 2.0;
454            }
455
456            y[i] = target + (thread_rng().random::<f64>() - 0.5) * 1.0;
457        }
458
459        let ground_truth = Some((0..n_relevant).collect());
460        let properties = DatasetProperties {
461            task_type: TaskType::Regression,
462            n_informative_features: n_relevant,
463            noise_level: 0.25,
464            correlation_structure: CorrelationStructure::HighlyCorrelated,
465            feature_types: vec![FeatureType::Continuous; n_features],
466        };
467
468        Ok(BenchmarkDataset {
469            name: format!("Correlated_{}x{}", n_samples, n_features),
470            X,
471            y,
472            ground_truth,
473            properties,
474        })
475    }
476
477    /// Create noisy dataset
478    fn create_noisy_dataset(
479        &self,
480        n_samples: usize,
481        n_features: usize,
482        n_relevant: usize,
483    ) -> Result<BenchmarkDataset> {
484        let mut X = Array2::zeros((n_samples, n_features));
485        let mut y = Array1::zeros(n_samples);
486
487        for i in 0..n_samples {
488            let mut target = 0.0;
489
490            for j in 0..n_relevant {
491                let value = thread_rng().random::<f64>() * 8.0 - 4.0;
492                X[[i, j]] = value;
493                target += value * (j + 1) as f64;
494            }
495
496            // Noise features
497            for j in n_relevant..n_features {
498                X[[i, j]] = thread_rng().random::<f64>() * 8.0 - 4.0;
499            }
500
501            // Heavy noise on target
502            let noise = (thread_rng().random::<f64>() - 0.5) * target.abs() * 0.5;
503            y[i] = target + noise;
504        }
505
506        let ground_truth = Some((0..n_relevant).collect());
507        let properties = DatasetProperties {
508            task_type: TaskType::Regression,
509            n_informative_features: n_relevant,
510            noise_level: 0.5,
511            correlation_structure: CorrelationStructure::Noisy,
512            feature_types: vec![FeatureType::Continuous; n_features],
513        };
514
515        Ok(BenchmarkDataset {
516            name: format!("Noisy_{}x{}", n_samples, n_features),
517            X,
518            y,
519            ground_truth,
520            properties,
521        })
522    }
523
524    // Helper methods
525    fn create_cv_split(&self, n_samples: usize, fold: usize) -> Result<(Vec<usize>, Vec<usize>)> {
526        let fold_size = n_samples / self.config.cv_folds;
527        let start = (fold % self.config.cv_folds) * fold_size;
528        let end = if fold % self.config.cv_folds == self.config.cv_folds - 1 {
529            n_samples
530        } else {
531            start + fold_size
532        };
533
534        let test_indices: Vec<usize> = (start..end).collect();
535        let train_indices: Vec<usize> = (0..start).chain(end..n_samples).collect();
536
537        Ok((train_indices, test_indices))
538    }
539
540    fn create_holdout_split(&self, n_samples: usize) -> Result<(Vec<usize>, Vec<usize>)> {
541        let n_test = (n_samples as f64 * self.config.test_size) as usize;
542        let n_train = n_samples - n_test;
543
544        // Simple sequential split for deterministic behavior
545        let train_indices: Vec<usize> = (0..n_train).collect();
546        let test_indices: Vec<usize> = (n_train..n_samples).collect();
547
548        Ok((train_indices, test_indices))
549    }
550
551    fn extract_samples(&self, X: ArrayView2<f64>, indices: &[usize]) -> Array2<f64> {
552        let mut samples = Array2::zeros((indices.len(), X.ncols()));
553        for (i, &idx) in indices.iter().enumerate() {
554            samples.row_mut(i).assign(&X.row(idx));
555        }
556        samples
557    }
558
559    fn extract_targets(&self, y: ArrayView1<f64>, indices: &[usize]) -> Array1<f64> {
560        let mut targets = Array1::zeros(indices.len());
561        for (i, &idx) in indices.iter().enumerate() {
562            targets[i] = y[idx];
563        }
564        targets
565    }
566
567    fn get_memory_usage(&self) -> usize {
568        // Simplified memory tracking - in a real implementation, this would
569        // use system calls or memory profiling tools
570        0
571    }
572
573    fn evaluate_feature_selection(
574        &self,
575        X_train: ArrayView2<f64>,
576        y_train: ArrayView1<f64>,
577        _X_test: ArrayView2<f64>,
578        _y_test: ArrayView1<f64>,
579        selected_features: &[usize],
580        ground_truth: &Option<Vec<usize>>,
581    ) -> Result<EvaluationMetrics> {
582        let mut metrics = EvaluationMetrics::default();
583
584        // Compute relevance score (correlation with target)
585        if !selected_features.is_empty() {
586            let mut total_relevance = 0.0;
587            for &feature_idx in selected_features {
588                if feature_idx < X_train.ncols() {
589                    let correlation =
590                        self.compute_correlation(X_train.column(feature_idx), y_train);
591                    total_relevance += correlation.abs();
592                }
593            }
594            metrics.relevance_score = total_relevance / selected_features.len() as f64;
595        }
596
597        // Compute redundancy (average pairwise correlation among selected features)
598        if selected_features.len() > 1 {
599            let mut total_correlation = 0.0;
600            let mut pair_count = 0;
601
602            for i in 0..selected_features.len() {
603                for j in (i + 1)..selected_features.len() {
604                    if selected_features[i] < X_train.ncols()
605                        && selected_features[j] < X_train.ncols()
606                    {
607                        let correlation = self.compute_correlation(
608                            X_train.column(selected_features[i]),
609                            X_train.column(selected_features[j]),
610                        );
611                        total_correlation += correlation.abs();
612                        pair_count += 1;
613                    }
614                }
615            }
616
617            if pair_count > 0 {
618                metrics.redundancy_score = total_correlation / pair_count as f64;
619            }
620        }
621
622        // Compute ground truth metrics if available
623        if let Some(true_features) = ground_truth {
624            let selected_set: std::collections::HashSet<_> = selected_features.iter().collect();
625            let true_set: std::collections::HashSet<_> = true_features.iter().collect();
626
627            let intersection = selected_set.intersection(&true_set).count() as f64;
628            let union = selected_set.union(&true_set).count() as f64;
629
630            if union > 0.0 {
631                metrics.jaccard_score = Some(intersection / union);
632            }
633
634            if !true_features.is_empty() {
635                metrics.precision = Some(intersection / selected_features.len() as f64);
636                metrics.recall = Some(intersection / true_features.len() as f64);
637
638                if let (Some(p), Some(r)) = (metrics.precision, metrics.recall) {
639                    if p + r > 0.0 {
640                        metrics.f1_score = Some(2.0 * p * r / (p + r));
641                    }
642                }
643            }
644        }
645
646        // Simplified predictive performance (correlation-based)
647        metrics.predictive_score = metrics.relevance_score * (1.0 - metrics.redundancy_score * 0.5);
648
649        Ok(metrics)
650    }
651
652    fn compute_feature_metrics(
653        &self,
654        X: ArrayView2<f64>,
655        y: ArrayView1<f64>,
656        selected_features: &[usize],
657    ) -> Result<FeatureMetrics> {
658        let mut metrics = FeatureMetrics::default();
659
660        if selected_features.is_empty() {
661            return Ok(metrics);
662        }
663
664        // Selection ratio
665        metrics.selection_ratio = selected_features.len() as f64 / X.ncols() as f64;
666
667        // Feature importance distribution
668        let mut importances = Vec::new();
669        for &feature_idx in selected_features {
670            if feature_idx < X.ncols() {
671                let importance = self.compute_correlation(X.column(feature_idx), y).abs();
672                importances.push(importance);
673            }
674        }
675
676        if !importances.is_empty() {
677            importances.sort_by(|a, b| b.partial_cmp(a).expect("operation should succeed"));
678            metrics.importance_distribution = ImportanceDistribution {
679                mean: importances.iter().sum::<f64>() / importances.len() as f64,
680                std: {
681                    let mean = importances.iter().sum::<f64>() / importances.len() as f64;
682                    let variance = importances.iter().map(|x| (x - mean).powi(2)).sum::<f64>()
683                        / importances.len() as f64;
684                    variance.sqrt()
685                },
686                max: importances[0],
687                min: importances[importances.len() - 1],
688            };
689        }
690
691        Ok(metrics)
692    }
693
694    fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
695        let n = x.len() as f64;
696        if n < 2.0 {
697            return 0.0;
698        }
699
700        let mean_x = x.mean().unwrap_or(0.0);
701        let mean_y = y.mean().unwrap_or(0.0);
702
703        let mut sum_xy = 0.0;
704        let mut sum_x2 = 0.0;
705        let mut sum_y2 = 0.0;
706
707        for i in 0..x.len() {
708            let dx = x[i] - mean_x;
709            let dy = y[i] - mean_y;
710            sum_xy += dx * dy;
711            sum_x2 += dx * dx;
712            sum_y2 += dy * dy;
713        }
714
715        let denom = (sum_x2 * sum_y2).sqrt();
716        if denom < 1e-10 {
717            0.0
718        } else {
719            sum_xy / denom
720        }
721    }
722
723    fn aggregate_method_results(&self, results: &[BenchmarkResult]) -> Result<BenchmarkResult> {
724        if results.is_empty() {
725            return Err(BenchmarkError::StatisticalAnalysisFailed.into());
726        }
727
728        let first = &results[0];
729
730        // Aggregate numerical metrics
731        let aggregate_evaluation_metrics = EvaluationMetrics {
732            relevance_score: results
733                .iter()
734                .map(|r| r.evaluation_metrics.relevance_score)
735                .sum::<f64>()
736                / results.len() as f64,
737            redundancy_score: results
738                .iter()
739                .map(|r| r.evaluation_metrics.redundancy_score)
740                .sum::<f64>()
741                / results.len() as f64,
742            predictive_score: results
743                .iter()
744                .map(|r| r.evaluation_metrics.predictive_score)
745                .sum::<f64>()
746                / results.len() as f64,
747            jaccard_score: if results
748                .iter()
749                .all(|r| r.evaluation_metrics.jaccard_score.is_some())
750            {
751                Some(
752                    results
753                        .iter()
754                        .map(|r| {
755                            r.evaluation_metrics
756                                .jaccard_score
757                                .expect("operation should succeed")
758                        })
759                        .sum::<f64>()
760                        / results.len() as f64,
761                )
762            } else {
763                None
764            },
765            precision: if results
766                .iter()
767                .all(|r| r.evaluation_metrics.precision.is_some())
768            {
769                Some(
770                    results
771                        .iter()
772                        .map(|r| {
773                            r.evaluation_metrics
774                                .precision
775                                .expect("operation should succeed")
776                        })
777                        .sum::<f64>()
778                        / results.len() as f64,
779                )
780            } else {
781                None
782            },
783            recall: if results
784                .iter()
785                .all(|r| r.evaluation_metrics.recall.is_some())
786            {
787                Some(
788                    results
789                        .iter()
790                        .map(|r| {
791                            r.evaluation_metrics
792                                .recall
793                                .expect("operation should succeed")
794                        })
795                        .sum::<f64>()
796                        / results.len() as f64,
797                )
798            } else {
799                None
800            },
801            f1_score: if results
802                .iter()
803                .all(|r| r.evaluation_metrics.f1_score.is_some())
804            {
805                Some(
806                    results
807                        .iter()
808                        .map(|r| {
809                            r.evaluation_metrics
810                                .f1_score
811                                .expect("operation should succeed")
812                        })
813                        .sum::<f64>()
814                        / results.len() as f64,
815                )
816            } else {
817                None
818            },
819        };
820
821        // Take most frequent feature selection
822        let most_common_features = first.selected_features.clone(); // Simplified
823
824        Ok(BenchmarkResult {
825            method_name: first.method_name.clone(),
826            dataset_name: first.dataset_name.clone(),
827            iteration: 999, // Special marker for aggregated result
828            selected_features: most_common_features,
829            n_features_selected: results.iter().map(|r| r.n_features_selected).sum::<usize>()
830                / results.len(),
831            n_features_total: first.n_features_total,
832            selection_time: Duration::from_nanos(
833                (results
834                    .iter()
835                    .map(|r| r.selection_time.as_nanos())
836                    .sum::<u128>()
837                    / results.len() as u128) as u64,
838            ),
839            evaluation_time: Duration::from_nanos(
840                (results
841                    .iter()
842                    .map(|r| r.evaluation_time.as_nanos())
843                    .sum::<u128>()
844                    / results.len() as u128) as u64,
845            ),
846            memory_usage: results.iter().map(|r| r.memory_usage).sum::<usize>() / results.len(),
847            evaluation_metrics: aggregate_evaluation_metrics,
848            feature_metrics: first.feature_metrics.clone(), // Simplified
849            dataset_properties: first.dataset_properties.clone(),
850        })
851    }
852
853    fn perform_statistical_analysis(
854        &self,
855        results: &[BenchmarkResult],
856    ) -> Result<StatisticalAnalysis> {
857        // Group results by method and dataset
858        let mut method_performances: HashMap<String, Vec<f64>> = HashMap::new();
859        let mut dataset_difficulties: HashMap<String, Vec<f64>> = HashMap::new();
860
861        for result in results {
862            method_performances
863                .entry(result.method_name.clone())
864                .or_default()
865                .push(result.evaluation_metrics.predictive_score);
866
867            dataset_difficulties
868                .entry(result.dataset_name.clone())
869                .or_default()
870                .push(result.evaluation_metrics.predictive_score);
871        }
872
873        // Compute method rankings
874        let mut method_rankings = Vec::new();
875        for (method, scores) in method_performances {
876            let mean_score = scores.iter().sum::<f64>() / scores.len() as f64;
877            let std_score = {
878                let variance = scores.iter().map(|s| (s - mean_score).powi(2)).sum::<f64>()
879                    / scores.len() as f64;
880                variance.sqrt()
881            };
882
883            method_rankings.push(MethodRanking {
884                method_name: method,
885                mean_score,
886                std_score,
887                scores,
888            });
889        }
890
891        method_rankings.sort_by(|a, b| {
892            b.mean_score
893                .partial_cmp(&a.mean_score)
894                .expect("operation should succeed")
895        });
896
897        // Compute dataset difficulties
898        let mut dataset_rankings = Vec::new();
899        for (dataset, scores) in dataset_difficulties {
900            let mean_score = scores.iter().sum::<f64>() / scores.len() as f64;
901
902            dataset_rankings.push(DatasetRanking {
903                dataset_name: dataset,
904                difficulty_score: 1.0 - mean_score, // Higher score = more difficult
905                mean_performance: mean_score,
906            });
907        }
908
909        dataset_rankings.sort_by(|a, b| {
910            b.difficulty_score
911                .partial_cmp(&a.difficulty_score)
912                .expect("operation should succeed")
913        });
914
915        let overall_best_method = method_rankings.first().map(|r| r.method_name.clone());
916
917        Ok(StatisticalAnalysis {
918            method_rankings,
919            dataset_rankings,
920            overall_best_method,
921            performance_summary: self.compute_performance_summary(results)?,
922        })
923    }
924
925    fn compute_performance_summary(
926        &self,
927        results: &[BenchmarkResult],
928    ) -> Result<PerformanceSummary> {
929        if results.is_empty() {
930            return Err(BenchmarkError::StatisticalAnalysisFailed.into());
931        }
932
933        let scores: Vec<f64> = results
934            .iter()
935            .map(|r| r.evaluation_metrics.predictive_score)
936            .collect();
937        let mean_score = scores.iter().sum::<f64>() / scores.len() as f64;
938
939        let std_score = {
940            let variance =
941                scores.iter().map(|s| (s - mean_score).powi(2)).sum::<f64>() / scores.len() as f64;
942            variance.sqrt()
943        };
944
945        let min_score = scores.iter().fold(f64::INFINITY, |acc, &x| acc.min(x));
946        let max_score = scores.iter().fold(f64::NEG_INFINITY, |acc, &x| acc.max(x));
947
948        let total_time: Duration = results
949            .iter()
950            .map(|r| r.selection_time + r.evaluation_time)
951            .sum();
952        let avg_features_selected = results.iter().map(|r| r.n_features_selected).sum::<usize>()
953            as f64
954            / results.len() as f64;
955
956        Ok(PerformanceSummary {
957            mean_score,
958            std_score,
959            min_score,
960            max_score,
961            total_execution_time: total_time,
962            avg_features_selected,
963        })
964    }
965
966    fn generate_rankings(&self, results: &[BenchmarkResult]) -> Result<Vec<MethodRanking>> {
967        let mut method_scores: HashMap<String, Vec<f64>> = HashMap::new();
968
969        for result in results {
970            method_scores
971                .entry(result.method_name.clone())
972                .or_default()
973                .push(result.evaluation_metrics.predictive_score);
974        }
975
976        let mut rankings = Vec::new();
977        for (method, scores) in method_scores {
978            let mean_score = scores.iter().sum::<f64>() / scores.len() as f64;
979            let std_score = {
980                let variance = scores.iter().map(|s| (s - mean_score).powi(2)).sum::<f64>()
981                    / scores.len() as f64;
982                variance.sqrt()
983            };
984
985            rankings.push(MethodRanking {
986                method_name: method,
987                mean_score,
988                std_score,
989                scores,
990            });
991        }
992
993        rankings.sort_by(|a, b| {
994            b.mean_score
995                .partial_cmp(&a.mean_score)
996                .expect("operation should succeed")
997        });
998        Ok(rankings)
999    }
1000}
1001
1002/// Trait for benchmarkable feature selection methods
1003pub trait BenchmarkableMethod: std::fmt::Debug + Send + Sync {
1004    /// fn
1005    fn select_features(
1006        &self,
1007        X: ArrayView2<f64>,
1008        y: ArrayView1<f64>,
1009    ) -> std::result::Result<Vec<usize>, BenchmarkError>;
1010    /// fn
1011    fn method_name(&self) -> &str;
1012    /// fn
1013    fn method_params(&self) -> HashMap<String, String>;
1014}
1015
1016// Data structures for benchmarking
1017
1018#[derive(Debug, Clone)]
1019/// BenchmarkDataset
1020pub struct BenchmarkDataset {
1021    /// name
1022    pub name: String,
1023    /// X
1024    pub X: Array2<f64>,
1025    /// y
1026    pub y: Array1<f64>,
1027    /// ground_truth
1028    pub ground_truth: Option<Vec<usize>>,
1029    /// properties
1030    pub properties: DatasetProperties,
1031}
1032
1033#[derive(Debug, Clone)]
1034/// DatasetProperties
1035pub struct DatasetProperties {
1036    /// task_type
1037    pub task_type: TaskType,
1038    /// n_informative_features
1039    pub n_informative_features: usize,
1040    /// noise_level
1041    pub noise_level: f64,
1042    /// correlation_structure
1043    pub correlation_structure: CorrelationStructure,
1044    /// feature_types
1045    pub feature_types: Vec<FeatureType>,
1046}
1047
1048#[derive(Debug, Clone, PartialEq)]
1049/// TaskType
1050pub enum TaskType {
1051    /// BinaryClassification
1052    BinaryClassification,
1053    /// MultiClassification
1054    MultiClassification,
1055    /// Regression
1056    Regression,
1057    /// MultiLabel
1058    MultiLabel,
1059}
1060
1061#[derive(Debug, Clone, PartialEq)]
1062/// CorrelationStructure
1063pub enum CorrelationStructure {
1064    /// Linear
1065    Linear,
1066    /// Nonlinear
1067    Nonlinear,
1068    /// Sparse
1069    Sparse,
1070    /// HighlyCorrelated
1071    HighlyCorrelated,
1072    /// Noisy
1073    Noisy,
1074}
1075
1076#[derive(Debug, Clone, PartialEq)]
1077/// FeatureType
1078pub enum FeatureType {
1079    /// Continuous
1080    Continuous,
1081    /// Discrete
1082    Discrete,
1083    /// Binary
1084    Binary,
1085    /// Categorical
1086    Categorical,
1087}
1088
1089#[derive(Debug, Clone)]
1090/// BenchmarkConfig
1091pub struct BenchmarkConfig {
1092    /// n_iterations
1093    pub n_iterations: usize,
1094    /// use_cross_validation
1095    pub use_cross_validation: bool,
1096    /// cv_folds
1097    pub cv_folds: usize,
1098    /// test_size
1099    pub test_size: f64,
1100    /// timeout_seconds
1101    pub timeout_seconds: Option<u64>,
1102    /// memory_limit_mb
1103    pub memory_limit_mb: Option<usize>,
1104    /// verbose
1105    pub verbose: bool,
1106    /// random_seed
1107    pub random_seed: Option<u64>,
1108}
1109
1110impl Default for BenchmarkConfig {
1111    fn default() -> Self {
1112        Self {
1113            n_iterations: 5,
1114            use_cross_validation: true,
1115            cv_folds: 3,
1116            test_size: 0.3,
1117            timeout_seconds: Some(300),  // 5 minutes
1118            memory_limit_mb: Some(2048), // 2 GB
1119            verbose: true,
1120            random_seed: Some(42),
1121        }
1122    }
1123}
1124
1125#[derive(Debug, Clone)]
1126/// BenchmarkResult
1127pub struct BenchmarkResult {
1128    /// method_name
1129    pub method_name: String,
1130    /// dataset_name
1131    pub dataset_name: String,
1132    /// iteration
1133    pub iteration: usize,
1134    /// selected_features
1135    pub selected_features: Vec<usize>,
1136    /// n_features_selected
1137    pub n_features_selected: usize,
1138    /// n_features_total
1139    pub n_features_total: usize,
1140    /// selection_time
1141    pub selection_time: Duration,
1142    /// evaluation_time
1143    pub evaluation_time: Duration,
1144    /// memory_usage
1145    pub memory_usage: usize,
1146    /// evaluation_metrics
1147    pub evaluation_metrics: EvaluationMetrics,
1148    /// feature_metrics
1149    pub feature_metrics: FeatureMetrics,
1150    /// dataset_properties
1151    pub dataset_properties: DatasetProperties,
1152}
1153
1154#[derive(Debug, Clone, Default)]
1155/// EvaluationMetrics
1156pub struct EvaluationMetrics {
1157    /// relevance_score
1158    pub relevance_score: f64,
1159    /// redundancy_score
1160    pub redundancy_score: f64,
1161    /// predictive_score
1162    pub predictive_score: f64,
1163    /// jaccard_score
1164    pub jaccard_score: Option<f64>,
1165    /// precision
1166    pub precision: Option<f64>,
1167    /// recall
1168    pub recall: Option<f64>,
1169    /// f1_score
1170    pub f1_score: Option<f64>,
1171}
1172
1173#[derive(Debug, Clone, Default)]
1174/// FeatureMetrics
1175pub struct FeatureMetrics {
1176    /// selection_ratio
1177    pub selection_ratio: f64,
1178    /// importance_distribution
1179    pub importance_distribution: ImportanceDistribution,
1180}
1181
1182#[derive(Debug, Clone, Default)]
1183/// ImportanceDistribution
1184pub struct ImportanceDistribution {
1185    /// mean
1186    pub mean: f64,
1187    /// std
1188    pub std: f64,
1189    /// max
1190    pub max: f64,
1191    /// min
1192    pub min: f64,
1193}
1194
1195#[derive(Debug, Clone)]
1196/// BenchmarkSuiteResults
1197pub struct BenchmarkSuiteResults {
1198    /// individual_results
1199    pub individual_results: Vec<BenchmarkResult>,
1200    /// statistical_analysis
1201    pub statistical_analysis: StatisticalAnalysis,
1202    /// rankings
1203    pub rankings: Vec<MethodRanking>,
1204    /// execution_summary
1205    pub execution_summary: ExecutionSummary,
1206    /// configuration
1207    pub configuration: BenchmarkConfig,
1208}
1209
1210impl BenchmarkSuiteResults {
1211    /// Generate comprehensive benchmark report
1212    pub fn report(&self) -> String {
1213        let mut report = String::new();
1214
1215        report.push_str(
1216            "╔═══════════════════════════════════════════════════════════════════════════╗\n",
1217        );
1218        report.push_str(
1219            "║                    Feature Selection Benchmark Report                    ║\n",
1220        );
1221        report.push_str(
1222            "╚═══════════════════════════════════════════════════════════════════════════╝\n\n",
1223        );
1224
1225        // Execution summary
1226        report.push_str("=== Execution Summary ===\n");
1227        report.push_str(&format!(
1228            "Total Duration: {:.2}s\n",
1229            self.execution_summary.total_duration.as_secs_f64()
1230        ));
1231        report.push_str(&format!(
1232            "Datasets: {}\n",
1233            self.execution_summary.n_datasets
1234        ));
1235        report.push_str(&format!("Methods: {}\n", self.execution_summary.n_methods));
1236        report.push_str(&format!(
1237            "Total Runs: {}\n",
1238            self.execution_summary.n_total_runs
1239        ));
1240
1241        // Method rankings
1242        if !self.rankings.is_empty() {
1243            report.push_str("\n=== Method Rankings ===\n");
1244            for (i, ranking) in self.rankings.iter().take(10).enumerate() {
1245                report.push_str(&format!(
1246                    "{}. {} - Score: {:.4} ± {:.4}\n",
1247                    i + 1,
1248                    ranking.method_name,
1249                    ranking.mean_score,
1250                    ranking.std_score
1251                ));
1252            }
1253        }
1254
1255        // Best method details
1256        if let Some(best_method) = &self.statistical_analysis.overall_best_method {
1257            report.push_str(&format!("\n=== Best Method: {} ===\n", best_method));
1258
1259            if let Some(best_ranking) = self.rankings.first() {
1260                report.push_str(&format!(
1261                    "Mean Performance: {:.4}\n",
1262                    best_ranking.mean_score
1263                ));
1264                report.push_str(&format!(
1265                    "Consistency (StdDev): {:.4}\n",
1266                    best_ranking.std_score
1267                ));
1268                report.push_str(&format!("Runs: {}\n", best_ranking.scores.len()));
1269            }
1270        }
1271
1272        // Dataset difficulty analysis
1273        if !self.statistical_analysis.dataset_rankings.is_empty() {
1274            report.push_str("\n=== Dataset Difficulty Ranking ===\n");
1275            for (i, dataset) in self
1276                .statistical_analysis
1277                .dataset_rankings
1278                .iter()
1279                .take(5)
1280                .enumerate()
1281            {
1282                report.push_str(&format!(
1283                    "{}. {} - Difficulty: {:.4} (Avg Performance: {:.4})\n",
1284                    i + 1,
1285                    dataset.dataset_name,
1286                    dataset.difficulty_score,
1287                    dataset.mean_performance
1288                ));
1289            }
1290        }
1291
1292        // Performance summary
1293        report.push_str("\n=== Overall Performance Summary ===\n");
1294        let summary = &self.statistical_analysis.performance_summary;
1295        report.push_str(&format!(
1296            "Mean Score: {:.4} ± {:.4}\n",
1297            summary.mean_score, summary.std_score
1298        ));
1299        report.push_str(&format!(
1300            "Score Range: [{:.4}, {:.4}]\n",
1301            summary.min_score, summary.max_score
1302        ));
1303        report.push_str(&format!(
1304            "Total Time: {:.2}s\n",
1305            summary.total_execution_time.as_secs_f64()
1306        ));
1307        report.push_str(&format!(
1308            "Avg Features Selected: {:.1}\n",
1309            summary.avg_features_selected
1310        ));
1311
1312        report
1313    }
1314
1315    /// Export results to CSV format (simplified)
1316    pub fn to_csv(&self) -> String {
1317        let mut csv = String::new();
1318
1319        csv.push_str("Method,Dataset,Iteration,FeaturesSelected,SelectionTime,RelevanceScore,RedundancyScore,PredictiveScore\n");
1320
1321        for result in &self.individual_results {
1322            csv.push_str(&format!(
1323                "{},{},{},{},{:.6},{:.4},{:.4},{:.4}\n",
1324                result.method_name,
1325                result.dataset_name,
1326                result.iteration,
1327                result.n_features_selected,
1328                result.selection_time.as_secs_f64(),
1329                result.evaluation_metrics.relevance_score,
1330                result.evaluation_metrics.redundancy_score,
1331                result.evaluation_metrics.predictive_score
1332            ));
1333        }
1334
1335        csv
1336    }
1337}
1338
1339#[derive(Debug, Clone)]
1340/// StatisticalAnalysis
1341pub struct StatisticalAnalysis {
1342    /// method_rankings
1343    pub method_rankings: Vec<MethodRanking>,
1344    /// dataset_rankings
1345    pub dataset_rankings: Vec<DatasetRanking>,
1346    /// overall_best_method
1347    pub overall_best_method: Option<String>,
1348    /// performance_summary
1349    pub performance_summary: PerformanceSummary,
1350}
1351
1352#[derive(Debug, Clone)]
1353/// MethodRanking
1354pub struct MethodRanking {
1355    /// method_name
1356    pub method_name: String,
1357    /// mean_score
1358    pub mean_score: f64,
1359    /// std_score
1360    pub std_score: f64,
1361    /// scores
1362    pub scores: Vec<f64>,
1363}
1364
1365#[derive(Debug, Clone)]
1366/// DatasetRanking
1367pub struct DatasetRanking {
1368    /// dataset_name
1369    pub dataset_name: String,
1370    /// difficulty_score
1371    pub difficulty_score: f64,
1372    /// mean_performance
1373    pub mean_performance: f64,
1374}
1375
1376#[derive(Debug, Clone)]
1377/// PerformanceSummary
1378pub struct PerformanceSummary {
1379    /// mean_score
1380    pub mean_score: f64,
1381    /// std_score
1382    pub std_score: f64,
1383    /// min_score
1384    pub min_score: f64,
1385    /// max_score
1386    pub max_score: f64,
1387    /// total_execution_time
1388    pub total_execution_time: Duration,
1389    /// avg_features_selected
1390    pub avg_features_selected: f64,
1391}
1392
1393#[derive(Debug, Clone)]
1394/// ExecutionSummary
1395pub struct ExecutionSummary {
1396    /// total_duration
1397    pub total_duration: Duration,
1398    /// n_datasets
1399    pub n_datasets: usize,
1400    /// n_methods
1401    pub n_methods: usize,
1402    /// n_total_runs
1403    pub n_total_runs: usize,
1404}
1405
1406// Example benchmarkable method implementations
1407#[derive(Debug)]
1408/// UnivariateFilterMethod
1409pub struct UnivariateFilterMethod {
1410    k: usize,
1411}
1412
1413impl UnivariateFilterMethod {
1414    /// new
1415    pub fn new(k: usize) -> Self {
1416        Self { k }
1417    }
1418}
1419
1420impl BenchmarkableMethod for UnivariateFilterMethod {
1421    fn select_features(
1422        &self,
1423        X: ArrayView2<f64>,
1424        y: ArrayView1<f64>,
1425    ) -> std::result::Result<Vec<usize>, BenchmarkError> {
1426        let mut correlations: Vec<(usize, f64)> = Vec::new();
1427
1428        for i in 0..X.ncols() {
1429            let correlation = self.compute_correlation(X.column(i), y);
1430            correlations.push((i, correlation.abs()));
1431        }
1432
1433        correlations.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
1434
1435        Ok(correlations
1436            .into_iter()
1437            .take(self.k)
1438            .map(|(idx, _)| idx)
1439            .collect())
1440    }
1441
1442    fn method_name(&self) -> &str {
1443        "UnivariateFilter"
1444    }
1445
1446    fn method_params(&self) -> HashMap<String, String> {
1447        let mut params = HashMap::new();
1448        params.insert("k".to_string(), self.k.to_string());
1449        params
1450    }
1451}
1452
1453impl UnivariateFilterMethod {
1454    fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
1455        let n = x.len() as f64;
1456        if n < 2.0 {
1457            return 0.0;
1458        }
1459
1460        let mean_x = x.mean().unwrap_or(0.0);
1461        let mean_y = y.mean().unwrap_or(0.0);
1462
1463        let mut sum_xy = 0.0;
1464        let mut sum_x2 = 0.0;
1465        let mut sum_y2 = 0.0;
1466
1467        for i in 0..x.len() {
1468            let dx = x[i] - mean_x;
1469            let dy = y[i] - mean_y;
1470            sum_xy += dx * dy;
1471            sum_x2 += dx * dx;
1472            sum_y2 += dy * dy;
1473        }
1474
1475        let denom = (sum_x2 * sum_y2).sqrt();
1476        if denom < 1e-10 {
1477            0.0
1478        } else {
1479            sum_xy / denom
1480        }
1481    }
1482}
1483
1484#[derive(Debug)]
1485/// RandomSelectionMethod
1486pub struct RandomSelectionMethod {
1487    k: usize,
1488}
1489
1490impl RandomSelectionMethod {
1491    /// new
1492    pub fn new(k: usize) -> Self {
1493        Self { k }
1494    }
1495}
1496
1497impl BenchmarkableMethod for RandomSelectionMethod {
1498    fn select_features(
1499        &self,
1500        X: ArrayView2<f64>,
1501        _y: ArrayView1<f64>,
1502    ) -> std::result::Result<Vec<usize>, BenchmarkError> {
1503        let mut features: Vec<usize> = (0..X.ncols()).collect();
1504
1505        // Simple random shuffling
1506        for i in (1..features.len()).rev() {
1507            let j = (thread_rng().random::<f64>() * (i + 1) as f64) as usize;
1508            features.swap(i, j);
1509        }
1510
1511        Ok(features.into_iter().take(self.k.min(X.ncols())).collect())
1512    }
1513
1514    fn method_name(&self) -> &str {
1515        "RandomSelection"
1516    }
1517
1518    fn method_params(&self) -> HashMap<String, String> {
1519        let mut params = HashMap::new();
1520        params.insert("k".to_string(), self.k.to_string());
1521        params
1522    }
1523}
1524
1525#[allow(non_snake_case)]
1526#[cfg(test)]
1527mod tests {
1528    use super::*;
1529    use scirs2_core::ndarray::array;
1530
1531    #[test]
1532    fn test_synthetic_dataset_creation() {
1533        let benchmark = FeatureSelectionBenchmark::new();
1534
1535        let dataset = benchmark
1536            .create_synthetic_linear_dataset(100, 20, 5)
1537            .expect("operation should succeed");
1538
1539        assert_eq!(dataset.X.nrows(), 100);
1540        assert_eq!(dataset.X.ncols(), 20);
1541        assert_eq!(dataset.y.len(), 100);
1542        assert_eq!(
1543            dataset
1544                .ground_truth
1545                .as_ref()
1546                .expect("operation should succeed")
1547                .len(),
1548            5
1549        );
1550        assert_eq!(dataset.properties.n_informative_features, 5);
1551    }
1552
1553    #[test]
1554    #[allow(non_snake_case)]
1555    fn test_benchmarkable_method() {
1556        let method = UnivariateFilterMethod::new(5);
1557
1558        let X = array![
1559            [1.0, 2.0, 3.0, 0.1],
1560            [2.0, 3.0, 4.0, 0.2],
1561            [3.0, 4.0, 5.0, 0.3],
1562            [4.0, 5.0, 6.0, 0.4],
1563        ];
1564        let y = array![1.0, 2.0, 3.0, 4.0];
1565
1566        let selected = method
1567            .select_features(X.view(), y.view())
1568            .expect("operation should succeed");
1569
1570        assert!(!selected.is_empty());
1571        assert!(selected.len() <= 4); // Can't select more than available
1572        assert!(selected.iter().all(|&idx| idx < X.ncols()));
1573    }
1574
1575    #[test]
1576    #[allow(non_snake_case)]
1577    fn test_benchmark_execution() {
1578        let mut benchmark = FeatureSelectionBenchmark::new();
1579        benchmark.config.n_iterations = 1; // Fast test
1580        benchmark.config.verbose = false;
1581
1582        // Add simple dataset
1583        let X = array![
1584            [1.0, 2.0, 0.1, 0.2],
1585            [2.0, 3.0, 0.2, 0.3],
1586            [3.0, 4.0, 0.3, 0.4],
1587            [4.0, 5.0, 0.4, 0.5],
1588            [5.0, 6.0, 0.5, 0.6],
1589        ];
1590        let y = array![1.0, 2.0, 3.0, 4.0, 5.0];
1591
1592        let dataset = BenchmarkDataset {
1593            name: "TestDataset".to_string(),
1594            X,
1595            y,
1596            ground_truth: Some(vec![0, 1]),
1597            properties: DatasetProperties {
1598                task_type: TaskType::Regression,
1599                n_informative_features: 2,
1600                noise_level: 0.1,
1601                correlation_structure: CorrelationStructure::Linear,
1602                feature_types: vec![FeatureType::Continuous; 4],
1603            },
1604        };
1605
1606        benchmark
1607            .add_dataset(dataset)
1608            .expect("operation should succeed");
1609        benchmark.register_method(
1610            "UnivariateFilt".to_string(),
1611            Box::new(UnivariateFilterMethod::new(2)),
1612        );
1613        benchmark.register_method(
1614            "Random".to_string(),
1615            Box::new(RandomSelectionMethod::new(2)),
1616        );
1617
1618        let results = benchmark.run_benchmark().expect("operation should succeed");
1619
1620        assert_eq!(results.individual_results.len(), 2); // 2 methods
1621        assert_eq!(results.rankings.len(), 2);
1622        assert!(results.statistical_analysis.overall_best_method.is_some());
1623
1624        let report = results.report();
1625        assert!(report.contains("Benchmark Report"));
1626        assert!(report.contains("Method Rankings"));
1627
1628        let csv = results.to_csv();
1629        assert!(csv.contains("Method,Dataset"));
1630    }
1631
1632    #[test]
1633    #[allow(non_snake_case)]
1634    fn test_evaluation_metrics() {
1635        let benchmark = FeatureSelectionBenchmark::new();
1636
1637        let X = array![[1.0, 2.0, 0.1], [2.0, 3.0, 0.2], [3.0, 4.0, 0.3],];
1638        let y = array![1.0, 2.0, 3.0];
1639
1640        let selected_features = vec![0, 1];
1641        let ground_truth = Some(vec![0, 1]);
1642
1643        let metrics = benchmark
1644            .evaluate_feature_selection(
1645                X.view(),
1646                y.view(),
1647                X.view(),
1648                y.view(),
1649                &selected_features,
1650                &ground_truth,
1651            )
1652            .expect("operation should succeed");
1653
1654        assert!(metrics.relevance_score >= 0.0);
1655        assert!(metrics.redundancy_score >= 0.0);
1656        assert!(metrics.predictive_score >= 0.0);
1657        assert!(metrics.jaccard_score.is_some());
1658        assert!(metrics.precision.is_some());
1659        assert!(metrics.recall.is_some());
1660        assert!(metrics.f1_score.is_some());
1661    }
1662}