Skip to main content

scirs2_cluster/
stability.rs

1//! Cluster stability assessment tools
2//!
3//! This module provides various methods for assessing the stability and
4//! quality of clustering results, including bootstrap validation,
5//! consensus clustering, and stability indices.
6
7use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use scirs2_core::random::seq::SliceRandom;
10use scirs2_core::random::{Rng, RngExt, SeedableRng};
11use std::collections::HashSet;
12use std::fmt::Debug;
13
14use crate::error::{ClusteringError, Result};
15use crate::metrics::adjusted_rand_index;
16use crate::vq::kmeans2;
17
18/// Configuration for stability assessment
19#[derive(Debug, Clone)]
20pub struct StabilityConfig {
21    /// Number of bootstrap iterations
22    pub n_bootstrap: usize,
23    /// Fraction of data to sample in each bootstrap iteration
24    pub subsample_ratio: f64,
25    /// Random seed for reproducible results
26    pub random_seed: Option<u64>,
27    /// Number of clustering algorithm runs per bootstrap
28    pub n_runs_per_bootstrap: usize,
29    /// Range of cluster numbers to test (for optimal k selection)
30    pub k_range: Option<(usize, usize)>,
31}
32
33impl Default for StabilityConfig {
34    fn default() -> Self {
35        Self {
36            n_bootstrap: 100,
37            subsample_ratio: 0.8,
38            random_seed: None,
39            n_runs_per_bootstrap: 10,
40            k_range: None,
41        }
42    }
43}
44
45/// Results of stability assessment
46#[derive(Debug, Clone)]
47pub struct StabilityResult<F: Float> {
48    /// Stability scores for each tested configuration
49    pub stability_scores: Vec<F>,
50    /// Consensus clustering result
51    pub consensus_labels: Option<Array1<usize>>,
52    /// Optimal number of clusters (if k_range was provided)
53    pub optimal_k: Option<usize>,
54    /// Mean stability score across all bootstrap iterations
55    pub mean_stability: F,
56    /// Standard deviation of stability scores
57    pub std_stability: F,
58    /// Bootstrap stability matrix
59    pub bootstrap_matrix: Array2<F>,
60}
61
62/// Bootstrap validation for clustering stability
63///
64/// This method assesses the stability of clustering by running the algorithm
65/// on multiple bootstrap samples of the data and measuring the consistency
66/// of the results.
67pub struct BootstrapValidator<F: Float> {
68    config: StabilityConfig,
69    phantom: std::marker::PhantomData<F>,
70}
71
72impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
73    BootstrapValidator<F>
74{
75    /// Create a new bootstrap validator
76    pub fn new(config: StabilityConfig) -> Self {
77        Self {
78            config,
79            phantom: std::marker::PhantomData,
80        }
81    }
82
83    /// Assess K-means clustering stability
84    pub fn assess_kmeans_stability(
85        &self,
86        data: ArrayView2<F>,
87        k: usize,
88    ) -> Result<StabilityResult<F>> {
89        let n_samples = data.shape()[0];
90        let n_features = data.shape()[1];
91
92        if n_samples < 2 {
93            return Err(ClusteringError::InvalidInput(
94                "Need at least 2 samples for stability assessment".into(),
95            ));
96        }
97
98        let subsample_size = ((n_samples as f64) * self.config.subsample_ratio) as usize;
99        if subsample_size < k {
100            return Err(ClusteringError::InvalidInput(
101                "Subsample size must be at least k".into(),
102            ));
103        }
104
105        let mut rng = match self.config.random_seed {
106            Some(seed) => scirs2_core::random::rngs::StdRng::seed_from_u64(seed),
107            None => {
108                // Use a default seed when no seed is provided
109                scirs2_core::random::rngs::StdRng::seed_from_u64(42)
110            }
111        };
112
113        let mut bootstrap_results = Vec::new();
114
115        // Perform bootstrap iterations
116        for _iteration in 0..self.config.n_bootstrap {
117            // Create bootstrap sample
118            let mut indices: Vec<usize> = (0..n_samples).collect();
119            indices.shuffle(&mut rng);
120            indices.truncate(subsample_size);
121
122            let mut bootstrap_data = Array2::zeros((subsample_size, n_features));
123            for (new_idx, &old_idx) in indices.iter().enumerate() {
124                bootstrap_data.row_mut(new_idx).assign(&data.row(old_idx));
125            }
126
127            // Run clustering multiple times on this bootstrap sample
128            let mut run_labels = Vec::new();
129            for _run in 0..self.config.n_runs_per_bootstrap {
130                let seed = rng.random::<u64>();
131
132                match kmeans2(
133                    bootstrap_data.view(),
134                    k,
135                    Some(100),   // max_iter
136                    None,        // threshold
137                    None,        // init method
138                    None,        // missing method
139                    Some(false), // check_finite
140                    Some(seed),
141                ) {
142                    Ok((_, labels)) => {
143                        let labels_usize: Array1<usize> = labels.mapv(|x| x);
144                        run_labels.push(labels_usize);
145                    }
146                    Err(_) => {
147                        // If clustering fails, create a dummy result
148                        let dummy_labels = Array1::zeros(subsample_size);
149                        run_labels.push(dummy_labels);
150                    }
151                }
152            }
153
154            bootstrap_results.push((indices, run_labels));
155        }
156
157        // Calculate stability metrics
158        let stability_scores = self.calculate_stability_scores(&bootstrap_results)?;
159        let mean_stability = stability_scores
160            .iter()
161            .copied()
162            .fold(F::zero(), |acc, x| acc + x)
163            / F::from(stability_scores.len()).expect("Operation failed");
164
165        let variance = stability_scores
166            .iter()
167            .map(|&x| {
168                let diff = x - mean_stability;
169                diff * diff
170            })
171            .fold(F::zero(), |acc, x| acc + x)
172            / F::from(stability_scores.len()).expect("Operation failed");
173        let std_stability = variance.sqrt();
174
175        // Create bootstrap stability matrix
176        let bootstrap_matrix = self.create_bootstrap_matrix(&bootstrap_results, n_samples)?;
177
178        Ok(StabilityResult {
179            stability_scores,
180            consensus_labels: None, // Would need consensus clustering implementation
181            optimal_k: None,
182            mean_stability,
183            std_stability,
184            bootstrap_matrix,
185        })
186    }
187
188    /// Calculate stability scores from bootstrap results
189    fn calculate_stability_scores(
190        &self,
191        bootstrap_results: &[(Vec<usize>, Vec<Array1<usize>>)],
192    ) -> Result<Vec<F>> {
193        let mut scores = Vec::new();
194
195        for (_, run_labels) in bootstrap_results {
196            if run_labels.len() < 2 {
197                continue;
198            }
199
200            // Calculate pairwise ARI between runs
201            let mut pairwise_aris = Vec::new();
202            for i in 0..run_labels.len() {
203                for j in (i + 1)..run_labels.len() {
204                    let labels1 = run_labels[i].mapv(|x| x as i32);
205                    let labels2 = run_labels[j].mapv(|x| x as i32);
206
207                    match adjusted_rand_index::<F>(labels1.view(), labels2.view()) {
208                        Ok(ari) => pairwise_aris.push(ari),
209                        Err(_) => pairwise_aris.push(F::zero()),
210                    }
211                }
212            }
213
214            if !pairwise_aris.is_empty() {
215                let mean_ari = pairwise_aris
216                    .iter()
217                    .copied()
218                    .fold(F::zero(), |acc, x| acc + x)
219                    / F::from(pairwise_aris.len()).expect("Operation failed");
220                scores.push(mean_ari);
221            }
222        }
223
224        Ok(scores)
225    }
226
227    /// Create bootstrap stability matrix
228    fn create_bootstrap_matrix(
229        &self,
230        bootstrap_results: &[(Vec<usize>, Vec<Array1<usize>>)],
231        n_samples: usize,
232    ) -> Result<Array2<F>> {
233        let mut co_occurrence_matrix: Array2<F> = Array2::zeros((n_samples, n_samples));
234        let mut count_matrix: Array2<F> = Array2::zeros((n_samples, n_samples));
235
236        for (indices, run_labels) in bootstrap_results {
237            if run_labels.is_empty() {
238                continue;
239            }
240
241            // Use the first run's labels for this bootstrap
242            let labels = &run_labels[0];
243
244            // Update co-occurrence matrix
245            for (i, &idx_i) in indices.iter().enumerate() {
246                for (j, &idx_j) in indices.iter().enumerate() {
247                    if i != j {
248                        count_matrix[[idx_i, idx_j]] = count_matrix[[idx_i, idx_j]] + F::one();
249
250                        if labels[i] == labels[j] {
251                            co_occurrence_matrix[[idx_i, idx_j]] =
252                                co_occurrence_matrix[[idx_i, idx_j]] + F::one();
253                        }
254                    }
255                }
256            }
257        }
258
259        // Convert to probabilities
260        let mut stability_matrix = Array2::zeros((n_samples, n_samples));
261        for i in 0..n_samples {
262            for j in 0..n_samples {
263                if count_matrix[[i, j]] > F::zero() {
264                    stability_matrix[[i, j]] = co_occurrence_matrix[[i, j]] / count_matrix[[i, j]];
265                }
266            }
267        }
268
269        Ok(stability_matrix)
270    }
271}
272
273/// Consensus clustering for robust cluster identification
274///
275/// This method combines multiple clustering results to identify
276/// stable cluster structures.
277pub struct ConsensusClusterer<F: Float> {
278    config: StabilityConfig,
279    phantom: std::marker::PhantomData<F>,
280}
281
282impl<F: Float + FromPrimitive + Debug + std::iter::Sum + std::fmt::Display> ConsensusClusterer<F> {
283    /// Create a new consensus clusterer
284    pub fn new(config: StabilityConfig) -> Self {
285        Self {
286            config,
287            phantom: std::marker::PhantomData,
288        }
289    }
290
291    /// Find consensus clusters using multiple algorithm runs
292    pub fn find_consensus_clusters(&self, data: ArrayView2<F>, k: usize) -> Result<Array1<usize>> {
293        let n_samples = data.shape()[0];
294
295        if n_samples < 2 {
296            return Err(ClusteringError::InvalidInput(
297                "Need at least 2 samples for consensus clustering".into(),
298            ));
299        }
300
301        let mut rng = match self.config.random_seed {
302            Some(seed) => scirs2_core::random::rngs::StdRng::seed_from_u64(seed),
303            None => {
304                // Use a default seed when no seed is provided
305                scirs2_core::random::rngs::StdRng::seed_from_u64(42)
306            }
307        };
308
309        let mut all_labels = Vec::new();
310
311        // Run clustering multiple times with different initializations
312        for _run in 0..self.config.n_bootstrap {
313            let seed = rng.random::<u64>();
314
315            match kmeans2(
316                data,
317                k,
318                Some(100),   // max_iter
319                None,        // threshold
320                None,        // init method
321                None,        // missing method
322                Some(false), // check_finite
323                Some(seed),
324            ) {
325                Ok((_, labels)) => {
326                    let labels_usize: Array1<usize> = labels.mapv(|x| x);
327                    all_labels.push(labels_usize);
328                }
329                Err(_) => {
330                    // Skip failed runs
331                    continue;
332                }
333            }
334        }
335
336        if all_labels.is_empty() {
337            return Err(ClusteringError::ComputationError(
338                "All clustering runs failed".into(),
339            ));
340        }
341
342        // Build consensus matrix
343        let mut consensus_matrix = Array2::zeros((n_samples, n_samples));
344
345        for labels in &all_labels {
346            for i in 0..n_samples {
347                for j in 0..n_samples {
348                    if labels[i] == labels[j] {
349                        consensus_matrix[[i, j]] = consensus_matrix[[i, j]] + F::one();
350                    }
351                }
352            }
353        }
354
355        // Normalize by number of runs
356        let n_runs = F::from(all_labels.len()).expect("Operation failed");
357        consensus_matrix.mapv_inplace(|x| x / n_runs);
358
359        // Extract consensus clusters using threshold
360        let threshold = F::from(0.5).expect("Failed to convert constant to float");
361        self.extract_consensus_clusters(&consensus_matrix, threshold, k)
362    }
363
364    /// Extract clusters from consensus matrix
365    fn extract_consensus_clusters(
366        &self,
367        consensus_matrix: &Array2<F>,
368        threshold: F,
369        k: usize,
370    ) -> Result<Array1<usize>> {
371        let n_samples = consensus_matrix.shape()[0];
372        let mut labels = Array1::from_elem(n_samples, usize::MAX); // Unassigned
373        let mut current_cluster = 0;
374
375        // Use a greedy approach to find dense consensus regions
376        let mut unassigned: HashSet<usize> = (0..n_samples).collect();
377
378        while current_cluster < k && !unassigned.is_empty() {
379            // Find the pair with highest consensus that includes an unassigned point
380            let mut best_consensus = F::zero();
381            let mut best_seed = None;
382
383            for &i in &unassigned {
384                for &j in &unassigned {
385                    if i != j && consensus_matrix[[i, j]] > best_consensus {
386                        best_consensus = consensus_matrix[[i, j]];
387                        best_seed = Some(i);
388                    }
389                }
390            }
391
392            if let Some(seed) = best_seed {
393                // Grow cluster from seed
394                let mut cluster_members = Vec::new();
395                cluster_members.push(seed);
396
397                // Add all points with high consensus to the seed
398                for &candidate in &unassigned {
399                    if candidate != seed && consensus_matrix[[seed, candidate]] >= threshold {
400                        cluster_members.push(candidate);
401                    }
402                }
403
404                // Assign cluster label
405                for &member in &cluster_members {
406                    labels[member] = current_cluster;
407                    unassigned.remove(&member);
408                }
409
410                current_cluster += 1;
411            } else {
412                // No more good consensus pairs, assign remaining points to nearest cluster
413                break;
414            }
415        }
416
417        // Assign remaining unassigned points to the nearest existing cluster
418        for &unassigned_point in &unassigned {
419            let mut best_cluster = 0;
420            let mut best_avg_consensus = F::zero();
421
422            for cluster_id in 0..current_cluster {
423                let mut total_consensus = F::zero();
424                let mut count = 0;
425
426                for i in 0..n_samples {
427                    if labels[i] == cluster_id {
428                        total_consensus = total_consensus + consensus_matrix[[unassigned_point, i]];
429                        count += 1;
430                    }
431                }
432
433                if count > 0 {
434                    let avg_consensus =
435                        total_consensus / F::from(count).expect("Failed to convert to float");
436                    if avg_consensus > best_avg_consensus {
437                        best_avg_consensus = avg_consensus;
438                        best_cluster = cluster_id;
439                    }
440                }
441            }
442
443            labels[unassigned_point] = best_cluster;
444        }
445
446        Ok(labels)
447    }
448}
449
450/// Optimal cluster number selection using stability criteria
451pub struct OptimalKSelector<F: Float> {
452    config: StabilityConfig,
453    phantom: std::marker::PhantomData<F>,
454}
455
456impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
457    OptimalKSelector<F>
458{
459    /// Create a new optimal k selector
460    pub fn new(config: StabilityConfig) -> Self {
461        Self {
462            config,
463            phantom: std::marker::PhantomData,
464        }
465    }
466
467    /// Find optimal number of clusters using stability gap statistic
468    pub fn find_optimal_k(&self, data: ArrayView2<F>) -> Result<(usize, Vec<F>)> {
469        let (k_min, k_max) = self.config.k_range.unwrap_or((2, 10));
470        let mut stability_scores = Vec::new();
471
472        for k in k_min..=k_max {
473            let validator = BootstrapValidator::new(self.config.clone());
474            match validator.assess_kmeans_stability(data, k) {
475                Ok(result) => stability_scores.push(result.mean_stability),
476                Err(_) => stability_scores.push(F::zero()),
477            }
478        }
479
480        // Find k with maximum stability
481        let mut best_k = k_min;
482        let mut best_score = F::neg_infinity();
483
484        for (i, &score) in stability_scores.iter().enumerate() {
485            if score > best_score {
486                best_score = score;
487                best_k = k_min + i;
488            }
489        }
490
491        Ok((best_k, stability_scores))
492    }
493
494    /// Find optimal k using gap statistic with reference distribution
495    pub fn gap_statistic(&self, data: ArrayView2<F>) -> Result<(usize, Vec<F>)> {
496        let (k_min, k_max) = self.config.k_range.unwrap_or((2, 10));
497        let n_samples = data.shape()[0];
498        let n_features = data.shape()[1];
499
500        let mut gap_scores = Vec::new();
501
502        // Find data bounds for reference distribution
503        let mut min_vals = Array1::from_elem(n_features, F::infinity());
504        let mut max_vals = Array1::from_elem(n_features, F::neg_infinity());
505
506        for i in 0..n_samples {
507            for j in 0..n_features {
508                let val = data[[i, j]];
509                if val < min_vals[j] {
510                    min_vals[j] = val;
511                }
512                if val > max_vals[j] {
513                    max_vals[j] = val;
514                }
515            }
516        }
517
518        for k in k_min..=k_max {
519            // Calculate log(W_k) for original data
520            let original_wk = self.calculate_within_cluster_dispersion(data, k)?;
521            let log_wk = original_wk.ln();
522
523            // Calculate expected log(W_k) from reference distribution
524            let mut reference_log_wks = Vec::new();
525            let mut rng = match self.config.random_seed {
526                Some(seed) => scirs2_core::random::rngs::StdRng::seed_from_u64(seed),
527                None => {
528                    // Use a default seed when no seed is provided
529                    scirs2_core::random::rngs::StdRng::seed_from_u64(42)
530                }
531            };
532
533            for _b in 0..self.config.n_bootstrap {
534                // Generate reference data
535                let mut reference_data = Array2::zeros((n_samples, n_features));
536                for i in 0..n_samples {
537                    for j in 0..n_features {
538                        let range = max_vals[j] - min_vals[j];
539                        let random_val = min_vals[j]
540                            + range * F::from(rng.random::<f64>()).expect("Operation failed");
541                        reference_data[[i, j]] = random_val;
542                    }
543                }
544
545                let reference_wk =
546                    self.calculate_within_cluster_dispersion(reference_data.view(), k)?;
547                reference_log_wks.push(reference_wk.ln());
548            }
549
550            // Calculate gap statistic
551            let expected_log_wk = reference_log_wks
552                .iter()
553                .copied()
554                .fold(F::zero(), |acc, x| acc + x)
555                / F::from(reference_log_wks.len()).expect("Operation failed");
556            let gap = expected_log_wk - log_wk;
557            gap_scores.push(gap);
558        }
559
560        // Find optimal k (first k where gap(k) >= gap(k+1) - s_{k+1})
561        let mut optimal_k = k_min;
562        for i in 0..(gap_scores.len() - 1) {
563            if gap_scores[i] >= gap_scores[i + 1] {
564                optimal_k = k_min + i;
565                break;
566            }
567        }
568
569        Ok((optimal_k, gap_scores))
570    }
571
572    /// Calculate within-cluster dispersion W_k
573    fn calculate_within_cluster_dispersion(&self, data: ArrayView2<F>, k: usize) -> Result<F> {
574        // Run K-means clustering
575        match kmeans2(
576            data,
577            k,
578            Some(100),   // max_iter
579            None,        // threshold
580            None,        // init method
581            None,        // missing method
582            Some(false), // check_finite
583            self.config.random_seed,
584        ) {
585            Ok((centroids, labels)) => {
586                let mut total_dispersion = F::zero();
587
588                for cluster_id in 0..k {
589                    let mut cluster_dispersion = F::zero();
590                    let mut cluster_size = 0;
591
592                    // Calculate sum of squared distances within cluster
593                    for i in 0..data.shape()[0] {
594                        if labels[i] == cluster_id {
595                            let mut sq_dist = F::zero();
596                            for j in 0..data.shape()[1] {
597                                let diff = data[[i, j]] - centroids[[cluster_id, j]];
598                                sq_dist = sq_dist + diff * diff;
599                            }
600                            cluster_dispersion = cluster_dispersion + sq_dist;
601                            cluster_size += 1;
602                        }
603                    }
604
605                    // Normalize by cluster size
606                    if cluster_size > 1 {
607                        total_dispersion = total_dispersion
608                            + cluster_dispersion
609                                / F::from(cluster_size).expect("Failed to convert to float");
610                    }
611                }
612
613                Ok(total_dispersion)
614            }
615            Err(e) => Err(e),
616        }
617    }
618}
619
620/// Advanced stability assessment methods
621pub mod advanced {
622    use super::*;
623    use crate::ensemble::{EnsembleClusterer, EnsembleConfig};
624    use crate::metrics::{mutual_info_score, silhouette_score};
625
626    /// Cross-validation based stability assessment
627    ///
628    /// This method uses k-fold cross-validation to assess clustering stability
629    /// by training on different subsets and testing on held-out data.
630    pub struct CrossValidationStability<F: Float> {
631        config: StabilityConfig,
632        n_folds: usize,
633        _phantom: std::marker::PhantomData<F>,
634    }
635
636    impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
637        CrossValidationStability<F>
638    {
639        /// Create a new cross-validation stability assessor
640        pub fn new(config: StabilityConfig, n_folds: usize) -> Self {
641            Self {
642                config,
643                n_folds,
644                _phantom: std::marker::PhantomData,
645            }
646        }
647
648        /// Assess clustering stability using cross-validation
649        pub fn assess_stability(
650            &self,
651            data: ArrayView2<F>,
652            k: usize,
653        ) -> Result<StabilityResult<F>> {
654            let n_samples = data.shape()[0];
655            let fold_size = n_samples / self.n_folds;
656            let mut stability_scores = Vec::new();
657            let mut bootstrap_matrix = Array2::zeros((self.n_folds, self.n_folds));
658
659            // Perform k-fold cross-validation
660            for fold in 0..self.n_folds {
661                let start_idx = fold * fold_size;
662                let end_idx = if fold == self.n_folds - 1 {
663                    n_samples
664                } else {
665                    (fold + 1) * fold_size
666                };
667
668                // Create training set (excluding current fold)
669                let mut train_indices = Vec::new();
670                for i in 0..n_samples {
671                    if i < start_idx || i >= end_idx {
672                        train_indices.push(i);
673                    }
674                }
675
676                // Create training data
677                let train_data =
678                    Array2::from_shape_fn((train_indices.len(), data.shape()[1]), |(i, j)| {
679                        data[[train_indices[i], j]]
680                    });
681
682                // Run clustering on training data
683                let (train_centroids, train_labels) = kmeans2(
684                    train_data.view(),
685                    k,
686                    Some(100), // max_iter
687                    Some(F::from(1e-6).expect("Failed to convert constant to float")), // threshold
688                    None,      // init method
689                    None,      // missing method
690                    None,      // check_finite
691                    Some(42),  // seed
692                )?;
693
694                // Assign test data to nearest centroids
695                let test_labels = Array1::from_shape_fn(end_idx - start_idx, |i| {
696                    let test_point = data.row(start_idx + i);
697                    let mut min_dist = F::infinity();
698                    let mut closest_cluster = 0;
699
700                    for (cluster_id, centroid) in train_centroids.outer_iter().enumerate() {
701                        let dist = test_point
702                            .iter()
703                            .zip(centroid.iter())
704                            .map(|(a, b)| (*a - *b) * (*a - *b))
705                            .sum::<F>()
706                            .sqrt();
707
708                        if dist < min_dist {
709                            min_dist = dist;
710                            closest_cluster = cluster_id;
711                        }
712                    }
713                    closest_cluster
714                });
715
716                // Calculate stability score for this fold
717                let stability = self.calculate_fold_stability(&test_labels, k)?;
718                stability_scores.push(stability);
719            }
720
721            // Calculate mean and standard deviation
722            let mean_stability = stability_scores.iter().fold(F::zero(), |acc, x| acc + *x)
723                / F::from(stability_scores.len()).expect("Operation failed");
724            let variance = stability_scores
725                .iter()
726                .map(|&s| (s - mean_stability) * (s - mean_stability))
727                .fold(F::zero(), |acc, x| acc + x)
728                / F::from(stability_scores.len()).expect("Operation failed");
729            let std_stability = variance.sqrt();
730
731            Ok(StabilityResult {
732                stability_scores,
733                consensus_labels: None,
734                optimal_k: None,
735                mean_stability,
736                std_stability,
737                bootstrap_matrix,
738            })
739        }
740
741        fn calculate_fold_stability(&self, labels: &Array1<usize>, k: usize) -> Result<F> {
742            // Calculate intra-cluster cohesion
743            let mut cluster_cohesion = F::zero();
744            let mut total_pairs = 0;
745
746            for cluster_id in 0..k {
747                let cluster_members: Vec<_> = labels
748                    .iter()
749                    .enumerate()
750                    .filter(|(_, &label)| label == cluster_id)
751                    .map(|(idx_, _)| idx_)
752                    .collect();
753
754                let cluster_size = cluster_members.len();
755                if cluster_size > 1 {
756                    let pairs = cluster_size * (cluster_size - 1) / 2;
757                    cluster_cohesion =
758                        cluster_cohesion + F::from(pairs).expect("Failed to convert to float");
759                    total_pairs += pairs;
760                }
761            }
762
763            if total_pairs == 0 {
764                Ok(F::zero())
765            } else {
766                Ok(cluster_cohesion / F::from(total_pairs).expect("Failed to convert to float"))
767            }
768        }
769    }
770
771    /// Perturbation-based stability assessment
772    ///
773    /// This method assesses stability by introducing controlled perturbations
774    /// to the data and measuring how much the clustering results change.
775    pub struct PerturbationStability<F: Float> {
776        config: StabilityConfig,
777        perturbation_types: Vec<PerturbationType>,
778        _phantom: std::marker::PhantomData<F>,
779    }
780
781    /// Types of perturbations for stability testing
782    #[derive(Debug, Clone)]
783    pub enum PerturbationType {
784        /// Add Gaussian noise
785        GaussianNoise { std_dev: f64 },
786        /// Remove random samples
787        SampleRemoval { removal_rate: f64 },
788        /// Add random features
789        FeatureNoise { noise_level: f64 },
790        /// Outlier injection
791        OutlierInjection {
792            outlier_rate: f64,
793            outlier_magnitude: f64,
794        },
795    }
796
797    impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
798        PerturbationStability<F>
799    {
800        /// Create a new perturbation stability assessor
801        pub fn new(config: StabilityConfig, perturbation_types: Vec<PerturbationType>) -> Self {
802            Self {
803                config,
804                perturbation_types,
805                _phantom: std::marker::PhantomData,
806            }
807        }
808
809        /// Assess clustering stability under perturbations
810        pub fn assess_stability(
811            &self,
812            data: ArrayView2<F>,
813            k: usize,
814        ) -> Result<StabilityResult<F>> {
815            let mut all_stability_scores = Vec::new();
816            let mut rng = scirs2_core::random::rng();
817
818            // Get baseline clustering
819            let (baseline_centroids, baseline_labels) = kmeans2(
820                data,
821                k,
822                Some(100), // max_iter
823                Some(F::from(1e-6).expect("Failed to convert constant to float")), // threshold
824                None,      // init method
825                None,      // missing method
826                None,      // check_finite
827                Some(42),  // seed
828            )?;
829
830            // Test each perturbation type
831            for perturbation in &self.perturbation_types {
832                let mut perturbation_scores = Vec::new();
833
834                for _ in 0..self.config.n_bootstrap {
835                    // Apply perturbation
836                    let perturbed_data = self.apply_perturbation(data, perturbation, &mut rng)?;
837
838                    // Run clustering on perturbed data
839                    let (_, perturbed_labels) = kmeans2(
840                        perturbed_data.view(),
841                        k,
842                        Some(100), // max_iter
843                        Some(F::from(1e-6).expect("Failed to convert constant to float")), // threshold
844                        None, // init method
845                        None, // missing method
846                        None, // check_finite
847                        None, // random seed
848                    )?;
849
850                    // Calculate similarity to baseline
851                    let similarity =
852                        self.calculate_label_similarity(&baseline_labels, &perturbed_labels)?;
853                    perturbation_scores.push(similarity);
854                }
855
856                all_stability_scores.extend(perturbation_scores);
857            }
858
859            // Calculate overall statistics
860            let mean_stability = all_stability_scores
861                .iter()
862                .fold(F::zero(), |acc, x| acc + *x)
863                / F::from(all_stability_scores.len()).expect("Operation failed");
864            let variance = all_stability_scores
865                .iter()
866                .map(|&s| (s - mean_stability) * (s - mean_stability))
867                .sum::<F>()
868                / F::from(all_stability_scores.len()).expect("Operation failed");
869            let std_stability = variance.sqrt();
870
871            let bootstrap_matrix =
872                Array2::zeros((self.config.n_bootstrap, self.perturbation_types.len()));
873
874            Ok(StabilityResult {
875                stability_scores: all_stability_scores,
876                consensus_labels: None,
877                optimal_k: None,
878                mean_stability,
879                std_stability,
880                bootstrap_matrix,
881            })
882        }
883
884        fn apply_perturbation(
885            &self,
886            data: ArrayView2<F>,
887            perturbation: &PerturbationType,
888            rng: &mut impl Rng,
889        ) -> Result<Array2<F>> {
890            let mut perturbed = data.to_owned();
891
892            match perturbation {
893                PerturbationType::GaussianNoise { std_dev } => {
894                    for elem in perturbed.iter_mut() {
895                        let noise = rng.random::<f64>() * std_dev;
896                        *elem = *elem + F::from(noise).expect("Failed to convert to float");
897                    }
898                }
899                PerturbationType::SampleRemoval { removal_rate } => {
900                    let n_samples = data.shape()[0];
901                    let n_remove = (n_samples as f64 * removal_rate) as usize;
902                    let mut indices: Vec<_> = (0..n_samples).collect();
903                    indices.shuffle(rng);
904                    indices.truncate(n_samples - n_remove);
905                    indices.sort();
906
907                    let mut new_data = Array2::zeros((indices.len(), data.shape()[1]));
908                    for (new_i, &old_i) in indices.iter().enumerate() {
909                        new_data.row_mut(new_i).assign(&data.row(old_i));
910                    }
911                    perturbed = new_data;
912                }
913                PerturbationType::FeatureNoise { noise_level } => {
914                    for elem in perturbed.iter_mut() {
915                        let noise = (rng.random::<f64>() - 0.5) * 2.0 * noise_level;
916                        *elem = *elem + F::from(noise).expect("Failed to convert to float");
917                    }
918                }
919                PerturbationType::OutlierInjection {
920                    outlier_rate,
921                    outlier_magnitude,
922                } => {
923                    let n_samples = data.shape()[0];
924                    let n_outliers = (n_samples as f64 * outlier_rate) as usize;
925
926                    for _ in 0..n_outliers {
927                        let sample_idx = rng.random_range(0..n_samples);
928                        let feature_idx = rng.random_range(0..data.shape()[1]);
929                        let outlier_value = rng.random::<f64>() * outlier_magnitude;
930                        perturbed[[sample_idx, feature_idx]] =
931                            F::from(outlier_value).expect("Failed to convert to float");
932                    }
933                }
934            }
935
936            Ok(perturbed)
937        }
938
939        fn calculate_label_similarity(
940            &self,
941            labels1: &Array1<usize>,
942            labels2: &Array1<usize>,
943        ) -> Result<F> {
944            if labels1.len() != labels2.len() {
945                return Ok(F::zero());
946            }
947
948            // Convert to i32 for ARI calculation
949            let labels1_i32: Array1<i32> = labels1.mapv(|x| x as i32);
950            let labels2_i32: Array1<i32> = labels2.mapv(|x| x as i32);
951
952            // Use adjusted rand index as similarity measure
953            let ari: f64 = adjusted_rand_index(labels1_i32.view(), labels2_i32.view())?;
954            Ok(F::from(ari).expect("Failed to convert to float"))
955        }
956    }
957
958    /// Multi-scale stability assessment
959    ///
960    /// This method assesses stability across different data scales and resolutions
961    /// to understand how clustering behaves at different granularities.
962    pub struct MultiScaleStability<F: Float> {
963        config: StabilityConfig,
964        scale_factors: Vec<f64>,
965        _phantom: std::marker::PhantomData<F>,
966    }
967
968    impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
969        MultiScaleStability<F>
970    {
971        /// Create a new multi-scale stability assessor
972        pub fn new(config: StabilityConfig, scale_factors: Vec<f64>) -> Self {
973            Self {
974                config,
975                scale_factors,
976                _phantom: std::marker::PhantomData,
977            }
978        }
979
980        /// Assess clustering stability across multiple scales
981        pub fn assess_stability(
982            &self,
983            data: ArrayView2<F>,
984            k_range: (usize, usize),
985        ) -> Result<Vec<StabilityResult<F>>> {
986            let mut results = Vec::new();
987
988            for &scale_factor in &self.scale_factors {
989                // Scale the data
990                let scaled_data =
991                    data.mapv(|x| x * F::from(scale_factor).expect("Failed to convert to float"));
992
993                // Assess stability at this scale for different k values
994                for k in k_range.0..=k_range.1 {
995                    let validator = BootstrapValidator::new(self.config.clone());
996                    let stability_result =
997                        validator.assess_kmeans_stability(scaled_data.view(), k)?;
998                    results.push(stability_result);
999                }
1000            }
1001
1002            Ok(results)
1003        }
1004
1005        /// Find the most stable scale and cluster count combination
1006        pub fn find_optimal_scale_and_k(
1007            &self,
1008            data: ArrayView2<F>,
1009            k_range: (usize, usize),
1010        ) -> Result<(f64, usize, F)> {
1011            let results = self.assess_stability(data, k_range)?;
1012
1013            let mut best_scale = self.scale_factors[0];
1014            let mut best_k = k_range.0;
1015            let mut best_stability = F::neg_infinity();
1016
1017            let mut result_idx = 0;
1018            for &scale_factor in &self.scale_factors {
1019                for k in k_range.0..=k_range.1 {
1020                    if result_idx < results.len() {
1021                        let stability = results[result_idx].mean_stability;
1022                        if stability > best_stability {
1023                            best_stability = stability;
1024                            best_scale = scale_factor;
1025                            best_k = k;
1026                        }
1027                        result_idx += 1;
1028                    }
1029                }
1030            }
1031
1032            Ok((best_scale, best_k, best_stability))
1033        }
1034    }
1035
1036    /// Prediction Strength Method for clustering validation
1037    ///
1038    /// This method assesses clustering stability by measuring how well cluster assignments
1039    /// from one dataset can predict assignments in another dataset. Based on Tibshirani & Walther's
1040    /// prediction strength criterion.
1041    pub struct PredictionStrength<F: Float> {
1042        /// Configuration for prediction strength assessment
1043        pub config: PredictionStrengthConfig,
1044        phantom: std::marker::PhantomData<F>,
1045    }
1046
1047    /// Configuration for prediction strength method
1048    #[derive(Debug, Clone)]
1049    pub struct PredictionStrengthConfig {
1050        /// Number of bootstrap iterations for assessment
1051        pub n_bootstrap: usize,
1052        /// Fraction of data to use for training in each split
1053        pub train_ratio: f64,
1054        /// Minimum prediction strength threshold for validation
1055        pub strength_threshold: f64,
1056        /// Random seed for reproducible results
1057        pub random_seed: Option<u64>,
1058    }
1059
1060    impl Default for PredictionStrengthConfig {
1061        fn default() -> Self {
1062            Self {
1063                n_bootstrap: 50,
1064                train_ratio: 0.5,
1065                strength_threshold: 0.8,
1066                random_seed: None,
1067            }
1068        }
1069    }
1070
1071    impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
1072        PredictionStrength<F>
1073    {
1074        /// Create a new prediction strength validator
1075        pub fn new(config: PredictionStrengthConfig) -> Self {
1076            Self {
1077                config,
1078                phantom: std::marker::PhantomData,
1079            }
1080        }
1081
1082        /// Assess prediction strength for a range of cluster numbers
1083        pub fn assess_k_range(
1084            &self,
1085            data: ArrayView2<F>,
1086            k_range: (usize, usize),
1087        ) -> Result<Vec<F>> {
1088            let mut prediction_strengths = Vec::new();
1089
1090            for k in k_range.0..=k_range.1 {
1091                let strength = self.compute_prediction_strength(data, k)?;
1092                prediction_strengths.push(strength);
1093            }
1094
1095            Ok(prediction_strengths)
1096        }
1097
1098        /// Compute prediction strength for a specific number of clusters
1099        pub fn compute_prediction_strength(&self, data: ArrayView2<F>, k: usize) -> Result<F> {
1100            let mut rng = match self.config.random_seed {
1101                Some(seed) => scirs2_core::random::rngs::StdRng::seed_from_u64(seed),
1102                None => scirs2_core::random::rngs::StdRng::seed_from_u64(
1103                    scirs2_core::random::rng().random(),
1104                ),
1105            };
1106
1107            let n_samples = data.nrows();
1108            let train_size = ((n_samples as f64) * self.config.train_ratio) as usize;
1109
1110            let mut prediction_scores = Vec::new();
1111
1112            for _ in 0..self.config.n_bootstrap {
1113                // Split data into training and test sets
1114                let mut indices: Vec<usize> = (0..n_samples).collect();
1115                indices.shuffle(&mut rng);
1116
1117                let train_indices = &indices[..train_size];
1118                let test_indices = &indices[train_size..];
1119
1120                if test_indices.is_empty() {
1121                    continue;
1122                }
1123
1124                // Create training and test data
1125                let train_data = data.select(scirs2_core::ndarray::Axis(0), train_indices);
1126                let test_data = data.select(scirs2_core::ndarray::Axis(0), test_indices);
1127
1128                // Cluster training data
1129                match kmeans2(train_data.view(), k, None, None, None, None, None, None) {
1130                    Ok((_, train_labels)) => {
1131                        // Cluster test data
1132                        match kmeans2(test_data.view(), k, None, None, None, None, None, None) {
1133                            Ok((_, test_labels)) => {
1134                                // Compute prediction strength
1135                                let strength = self.compute_pairwise_prediction_strength(
1136                                    &train_data,
1137                                    &test_data,
1138                                    &train_labels,
1139                                    &test_labels,
1140                                )?;
1141                                prediction_scores.push(strength);
1142                            }
1143                            Err(_) => continue,
1144                        }
1145                    }
1146                    Err(_) => continue,
1147                }
1148            }
1149
1150            if prediction_scores.is_empty() {
1151                return Ok(F::zero());
1152            }
1153
1154            // Return mean prediction strength
1155            let sum: F = prediction_scores.iter().fold(F::zero(), |acc, &x| acc + x);
1156            Ok(sum / F::from(prediction_scores.len()).expect("Operation failed"))
1157        }
1158
1159        /// Compute pairwise prediction strength between training and test assignments
1160        fn compute_pairwise_prediction_strength(
1161            &self,
1162            train_data: &Array2<F>,
1163            test_data: &Array2<F>,
1164            train_labels: &Array1<usize>,
1165            test_labels: &Array1<usize>,
1166        ) -> Result<F> {
1167            let test_size = test_data.nrows();
1168            let mut correct_predictions = 0;
1169            let mut total_predictions = 0;
1170
1171            // For each pair of test points
1172            for i in 0..test_size {
1173                for j in (i + 1)..test_size {
1174                    // Find closest points in training _data
1175                    let closest_train_i = self.find_closest_point(&test_data.row(i), train_data)?;
1176                    let closest_train_j = self.find_closest_point(&test_data.row(j), train_data)?;
1177
1178                    // Predict whether test points should be in same cluster
1179                    let predicted_same =
1180                        train_labels[closest_train_i] == train_labels[closest_train_j];
1181                    let actual_same = test_labels[i] == test_labels[j];
1182
1183                    if predicted_same == actual_same {
1184                        correct_predictions += 1;
1185                    }
1186                    total_predictions += 1;
1187                }
1188            }
1189
1190            if total_predictions == 0 {
1191                return Ok(F::zero());
1192            }
1193
1194            Ok(
1195                F::from(correct_predictions as f64 / total_predictions as f64)
1196                    .expect("Failed to convert to float"),
1197            )
1198        }
1199
1200        /// Find closest point in training data to a test point
1201        fn find_closest_point(
1202            &self,
1203            test_point: &scirs2_core::ndarray::ArrayView1<F>,
1204            train_data: &Array2<F>,
1205        ) -> Result<usize> {
1206            let mut min_distance = F::infinity();
1207            let mut closest_idx = 0;
1208
1209            for (idx, train_point) in train_data.rows().into_iter().enumerate() {
1210                let distance = test_point
1211                    .iter()
1212                    .zip(train_point.iter())
1213                    .map(|(a, b)| (*a - *b) * (*a - *b))
1214                    .fold(F::zero(), |acc, x| acc + x)
1215                    .sqrt();
1216
1217                if distance < min_distance {
1218                    min_distance = distance;
1219                    closest_idx = idx;
1220                }
1221            }
1222
1223            Ok(closest_idx)
1224        }
1225
1226        /// Find optimal number of clusters using prediction strength
1227        pub fn find_optimal_k(
1228            &self,
1229            data: ArrayView2<F>,
1230            k_range: (usize, usize),
1231        ) -> Result<usize> {
1232            let strengths = self.assess_k_range(data, k_range)?;
1233
1234            // Find largest k with prediction strength above threshold
1235            for (idx, &strength) in strengths.iter().enumerate().rev() {
1236                if strength
1237                    >= F::from(self.config.strength_threshold).expect("Failed to convert to float")
1238                {
1239                    return Ok(k_range.0 + idx);
1240                }
1241            }
1242
1243            // If no k meets threshold, return the one with highest strength
1244            let best_idx = strengths
1245                .iter()
1246                .enumerate()
1247                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1248                .map(|(idx_, _)| idx_)
1249                .unwrap_or(0);
1250
1251            Ok(k_range.0 + best_idx)
1252        }
1253    }
1254
1255    /// Jaccard Stability Index for clustering validation
1256    ///
1257    /// Measures stability using Jaccard similarity between cluster assignments
1258    /// across different bootstrap samples or parameter settings.
1259    pub struct JaccardStability<F: Float> {
1260        /// Number of bootstrap iterations
1261        pub n_bootstrap: usize,
1262        /// Subsample ratio for each bootstrap
1263        pub subsample_ratio: f64,
1264        /// Random seed for reproducible results
1265        pub random_seed: Option<u64>,
1266        _phantom: std::marker::PhantomData<F>,
1267    }
1268
1269    impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
1270        JaccardStability<F>
1271    {
1272        /// Create a new Jaccard stability validator
1273        pub fn new(n_bootstrap: usize, subsample_ratio: f64, random_seed: Option<u64>) -> Self {
1274            Self {
1275                n_bootstrap,
1276                subsample_ratio,
1277                random_seed,
1278                _phantom: std::marker::PhantomData,
1279            }
1280        }
1281
1282        /// Compute Jaccard stability index for given data and cluster number
1283        pub fn compute_stability(&self, data: ArrayView2<F>, k: usize) -> Result<F> {
1284            let mut rng = match self.random_seed {
1285                Some(seed) => scirs2_core::random::rngs::StdRng::seed_from_u64(seed),
1286                None => scirs2_core::random::rngs::StdRng::seed_from_u64(
1287                    scirs2_core::random::rng().random(),
1288                ),
1289            };
1290
1291            let n_samples = data.nrows();
1292            let subsample_size = ((n_samples as f64) * self.subsample_ratio) as usize;
1293
1294            let mut jaccard_scores = Vec::new();
1295
1296            // Generate pairs of bootstrap samples and compute Jaccard similarity
1297            for _ in 0..self.n_bootstrap {
1298                // First bootstrap sample
1299                let mut indices1: Vec<usize> = (0..n_samples).collect();
1300                indices1.shuffle(&mut rng);
1301                let sample_indices1 = &indices1[..subsample_size];
1302                let sample_data1 = data.select(scirs2_core::ndarray::Axis(0), sample_indices1);
1303
1304                // Second bootstrap sample
1305                let mut indices2: Vec<usize> = (0..n_samples).collect();
1306                indices2.shuffle(&mut rng);
1307                let sample_indices2 = &indices2[..subsample_size];
1308                let sample_data2 = data.select(scirs2_core::ndarray::Axis(0), sample_indices2);
1309
1310                // Cluster both samples
1311                match (
1312                    kmeans2(sample_data1.view(), k, None, None, None, None, None, None),
1313                    kmeans2(sample_data2.view(), k, None, None, None, None, None, None),
1314                ) {
1315                    (Ok((_, labels1)), Ok((_, labels2))) => {
1316                        // Find overlapping samples
1317                        let overlap_indices: Vec<(usize, usize)> = sample_indices1
1318                            .iter()
1319                            .enumerate()
1320                            .filter_map(|(i1, &idx1)| {
1321                                sample_indices2
1322                                    .iter()
1323                                    .enumerate()
1324                                    .find(|(_, &idx2)| idx1 == idx2)
1325                                    .map(|(i2_, _)| (i1, i2_))
1326                            })
1327                            .collect();
1328
1329                        if overlap_indices.len() >= 2 {
1330                            let jaccard = self.compute_jaccard_similarity(
1331                                &labels1,
1332                                &labels2,
1333                                &overlap_indices,
1334                            )?;
1335                            jaccard_scores.push(jaccard);
1336                        }
1337                    }
1338                    _ => continue,
1339                }
1340            }
1341
1342            if jaccard_scores.is_empty() {
1343                return Ok(F::zero());
1344            }
1345
1346            // Return mean Jaccard similarity
1347            let sum: F = jaccard_scores.iter().fold(F::zero(), |acc, &x| acc + x);
1348            Ok(sum / F::from(jaccard_scores.len()).expect("Operation failed"))
1349        }
1350
1351        /// Compute Jaccard similarity between two cluster assignments
1352        fn compute_jaccard_similarity(
1353            &self,
1354            labels1: &Array1<usize>,
1355            labels2: &Array1<usize>,
1356            overlap_indices: &[(usize, usize)],
1357        ) -> Result<F> {
1358            let mut same_cluster_both = 0;
1359            let mut same_cluster_either = 0;
1360
1361            let n_overlap = overlap_indices.len();
1362
1363            for i in 0..n_overlap {
1364                for j in (i + 1)..n_overlap {
1365                    let (idx1_i, idx2_i) = overlap_indices[i];
1366                    let (idx1_j, idx2_j) = overlap_indices[j];
1367
1368                    let same_in_clustering1 = labels1[idx1_i] == labels1[idx1_j];
1369                    let same_in_clustering2 = labels2[idx2_i] == labels2[idx2_j];
1370
1371                    if same_in_clustering1 && same_in_clustering2 {
1372                        same_cluster_both += 1;
1373                    }
1374                    if same_in_clustering1 || same_in_clustering2 {
1375                        same_cluster_either += 1;
1376                    }
1377                }
1378            }
1379
1380            if same_cluster_either == 0 {
1381                return Ok(F::one()); // All pairs are different in both clusterings
1382            }
1383
1384            Ok(
1385                F::from(same_cluster_both as f64 / same_cluster_either as f64)
1386                    .expect("Failed to convert to float"),
1387            )
1388        }
1389
1390        /// Assess stability across a range of cluster numbers
1391        pub fn assess_k_range(
1392            &self,
1393            data: ArrayView2<F>,
1394            k_range: (usize, usize),
1395        ) -> Result<Vec<F>> {
1396            let mut stabilities = Vec::new();
1397
1398            for k in k_range.0..=k_range.1 {
1399                let stability = self.compute_stability(data, k)?;
1400                stabilities.push(stability);
1401            }
1402
1403            Ok(stabilities)
1404        }
1405    }
1406
1407    /// Cluster-Specific Stability Indices
1408    ///
1409    /// Provides stability metrics for individual clusters rather than
1410    /// global stability measures.
1411    pub struct ClusterSpecificStability<F: Float> {
1412        /// Configuration for cluster-specific stability assessment
1413        pub config: StabilityConfig,
1414        phantom: std::marker::PhantomData<F>,
1415    }
1416
1417    /// Results of cluster-specific stability assessment
1418    #[derive(Debug, Clone)]
1419    pub struct ClusterStabilityResult<F: Float> {
1420        /// Stability score for each cluster
1421        pub cluster_stabilities: Vec<F>,
1422        /// Mean stability across all clusters
1423        pub mean_stability: F,
1424        /// Standard deviation of cluster stabilities
1425        pub std_stability: F,
1426        /// Cluster size consistency across bootstrap samples
1427        pub size_consistency: Vec<F>,
1428    }
1429
1430    impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
1431        ClusterSpecificStability<F>
1432    {
1433        /// Create a new cluster-specific stability validator
1434        pub fn new(config: StabilityConfig) -> Self {
1435            Self {
1436                config,
1437                phantom: std::marker::PhantomData,
1438            }
1439        }
1440
1441        /// Assess stability for each cluster individually
1442        pub fn assess_cluster_stability(
1443            &self,
1444            data: ArrayView2<F>,
1445            k: usize,
1446        ) -> Result<ClusterStabilityResult<F>> {
1447            let mut rng = match self.config.random_seed {
1448                Some(seed) => scirs2_core::random::rngs::StdRng::seed_from_u64(seed),
1449                None => scirs2_core::random::rngs::StdRng::seed_from_u64(
1450                    scirs2_core::random::rng().random(),
1451                ),
1452            };
1453
1454            let n_samples = data.nrows();
1455            let subsample_size = ((n_samples as f64) * self.config.subsample_ratio) as usize;
1456
1457            let mut cluster_memberships: Vec<Vec<HashSet<usize>>> = vec![Vec::new(); k];
1458            let mut cluster_sizes: Vec<Vec<usize>> = vec![Vec::new(); k];
1459
1460            // Bootstrap sampling and clustering
1461            for _ in 0..self.config.n_bootstrap {
1462                let mut indices: Vec<usize> = (0..n_samples).collect();
1463                indices.shuffle(&mut rng);
1464                let sample_indices = &indices[..subsample_size];
1465                let sample_data = data.select(scirs2_core::ndarray::Axis(0), sample_indices);
1466
1467                match kmeans2(sample_data.view(), k, None, None, None, None, None, None) {
1468                    Ok((_, labels)) => {
1469                        // Track cluster memberships
1470                        for cluster_id in 0..k {
1471                            let mut cluster_members = HashSet::new();
1472                            for (local_idx, &label) in labels.iter().enumerate() {
1473                                if label == cluster_id {
1474                                    cluster_members.insert(sample_indices[local_idx]);
1475                                }
1476                            }
1477                            cluster_memberships[cluster_id].push(cluster_members.clone());
1478                            cluster_sizes[cluster_id].push(cluster_members.len());
1479                        }
1480                    }
1481                    Err(_) => continue,
1482                }
1483            }
1484
1485            // Compute stability for each cluster
1486            let mut cluster_stabilities = Vec::new();
1487            let mut size_consistency = Vec::new();
1488
1489            for cluster_id in 0..k {
1490                let stability = self.compute_cluster_stability(&cluster_memberships[cluster_id])?;
1491                cluster_stabilities.push(stability);
1492
1493                let consistency = self.compute_size_consistency(&cluster_sizes[cluster_id])?;
1494                size_consistency.push(consistency);
1495            }
1496
1497            // Compute statistics
1498            let mean_stability = cluster_stabilities
1499                .iter()
1500                .fold(F::zero(), |acc, &x| acc + x)
1501                / F::from(cluster_stabilities.len()).expect("Operation failed");
1502
1503            let variance = cluster_stabilities
1504                .iter()
1505                .map(|&x| (x - mean_stability) * (x - mean_stability))
1506                .fold(F::zero(), |acc, x| acc + x)
1507                / F::from(cluster_stabilities.len()).expect("Operation failed");
1508            let std_stability = variance.sqrt();
1509
1510            Ok(ClusterStabilityResult {
1511                cluster_stabilities,
1512                mean_stability,
1513                std_stability,
1514                size_consistency,
1515            })
1516        }
1517
1518        /// Compute stability for a single cluster across bootstrap samples
1519        fn compute_cluster_stability(&self, cluster_samples: &[HashSet<usize>]) -> Result<F> {
1520            if cluster_samples.len() < 2 {
1521                return Ok(F::zero());
1522            }
1523
1524            let mut jaccard_scores = Vec::new();
1525
1526            // Compute pairwise Jaccard similarities
1527            for i in 0..cluster_samples.len() {
1528                for j in (i + 1)..cluster_samples.len() {
1529                    let intersection_size =
1530                        cluster_samples[i].intersection(&cluster_samples[j]).count();
1531                    let union_size = cluster_samples[i].union(&cluster_samples[j]).count();
1532
1533                    if union_size > 0 {
1534                        let jaccard = intersection_size as f64 / union_size as f64;
1535                        jaccard_scores.push(F::from(jaccard).expect("Failed to convert to float"));
1536                    }
1537                }
1538            }
1539
1540            if jaccard_scores.is_empty() {
1541                return Ok(F::zero());
1542            }
1543
1544            // Return mean Jaccard similarity
1545            let sum: F = jaccard_scores.iter().fold(F::zero(), |acc, &x| acc + x);
1546            Ok(sum / F::from(jaccard_scores.len()).expect("Operation failed"))
1547        }
1548
1549        /// Compute size consistency for a cluster across bootstrap samples
1550        fn compute_size_consistency(&self, sizes: &[usize]) -> Result<F> {
1551            if sizes.is_empty() {
1552                return Ok(F::zero());
1553            }
1554
1555            let mean_size = sizes.iter().sum::<usize>() as f64 / sizes.len() as f64;
1556            let variance = sizes
1557                .iter()
1558                .map(|&size| (size as f64 - mean_size).powi(2))
1559                .sum::<f64>()
1560                / sizes.len() as f64;
1561
1562            let cv = if mean_size > 0.0 {
1563                variance.sqrt() / mean_size
1564            } else {
1565                0.0
1566            };
1567            Ok(F::one() - F::from(cv).expect("Failed to convert to float")) // Consistency = 1 - CV
1568        }
1569    }
1570
1571    /// Parameter Stability Analysis
1572    ///
1573    /// Assesses how sensitive clustering results are to parameter changes
1574    /// across different algorithm settings.
1575    pub struct ParameterStabilityAnalyzer<F: Float> {
1576        /// Base parameters for analysis
1577        pub base_k: usize,
1578        /// Parameter perturbation ranges
1579        pub perturbation_ranges: Vec<f64>,
1580        /// Number of random parameter samples per range
1581        pub n_samples_per_range: usize,
1582        /// Random seed for reproducible results
1583        pub random_seed: Option<u64>,
1584        _phantom: std::marker::PhantomData<F>,
1585    }
1586
1587    /// Results of parameter stability analysis
1588    #[derive(Debug, Clone)]
1589    pub struct ParameterStabilityResult<F: Float> {
1590        /// Stability scores for different perturbation levels
1591        pub stability_by_perturbation: Vec<F>,
1592        /// Parameter sensitivity profile
1593        pub sensitivity_profile: Vec<F>,
1594        /// Robust parameter range recommendation
1595        pub robust_range: (f64, f64),
1596    }
1597
1598    impl<F: Float + FromPrimitive + Debug + 'static + std::iter::Sum + std::fmt::Display>
1599        ParameterStabilityAnalyzer<F>
1600    {
1601        /// Create a new parameter stability analyzer
1602        pub fn new(
1603            base_k: usize,
1604            perturbation_ranges: Vec<f64>,
1605            n_samples_per_range: usize,
1606            random_seed: Option<u64>,
1607        ) -> Self {
1608            Self {
1609                base_k,
1610                perturbation_ranges,
1611                n_samples_per_range,
1612                random_seed,
1613                _phantom: std::marker::PhantomData,
1614            }
1615        }
1616
1617        /// Analyze parameter stability across perturbation ranges
1618        pub fn analyze_stability(
1619            &self,
1620            data: ArrayView2<F>,
1621        ) -> Result<ParameterStabilityResult<F>> {
1622            let mut rng = match self.random_seed {
1623                Some(seed) => scirs2_core::random::rngs::StdRng::seed_from_u64(seed),
1624                None => scirs2_core::random::rngs::StdRng::seed_from_u64(
1625                    scirs2_core::random::rng().random(),
1626                ),
1627            };
1628
1629            let mut stability_by_perturbation = Vec::new();
1630            let mut sensitivity_profile = Vec::new();
1631
1632            // Get baseline clustering
1633            let baseline_result = kmeans2(data, self.base_k, None, None, None, None, None, None)?;
1634
1635            for &perturbation_level in &self.perturbation_ranges {
1636                let mut stability_scores = Vec::new();
1637
1638                for _ in 0..self.n_samples_per_range {
1639                    // Perturb parameters (here we vary k as an example)
1640                    let k_perturbation = (F::from(rng.random::<f64>()).expect("Operation failed")
1641                        - F::from(0.5).expect("Failed to convert constant to float"))
1642                        * F::from(2.0).expect("Failed to convert constant to float")
1643                        * F::from(perturbation_level).expect("Failed to convert to float");
1644                    let perturbed_k = (self.base_k as f64
1645                        * (1.0 + k_perturbation.to_f64().expect("Operation failed")))
1646                    .round()
1647                    .max(1.0) as usize;
1648
1649                    match kmeans2(data, perturbed_k, None, None, None, None, None, None) {
1650                        Ok((_, perturbed_labels)) => {
1651                            // Compute stability using ARI with baseline
1652                            // Convert usize labels to i32 for ARI computation
1653                            let baseline_i32 = baseline_result.1.mapv(|x| x as i32);
1654                            let perturbed_i32 = perturbed_labels.mapv(|x| x as i32);
1655                            match adjusted_rand_index(baseline_i32.view(), perturbed_i32.view()) {
1656                                Ok(stability) => stability_scores.push(stability),
1657                                Err(_) => continue,
1658                            }
1659                        }
1660                        Err(_) => continue,
1661                    }
1662                }
1663
1664                if !stability_scores.is_empty() {
1665                    let mean_stability = stability_scores.iter().fold(F::zero(), |acc, &x| acc + x)
1666                        / F::from(stability_scores.len()).expect("Operation failed");
1667                    stability_by_perturbation.push(mean_stability);
1668
1669                    // Compute sensitivity (1 - stability)
1670                    sensitivity_profile.push(F::one() - mean_stability);
1671                }
1672            }
1673
1674            // Find robust parameter range (where sensitivity is low)
1675            let robust_range = self.find_robust_range(&sensitivity_profile);
1676
1677            Ok(ParameterStabilityResult {
1678                stability_by_perturbation,
1679                sensitivity_profile,
1680                robust_range,
1681            })
1682        }
1683
1684        /// Find the range of perturbations with lowest sensitivity
1685        fn find_robust_range(&self, sensitivity_profile: &[F]) -> (f64, f64) {
1686            if sensitivity_profile.is_empty() {
1687                return (0.0, 0.0);
1688            }
1689
1690            // Find minimum sensitivity
1691            let min_sensitivity = sensitivity_profile
1692                .iter()
1693                .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1694                .expect("Operation failed");
1695
1696            // Define threshold as min + 10% of range
1697            let max_sensitivity = sensitivity_profile
1698                .iter()
1699                .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1700                .expect("Operation failed");
1701            let threshold = *min_sensitivity
1702                + (*max_sensitivity - *min_sensitivity)
1703                    * F::from(0.1).expect("Failed to convert constant to float");
1704
1705            // Find first and last indices below threshold
1706            let mut start_idx = None;
1707            let mut end_idx = None;
1708
1709            for (idx, &sensitivity) in sensitivity_profile.iter().enumerate() {
1710                if sensitivity <= threshold {
1711                    if start_idx.is_none() {
1712                        start_idx = Some(idx);
1713                    }
1714                    end_idx = Some(idx);
1715                }
1716            }
1717
1718            let start_range = start_idx
1719                .map(|idx| self.perturbation_ranges[idx])
1720                .unwrap_or(0.0);
1721            let end_range = end_idx
1722                .map(|idx| self.perturbation_ranges[idx])
1723                .unwrap_or(0.0);
1724
1725            (start_range, end_range)
1726        }
1727    }
1728}
1729
1730#[cfg(test)]
1731mod tests {
1732    use super::*;
1733    use scirs2_core::ndarray::Array2;
1734
1735    #[test]
1736    fn test_stability_config_default() {
1737        let config = StabilityConfig::default();
1738        assert_eq!(config.n_bootstrap, 100);
1739        assert_eq!(config.subsample_ratio, 0.8);
1740        assert_eq!(config.n_runs_per_bootstrap, 10);
1741        assert!(config.random_seed.is_none());
1742    }
1743
1744    #[test]
1745    fn test_bootstrap_validator() {
1746        let data = Array2::from_shape_vec((20, 2), (0..40).map(|i| i as f64 / 10.0).collect())
1747            .expect("Operation failed");
1748
1749        let config = StabilityConfig {
1750            n_bootstrap: 5,
1751            subsample_ratio: 0.8,
1752            n_runs_per_bootstrap: 3,
1753            random_seed: Some(42),
1754            k_range: None,
1755        };
1756
1757        let validator = BootstrapValidator::new(config);
1758        let result = validator.assess_kmeans_stability(data.view(), 2);
1759
1760        assert!(result.is_ok());
1761        let stability_result = result.expect("Operation failed");
1762        assert!(stability_result.mean_stability >= 0.0);
1763        assert!(stability_result.mean_stability <= 1.0);
1764        assert_eq!(stability_result.bootstrap_matrix.shape(), &[20, 20]);
1765    }
1766
1767    #[test]
1768    fn test_consensus_clusterer() {
1769        let data = Array2::from_shape_vec(
1770            (6, 2),
1771            vec![0.0, 0.0, 0.1, 0.1, 0.2, 0.2, 5.0, 5.0, 5.1, 5.1, 5.2, 5.2],
1772        )
1773        .expect("Operation failed");
1774
1775        let config = StabilityConfig {
1776            n_bootstrap: 10,
1777            random_seed: Some(42),
1778            ..Default::default()
1779        };
1780
1781        let consensus = ConsensusClusterer::new(config);
1782        let result = consensus.find_consensus_clusters(data.view(), 2);
1783
1784        assert!(result.is_ok());
1785        let labels = result.expect("Operation failed");
1786        assert_eq!(labels.len(), 6);
1787
1788        // Check that we have exactly 2 clusters
1789        let unique_labels: std::collections::HashSet<_> = labels.iter().copied().collect();
1790        assert_eq!(unique_labels.len(), 2);
1791    }
1792
1793    #[test]
1794    fn test_optimal_k_selector() {
1795        let data = Array2::from_shape_vec(
1796            (12, 2),
1797            vec![
1798                0.0, 0.0, 0.1, 0.1, 0.2, 0.2, // Cluster 1
1799                5.0, 5.0, 5.1, 5.1, 5.2, 5.2, // Cluster 2
1800                10.0, 10.0, 10.1, 10.1, 10.2, 10.2, // Cluster 3
1801                15.0, 15.0, 15.1, 15.1, 15.2, 15.2, // Cluster 4
1802            ],
1803        )
1804        .expect("Operation failed");
1805
1806        let config = StabilityConfig {
1807            k_range: Some((2, 5)),
1808            n_bootstrap: 5,
1809            random_seed: Some(42),
1810            ..Default::default()
1811        };
1812
1813        let selector = OptimalKSelector::new(config);
1814        let result = selector.find_optimal_k(data.view());
1815
1816        assert!(result.is_ok());
1817        let (optimal_k, scores) = result.expect("Operation failed");
1818        assert!((2..=5).contains(&optimal_k));
1819        assert_eq!(scores.len(), 4); // k=2,3,4,5
1820    }
1821
1822    #[test]
1823    fn test_gap_statistic() {
1824        let data = Array2::from_shape_vec(
1825            (8, 2),
1826            vec![
1827                0.0, 0.0, 0.1, 0.1, 0.2, 0.2, 0.3, 0.3, 5.0, 5.0, 5.1, 5.1, 5.2, 5.2, 5.3, 5.3,
1828            ],
1829        )
1830        .expect("Operation failed");
1831
1832        let config = StabilityConfig {
1833            k_range: Some((2, 4)),
1834            n_bootstrap: 5,
1835            random_seed: Some(42),
1836            ..Default::default()
1837        };
1838
1839        let selector = OptimalKSelector::new(config);
1840        let result = selector.gap_statistic(data.view());
1841
1842        assert!(result.is_ok());
1843        let (optimal_k, gap_scores) = result.expect("Operation failed");
1844        assert!((2..=4).contains(&optimal_k));
1845        assert_eq!(gap_scores.len(), 3); // k=2,3,4
1846    }
1847}