1use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
14pub enum ValidationMetric {
15 #[default]
20 Euclidean,
21
22 Manhattan,
27
28 Cosine,
33
34 Chebyshev,
39
40 Minkowski(f64),
45}
46
47impl ValidationMetric {
48 pub fn compute_distance(&self, a: &[f64], b: &[f64]) -> f64 {
50 if a.len() != b.len() {
51 return f64::NAN;
52 }
53
54 match self {
55 ValidationMetric::Euclidean => a
56 .iter()
57 .zip(b.iter())
58 .map(|(x, y)| (x - y).powi(2))
59 .sum::<f64>()
60 .sqrt(),
61
62 ValidationMetric::Manhattan => a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()).sum(),
63
64 ValidationMetric::Cosine => {
65 let dot_product: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
66 let norm_a: f64 = a.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
67 let norm_b: f64 = b.iter().map(|x| x.powi(2)).sum::<f64>().sqrt();
68
69 if norm_a == 0.0 || norm_b == 0.0 {
70 1.0
71 } else {
72 1.0 - (dot_product / (norm_a * norm_b))
73 }
74 }
75
76 ValidationMetric::Chebyshev => a
77 .iter()
78 .zip(b.iter())
79 .map(|(x, y)| (x - y).abs())
80 .fold(0.0, f64::max),
81
82 ValidationMetric::Minkowski(p) => {
83 if *p <= 0.0 {
84 return f64::NAN;
85 }
86 a.iter()
87 .zip(b.iter())
88 .map(|(x, y)| (x - y).abs().powf(*p))
89 .sum::<f64>()
90 .powf(1.0 / p)
91 }
92 }
93 }
94
95 pub fn name(&self) -> &str {
97 match self {
98 ValidationMetric::Euclidean => "Euclidean",
99 ValidationMetric::Manhattan => "Manhattan",
100 ValidationMetric::Cosine => "Cosine",
101 ValidationMetric::Chebyshev => "Chebyshev",
102 ValidationMetric::Minkowski(p) => {
103 if *p == 1.0 {
104 "Minkowski (L1)"
105 } else if *p == 2.0 {
106 "Minkowski (L2)"
107 } else {
108 "Minkowski"
109 }
110 }
111 }
112 }
113
114 pub fn is_high_dimensional_suitable(&self) -> bool {
116 matches!(self, ValidationMetric::Manhattan | ValidationMetric::Cosine)
117 }
118
119 pub fn requires_scaling(&self) -> bool {
121 matches!(
122 self,
123 ValidationMetric::Euclidean
124 | ValidationMetric::Chebyshev
125 | ValidationMetric::Minkowski(_)
126 )
127 }
128}
129
130#[derive(Debug, Clone)]
135pub struct SilhouetteResult {
136 pub sample_silhouettes: Vec<f64>,
143
144 pub mean_silhouette: f64,
152
153 pub cluster_silhouettes: HashMap<i32, f64>,
157
158 pub cluster_sizes: HashMap<i32, usize>,
160
161 pub confidence_interval: Option<(f64, f64)>,
163}
164
165impl SilhouetteResult {
166 pub fn best_cluster(&self) -> Option<(i32, f64)> {
168 self.cluster_silhouettes
169 .iter()
170 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
171 .map(|(&id, &score)| (id, score))
172 }
173
174 pub fn worst_cluster(&self) -> Option<(i32, f64)> {
176 self.cluster_silhouettes
177 .iter()
178 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
179 .map(|(&id, &score)| (id, score))
180 }
181
182 pub fn problematic_samples(&self, threshold: f64) -> Vec<usize> {
184 self.sample_silhouettes
185 .iter()
186 .enumerate()
187 .filter(|(_, &score)| score < threshold)
188 .map(|(idx, _)| idx)
189 .collect()
190 }
191
192 pub fn quality_assessment(&self) -> SilhouetteQuality {
194 if self.mean_silhouette >= 0.7 {
195 SilhouetteQuality::Excellent
196 } else if self.mean_silhouette >= 0.5 {
197 SilhouetteQuality::Good
198 } else if self.mean_silhouette >= 0.25 {
199 SilhouetteQuality::Fair
200 } else {
201 SilhouetteQuality::Poor
202 }
203 }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum SilhouetteQuality {
209 Excellent,
211 Good,
213 Fair,
215 Poor,
217}
218
219impl std::fmt::Display for SilhouetteQuality {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 match self {
222 SilhouetteQuality::Excellent => write!(f, "Excellent"),
223 SilhouetteQuality::Good => write!(f, "Good"),
224 SilhouetteQuality::Fair => write!(f, "Fair"),
225 SilhouetteQuality::Poor => write!(f, "Poor"),
226 }
227 }
228}
229
230#[derive(Debug, Clone)]
235pub struct GapStatisticResult {
236 pub gap_values: Vec<f64>,
240
241 pub gap_std_errors: Vec<f64>,
245
246 pub optimal_k: usize,
251
252 pub k_values: Vec<usize>,
254
255 pub within_cluster_ss: Vec<f64>,
257
258 pub reference_statistics: Vec<ReferenceStatistics>,
260
261 pub n_references: usize,
263}
264
265impl GapStatisticResult {
266 pub fn gap_for_k(&self, k: usize) -> Option<f64> {
268 self.k_values
269 .iter()
270 .position(|&x| x == k)
271 .map(|idx| self.gap_values[idx])
272 }
273
274 pub fn recommended_k_values(&self, top_n: usize) -> Vec<(usize, f64)> {
276 let mut k_gap_pairs: Vec<_> = self
277 .k_values
278 .iter()
279 .zip(self.gap_values.iter())
280 .map(|(&k, &gap)| (k, gap))
281 .collect();
282
283 k_gap_pairs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
284 k_gap_pairs.truncate(top_n);
285 k_gap_pairs
286 }
287
288 pub fn is_optimal_significant(&self) -> bool {
290 if let Some(optimal_idx) = self.k_values.iter().position(|&k| k == self.optimal_k) {
291 if optimal_idx + 1 < self.gap_values.len() {
292 let gap_k = self.gap_values[optimal_idx];
293 let gap_k_plus_1 = self.gap_values[optimal_idx + 1];
294 let se_k_plus_1 = self.gap_std_errors[optimal_idx + 1];
295
296 return gap_k >= gap_k_plus_1 - se_k_plus_1;
297 }
298 }
299 false
300 }
301}
302
303#[derive(Debug, Clone)]
305pub struct ReferenceStatistics {
306 pub k: usize,
308 pub mean_log_w: f64,
310 pub std_log_w: f64,
312 pub reference_log_w_values: Vec<f64>,
314}
315
316#[derive(Debug, Clone)]
321pub struct ValidationMetrics {
322 pub silhouette: SilhouetteResult,
324
325 pub calinski_harabasz: f64,
331
332 pub davies_bouldin: f64,
338
339 pub inertia: f64,
343
344 pub dunn_index: Option<f64>,
348
349 pub silhouette_variance: f64,
353
354 pub n_clusters: usize,
356
357 pub n_samples: usize,
359
360 pub metric_used: ValidationMetric,
362}
363
364impl ValidationMetrics {
365 pub fn composite_score(&self) -> f64 {
369 let silhouette_norm = (self.silhouette.mean_silhouette + 1.0) / 2.0; let ch_norm = (self.calinski_harabasz / (1.0 + self.calinski_harabasz)).min(1.0);
372 let db_norm = 1.0 / (1.0 + self.davies_bouldin); let dunn_norm = self.dunn_index.unwrap_or(0.0).min(1.0);
374
375 let weights = (0.4, 0.3, 0.2, 0.1); weights.0 * silhouette_norm
378 + weights.1 * ch_norm
379 + weights.2 * db_norm
380 + weights.3 * dunn_norm
381 }
382
383 pub fn overall_quality(&self) -> ClusterQuality {
385 let score = self.composite_score();
386 if score >= 0.8 {
387 ClusterQuality::Excellent
388 } else if score >= 0.6 {
389 ClusterQuality::Good
390 } else if score >= 0.4 {
391 ClusterQuality::Fair
392 } else {
393 ClusterQuality::Poor
394 }
395 }
396
397 pub fn quality_summary(&self) -> String {
399 format!(
400 "Clustering Quality Summary:\n\
401 - {} clusters, {} samples\n\
402 - Silhouette score: {:.3} ({})\n\
403 - Calinski-Harabasz: {:.2}\n\
404 - Davies-Bouldin: {:.3}\n\
405 - Composite score: {:.3}\n\
406 - Overall quality: {:?}",
407 self.n_clusters,
408 self.n_samples,
409 self.silhouette.mean_silhouette,
410 self.silhouette.quality_assessment(),
411 self.calinski_harabasz,
412 self.davies_bouldin,
413 self.composite_score(),
414 self.overall_quality()
415 )
416 }
417}
418
419#[derive(Debug, Clone, Default)]
423pub struct ExternalValidationMetrics {
424 pub adjusted_rand_index: f64,
429
430 pub normalized_mutual_info: f64,
435
436 pub v_measure: f64,
441
442 pub homogeneity: f64,
447
448 pub completeness: f64,
453
454 pub fowlkes_mallows: f64,
459
460 pub jaccard_index: Option<f64>,
465
466 pub purity: Option<f64>,
471
472 pub inverse_purity: Option<f64>,
476}
477
478impl ExternalValidationMetrics {
479 pub fn consensus_score(&self) -> f64 {
481 let scores = [
482 self.adjusted_rand_index.max(0.0), self.normalized_mutual_info,
484 self.v_measure,
485 self.fowlkes_mallows,
486 ];
487
488 scores.iter().sum::<f64>() / scores.len() as f64
489 }
490
491 pub fn is_significant_match(&self, threshold: f64) -> bool {
493 self.consensus_score() >= threshold
494 }
495
496 pub fn best_metric(&self) -> (&str, f64) {
498 let metrics = vec![
499 ("ARI", self.adjusted_rand_index.max(0.0)),
500 ("NMI", self.normalized_mutual_info),
501 ("V-measure", self.v_measure),
502 ("Fowlkes-Mallows", self.fowlkes_mallows),
503 ];
504
505 metrics
506 .into_iter()
507 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
508 .unwrap_or(("None", 0.0))
509 }
510}
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
514pub enum ClusterQuality {
515 Excellent,
517 Good,
519 Fair,
521 Poor,
523}
524
525impl ClusterQuality {
526 pub fn description(&self) -> &str {
528 match self {
529 ClusterQuality::Excellent => "Excellent: Strong, well-separated clusters",
530 ClusterQuality::Good => "Good: Clear cluster structure with minor issues",
531 ClusterQuality::Fair => "Fair: Some cluster structure, but improvements needed",
532 ClusterQuality::Poor => "Poor: Weak or no discernible cluster structure",
533 }
534 }
535
536 pub fn score(&self) -> f64 {
538 match self {
539 ClusterQuality::Excellent => 0.9,
540 ClusterQuality::Good => 0.7,
541 ClusterQuality::Fair => 0.5,
542 ClusterQuality::Poor => 0.2,
543 }
544 }
545}
546
547#[derive(Debug, Clone)]
549pub struct ValidationConfig {
550 pub metric: ValidationMetric,
552
553 pub compute_confidence_intervals: bool,
555
556 pub confidence_level: f64,
558
559 pub compute_expensive_metrics: bool,
561
562 pub random_seed: Option<u64>,
564
565 pub n_bootstrap_samples: usize,
567
568 pub use_parallel: bool,
570
571 pub problematic_threshold: f64,
573}
574
575impl Default for ValidationConfig {
576 fn default() -> Self {
577 Self {
578 metric: ValidationMetric::Euclidean,
579 compute_confidence_intervals: false,
580 confidence_level: 0.95,
581 compute_expensive_metrics: false,
582 random_seed: None,
583 n_bootstrap_samples: 1000,
584 use_parallel: true,
585 problematic_threshold: 0.0,
586 }
587 }
588}
589
590impl ValidationConfig {
591 pub fn fast() -> Self {
593 Self {
594 compute_confidence_intervals: false,
595 compute_expensive_metrics: false,
596 n_bootstrap_samples: 100,
597 ..Default::default()
598 }
599 }
600
601 pub fn comprehensive() -> Self {
603 Self {
604 compute_confidence_intervals: true,
605 compute_expensive_metrics: true,
606 n_bootstrap_samples: 2000,
607 ..Default::default()
608 }
609 }
610
611 pub fn high_dimensional() -> Self {
613 Self {
614 metric: ValidationMetric::Cosine,
615 compute_expensive_metrics: false, ..Default::default()
617 }
618 }
619}
620
621#[allow(non_snake_case)]
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[test]
627 fn test_validation_metric_distances() {
628 let a = vec![1.0, 2.0, 3.0];
629 let b = vec![4.0, 5.0, 6.0];
630
631 let euclidean = ValidationMetric::Euclidean;
633 let dist = euclidean.compute_distance(&a, &b);
634 assert!((dist - 5.196152422706632).abs() < 1e-10);
635
636 let manhattan = ValidationMetric::Manhattan;
638 let dist = manhattan.compute_distance(&a, &b);
639 assert!((dist - 9.0).abs() < 1e-10);
640
641 let cosine = ValidationMetric::Cosine;
643 let dist = cosine.compute_distance(&a, &b);
644 assert!((0.0..=2.0).contains(&dist));
645 }
646
647 #[test]
648 fn test_validation_metric_properties() {
649 let euclidean = ValidationMetric::Euclidean;
650 assert_eq!(euclidean.name(), "Euclidean");
651 assert!(!euclidean.is_high_dimensional_suitable());
652 assert!(euclidean.requires_scaling());
653
654 let cosine = ValidationMetric::Cosine;
655 assert_eq!(cosine.name(), "Cosine");
656 assert!(cosine.is_high_dimensional_suitable());
657 assert!(!cosine.requires_scaling());
658 }
659
660 #[test]
661 fn test_silhouette_result_methods() {
662 let sample_silhouettes = vec![0.8, 0.7, 0.9, 0.3, 0.6];
663 let mut cluster_silhouettes = HashMap::new();
664 cluster_silhouettes.insert(0, 0.8);
665 cluster_silhouettes.insert(1, 0.5);
666
667 let result = SilhouetteResult {
668 sample_silhouettes: sample_silhouettes.clone(),
669 mean_silhouette: 0.66,
670 cluster_silhouettes,
671 cluster_sizes: HashMap::new(),
672 confidence_interval: None,
673 };
674
675 assert_eq!(result.quality_assessment(), SilhouetteQuality::Good);
676 assert_eq!(result.best_cluster(), Some((0, 0.8)));
677 assert_eq!(result.worst_cluster(), Some((1, 0.5)));
678
679 let problematic = result.problematic_samples(0.4);
680 assert_eq!(problematic, vec![3]);
681 }
682
683 #[test]
684 fn test_cluster_quality_methods() {
685 let excellent = ClusterQuality::Excellent;
686 assert_eq!(excellent.score(), 0.9);
687 assert!(excellent.description().contains("Excellent"));
688
689 let poor = ClusterQuality::Poor;
690 assert_eq!(poor.score(), 0.2);
691 assert!(poor.description().contains("Poor"));
692 }
693
694 #[test]
695 fn test_validation_config_presets() {
696 let fast = ValidationConfig::fast();
697 assert!(!fast.compute_confidence_intervals);
698 assert_eq!(fast.n_bootstrap_samples, 100);
699
700 let comprehensive = ValidationConfig::comprehensive();
701 assert!(comprehensive.compute_confidence_intervals);
702 assert_eq!(comprehensive.n_bootstrap_samples, 2000);
703
704 let high_dim = ValidationConfig::high_dimensional();
705 assert_eq!(high_dim.metric, ValidationMetric::Cosine);
706 assert!(!high_dim.compute_expensive_metrics);
707 }
708
709 #[test]
710 fn test_minkowski_distance() {
711 let a = vec![1.0, 2.0];
712 let b = vec![3.0, 4.0];
713
714 let minkowski_1 = ValidationMetric::Minkowski(1.0);
716 let manhattan = ValidationMetric::Manhattan;
717
718 let dist_m1 = minkowski_1.compute_distance(&a, &b);
719 let dist_man = manhattan.compute_distance(&a, &b);
720 assert!((dist_m1 - dist_man).abs() < 1e-10);
721
722 let minkowski_2 = ValidationMetric::Minkowski(2.0);
724 let euclidean = ValidationMetric::Euclidean;
725
726 let dist_m2 = minkowski_2.compute_distance(&a, &b);
727 let dist_euc = euclidean.compute_distance(&a, &b);
728 assert!((dist_m2 - dist_euc).abs() < 1e-10);
729 }
730
731 #[test]
732 fn test_gap_statistic_result_methods() {
733 let result = GapStatisticResult {
734 gap_values: vec![0.5, 0.8, 0.6, 0.4],
735 gap_std_errors: vec![0.1, 0.15, 0.12, 0.08],
736 optimal_k: 2,
737 k_values: vec![1, 2, 3, 4],
738 within_cluster_ss: vec![10.0, 5.0, 7.0, 8.0],
739 reference_statistics: Vec::new(),
740 n_references: 10,
741 };
742
743 assert_eq!(result.gap_for_k(2), Some(0.8));
744 assert_eq!(result.gap_for_k(5), None);
745
746 let recommended = result.recommended_k_values(2);
747 assert_eq!(recommended.len(), 2);
748 assert_eq!(recommended[0], (2, 0.8)); }
750
751 #[test]
752 fn test_external_validation_metrics() {
753 let metrics = ExternalValidationMetrics {
754 adjusted_rand_index: 0.8,
755 normalized_mutual_info: 0.75,
756 v_measure: 0.78,
757 homogeneity: 0.8,
758 completeness: 0.76,
759 fowlkes_mallows: 0.82,
760 jaccard_index: Some(0.7),
761 purity: Some(0.85),
762 inverse_purity: Some(0.8),
763 };
764
765 let consensus = metrics.consensus_score();
766 assert!(consensus > 0.7 && consensus < 0.9);
767
768 assert!(metrics.is_significant_match(0.7));
769 assert!(!metrics.is_significant_match(0.9));
770
771 let (best_name, _) = metrics.best_metric();
772 assert_eq!(best_name, "Fowlkes-Mallows");
773 }
774}