use crate::error::{DatarustError, Result};
use crate::matrix::Matrix;
#[inline]
fn sq_dist(a: &[f64], b: &[f64]) -> f64 {
a.iter()
.zip(b.iter())
.map(|(ai, bi)| {
let d = ai - bi;
d * d
})
.sum()
}
pub fn silhouette_score(x: &Matrix, labels: &[usize]) -> Result<f64> {
let n = x.nrows();
if labels.len() != n {
return Err(DatarustError::ShapeMismatch {
expected: format!("{n} labels"),
actual: format!("{} labels", labels.len()),
});
}
if n < 2 {
return Err(DatarustError::InvalidInput(
"silhouette_score requires at least 2 samples".into(),
));
}
let k = labels.iter().copied().max().map(|m| m + 1).unwrap_or(0);
if k < 2 {
return Err(DatarustError::InvalidInput(
"silhouette_score requires at least 2 clusters".into(),
));
}
let mut counts = vec![0usize; k];
for &c in labels {
counts[c] += 1;
}
let p = x.ncols();
let data = x.as_slice();
let mut total = 0.0_f64;
for i in 0..n {
let row_i = &data[i * p..(i + 1) * p];
let ci = labels[i];
let mut sum_same = 0.0_f64;
let mut count_same = 0usize;
let mut cluster_sums = vec![0.0_f64; k];
let mut cluster_counts = vec![0usize; k];
for j in 0..n {
if i == j {
continue;
}
let d = sq_dist(row_i, &data[j * p..(j + 1) * p]).sqrt();
let cj = labels[j];
cluster_sums[cj] += d;
cluster_counts[cj] += 1;
if cj == ci {
sum_same += d;
count_same += 1;
}
}
let a_i = if count_same > 0 {
sum_same / count_same as f64
} else {
0.0 };
let mut b_i = f64::INFINITY;
for c in 0..k {
if c == ci || cluster_counts[c] == 0 {
continue;
}
let mean_d = cluster_sums[c] / cluster_counts[c] as f64;
if mean_d < b_i {
b_i = mean_d;
}
}
if b_i.is_infinite() {
continue;
}
let denom = a_i.max(b_i);
if denom > 0.0 {
total += (b_i - a_i) / denom;
}
}
Ok(total / n as f64)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn perfect_separation_high_silhouette() {
let x = Matrix::new(vec![
vec![0.0, 0.0],
vec![0.1, 0.0],
vec![0.0, 0.1],
vec![10.0, 10.0],
vec![10.1, 10.0],
vec![10.0, 10.1],
])
.unwrap();
let labels = vec![0, 0, 0, 1, 1, 1];
let s = silhouette_score(&x, &labels).unwrap();
assert!(s > 0.5, "expected high silhouette, got {s}");
}
#[test]
fn overlapping_clusters_low_silhouette() {
let x = Matrix::new(vec![vec![0.0], vec![1.0], vec![2.0], vec![3.0]]).unwrap();
let labels = vec![0, 1, 0, 1];
let s = silhouette_score(&x, &labels).unwrap();
assert!(
s < 0.3,
"expected low silhouette for interleaved clusters: {s}"
);
}
#[test]
fn single_cluster_errors() {
let x = Matrix::new(vec![vec![0.0], vec![1.0]]).unwrap();
let labels = vec![0, 0];
assert!(silhouette_score(&x, &labels).is_err());
}
#[test]
fn label_count_mismatch_errors() {
let x = Matrix::new(vec![vec![0.0], vec![1.0]]).unwrap();
assert!(silhouette_score(&x, &[0]).is_err());
}
#[test]
fn three_clusters() {
let x = Matrix::new(vec![
vec![0.0, 0.0],
vec![0.1, 0.0],
vec![10.0, 10.0],
vec![10.1, 10.0],
vec![20.0, 20.0],
vec![20.1, 20.0],
])
.unwrap();
let labels = vec![0, 0, 1, 1, 2, 2];
let s = silhouette_score(&x, &labels).unwrap();
assert!(s > 0.5, "three well-separated clusters: {s}");
assert!(approx(s, s, 1e-12)); }
}