Skip to main content

sklears_feature_selection/evaluation/
redundancy_measures.rs

1//! Redundancy measures for feature set quality assessment
2//!
3//! This module implements comprehensive redundancy assessment methods to evaluate
4//! the degree of redundancy in selected feature sets. All implementations follow
5//! the SciRS2 policy using scirs2-core for numerical computations.
6
7use 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/// Correlation-based redundancy measures
32#[derive(Debug, Clone)]
33pub struct CorrelationRedundancy {
34    correlation_threshold: f64,
35    absolute_correlation: bool,
36}
37
38impl CorrelationRedundancy {
39    /// Create a new correlation redundancy analyzer
40    pub fn new(correlation_threshold: f64, absolute_correlation: bool) -> Self {
41        Self {
42            correlation_threshold,
43            absolute_correlation,
44        }
45    }
46
47    /// Compute pairwise correlation redundancy for selected features
48    pub fn compute(&self, X: ArrayView2<f64>, feature_indices: &[usize]) -> Result<f64> {
49        if feature_indices.len() < 2 {
50            return Ok(0.0); // No redundancy with less than 2 features
51        }
52
53        if X.is_empty() {
54            return Err(RedundancyError::EmptyFeatureMatrix.into());
55        }
56
57        // Extract selected features
58        let selected_features = self.extract_features(X, feature_indices)?;
59
60        // Compute correlation matrix
61        let correlation_matrix = self.compute_correlation_matrix(&selected_features)?;
62
63        // Calculate redundancy score
64        self.calculate_redundancy_score(&correlation_matrix)
65    }
66
67    /// Extract selected feature columns
68    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    /// Compute correlation matrix for features
87    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    /// Compute Pearson correlation between two features
112    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    /// Calculate redundancy score from correlation matrix
142    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        // Sum upper triangular correlations (excluding diagonal)
152        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    /// Identify highly correlated feature pairs
167    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        // Sort by correlation strength
187        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/// Mutual information-based redundancy assessment
198#[derive(Debug, Clone)]
199pub struct MutualInformationRedundancy {
200    mi_threshold: f64,
201    n_bins: usize,
202}
203
204impl MutualInformationRedundancy {
205    /// Create a new mutual information redundancy analyzer
206    pub fn new(mi_threshold: f64, n_bins: usize) -> Self {
207        Self {
208            mi_threshold,
209            n_bins,
210        }
211    }
212
213    /// Compute mutual information-based redundancy
214    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    /// Compute mutual information matrix
244    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    /// Compute entropy of a feature
264    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    /// Compute mutual information between two features
280    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    /// Create histogram for discretization
308    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    /// Create joint histogram for two features
329    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/// Variance Inflation Factor for multicollinearity assessment
394#[derive(Debug, Clone)]
395pub struct VarianceInflationFactor {
396    vif_threshold: f64,
397}
398
399impl VarianceInflationFactor {
400    /// Create a new VIF analyzer
401    pub fn new(vif_threshold: f64) -> Self {
402        Self { vif_threshold }
403    }
404
405    /// Compute VIF for all features
406    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    /// Compute VIF for a single feature
441    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        // Extract target feature
452        let target_feature = features.column(target_feature_idx);
453
454        // Extract other features
455        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        // Compute R-squared using simple linear regression approximation
468        let r_squared = self.compute_r_squared(target_feature, other_features.view())?;
469
470        // VIF = 1 / (1 - R²)
471        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    /// Compute R-squared approximation using correlation
479    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        // Simple approximation: use maximum squared correlation
485        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    /// Identify features with high VIF
526    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        // Sort by VIF score (descending)
541        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/// Comprehensive redundancy matrix computation
548#[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    /// Create a new redundancy matrix analyzer
558    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    /// Compute comprehensive redundancy assessment
573    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/// Comprehensive redundancy assessment results
605#[derive(Debug, Clone)]
606pub struct RedundancyAssessment {
607    /// correlation_redundancy_score
608    pub correlation_redundancy_score: f64,
609    /// mutual_information_redundancy_score
610    pub mutual_information_redundancy_score: f64,
611    /// average_vif
612    pub average_vif: f64,
613    /// max_vif
614    pub max_vif: f64,
615    /// redundant_correlation_pairs
616    pub redundant_correlation_pairs: Vec<(usize, usize, f64)>,
617    /// high_vif_features
618    pub high_vif_features: Vec<(usize, f64)>,
619    /// vif_scores
620    pub vif_scores: Vec<f64>,
621    /// n_features
622    pub n_features: usize,
623}
624
625impl RedundancyAssessment {
626    /// Generate comprehensive redundancy report
627    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/// Comprehensive redundancy measures aggregator
763#[derive(Debug, Clone)]
764pub struct RedundancyMeasures;
765
766impl RedundancyMeasures {
767    /// Compute comprehensive redundancy assessment using default parameters
768    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    /// Compute comprehensive redundancy assessment with custom parameters
774    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]; // Features 0 and 1 are highly correlated, 2 is similar
805        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        // Might be empty if VIF scores are below threshold
862        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); // No redundancy with single feature
902    }
903}