1use scirs2_core::ndarray::{Array2, ArrayView1, ArrayView2};
8use sklears_core::error::{Result as SklResult, SklearsError};
9type Result<T> = SklResult<T>;
10
11impl From<RedundancyError> for SklearsError {
12 fn from(err: RedundancyError) -> Self {
13 SklearsError::FitError(format!("Redundancy analysis error: {}", err))
14 }
15}
16use std::collections::HashMap;
17use thiserror::Error;
18
19#[derive(Debug, Error)]
20pub enum RedundancyError {
21 #[error("Feature matrix is empty")]
22 EmptyFeatureMatrix,
23 #[error("Feature indices must be valid")]
24 InvalidFeatureIndices,
25 #[error("Insufficient variance for redundancy computation")]
26 InsufficientVariance,
27 #[error("Correlation matrix computation failed")]
28 CorrelationComputationFailed,
29}
30
31#[derive(Debug, Clone)]
33pub struct CorrelationRedundancy {
34 correlation_threshold: f64,
35 absolute_correlation: bool,
36}
37
38impl CorrelationRedundancy {
39 pub fn new(correlation_threshold: f64, absolute_correlation: bool) -> Self {
41 Self {
42 correlation_threshold,
43 absolute_correlation,
44 }
45 }
46
47 pub fn compute(&self, X: ArrayView2<f64>, feature_indices: &[usize]) -> Result<f64> {
49 if feature_indices.len() < 2 {
50 return Ok(0.0); }
52
53 if X.is_empty() {
54 return Err(RedundancyError::EmptyFeatureMatrix.into());
55 }
56
57 let selected_features = self.extract_features(X, feature_indices)?;
59
60 let correlation_matrix = self.compute_correlation_matrix(&selected_features)?;
62
63 self.calculate_redundancy_score(&correlation_matrix)
65 }
66
67 fn extract_features(
69 &self,
70 X: ArrayView2<f64>,
71 feature_indices: &[usize],
72 ) -> Result<Array2<f64>> {
73 let n_samples = X.nrows();
74 let mut selected = Array2::zeros((n_samples, feature_indices.len()));
75
76 for (col_idx, &feature_idx) in feature_indices.iter().enumerate() {
77 if feature_idx >= X.ncols() {
78 return Err(RedundancyError::InvalidFeatureIndices.into());
79 }
80 selected.column_mut(col_idx).assign(&X.column(feature_idx));
81 }
82
83 Ok(selected)
84 }
85
86 fn compute_correlation_matrix(&self, features: &Array2<f64>) -> Result<Array2<f64>> {
88 let n_features = features.ncols();
89 let mut correlation_matrix = Array2::zeros((n_features, n_features));
90
91 for i in 0..n_features {
92 for j in 0..n_features {
93 if i == j {
94 correlation_matrix[[i, j]] = 1.0;
95 } else {
96 let feature1 = features.column(i);
97 let feature2 = features.column(j);
98 let correlation = self.compute_correlation(feature1, feature2)?;
99 correlation_matrix[[i, j]] = if self.absolute_correlation {
100 correlation.abs()
101 } else {
102 correlation
103 };
104 }
105 }
106 }
107
108 Ok(correlation_matrix)
109 }
110
111 fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> Result<f64> {
113 let n = x.len() as f64;
114 if n < 2.0 {
115 return Ok(0.0);
116 }
117
118 let mean_x = x.mean().unwrap_or(0.0);
119 let mean_y = y.mean().unwrap_or(0.0);
120
121 let mut sum_xy = 0.0;
122 let mut sum_x2 = 0.0;
123 let mut sum_y2 = 0.0;
124
125 for i in 0..x.len() {
126 let dx = x[i] - mean_x;
127 let dy = y[i] - mean_y;
128 sum_xy += dx * dy;
129 sum_x2 += dx * dx;
130 sum_y2 += dy * dy;
131 }
132
133 let denom = (sum_x2 * sum_y2).sqrt();
134 if denom < 1e-10 {
135 return Ok(0.0);
136 }
137
138 Ok(sum_xy / denom)
139 }
140
141 fn calculate_redundancy_score(&self, correlation_matrix: &Array2<f64>) -> Result<f64> {
143 let n_features = correlation_matrix.nrows();
144 if n_features < 2 {
145 return Ok(0.0);
146 }
147
148 let mut total_redundancy = 0.0;
149 let mut _pair_count = 0;
150
151 for i in 0..n_features {
153 for j in (i + 1)..n_features {
154 let correlation = correlation_matrix[[i, j]];
155 if correlation.abs() >= self.correlation_threshold {
156 total_redundancy += correlation.abs();
157 _pair_count += 1;
158 }
159 }
160 }
161
162 let total_pairs = (n_features * (n_features - 1)) / 2;
163 Ok(total_redundancy / total_pairs as f64)
164 }
165
166 pub fn identify_redundant_pairs(
168 &self,
169 X: ArrayView2<f64>,
170 feature_indices: &[usize],
171 ) -> Result<Vec<(usize, usize, f64)>> {
172 let selected_features = self.extract_features(X, feature_indices)?;
173 let correlation_matrix = self.compute_correlation_matrix(&selected_features)?;
174
175 let mut redundant_pairs = Vec::new();
176
177 for i in 0..feature_indices.len() {
178 for j in (i + 1)..feature_indices.len() {
179 let correlation = correlation_matrix[[i, j]];
180 if correlation.abs() >= self.correlation_threshold {
181 redundant_pairs.push((feature_indices[i], feature_indices[j], correlation));
182 }
183 }
184 }
185
186 redundant_pairs.sort_by(|a, b| {
188 b.2.abs()
189 .partial_cmp(&a.2.abs())
190 .expect("operation should succeed")
191 });
192
193 Ok(redundant_pairs)
194 }
195}
196
197#[derive(Debug, Clone)]
199pub struct MutualInformationRedundancy {
200 mi_threshold: f64,
201 n_bins: usize,
202}
203
204impl MutualInformationRedundancy {
205 pub fn new(mi_threshold: f64, n_bins: usize) -> Self {
207 Self {
208 mi_threshold,
209 n_bins,
210 }
211 }
212
213 pub fn compute(&self, X: ArrayView2<f64>, feature_indices: &[usize]) -> Result<f64> {
215 if feature_indices.len() < 2 {
216 return Ok(0.0);
217 }
218
219 let selected_features = self.extract_features(X, feature_indices)?;
220 let mi_matrix = self.compute_mi_matrix(&selected_features)?;
221
222 self.calculate_mi_redundancy_score(&mi_matrix)
223 }
224
225 fn extract_features(
226 &self,
227 X: ArrayView2<f64>,
228 feature_indices: &[usize],
229 ) -> Result<Array2<f64>> {
230 let n_samples = X.nrows();
231 let mut selected = Array2::zeros((n_samples, feature_indices.len()));
232
233 for (col_idx, &feature_idx) in feature_indices.iter().enumerate() {
234 if feature_idx >= X.ncols() {
235 return Err(RedundancyError::InvalidFeatureIndices.into());
236 }
237 selected.column_mut(col_idx).assign(&X.column(feature_idx));
238 }
239
240 Ok(selected)
241 }
242
243 fn compute_mi_matrix(&self, features: &Array2<f64>) -> Result<Array2<f64>> {
245 let n_features = features.ncols();
246 let mut mi_matrix = Array2::zeros((n_features, n_features));
247
248 for i in 0..n_features {
249 for j in 0..n_features {
250 if i == j {
251 mi_matrix[[i, j]] = self.compute_entropy(features.column(i))?;
252 } else {
253 let mi =
254 self.compute_mutual_information(features.column(i), features.column(j))?;
255 mi_matrix[[i, j]] = mi;
256 }
257 }
258 }
259
260 Ok(mi_matrix)
261 }
262
263 fn compute_entropy(&self, feature: ArrayView1<f64>) -> Result<f64> {
265 let histogram = self.create_histogram(feature);
266 let total_count = feature.len() as f64;
267
268 let mut entropy = 0.0;
269 for &count in histogram.values() {
270 if count > 0 {
271 let probability = count as f64 / total_count;
272 entropy -= probability * probability.ln();
273 }
274 }
275
276 Ok(entropy)
277 }
278
279 fn compute_mutual_information(
281 &self,
282 feature1: ArrayView1<f64>,
283 feature2: ArrayView1<f64>,
284 ) -> Result<f64> {
285 let hist1 = self.create_histogram(feature1);
286 let hist2 = self.create_histogram(feature2);
287 let joint_hist = self.create_joint_histogram(feature1, feature2);
288
289 let n = feature1.len() as f64;
290 let mut mi = 0.0;
291
292 for (&(val1, val2), &joint_count) in joint_hist.iter() {
293 if joint_count > 0 {
294 let p_xy = joint_count as f64 / n;
295 let p_x = *hist1.get(&val1).unwrap_or(&0) as f64 / n;
296 let p_y = *hist2.get(&val2).unwrap_or(&0) as f64 / n;
297
298 if p_x > 0.0 && p_y > 0.0 {
299 mi += p_xy * (p_xy / (p_x * p_y)).ln();
300 }
301 }
302 }
303
304 Ok(mi)
305 }
306
307 fn create_histogram(&self, feature: ArrayView1<f64>) -> HashMap<i32, usize> {
309 let min_val = feature.iter().fold(f64::INFINITY, |acc, &x| acc.min(x));
310 let max_val = feature.iter().fold(f64::NEG_INFINITY, |acc, &x| acc.max(x));
311 let bin_width = (max_val - min_val) / self.n_bins as f64;
312
313 let mut histogram = HashMap::new();
314
315 for &value in feature.iter() {
316 let bin = if bin_width > 0.0 {
317 ((value - min_val) / bin_width).floor() as i32
318 } else {
319 0
320 };
321 let bin = bin.min((self.n_bins - 1) as i32).max(0);
322 *histogram.entry(bin).or_insert(0) += 1;
323 }
324
325 histogram
326 }
327
328 fn create_joint_histogram(
330 &self,
331 feature1: ArrayView1<f64>,
332 feature2: ArrayView1<f64>,
333 ) -> HashMap<(i32, i32), usize> {
334 let min1 = feature1.iter().fold(f64::INFINITY, |acc, &x| acc.min(x));
335 let max1 = feature1
336 .iter()
337 .fold(f64::NEG_INFINITY, |acc, &x| acc.max(x));
338 let min2 = feature2.iter().fold(f64::INFINITY, |acc, &x| acc.min(x));
339 let max2 = feature2
340 .iter()
341 .fold(f64::NEG_INFINITY, |acc, &x| acc.max(x));
342
343 let bin_width1 = (max1 - min1) / self.n_bins as f64;
344 let bin_width2 = (max2 - min2) / self.n_bins as f64;
345
346 let mut joint_histogram = HashMap::new();
347
348 for i in 0..feature1.len() {
349 let bin1 = if bin_width1 > 0.0 {
350 ((feature1[i] - min1) / bin_width1).floor() as i32
351 } else {
352 0
353 };
354 let bin2 = if bin_width2 > 0.0 {
355 ((feature2[i] - min2) / bin_width2).floor() as i32
356 } else {
357 0
358 };
359
360 let bin1 = bin1.min((self.n_bins - 1) as i32).max(0);
361 let bin2 = bin2.min((self.n_bins - 1) as i32).max(0);
362
363 *joint_histogram.entry((bin1, bin2)).or_insert(0) += 1;
364 }
365
366 joint_histogram
367 }
368
369 fn calculate_mi_redundancy_score(&self, mi_matrix: &Array2<f64>) -> Result<f64> {
370 let n_features = mi_matrix.nrows();
371 if n_features < 2 {
372 return Ok(0.0);
373 }
374
375 let mut total_redundancy = 0.0;
376 let mut _pair_count = 0;
377
378 for i in 0..n_features {
379 for j in (i + 1)..n_features {
380 let mi = mi_matrix[[i, j]];
381 if mi >= self.mi_threshold {
382 total_redundancy += mi;
383 _pair_count += 1;
384 }
385 }
386 }
387
388 let total_pairs = (n_features * (n_features - 1)) / 2;
389 Ok(total_redundancy / total_pairs as f64)
390 }
391}
392
393#[derive(Debug, Clone)]
395pub struct VarianceInflationFactor {
396 vif_threshold: f64,
397}
398
399impl VarianceInflationFactor {
400 pub fn new(vif_threshold: f64) -> Self {
402 Self { vif_threshold }
403 }
404
405 pub fn compute_all(&self, X: ArrayView2<f64>, feature_indices: &[usize]) -> Result<Vec<f64>> {
407 if feature_indices.len() < 2 {
408 return Ok(vec![1.0; feature_indices.len()]);
409 }
410
411 let selected_features = self.extract_features(X, feature_indices)?;
412 let mut vif_scores = Vec::with_capacity(feature_indices.len());
413
414 for i in 0..feature_indices.len() {
415 let vif = self.compute_single_vif(&selected_features, i)?;
416 vif_scores.push(vif);
417 }
418
419 Ok(vif_scores)
420 }
421
422 fn extract_features(
423 &self,
424 X: ArrayView2<f64>,
425 feature_indices: &[usize],
426 ) -> Result<Array2<f64>> {
427 let n_samples = X.nrows();
428 let mut selected = Array2::zeros((n_samples, feature_indices.len()));
429
430 for (col_idx, &feature_idx) in feature_indices.iter().enumerate() {
431 if feature_idx >= X.ncols() {
432 return Err(RedundancyError::InvalidFeatureIndices.into());
433 }
434 selected.column_mut(col_idx).assign(&X.column(feature_idx));
435 }
436
437 Ok(selected)
438 }
439
440 fn compute_single_vif(&self, features: &Array2<f64>, target_feature_idx: usize) -> Result<f64> {
442 if target_feature_idx >= features.ncols() {
443 return Err(RedundancyError::InvalidFeatureIndices.into());
444 }
445
446 let n_features = features.ncols();
447 if n_features < 2 {
448 return Ok(1.0);
449 }
450
451 let target_feature = features.column(target_feature_idx);
453
454 let mut other_features = Array2::zeros((features.nrows(), n_features - 1));
456 let mut col_idx = 0;
457
458 for i in 0..n_features {
459 if i != target_feature_idx {
460 other_features
461 .column_mut(col_idx)
462 .assign(&features.column(i));
463 col_idx += 1;
464 }
465 }
466
467 let r_squared = self.compute_r_squared(target_feature, other_features.view())?;
469
470 if (1.0 - r_squared).abs() < 1e-10 {
472 return Ok(f64::INFINITY);
473 }
474
475 Ok(1.0 / (1.0 - r_squared))
476 }
477
478 fn compute_r_squared(&self, target: ArrayView1<f64>, features: ArrayView2<f64>) -> Result<f64> {
480 if features.ncols() == 0 {
481 return Ok(0.0);
482 }
483
484 let mut max_r_squared: f64 = 0.0;
486
487 for i in 0..features.ncols() {
488 let feature = features.column(i);
489 let correlation = self.compute_correlation(target, feature)?;
490 max_r_squared = max_r_squared.max(correlation * correlation);
491 }
492
493 Ok(max_r_squared)
494 }
495
496 fn compute_correlation(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> Result<f64> {
497 let n = x.len() as f64;
498 if n < 2.0 {
499 return Ok(0.0);
500 }
501
502 let mean_x = x.mean().unwrap_or(0.0);
503 let mean_y = y.mean().unwrap_or(0.0);
504
505 let mut sum_xy = 0.0;
506 let mut sum_x2 = 0.0;
507 let mut sum_y2 = 0.0;
508
509 for i in 0..x.len() {
510 let dx = x[i] - mean_x;
511 let dy = y[i] - mean_y;
512 sum_xy += dx * dy;
513 sum_x2 += dx * dx;
514 sum_y2 += dy * dy;
515 }
516
517 let denom = (sum_x2 * sum_y2).sqrt();
518 if denom < 1e-10 {
519 return Ok(0.0);
520 }
521
522 Ok(sum_xy / denom)
523 }
524
525 pub fn identify_high_vif_features(
527 &self,
528 X: ArrayView2<f64>,
529 feature_indices: &[usize],
530 ) -> Result<Vec<(usize, f64)>> {
531 let vif_scores = self.compute_all(X, feature_indices)?;
532 let mut high_vif_features = Vec::new();
533
534 for (i, &vif) in vif_scores.iter().enumerate() {
535 if vif >= self.vif_threshold {
536 high_vif_features.push((feature_indices[i], vif));
537 }
538 }
539
540 high_vif_features.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
542
543 Ok(high_vif_features)
544 }
545}
546
547#[derive(Debug, Clone)]
549pub struct RedundancyMatrix {
550 correlation_threshold: f64,
551 mi_threshold: f64,
552 vif_threshold: f64,
553 n_bins: usize,
554}
555
556impl RedundancyMatrix {
557 pub fn new(
559 correlation_threshold: f64,
560 mi_threshold: f64,
561 vif_threshold: f64,
562 n_bins: usize,
563 ) -> Self {
564 Self {
565 correlation_threshold,
566 mi_threshold,
567 vif_threshold,
568 n_bins,
569 }
570 }
571
572 pub fn compute(
574 &self,
575 X: ArrayView2<f64>,
576 feature_indices: &[usize],
577 ) -> Result<RedundancyAssessment> {
578 let correlation_redundancy = CorrelationRedundancy::new(self.correlation_threshold, true);
579 let mi_redundancy = MutualInformationRedundancy::new(self.mi_threshold, self.n_bins);
580 let vif_analyzer = VarianceInflationFactor::new(self.vif_threshold);
581
582 let correlation_score = correlation_redundancy.compute(X, feature_indices)?;
583 let redundant_pairs =
584 correlation_redundancy.identify_redundant_pairs(X, feature_indices)?;
585
586 let mi_score = mi_redundancy.compute(X, feature_indices)?;
587
588 let vif_scores = vif_analyzer.compute_all(X, feature_indices)?;
589 let high_vif_features = vif_analyzer.identify_high_vif_features(X, feature_indices)?;
590
591 Ok(RedundancyAssessment {
592 correlation_redundancy_score: correlation_score,
593 mutual_information_redundancy_score: mi_score,
594 average_vif: vif_scores.iter().sum::<f64>() / vif_scores.len() as f64,
595 max_vif: vif_scores.iter().fold(0.0, |acc, &x| acc.max(x)),
596 redundant_correlation_pairs: redundant_pairs,
597 high_vif_features,
598 vif_scores,
599 n_features: feature_indices.len(),
600 })
601 }
602}
603
604#[derive(Debug, Clone)]
606pub struct RedundancyAssessment {
607 pub correlation_redundancy_score: f64,
609 pub mutual_information_redundancy_score: f64,
611 pub average_vif: f64,
613 pub max_vif: f64,
615 pub redundant_correlation_pairs: Vec<(usize, usize, f64)>,
617 pub high_vif_features: Vec<(usize, f64)>,
619 pub vif_scores: Vec<f64>,
621 pub n_features: usize,
623}
624
625impl RedundancyAssessment {
626 pub fn report(&self) -> String {
628 let mut report = String::new();
629
630 report.push_str("=== Feature Set Redundancy Assessment ===\n\n");
631
632 report.push_str(&format!(
633 "Number of features analyzed: {}\n\n",
634 self.n_features
635 ));
636
637 report.push_str(&format!(
638 "Correlation Redundancy Score: {:.4}\n",
639 self.correlation_redundancy_score
640 ));
641 report.push_str(&format!(
642 " Interpretation: {}\n",
643 self.interpret_correlation_redundancy()
644 ));
645
646 report.push_str(&format!(
647 "\nMutual Information Redundancy Score: {:.4}\n",
648 self.mutual_information_redundancy_score
649 ));
650 report.push_str(&format!(
651 " Interpretation: {}\n",
652 self.interpret_mi_redundancy()
653 ));
654
655 report.push_str("\nVariance Inflation Factor Analysis:\n");
656 report.push_str(&format!(" Average VIF: {:.4}\n", self.average_vif));
657 report.push_str(&format!(" Maximum VIF: {:.4}\n", self.max_vif));
658 report.push_str(&format!(" Interpretation: {}\n", self.interpret_vif()));
659
660 if !self.redundant_correlation_pairs.is_empty() {
661 report.push_str(&format!(
662 "\nHighly Correlated Feature Pairs ({}):\n",
663 self.redundant_correlation_pairs.len()
664 ));
665 for (i, &(feat1, feat2, corr)) in
666 self.redundant_correlation_pairs.iter().take(10).enumerate()
667 {
668 report.push_str(&format!(
669 " {}. Features {} and {}: correlation = {:.4}\n",
670 i + 1,
671 feat1,
672 feat2,
673 corr
674 ));
675 }
676 if self.redundant_correlation_pairs.len() > 10 {
677 report.push_str(&format!(
678 " ... and {} more pairs\n",
679 self.redundant_correlation_pairs.len() - 10
680 ));
681 }
682 }
683
684 if !self.high_vif_features.is_empty() {
685 report.push_str(&format!(
686 "\nHigh VIF Features ({}):\n",
687 self.high_vif_features.len()
688 ));
689 for (i, &(feat, vif)) in self.high_vif_features.iter().take(10).enumerate() {
690 report.push_str(&format!(
691 " {}. Feature {}: VIF = {:.4}\n",
692 i + 1,
693 feat,
694 vif
695 ));
696 }
697 if self.high_vif_features.len() > 10 {
698 report.push_str(&format!(
699 " ... and {} more features\n",
700 self.high_vif_features.len() - 10
701 ));
702 }
703 }
704
705 report.push_str(&format!(
706 "\nOverall Redundancy Assessment: {}\n",
707 self.overall_assessment()
708 ));
709
710 report
711 }
712
713 fn interpret_correlation_redundancy(&self) -> &'static str {
714 match self.correlation_redundancy_score {
715 x if x >= 0.8 => "Very high correlation redundancy - many highly correlated features",
716 x if x >= 0.6 => "High correlation redundancy - significant feature overlap",
717 x if x >= 0.4 => "Moderate correlation redundancy - some correlated features",
718 x if x >= 0.2 => "Low correlation redundancy - minimal feature overlap",
719 _ => "Very low correlation redundancy - features are largely independent",
720 }
721 }
722
723 fn interpret_mi_redundancy(&self) -> &'static str {
724 match self.mutual_information_redundancy_score {
725 x if x >= 0.8 => "Very high information redundancy - features share much information",
726 x if x >= 0.6 => "High information redundancy - significant information overlap",
727 x if x >= 0.4 => "Moderate information redundancy - some shared information",
728 x if x >= 0.2 => "Low information redundancy - minimal information overlap",
729 _ => "Very low information redundancy - features provide unique information",
730 }
731 }
732
733 fn interpret_vif(&self) -> &'static str {
734 match self.max_vif {
735 x if x >= 10.0 => "Severe multicollinearity - high VIF values detected",
736 x if x >= 5.0 => "Moderate multicollinearity - concerning VIF values",
737 x if x >= 2.5 => "Mild multicollinearity - some elevated VIF values",
738 _ => "No multicollinearity concerns - acceptable VIF values",
739 }
740 }
741
742 fn overall_assessment(&self) -> &'static str {
743 let redundancy_indicators = [
744 self.correlation_redundancy_score >= 0.6,
745 self.mutual_information_redundancy_score >= 0.6,
746 self.max_vif >= 5.0,
747 self.redundant_correlation_pairs.len() > self.n_features / 2,
748 ];
749
750 let high_redundancy_count = redundancy_indicators.iter().filter(|&&x| x).count();
751
752 match high_redundancy_count {
753 4 => "CRITICAL: Very high redundancy detected across all measures - major feature set cleanup needed",
754 3 => "HIGH: High redundancy detected - consider feature reduction strategies",
755 2 => "MODERATE: Some redundancy detected - review feature selection carefully",
756 1 => "LOW: Minimal redundancy - feature set appears reasonable",
757 _ => "EXCELLENT: Low redundancy across all measures - well-diversified feature set",
758 }
759 }
760}
761
762#[derive(Debug, Clone)]
764pub struct RedundancyMeasures;
765
766impl RedundancyMeasures {
767 pub fn compute(X: ArrayView2<f64>, feature_indices: &[usize]) -> Result<RedundancyAssessment> {
769 let redundancy_matrix = RedundancyMatrix::new(0.7, 0.1, 5.0, 10);
770 redundancy_matrix.compute(X, feature_indices)
771 }
772
773 pub fn compute_with_params(
775 X: ArrayView2<f64>,
776 feature_indices: &[usize],
777 correlation_threshold: f64,
778 mi_threshold: f64,
779 vif_threshold: f64,
780 n_bins: usize,
781 ) -> Result<RedundancyAssessment> {
782 let redundancy_matrix =
783 RedundancyMatrix::new(correlation_threshold, mi_threshold, vif_threshold, n_bins);
784 redundancy_matrix.compute(X, feature_indices)
785 }
786}
787
788#[allow(non_snake_case)]
789#[cfg(test)]
790mod tests {
791 use super::*;
792 use scirs2_core::ndarray::array;
793
794 #[test]
795 #[allow(non_snake_case)]
796 fn test_correlation_redundancy() {
797 let X = array![
798 [1.0, 2.0, 1.1, 5.0],
799 [2.0, 4.0, 2.1, 6.0],
800 [3.0, 6.0, 3.1, 7.0],
801 [4.0, 8.0, 4.1, 8.0],
802 ];
803
804 let feature_indices = vec![0, 1, 2]; let redundancy = CorrelationRedundancy::new(0.5, true);
806 let score = redundancy
807 .compute(X.view(), &feature_indices)
808 .expect("operation should succeed");
809
810 assert!(score > 0.0);
811
812 let pairs = redundancy
813 .identify_redundant_pairs(X.view(), &feature_indices)
814 .expect("operation should succeed");
815 assert!(!pairs.is_empty());
816 }
817
818 #[test]
819 #[allow(non_snake_case)]
820 fn test_mi_redundancy() {
821 let X = array![
822 [1.0, 1.0, 5.0],
823 [2.0, 2.0, 6.0],
824 [3.0, 3.0, 7.0],
825 [4.0, 4.0, 8.0],
826 ];
827
828 let feature_indices = vec![0, 1, 2];
829 let redundancy = MutualInformationRedundancy::new(0.1, 3);
830 let score = redundancy
831 .compute(X.view(), &feature_indices)
832 .expect("operation should succeed");
833
834 assert!(score >= 0.0);
835 }
836
837 #[test]
838 #[allow(non_snake_case)]
839 fn test_vif() {
840 let X = array![
841 [1.0, 2.0, 1.5],
842 [2.0, 4.0, 3.0],
843 [3.0, 6.0, 4.5],
844 [4.0, 8.0, 6.0],
845 ];
846
847 let feature_indices = vec![0, 1, 2];
848 let vif_analyzer = VarianceInflationFactor::new(5.0);
849 let vif_scores = vif_analyzer
850 .compute_all(X.view(), &feature_indices)
851 .expect("operation should succeed");
852
853 assert_eq!(vif_scores.len(), 3);
854 for score in &vif_scores {
855 assert!(score >= &1.0);
856 }
857
858 let high_vif = vif_analyzer
859 .identify_high_vif_features(X.view(), &feature_indices)
860 .expect("operation should succeed");
861 assert!(high_vif.len() <= feature_indices.len());
863 }
864
865 #[test]
866 #[allow(non_snake_case)]
867 fn test_redundancy_measures() {
868 let X = array![
869 [1.0, 2.0, 1.1, 5.0, 0.5],
870 [2.0, 4.0, 2.1, 6.0, 1.0],
871 [3.0, 6.0, 3.1, 7.0, 1.5],
872 [4.0, 8.0, 4.1, 8.0, 2.0],
873 [5.0, 10.0, 5.1, 9.0, 2.5],
874 ];
875
876 let feature_indices = vec![0, 1, 2, 3, 4];
877 let assessment = RedundancyMeasures::compute(X.view(), &feature_indices)
878 .expect("operation should succeed");
879
880 assert!(assessment.correlation_redundancy_score >= 0.0);
881 assert!(assessment.mutual_information_redundancy_score >= 0.0);
882 assert!(assessment.average_vif >= 1.0);
883 assert!(assessment.max_vif >= 1.0);
884 assert_eq!(assessment.vif_scores.len(), 5);
885
886 let report = assessment.report();
887 assert!(report.contains("Redundancy Assessment"));
888 assert!(report.contains("Overall"));
889 }
890
891 #[test]
892 #[allow(non_snake_case)]
893 fn test_empty_feature_set() {
894 let X = array![[1.0], [2.0], [3.0]];
895 let feature_indices = vec![0];
896
897 let redundancy = CorrelationRedundancy::new(0.5, true);
898 let score = redundancy
899 .compute(X.view(), &feature_indices)
900 .expect("operation should succeed");
901 assert_eq!(score, 0.0); }
903}