1use crate::base::FeatureSelector;
7use scirs2_core::ndarray::{Array2, ArrayBase, Axis, Dim, OwnedRepr};
8
9type 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
23pub 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 pub fn new() -> Self {
39 Self {
40 confidence_level: 0.95,
41 n_permutations: 1000,
42 random_state: None,
43 }
44 }
45
46 pub fn confidence_level(mut self, level: Float) -> Self {
48 self.confidence_level = level;
49 self
50 }
51
52 pub fn n_permutations(mut self, n: usize) -> Self {
54 self.n_permutations = n;
55 self
56 }
57
58 pub fn random_state(mut self, seed: u64) -> Self {
60 self.random_state = Some(seed);
61 self
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct StatisticalValidationResults {
68 pub test_name: String,
70 pub test_statistic: Float,
72 pub p_value: Float,
74 pub is_significant: bool,
76 pub confidence_level: Float,
78 pub effect_size: Option<Float>,
80 pub description: String,
82}
83
84impl StatisticalValidationResults {
85 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 pub fn with_effect_size(mut self, effect_size: Float) -> Self {
107 self.effect_size = Some(effect_size);
108 self
109 }
110}
111
112pub struct SelectionConsistencyTest;
114
115impl SelectionConsistencyTest {
116 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 for trial in 0..n_splits {
152 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 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 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 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 let test_statistic = mean_similarity;
204
205 let expected_random_similarity = 0.1; 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); 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 fn normal_cdf(x: Float) -> Float {
243 0.5 * (1.0 + Self::erf(x / 2.0_f64.sqrt()))
244 }
245
246 fn erf(x: Float) -> Float {
248 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
266pub struct PermutationSignificanceTest;
268
269impl PermutationSignificanceTest {
270 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 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 let actual_score = Self::compute_selection_score(features, selected_features)?;
297
298 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 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 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 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 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 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
377pub struct DistributionalPropertyTest;
379
380impl DistributionalPropertyTest {
381 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 let clustering_score = Self::compute_clustering_score(selected_features, features.ncols());
405
406 let variance_score = Self::compute_variance_score(features, selected_features)?;
408
409 let correlation_score = Self::compute_correlation_score(features, selected_features)?;
411
412 let combined_score = (clustering_score + variance_score + correlation_score) / 3.0;
414
415 let threshold = 0.5;
417 let p_value = if combined_score > threshold {
418 0.05 } else {
420 0.95 };
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 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 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 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 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 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 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
569pub struct RobustnessTest;
571
572impl RobustnessTest {
573 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 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 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 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 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 let robustness_score = stability_scores
638 .iter()
639 .map(|(_, similarity)| *similarity)
640 .sum::<Float>()
641 / stability_scores.len() as Float;
642
643 let threshold = 0.5; let p_value = if robustness_score > threshold {
646 0.01 } else {
648 0.99 };
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 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 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 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}