1use scirs2_core::ndarray::ArrayStatCompat;
7use scirs2_core::ndarray::{Array1, Array2};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct EnsembleConfig {
14 pub n_estimators: usize,
16 pub sampling_strategy: SamplingStrategy,
18 pub consensus_method: ConsensusMethod,
20 pub random_seed: Option<u64>,
22 pub diversity_strategy: Option<DiversityStrategy>,
24 pub quality_threshold: Option<f64>,
26 pub max_clusters: Option<usize>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub enum SamplingStrategy {
33 Bootstrap { sample_ratio: f64 },
35 RandomSubspace { feature_ratio: f64 },
37 BootstrapSubspace {
39 sample_ratio: f64,
40 feature_ratio: f64,
41 },
42 RandomProjection { target_dimensions: usize },
44 NoiseInjection {
46 noise_level: f64,
47 noise_type: NoiseType,
48 },
49 None,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum NoiseType {
56 Gaussian,
58 Uniform,
60 Outliers { outlier_ratio: f64 },
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub enum ConsensusMethod {
67 MajorityVoting,
69 WeightedConsensus,
71 GraphBased { similarity_threshold: f64 },
73 Hierarchical { linkage_method: String },
75 CoAssociation { threshold: f64 },
77 EvidenceAccumulation,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub enum DiversityStrategy {
84 AlgorithmDiversity {
86 algorithms: Vec<ClusteringAlgorithm>,
87 },
88 ParameterDiversity {
90 algorithm: ClusteringAlgorithm,
91 parameter_ranges: HashMap<String, ParameterRange>,
92 },
93 DataDiversity {
95 sampling_strategies: Vec<SamplingStrategy>,
96 },
97 Combined { strategies: Vec<DiversityStrategy> },
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub enum ClusteringAlgorithm {
104 KMeans { k_range: (usize, usize) },
106 DBSCAN {
108 eps_range: (f64, f64),
109 min_samples_range: (usize, usize),
110 },
111 MeanShift { bandwidth_range: (f64, f64) },
113 Hierarchical { methods: Vec<String> },
115 Spectral { k_range: (usize, usize) },
117 AffinityPropagation { damping_range: (f64, f64) },
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub enum ParameterRange {
124 Integer(i64, i64),
126 Float(f64, f64),
128 Categorical(Vec<String>),
130 Boolean,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct ClusteringResult {
137 pub labels: Array1<i32>,
139 pub algorithm: String,
141 pub parameters: HashMap<String, String>,
143 pub quality_score: f64,
145 pub stability_score: Option<f64>,
147 pub n_clusters: usize,
149 pub runtime: f64,
151}
152
153impl ClusteringResult {
154 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 pub fn with_stability_score(mut self, score: f64) -> Self {
183 self.stability_score = Some(score);
184 self
185 }
186
187 pub fn has_noise(&self) -> bool {
189 self.labels.iter().any(|&x| x < 0)
190 }
191
192 pub fn noise_count(&self) -> usize {
194 self.labels.iter().filter(|&&x| x < 0).count()
195 }
196
197 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#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct EnsembleResult {
215 pub consensus_labels: Array1<i32>,
217 pub individual_results: Vec<ClusteringResult>,
219 pub consensus_stats: ConsensusStatistics,
221 pub diversity_metrics: DiversityMetrics,
223 pub ensemble_quality: f64,
225 pub stability_score: f64,
227}
228
229impl EnsembleResult {
230 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 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct ConsensusStatistics {
310 pub agreement_matrix: Array2<f64>,
312 pub consensus_strength: Array1<f64>,
314 pub cluster_stability: Vec<f64>,
316 pub agreement_counts: Array1<usize>,
318}
319
320impl ConsensusStatistics {
321 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 pub fn average_consensus_strength(&self) -> f64 {
338 self.consensus_strength.mean_or(0.0)
339 }
340
341 pub fn min_consensus_strength(&self) -> f64 {
343 self.consensus_strength
344 .iter()
345 .cloned()
346 .fold(f64::INFINITY, f64::min)
347 }
348
349 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct DiversityMetrics {
370 pub average_diversity: f64,
372 pub diversity_matrix: Array2<f64>,
374 pub algorithm_distribution: HashMap<String, usize>,
376 pub parameter_diversity: HashMap<String, f64>,
378}
379
380impl DiversityMetrics {
381 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 pub fn max_diversity(&self) -> f64 {
398 self.diversity_matrix
399 .iter()
400 .cloned()
401 .fold(f64::NEG_INFINITY, f64::max)
402 }
403
404 pub fn min_diversity(&self) -> f64 {
406 self.diversity_matrix
407 .iter()
408 .cloned()
409 .fold(f64::INFINITY, f64::min)
410 }
411
412 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 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}