Skip to main content

sklears_feature_selection/
validation.rs

1//! Statistical validation framework for feature selection methods
2//!
3//! This module provides comprehensive statistical tests to validate the
4//! properties and quality of feature selection algorithms.
5
6use crate::base::FeatureSelector;
7use scirs2_core::ndarray::{Array2, ArrayBase, Axis, Dim, OwnedRepr};
8
9/// Fully expanded ndarray 0.17 owned 2D f64 matrix type.
10/// Using the full 3-parameter form bypasses Rust's trait-solver normalization failure
11/// with generic bounds that occurred in ndarray 0.17 when the default 3rd type param was added.
12type Mat2f64 = ArrayBase<OwnedRepr<f64>, Dim<[usize; 2]>, f64>;
13
14use scirs2_core::rand_prelude::SliceRandom;
15use scirs2_core::random::rngs::StdRng;
16use scirs2_core::random::{RngExt, SeedableRng};
17use sklears_core::{
18    error::{Result as SklResult, SklearsError},
19    traits::{Fit, Transform},
20    types::Float,
21};
22
23/// Statistical validity tests for feature selection methods
24pub struct StatisticalValidationFramework {
25    confidence_level: Float,
26    n_permutations: usize,
27    random_state: Option<u64>,
28}
29
30impl Default for StatisticalValidationFramework {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36impl StatisticalValidationFramework {
37    /// Create a new statistical validation framework
38    pub fn new() -> Self {
39        Self {
40            confidence_level: 0.95,
41            n_permutations: 1000,
42            random_state: None,
43        }
44    }
45
46    /// Set the confidence level for statistical tests
47    pub fn confidence_level(mut self, level: Float) -> Self {
48        self.confidence_level = level;
49        self
50    }
51
52    /// Set the number of permutations for permutation tests
53    pub fn n_permutations(mut self, n: usize) -> Self {
54        self.n_permutations = n;
55        self
56    }
57
58    /// Set random state for reproducibility
59    pub fn random_state(mut self, seed: u64) -> Self {
60        self.random_state = Some(seed);
61        self
62    }
63}
64
65/// Results from statistical validity tests
66#[derive(Debug, Clone)]
67pub struct StatisticalValidationResults {
68    /// test_name
69    pub test_name: String,
70    /// test_statistic
71    pub test_statistic: Float,
72    /// p_value
73    pub p_value: Float,
74    /// is_significant
75    pub is_significant: bool,
76    /// confidence_level
77    pub confidence_level: Float,
78    /// effect_size
79    pub effect_size: Option<Float>,
80    /// description
81    pub description: String,
82}
83
84impl StatisticalValidationResults {
85    /// new
86    pub fn new(
87        test_name: String,
88        test_statistic: Float,
89        p_value: Float,
90        confidence_level: Float,
91        description: String,
92    ) -> Self {
93        let is_significant = p_value < (1.0 - confidence_level);
94        Self {
95            test_name,
96            test_statistic,
97            p_value,
98            is_significant,
99            confidence_level,
100            effect_size: None,
101            description,
102        }
103    }
104
105    /// with_effect_size
106    pub fn with_effect_size(mut self, effect_size: Float) -> Self {
107        self.effect_size = Some(effect_size);
108        self
109    }
110}
111
112/// Test for selection consistency across data splits
113pub struct SelectionConsistencyTest;
114
115impl SelectionConsistencyTest {
116    /// Test if feature selection is consistent across random data splits
117    pub fn test_split_consistency<S, T>(
118        selector_factory: impl Fn() -> S + Clone,
119        features: &Array2<Float>,
120        target: &T,
121        n_splits: usize,
122        test_size: Float,
123        confidence_level: Float,
124    ) -> SklResult<StatisticalValidationResults>
125    where
126        S: Fit<Mat2f64, T> + Clone,
127        S::Fitted: FeatureSelector + Transform<Array2<Float>>,
128        T: Clone,
129    {
130        if n_splits < 2 {
131            return Err(SklearsError::InvalidInput(
132                "Need at least 2 splits for consistency test".to_string(),
133            ));
134        }
135
136        let n_samples = features.nrows();
137        let n_test = (n_samples as Float * test_size) as usize;
138        let n_train = n_samples - n_test;
139
140        if n_train == 0 || n_test == 0 {
141            return Err(SklearsError::InvalidInput(
142                "Insufficient data for train/test split".to_string(),
143            ));
144        }
145
146        let mut rng = StdRng::seed_from_u64(42);
147        let mut jaccard_similarities = Vec::new();
148        let mut selected_features_list = Vec::new();
149
150        // Perform multiple trials with slightly perturbed data
151        for trial in 0..n_splits {
152            // Add small amount of noise to test robustness
153            let mut perturbed_features = features.clone();
154            let noise_level = 0.01 * (trial + 1) as Float;
155
156            for mut row in perturbed_features.axis_iter_mut(Axis(0)) {
157                for element in row.iter_mut() {
158                    let u1: Float = rng.random::<Float>();
159                    let u2: Float = rng.random::<Float>();
160                    let noise = (-2.0f64 * u1.ln()).sqrt()
161                        * (2.0 * std::f64::consts::PI * u2).cos()
162                        * noise_level;
163                    *element += noise;
164                }
165            }
166
167            // Fit selector on perturbed data
168            let selector = selector_factory();
169            if let Ok(fitted_selector) = selector.fit(&perturbed_features, target) {
170                let selected = fitted_selector.selected_features().clone();
171                selected_features_list.push(selected);
172            }
173        }
174
175        // Compute pairwise Jaccard similarities
176        for i in 0..selected_features_list.len() {
177            for j in (i + 1)..selected_features_list.len() {
178                let similarity = Self::jaccard_similarity(
179                    &selected_features_list[i],
180                    &selected_features_list[j],
181                );
182                jaccard_similarities.push(similarity);
183            }
184        }
185
186        if jaccard_similarities.is_empty() {
187            return Err(SklearsError::InvalidInput(
188                "No valid feature selections obtained".to_string(),
189            ));
190        }
191
192        // Compute statistics
193        let mean_similarity: Float =
194            jaccard_similarities.iter().sum::<Float>() / jaccard_similarities.len() as Float;
195        let variance = jaccard_similarities
196            .iter()
197            .map(|&x| (x - mean_similarity) * (x - mean_similarity))
198            .sum::<Float>()
199            / (jaccard_similarities.len() - 1) as Float;
200        let std_dev = variance.sqrt();
201
202        // Test statistic: mean similarity (higher = more consistent)
203        let test_statistic = mean_similarity;
204
205        // Rough p-value based on how far from random expectation
206        // Random Jaccard similarity for feature sets is typically low
207        let expected_random_similarity = 0.1; // Rough estimate
208        let z_score = (mean_similarity - expected_random_similarity)
209            / (std_dev / (jaccard_similarities.len() as Float).sqrt());
210        let p_value = (2.0 * (1.0 - Self::normal_cdf(z_score.abs()))).max(0.001); // Two-tailed test
211
212        let description = format!(
213            "Selection consistency test across {} splits. Mean Jaccard similarity: {:.3}, Std: {:.3}",
214            n_splits, mean_similarity, std_dev
215        );
216
217        Ok(StatisticalValidationResults::new(
218            "Split Consistency Test".to_string(),
219            test_statistic,
220            p_value,
221            confidence_level,
222            description,
223        )
224        .with_effect_size(z_score))
225    }
226
227    fn jaccard_similarity(set1: &[usize], set2: &[usize]) -> Float {
228        let set1: std::collections::HashSet<usize> = set1.iter().cloned().collect();
229        let set2: std::collections::HashSet<usize> = set2.iter().cloned().collect();
230
231        let intersection = set1.intersection(&set2).count();
232        let union = set1.union(&set2).count();
233
234        if union == 0 {
235            1.0
236        } else {
237            intersection as Float / union as Float
238        }
239    }
240
241    /// Approximate normal CDF for p-value calculation
242    fn normal_cdf(x: Float) -> Float {
243        0.5 * (1.0 + Self::erf(x / 2.0_f64.sqrt()))
244    }
245
246    /// Approximate error function
247    fn erf(x: Float) -> Float {
248        // Abramowitz and Stegun approximation
249        let a1 = 0.254829592;
250        let a2 = -0.284496736;
251        let a3 = 1.421413741;
252        let a4 = -1.453152027;
253        let a5 = 1.061405429;
254        let p = 0.3275911;
255
256        let sign = if x < 0.0 { -1.0 } else { 1.0 };
257        let x = x.abs();
258
259        let t = 1.0 / (1.0 + p * x);
260        let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
261
262        sign * y
263    }
264}
265
266/// Test for feature selection significance using permutation testing
267pub struct PermutationSignificanceTest;
268
269impl PermutationSignificanceTest {
270    /// Test if selected features are significantly better than random
271    pub fn test_feature_significance<S, T>(
272        selector_factory: impl Fn() -> S + Clone,
273        features: &Array2<Float>,
274        target: &T,
275        n_permutations: usize,
276        confidence_level: Float,
277    ) -> SklResult<StatisticalValidationResults>
278    where
279        S: Fit<Mat2f64, T> + Clone,
280        S::Fitted: FeatureSelector + Transform<Array2<Float>>,
281        T: Clone,
282    {
283        // Get the actual feature selection
284        let selector = selector_factory();
285        let fitted_selector = selector.fit(features, target)?;
286        let selected_features = fitted_selector.selected_features();
287        let n_selected = selected_features.len();
288
289        if n_selected == 0 {
290            return Err(SklearsError::InvalidInput(
291                "No features were selected".to_string(),
292            ));
293        }
294
295        // Compute score for actual selection
296        let actual_score = Self::compute_selection_score(features, selected_features)?;
297
298        // Perform permutation test
299        let mut rng = StdRng::seed_from_u64(42);
300        let mut permutation_scores = Vec::new();
301        let n_features = features.ncols();
302
303        for _ in 0..n_permutations {
304            // Create random feature selection of same size
305            let mut random_features: Vec<usize> = (0..n_features).collect();
306            random_features.shuffle(&mut rng);
307            let random_selection = &random_features[..n_selected];
308
309            let random_score = Self::compute_selection_score(features, random_selection)?;
310            permutation_scores.push(random_score);
311        }
312
313        // Compute p-value
314        let n_better = permutation_scores
315            .iter()
316            .filter(|&&score| score >= actual_score)
317            .count();
318        let p_value = (n_better + 1) as Float / (n_permutations + 1) as Float;
319
320        // Effect size (standardized difference)
321        let mean_random: Float =
322            permutation_scores.iter().sum::<Float>() / permutation_scores.len() as Float;
323        let variance_random = permutation_scores
324            .iter()
325            .map(|&x| (x - mean_random) * (x - mean_random))
326            .sum::<Float>()
327            / (permutation_scores.len() - 1) as Float;
328        let std_random = variance_random.sqrt();
329
330        let effect_size = if std_random > 0.0 {
331            (actual_score - mean_random) / std_random
332        } else {
333            0.0
334        };
335
336        let description = format!(
337            "Permutation test with {} permutations. Actual score: {:.3}, Random mean: {:.3} ± {:.3}",
338            n_permutations, actual_score, mean_random, std_random
339        );
340
341        Ok(StatisticalValidationResults::new(
342            "Permutation Significance Test".to_string(),
343            actual_score,
344            p_value,
345            confidence_level,
346            description,
347        )
348        .with_effect_size(effect_size))
349    }
350
351    /// Compute a score for feature selection quality
352    fn compute_selection_score(
353        features: &Array2<Float>,
354        selected_features: &[usize],
355    ) -> SklResult<Float> {
356        if selected_features.is_empty() {
357            return Ok(0.0);
358        }
359
360        // Use variance as a simple quality metric
361        // Better selections should have higher variance (more informative features)
362        let mut total_variance = 0.0;
363        for &feature_idx in selected_features {
364            let column = features.column(feature_idx);
365            let mean = column.mean().unwrap_or(0.0);
366            let variance = column
367                .mapv(|x| (x - mean) * (x - mean))
368                .mean()
369                .unwrap_or(0.0);
370            total_variance += variance;
371        }
372
373        Ok(total_variance / selected_features.len() as Float)
374    }
375}
376
377/// Test for distributional properties of feature selection methods
378pub struct DistributionalPropertyTest;
379
380impl DistributionalPropertyTest {
381    /// Test if feature selection respects known data structure
382    pub fn test_structural_validity<S, T>(
383        selector_factory: impl Fn() -> S,
384        features: &Array2<Float>,
385        target: &T,
386        confidence_level: Float,
387    ) -> SklResult<StatisticalValidationResults>
388    where
389        S: Fit<Mat2f64, T>,
390        S::Fitted: FeatureSelector + Transform<Array2<Float>>,
391        T: Clone,
392    {
393        let selector = selector_factory();
394        let fitted_selector = selector.fit(features, target)?;
395        let selected_features = fitted_selector.selected_features();
396
397        if selected_features.is_empty() {
398            return Err(SklearsError::InvalidInput(
399                "No features were selected".to_string(),
400            ));
401        }
402
403        // Test 1: Check if selection is not overly clustered (spatial distribution)
404        let clustering_score = Self::compute_clustering_score(selected_features, features.ncols());
405
406        // Test 2: Check if selected features have reasonable variance
407        let variance_score = Self::compute_variance_score(features, selected_features)?;
408
409        // Test 3: Check for correlation structure
410        let correlation_score = Self::compute_correlation_score(features, selected_features)?;
411
412        // Combine scores into overall structural validity
413        let combined_score = (clustering_score + variance_score + correlation_score) / 3.0;
414
415        // Simple threshold-based test (could be improved with proper statistical testing)
416        let threshold = 0.5;
417        let p_value = if combined_score > threshold {
418            0.05 // Likely valid structure
419        } else {
420            0.95 // Likely invalid structure
421        };
422
423        let description = format!(
424            "Structural validity test. Clustering: {:.3}, Variance: {:.3}, Correlation: {:.3}, Combined: {:.3}",
425            clustering_score, variance_score, correlation_score, combined_score
426        );
427
428        Ok(StatisticalValidationResults::new(
429            "Structural Validity Test".to_string(),
430            combined_score,
431            p_value,
432            confidence_level,
433            description,
434        ))
435    }
436
437    fn compute_clustering_score(selected_features: &[usize], _n_features: usize) -> Float {
438        if selected_features.len() <= 1 {
439            return 1.0;
440        }
441
442        // Measure how evenly distributed the selected features are
443        let mut min_gaps = Vec::new();
444        let mut sorted_features = selected_features.to_vec();
445        sorted_features.sort();
446
447        for i in 0..sorted_features.len() - 1 {
448            let gap = sorted_features[i + 1] - sorted_features[i];
449            min_gaps.push(gap);
450        }
451
452        // Compute coefficient of variation of gaps
453        let mean_gap: Float = min_gaps.iter().sum::<usize>() as Float / min_gaps.len() as Float;
454        let gap_variance = min_gaps
455            .iter()
456            .map(|&gap| (gap as Float - mean_gap) * (gap as Float - mean_gap))
457            .sum::<Float>()
458            / min_gaps.len() as Float;
459        let gap_std = gap_variance.sqrt();
460
461        // Lower coefficient of variation = more evenly distributed = better
462        let cv = if mean_gap > 0.0 {
463            gap_std / mean_gap
464        } else {
465            0.0
466        };
467        (1.0 / (1.0 + cv)).min(1.0)
468    }
469
470    fn compute_variance_score(
471        features: &Array2<Float>,
472        selected_features: &[usize],
473    ) -> SklResult<Float> {
474        let mut total_variance = 0.0;
475        let mut count = 0;
476
477        for &feature_idx in selected_features {
478            let column = features.column(feature_idx);
479            let mean = column.mean().unwrap_or(0.0);
480            let variance = column
481                .mapv(|x| (x - mean) * (x - mean))
482                .mean()
483                .unwrap_or(0.0);
484
485            // Only count features with non-zero variance
486            if variance > 1e-12 {
487                total_variance += variance;
488                count += 1;
489            }
490        }
491
492        if count == 0 {
493            return Ok(0.0);
494        }
495
496        let avg_variance = total_variance / count as Float;
497
498        // Normalize to 0-1 scale (could be improved with better normalization)
499        Ok((avg_variance / (avg_variance + 1.0)).min(1.0))
500    }
501
502    fn compute_correlation_score(
503        features: &Array2<Float>,
504        selected_features: &[usize],
505    ) -> SklResult<Float> {
506        if selected_features.len() <= 1 {
507            return Ok(1.0);
508        }
509
510        let mut correlation_sum = 0.0;
511        let mut pair_count = 0;
512
513        for i in 0..selected_features.len() {
514            for j in (i + 1)..selected_features.len() {
515                let col1 = features.column(selected_features[i]);
516                let col2 = features.column(selected_features[j]);
517
518                let correlation = Self::compute_correlation(&col1, &col2);
519                correlation_sum += correlation.abs();
520                pair_count += 1;
521            }
522        }
523
524        if pair_count == 0 {
525            return Ok(1.0);
526        }
527
528        let avg_correlation = correlation_sum / pair_count as Float;
529
530        // Good feature selection should have moderate correlation (not too high, not too low)
531        let optimal_correlation = 0.3;
532        let score = 1.0 - (avg_correlation - optimal_correlation).abs() / optimal_correlation;
533        Ok(score.max(0.0))
534    }
535
536    fn compute_correlation(
537        x: &scirs2_core::ndarray::ArrayView1<Float>,
538        y: &scirs2_core::ndarray::ArrayView1<Float>,
539    ) -> Float {
540        let n = x.len() as Float;
541        if n <= 1.0 {
542            return 0.0;
543        }
544
545        let mean_x = x.mean().unwrap_or(0.0);
546        let mean_y = y.mean().unwrap_or(0.0);
547
548        let mut cov = 0.0;
549        let mut var_x = 0.0;
550        let mut var_y = 0.0;
551
552        for (&xi, &yi) in x.iter().zip(y.iter()) {
553            let dx = xi - mean_x;
554            let dy = yi - mean_y;
555            cov += dx * dy;
556            var_x += dx * dx;
557            var_y += dy * dy;
558        }
559
560        let denom = (var_x * var_y).sqrt();
561        if denom > 1e-12 {
562            cov / denom
563        } else {
564            0.0
565        }
566    }
567}
568
569/// Test for robustness of feature selection to noise
570pub struct RobustnessTest;
571
572impl RobustnessTest {
573    /// Test robustness to additive Gaussian noise
574    pub fn test_noise_robustness<S, T>(
575        selector_factory: impl Fn() -> S + Clone,
576        features: &Array2<Float>,
577        target: &T,
578        noise_levels: &[Float],
579        n_trials: usize,
580        confidence_level: Float,
581    ) -> SklResult<StatisticalValidationResults>
582    where
583        S: Fit<Mat2f64, T> + Clone,
584        S::Fitted: FeatureSelector + Transform<Array2<Float>>,
585        T: Clone,
586    {
587        // Get baseline selection without noise
588        let selector = selector_factory();
589        let baseline_selection = selector.fit(features, target)?;
590        let baseline_features = baseline_selection.selected_features().clone();
591
592        let mut stability_scores = Vec::new();
593        let mut rng = StdRng::seed_from_u64(42);
594
595        for &noise_level in noise_levels {
596            let mut trial_similarities = Vec::new();
597
598            for _ in 0..n_trials {
599                // Add Gaussian noise to features
600                let mut noisy_features = features.clone();
601                for mut row in noisy_features.axis_iter_mut(Axis(0)) {
602                    for element in row.iter_mut() {
603                        // Simple Box-Muller transform for Gaussian noise
604                        let u1: Float = rng.random::<Float>();
605                        let u2: Float = rng.random::<Float>();
606                        let noise = (-2.0f64 * u1.ln()).sqrt()
607                            * (2.0 * std::f64::consts::PI * u2).cos()
608                            * noise_level;
609                        *element += noise;
610                    }
611                }
612
613                // Fit selector on noisy data
614                let noisy_selector = selector_factory();
615                if let Ok(noisy_fitted) = noisy_selector.fit(&noisy_features, target) {
616                    let noisy_features_selected = noisy_fitted.selected_features();
617                    let similarity =
618                        Self::jaccard_similarity(&baseline_features, noisy_features_selected);
619                    trial_similarities.push(similarity);
620                }
621            }
622
623            if !trial_similarities.is_empty() {
624                let mean_similarity: Float =
625                    trial_similarities.iter().sum::<Float>() / trial_similarities.len() as Float;
626                stability_scores.push((noise_level, mean_similarity));
627            }
628        }
629
630        if stability_scores.is_empty() {
631            return Err(SklearsError::InvalidInput(
632                "No successful trials completed".to_string(),
633            ));
634        }
635
636        // Compute robustness score (area under stability curve)
637        let robustness_score = stability_scores
638            .iter()
639            .map(|(_, similarity)| *similarity)
640            .sum::<Float>()
641            / stability_scores.len() as Float;
642
643        // Simple threshold-based significance test
644        let threshold = 0.5; // Robust methods should maintain >50% similarity
645        let p_value = if robustness_score > threshold {
646            0.01 // Highly robust
647        } else {
648            0.99 // Not robust
649        };
650
651        let description = format!(
652            "Noise robustness test across {} noise levels with {} trials each. Average stability: {:.3}",
653            noise_levels.len(), n_trials, robustness_score
654        );
655
656        Ok(StatisticalValidationResults::new(
657            "Noise Robustness Test".to_string(),
658            robustness_score,
659            p_value,
660            confidence_level,
661            description,
662        ))
663    }
664
665    fn jaccard_similarity(set1: &[usize], set2: &[usize]) -> Float {
666        let set1: std::collections::HashSet<usize> = set1.iter().cloned().collect();
667        let set2: std::collections::HashSet<usize> = set2.iter().cloned().collect();
668
669        let intersection = set1.intersection(&set2).count();
670        let union = set1.union(&set2).count();
671
672        if union == 0 {
673            1.0
674        } else {
675            intersection as Float / union as Float
676        }
677    }
678}
679
680#[allow(non_snake_case)]
681#[cfg(test)]
682mod tests {
683    use super::*;
684    use crate::filter::SelectKBest;
685    use scirs2_core::ndarray::Array2;
686    use sklears_core::prelude::Array1;
687
688    fn create_test_data() -> (Array2<Float>, Array1<i32>) {
689        // Create synthetic data for testing
690        let n_samples = 100;
691        let n_features = 20;
692        let mut features = Array2::zeros((n_samples, n_features));
693        let mut target = Array1::zeros(n_samples);
694
695        // Create structured data with some signal
696        for i in 0..n_samples {
697            for j in 0..n_features {
698                features[[i, j]] = (i as Float * 0.1 + j as Float * 0.01).sin();
699            }
700            // Create binary classification target based on first few features
701            let score = features[[i, 0]] + 0.5 * features[[i, 1]] + 0.2 * features[[i, 2]];
702            target[i] = if score > 0.0 { 1 } else { 0 };
703        }
704
705        (features, target)
706    }
707
708    #[test]
709    fn test_selection_consistency() {
710        let (features, target) = create_test_data();
711
712        let selector_factory = || SelectKBest::new(5, "f_classif");
713
714        let result = SelectionConsistencyTest::test_split_consistency(
715            selector_factory,
716            &features,
717            &target,
718            5,
719            0.3,
720            0.95,
721        );
722
723        if let Err(e) = &result {
724            eprintln!("Error in selection consistency test: {:?}", e);
725        }
726        assert!(result.is_ok());
727        let validation_result = result.expect("operation should succeed");
728        assert_eq!(validation_result.test_name, "Split Consistency Test");
729        assert!(validation_result.test_statistic >= 0.0);
730        assert!(validation_result.test_statistic <= 1.0);
731    }
732
733    #[test]
734    fn test_permutation_significance() {
735        let (features, target) = create_test_data();
736
737        let selector_factory = || SelectKBest::new(3, "f_classif");
738
739        let result = PermutationSignificanceTest::test_feature_significance(
740            selector_factory,
741            &features,
742            &target,
743            100,
744            0.95,
745        );
746
747        assert!(result.is_ok());
748        let validation_result = result.expect("operation should succeed");
749        assert_eq!(validation_result.test_name, "Permutation Significance Test");
750        assert!(validation_result.p_value >= 0.0);
751        assert!(validation_result.p_value <= 1.0);
752    }
753
754    #[test]
755    fn test_structural_validity() {
756        let (features, target) = create_test_data();
757
758        let selector_factory = || SelectKBest::new(5, "f_classif");
759
760        let result = DistributionalPropertyTest::test_structural_validity(
761            selector_factory,
762            &features,
763            &target,
764            0.95,
765        );
766
767        assert!(result.is_ok());
768        let validation_result = result.expect("operation should succeed");
769        assert_eq!(validation_result.test_name, "Structural Validity Test");
770        assert!(validation_result.test_statistic >= 0.0);
771        assert!(validation_result.test_statistic <= 1.0);
772    }
773
774    #[test]
775    fn test_noise_robustness() {
776        let (features, target) = create_test_data();
777
778        let selector_factory = || SelectKBest::new(3, "f_classif");
779
780        let noise_levels = vec![0.1, 0.2, 0.5];
781        let result = RobustnessTest::test_noise_robustness(
782            selector_factory,
783            &features,
784            &target,
785            &noise_levels,
786            10,
787            0.95,
788        );
789
790        assert!(result.is_ok());
791        let validation_result = result.expect("operation should succeed");
792        assert_eq!(validation_result.test_name, "Noise Robustness Test");
793        assert!(validation_result.test_statistic >= 0.0);
794        assert!(validation_result.test_statistic <= 1.0);
795    }
796
797    #[test]
798    fn test_statistical_validation_framework() {
799        let framework = StatisticalValidationFramework::new()
800            .confidence_level(0.99)
801            .n_permutations(500)
802            .random_state(123);
803
804        assert_eq!(framework.confidence_level, 0.99);
805        assert_eq!(framework.n_permutations, 500);
806        assert_eq!(framework.random_state, Some(123));
807    }
808
809    #[test]
810    fn test_validation_results() {
811        let result = StatisticalValidationResults::new(
812            "Test".to_string(),
813            0.75,
814            0.03,
815            0.95,
816            "Test description".to_string(),
817        )
818        .with_effect_size(1.2);
819
820        assert_eq!(result.test_name, "Test");
821        assert_eq!(result.test_statistic, 0.75);
822        assert_eq!(result.p_value, 0.03);
823        assert!(result.is_significant);
824        assert_eq!(result.effect_size, Some(1.2));
825    }
826}