Skip to main content

scirs2_cluster/ensemble/
core.rs

1//! Core types and configurations for ensemble clustering
2//!
3//! This module provides the fundamental data structures and configurations
4//! used throughout the ensemble clustering system.
5
6use scirs2_core::ndarray::ArrayStatCompat;
7use scirs2_core::ndarray::{Array1, Array2};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Configuration for ensemble clustering
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct EnsembleConfig {
14    /// Number of base clustering algorithms to use
15    pub n_estimators: usize,
16    /// Sampling strategy for data subsets
17    pub sampling_strategy: SamplingStrategy,
18    /// Consensus method for combining results
19    pub consensus_method: ConsensusMethod,
20    /// Random seed for reproducible results
21    pub random_seed: Option<u64>,
22    /// Diversity enforcement strategy
23    pub diversity_strategy: Option<DiversityStrategy>,
24    /// Quality threshold for including results
25    pub quality_threshold: Option<f64>,
26    /// Maximum number of clusters to consider
27    pub max_clusters: Option<usize>,
28}
29
30/// Sampling strategies for creating diverse datasets
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub enum SamplingStrategy {
33    /// Bootstrap sampling with replacement
34    Bootstrap { sample_ratio: f64 },
35    /// Random subspace sampling (feature selection)
36    RandomSubspace { feature_ratio: f64 },
37    /// Combined bootstrap and subspace sampling
38    BootstrapSubspace {
39        sample_ratio: f64,
40        feature_ratio: f64,
41    },
42    /// Random projection to lower dimensions
43    RandomProjection { target_dimensions: usize },
44    /// Noise injection for robustness testing
45    NoiseInjection {
46        noise_level: f64,
47        noise_type: NoiseType,
48    },
49    /// No sampling (use full dataset)
50    None,
51}
52
53/// Types of noise for injection
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum NoiseType {
56    /// Gaussian noise
57    Gaussian,
58    /// Uniform noise
59    Uniform,
60    /// Outlier injection
61    Outliers { outlier_ratio: f64 },
62}
63
64/// Methods for combining clustering results
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub enum ConsensusMethod {
67    /// Simple majority voting
68    MajorityVoting,
69    /// Weighted consensus based on quality scores
70    WeightedConsensus,
71    /// Graph-based consensus clustering
72    GraphBased { similarity_threshold: f64 },
73    /// Hierarchical consensus
74    Hierarchical { linkage_method: String },
75    /// Co-association matrix approach
76    CoAssociation { threshold: f64 },
77    /// Evidence accumulation clustering
78    EvidenceAccumulation,
79}
80
81/// Strategies for enforcing diversity among base clusterers
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub enum DiversityStrategy {
84    /// Algorithm diversity (use different algorithms)
85    AlgorithmDiversity {
86        algorithms: Vec<ClusteringAlgorithm>,
87    },
88    /// Parameter diversity (same algorithm, different parameters)
89    ParameterDiversity {
90        algorithm: ClusteringAlgorithm,
91        parameter_ranges: HashMap<String, ParameterRange>,
92    },
93    /// Data diversity (different data subsets)
94    DataDiversity {
95        sampling_strategies: Vec<SamplingStrategy>,
96    },
97    /// Combined diversity strategy
98    Combined { strategies: Vec<DiversityStrategy> },
99}
100
101/// Supported clustering algorithms for ensemble
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub enum ClusteringAlgorithm {
104    /// K-means clustering
105    KMeans { k_range: (usize, usize) },
106    /// DBSCAN clustering
107    DBSCAN {
108        eps_range: (f64, f64),
109        min_samples_range: (usize, usize),
110    },
111    /// Mean shift clustering
112    MeanShift { bandwidth_range: (f64, f64) },
113    /// Hierarchical clustering
114    Hierarchical { methods: Vec<String> },
115    /// Spectral clustering
116    Spectral { k_range: (usize, usize) },
117    /// Affinity propagation
118    AffinityPropagation { damping_range: (f64, f64) },
119}
120
121/// Parameter ranges for diversity
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub enum ParameterRange {
124    /// Integer range
125    Integer(i64, i64),
126    /// Float range
127    Float(f64, f64),
128    /// Categorical choices
129    Categorical(Vec<String>),
130    /// Boolean choice
131    Boolean,
132}
133
134/// Result of a single clustering run
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ClusteringResult {
137    /// Cluster labels
138    pub labels: Array1<i32>,
139    /// Algorithm used
140    pub algorithm: String,
141    /// Parameters used
142    pub parameters: HashMap<String, String>,
143    /// Quality score
144    pub quality_score: f64,
145    /// Stability score (if available)
146    pub stability_score: Option<f64>,
147    /// Number of clusters found
148    pub n_clusters: usize,
149    /// Runtime in seconds
150    pub runtime: f64,
151}
152
153impl ClusteringResult {
154    /// Create a new clustering result
155    pub fn new(
156        labels: Array1<i32>,
157        algorithm: String,
158        parameters: HashMap<String, String>,
159        quality_score: f64,
160        runtime: f64,
161    ) -> Self {
162        let n_clusters = labels
163            .iter()
164            .copied()
165            .filter(|&x| x >= 0)
166            .max()
167            .map(|x| x as usize + 1)
168            .unwrap_or(0);
169
170        Self {
171            labels,
172            algorithm,
173            parameters,
174            quality_score,
175            stability_score: None,
176            n_clusters,
177            runtime,
178        }
179    }
180
181    /// Set stability score
182    pub fn with_stability_score(mut self, score: f64) -> Self {
183        self.stability_score = Some(score);
184        self
185    }
186
187    /// Check if this result has noise points
188    pub fn has_noise(&self) -> bool {
189        self.labels.iter().any(|&x| x < 0)
190    }
191
192    /// Get number of noise points
193    pub fn noise_count(&self) -> usize {
194        self.labels.iter().filter(|&&x| x < 0).count()
195    }
196
197    /// Get cluster sizes
198    pub fn cluster_sizes(&self) -> Vec<usize> {
199        let mut sizes = vec![0; self.n_clusters];
200        for &label in self.labels.iter() {
201            if label >= 0 {
202                let cluster_id = label as usize;
203                if cluster_id < sizes.len() {
204                    sizes[cluster_id] += 1;
205                }
206            }
207        }
208        sizes
209    }
210}
211
212/// Ensemble clustering result
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct EnsembleResult {
215    /// Final consensus labels
216    pub consensus_labels: Array1<i32>,
217    /// Individual clustering results
218    pub individual_results: Vec<ClusteringResult>,
219    /// Consensus statistics
220    pub consensus_stats: ConsensusStatistics,
221    /// Diversity metrics
222    pub diversity_metrics: DiversityMetrics,
223    /// Overall quality score
224    pub ensemble_quality: f64,
225    /// Stability score
226    pub stability_score: f64,
227}
228
229impl EnsembleResult {
230    /// Create a new ensemble result
231    pub fn new(
232        consensus_labels: Array1<i32>,
233        individual_results: Vec<ClusteringResult>,
234        consensus_stats: ConsensusStatistics,
235        diversity_metrics: DiversityMetrics,
236        ensemble_quality: f64,
237        stability_score: f64,
238    ) -> Self {
239        Self {
240            consensus_labels,
241            individual_results,
242            consensus_stats,
243            diversity_metrics,
244            ensemble_quality,
245            stability_score,
246        }
247    }
248
249    /// Get number of consensus clusters
250    pub fn n_consensus_clusters(&self) -> usize {
251        self.consensus_labels
252            .iter()
253            .copied()
254            .filter(|&x| x >= 0)
255            .max()
256            .map(|x| x as usize + 1)
257            .unwrap_or(0)
258    }
259
260    /// Get consensus cluster sizes
261    pub fn consensus_cluster_sizes(&self) -> Vec<usize> {
262        let n_clusters = self.n_consensus_clusters();
263        let mut sizes = vec![0; n_clusters];
264        for &label in self.consensus_labels.iter() {
265            if label >= 0 {
266                let cluster_id = label as usize;
267                if cluster_id < sizes.len() {
268                    sizes[cluster_id] += 1;
269                }
270            }
271        }
272        sizes
273    }
274
275    /// Get average quality of individual results
276    pub fn average_individual_quality(&self) -> f64 {
277        if self.individual_results.is_empty() {
278            0.0
279        } else {
280            self.individual_results
281                .iter()
282                .map(|r| r.quality_score)
283                .sum::<f64>()
284                / self.individual_results.len() as f64
285        }
286    }
287
288    /// Get best individual result
289    pub fn best_individual_result(&self) -> Option<&ClusteringResult> {
290        self.individual_results.iter().max_by(|a, b| {
291            a.quality_score
292                .partial_cmp(&b.quality_score)
293                .unwrap_or(std::cmp::Ordering::Equal)
294        })
295    }
296
297    /// Get algorithm distribution
298    pub fn algorithm_distribution(&self) -> HashMap<String, usize> {
299        let mut distribution = HashMap::new();
300        for result in &self.individual_results {
301            *distribution.entry(result.algorithm.clone()).or_insert(0) += 1;
302        }
303        distribution
304    }
305}
306
307/// Statistics about the consensus process
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ConsensusStatistics {
310    /// Agreement matrix between clusterers
311    pub agreement_matrix: Array2<f64>,
312    /// Per-sample consensus strength
313    pub consensus_strength: Array1<f64>,
314    /// Cluster stability scores
315    pub cluster_stability: Vec<f64>,
316    /// Number of clusterers agreeing on each sample
317    pub agreement_counts: Array1<usize>,
318}
319
320impl ConsensusStatistics {
321    /// Create new consensus statistics
322    pub fn new(
323        agreement_matrix: Array2<f64>,
324        consensus_strength: Array1<f64>,
325        cluster_stability: Vec<f64>,
326        agreement_counts: Array1<usize>,
327    ) -> Self {
328        Self {
329            agreement_matrix,
330            consensus_strength,
331            cluster_stability,
332            agreement_counts,
333        }
334    }
335
336    /// Get average consensus strength
337    pub fn average_consensus_strength(&self) -> f64 {
338        self.consensus_strength.mean_or(0.0)
339    }
340
341    /// Get minimum consensus strength
342    pub fn min_consensus_strength(&self) -> f64 {
343        self.consensus_strength
344            .iter()
345            .cloned()
346            .fold(f64::INFINITY, f64::min)
347    }
348
349    /// Get maximum consensus strength
350    pub fn max_consensus_strength(&self) -> f64 {
351        self.consensus_strength
352            .iter()
353            .cloned()
354            .fold(f64::NEG_INFINITY, f64::max)
355    }
356
357    /// Get average cluster stability
358    pub fn average_cluster_stability(&self) -> f64 {
359        if self.cluster_stability.is_empty() {
360            0.0
361        } else {
362            self.cluster_stability.iter().sum::<f64>() / self.cluster_stability.len() as f64
363        }
364    }
365}
366
367/// Diversity metrics for the ensemble
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct DiversityMetrics {
370    /// Average pairwise diversity (1 - ARI)
371    pub average_diversity: f64,
372    /// Diversity matrix between all pairs
373    pub diversity_matrix: Array2<f64>,
374    /// Algorithm distribution
375    pub algorithm_distribution: HashMap<String, usize>,
376    /// Parameter diversity statistics
377    pub parameter_diversity: HashMap<String, f64>,
378}
379
380impl DiversityMetrics {
381    /// Create new diversity metrics
382    pub fn new(
383        average_diversity: f64,
384        diversity_matrix: Array2<f64>,
385        algorithm_distribution: HashMap<String, usize>,
386        parameter_diversity: HashMap<String, f64>,
387    ) -> Self {
388        Self {
389            average_diversity,
390            diversity_matrix,
391            algorithm_distribution,
392            parameter_diversity,
393        }
394    }
395
396    /// Get maximum pairwise diversity
397    pub fn max_diversity(&self) -> f64 {
398        self.diversity_matrix
399            .iter()
400            .cloned()
401            .fold(f64::NEG_INFINITY, f64::max)
402    }
403
404    /// Get minimum pairwise diversity
405    pub fn min_diversity(&self) -> f64 {
406        self.diversity_matrix
407            .iter()
408            .cloned()
409            .fold(f64::INFINITY, f64::min)
410    }
411
412    /// Get diversity variance
413    pub fn diversity_variance(&self) -> f64 {
414        let mean = self.average_diversity;
415        let variance = self
416            .diversity_matrix
417            .iter()
418            .map(|&x| (x - mean).powi(2))
419            .sum::<f64>()
420            / (self.diversity_matrix.len() as f64);
421        variance
422    }
423
424    /// Check if ensemble has good diversity
425    pub fn has_good_diversity(&self, threshold: f64) -> bool {
426        self.average_diversity >= threshold
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use scirs2_core::ndarray::arr1;
434
435    #[test]
436    fn test_ensemble_config_default() {
437        let config = EnsembleConfig::default();
438        assert_eq!(config.n_estimators, 10);
439        assert!(matches!(
440            config.sampling_strategy,
441            SamplingStrategy::Bootstrap { .. }
442        ));
443        assert!(matches!(
444            config.consensus_method,
445            ConsensusMethod::MajorityVoting
446        ));
447    }
448
449    #[test]
450    fn test_clustering_result_creation() {
451        let labels = arr1(&[0, 0, 1, 1, -1]);
452        let mut params = HashMap::new();
453        params.insert("k".to_string(), "2".to_string());
454
455        let result = ClusteringResult::new(labels, "kmeans".to_string(), params, 0.8, 1.5);
456
457        assert_eq!(result.n_clusters, 2);
458        assert!(result.has_noise());
459        assert_eq!(result.noise_count(), 1);
460        assert_eq!(result.cluster_sizes(), vec![2, 2]);
461    }
462
463    #[test]
464    fn test_ensemble_result_metrics() {
465        let consensus_labels = arr1(&[0, 0, 1, 1]);
466        let individual_results = vec![
467            ClusteringResult::new(
468                arr1(&[0, 0, 1, 1]),
469                "kmeans".to_string(),
470                HashMap::new(),
471                0.8,
472                1.0,
473            ),
474            ClusteringResult::new(
475                arr1(&[1, 1, 0, 0]),
476                "dbscan".to_string(),
477                HashMap::new(),
478                0.7,
479                1.5,
480            ),
481        ];
482
483        let consensus_stats = ConsensusStatistics::new(
484            Array2::zeros((2, 2)),
485            arr1(&[0.9, 0.9, 0.8, 0.8]),
486            vec![0.9, 0.8],
487            arr1(&[2, 2, 2, 2]),
488        );
489
490        let diversity_metrics =
491            DiversityMetrics::new(0.5, Array2::zeros((2, 2)), HashMap::new(), HashMap::new());
492
493        let result = EnsembleResult::new(
494            consensus_labels,
495            individual_results,
496            consensus_stats,
497            diversity_metrics,
498            0.85,
499            0.9,
500        );
501
502        assert_eq!(result.n_consensus_clusters(), 2);
503        assert_eq!(result.average_individual_quality(), 0.75);
504        assert!(result.best_individual_result().is_some());
505    }
506
507    #[test]
508    fn test_consensus_statistics() {
509        let stats = ConsensusStatistics::new(
510            Array2::zeros((3, 3)),
511            arr1(&[0.8, 0.9, 0.7]),
512            vec![0.9, 0.8, 0.85],
513            arr1(&[3, 2, 3]),
514        );
515
516        assert!((stats.average_consensus_strength() - 0.8).abs() < 1e-10);
517        assert_eq!(stats.min_consensus_strength(), 0.7);
518        assert_eq!(stats.max_consensus_strength(), 0.9);
519        assert!((stats.average_cluster_stability() - 0.85).abs() < 1e-10);
520    }
521
522    #[test]
523    fn test_diversity_metrics() {
524        let metrics = DiversityMetrics::new(
525            0.6,
526            Array2::from_shape_vec((2, 2), vec![0.0, 0.8, 0.8, 0.0]).expect("Operation failed"),
527            HashMap::new(),
528            HashMap::new(),
529        );
530
531        assert_eq!(metrics.max_diversity(), 0.8);
532        assert_eq!(metrics.min_diversity(), 0.0);
533        assert!(metrics.has_good_diversity(0.5));
534        assert!(!metrics.has_good_diversity(0.7));
535    }
536}