use super::traits::Clustering;
use crate::error::{Error, Result};
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct Dbscan {
epsilon: f32,
min_pts: usize,
}
pub const NOISE: usize = usize::MAX;
impl Dbscan {
pub fn new(epsilon: f32, min_pts: usize) -> Self {
Self { epsilon, min_pts }
}
pub fn with_epsilon(mut self, epsilon: f32) -> Self {
self.epsilon = epsilon;
self
}
pub fn with_min_pts(mut self, min_pts: usize) -> Self {
self.min_pts = min_pts;
self
}
#[inline]
fn distance(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x - y).powi(2))
.sum::<f32>()
.sqrt()
}
fn region_query(&self, data: &[Vec<f32>], point_idx: usize) -> Vec<usize> {
let point = &data[point_idx];
data.iter()
.enumerate()
.filter(|(idx, other)| {
*idx != point_idx && Self::distance(point, other) <= self.epsilon
})
.map(|(idx, _)| idx)
.collect()
}
fn expand_cluster(
&self,
data: &[Vec<f32>],
point_idx: usize,
neighbors: &[usize],
labels: &mut [usize],
cluster_id: usize,
visited: &mut HashSet<usize>,
) {
labels[point_idx] = cluster_id;
let mut to_process: Vec<usize> = neighbors.to_vec();
while let Some(neighbor_idx) = to_process.pop() {
if visited.contains(&neighbor_idx) {
continue;
}
let _ = visited.insert(neighbor_idx);
if labels[neighbor_idx] == NOISE {
labels[neighbor_idx] = cluster_id;
}
if labels[neighbor_idx] == NOISE {
labels[neighbor_idx] = cluster_id;
}
let neighbor_neighbors = self.region_query(data, neighbor_idx);
if neighbor_neighbors.len() + 1 >= self.min_pts {
labels[neighbor_idx] = cluster_id;
for nn in neighbor_neighbors {
if !visited.contains(&nn) {
to_process.push(nn);
}
}
}
}
}
}
impl Default for Dbscan {
fn default() -> Self {
Self::new(0.5, 5)
}
}
impl Clustering for Dbscan {
fn fit_predict(&self, data: &[Vec<f32>]) -> Result<Vec<usize>> {
let n = data.len();
if n == 0 {
return Err(Error::EmptyInput);
}
let d = data[0].len();
if let Some((_, p)) = data.iter().enumerate().find(|(_, p)| p.len() != d) {
return Err(Error::DimensionMismatch {
expected: d,
found: p.len(),
});
}
if self.epsilon <= 0.0 {
return Err(Error::InvalidParameter {
name: "epsilon",
message: "must be positive",
});
}
if self.min_pts == 0 {
return Err(Error::InvalidParameter {
name: "min_pts",
message: "must be at least 1",
});
}
let mut labels = vec![NOISE; n];
let mut visited = HashSet::with_capacity(n);
let mut cluster_id = 0;
for point_idx in 0..n {
if visited.contains(&point_idx) {
continue;
}
let _ = visited.insert(point_idx);
let neighbors = self.region_query(data, point_idx);
if neighbors.len() + 1 < self.min_pts {
continue;
}
self.expand_cluster(
data,
point_idx,
&neighbors,
&mut labels,
cluster_id,
&mut visited,
);
cluster_id += 1;
}
if labels.contains(&NOISE) {
for label in &mut labels {
if *label == NOISE {
*label = cluster_id;
}
}
}
Ok(labels)
}
fn n_clusters(&self) -> usize {
0 }
}
pub trait DbscanExt {
fn fit_predict_with_noise(&self, data: &[Vec<f32>]) -> Result<Vec<Option<usize>>>;
fn is_noise(label: usize) -> bool {
label == NOISE
}
}
impl DbscanExt for Dbscan {
fn fit_predict_with_noise(&self, data: &[Vec<f32>]) -> Result<Vec<Option<usize>>> {
let n = data.len();
if n == 0 {
return Err(Error::EmptyInput);
}
if self.epsilon <= 0.0 {
return Err(Error::InvalidParameter {
name: "epsilon",
message: "must be positive",
});
}
let mut labels = vec![NOISE; n];
let mut visited = HashSet::with_capacity(n);
let mut cluster_id = 0;
for point_idx in 0..n {
if visited.contains(&point_idx) {
continue;
}
let _ = visited.insert(point_idx);
let neighbors = self.region_query(data, point_idx);
if neighbors.len() + 1 < self.min_pts {
continue;
}
self.expand_cluster(
data,
point_idx,
&neighbors,
&mut labels,
cluster_id,
&mut visited,
);
cluster_id += 1;
}
Ok(labels
.into_iter()
.map(|l| if l == NOISE { None } else { Some(l) })
.collect())
}
}
#[cfg(test)]
#[allow(clippy::needless_range_loop)]
mod tests {
use super::*;
#[test]
fn test_dbscan_two_clusters() {
let data = vec![
vec![0.0, 0.0],
vec![0.1, 0.0],
vec![0.0, 0.1],
vec![0.1, 0.1],
vec![0.05, 0.05],
vec![5.0, 5.0],
vec![5.1, 5.0],
vec![5.0, 5.1],
vec![5.1, 5.1],
vec![5.05, 5.05],
];
let dbscan = Dbscan::new(0.3, 3);
let labels = dbscan.fit_predict(&data).unwrap();
assert_eq!(labels.len(), 10);
let cluster1 = labels[0];
for label in &labels[1..5] {
assert_eq!(*label, cluster1);
}
let cluster2 = labels[5];
for label in &labels[6..10] {
assert_eq!(*label, cluster2);
}
assert_ne!(cluster1, cluster2);
}
#[test]
fn test_dbscan_with_noise() {
let data = vec![
vec![0.0, 0.0],
vec![0.1, 0.0],
vec![0.0, 0.1],
vec![0.1, 0.1],
vec![100.0, 100.0],
vec![5.0, 5.0],
vec![5.1, 5.0],
vec![5.0, 5.1],
vec![5.1, 5.1],
];
let dbscan = Dbscan::new(0.3, 3);
let labels = dbscan.fit_predict_with_noise(&data).unwrap();
assert_eq!(labels.len(), 9);
assert!(labels[4].is_none());
for (i, label) in labels.iter().enumerate() {
if i != 4 {
assert!(label.is_some());
}
}
}
#[test]
fn test_dbscan_all_noise() {
let data = vec![
vec![0.0, 0.0],
vec![10.0, 0.0],
vec![0.0, 10.0],
vec![10.0, 10.0],
];
let dbscan = Dbscan::new(0.5, 3);
let labels = dbscan.fit_predict_with_noise(&data).unwrap();
for label in labels {
assert!(label.is_none());
}
}
#[test]
fn test_dbscan_all_one_cluster() {
let data = vec![
vec![0.0, 0.0],
vec![0.1, 0.0],
vec![0.0, 0.1],
vec![0.1, 0.1],
];
let dbscan = Dbscan::new(0.5, 2);
let labels = dbscan.fit_predict(&data).unwrap();
let cluster = labels[0];
for label in labels {
assert_eq!(label, cluster);
}
}
#[test]
fn test_dbscan_empty() {
let data: Vec<Vec<f32>> = vec![];
let dbscan = Dbscan::new(0.5, 3);
let result = dbscan.fit_predict(&data);
assert!(result.is_err());
}
#[test]
fn test_dbscan_invalid_params() {
let data = vec![vec![0.0, 0.0]];
let dbscan = Dbscan::new(0.0, 3);
assert!(dbscan.fit_predict(&data).is_err());
let dbscan = Dbscan::new(-1.0, 3);
assert!(dbscan.fit_predict(&data).is_err());
let dbscan = Dbscan::new(0.5, 0);
assert!(dbscan.fit_predict(&data).is_err());
}
#[test]
fn test_dbscan_chain() {
let data: Vec<Vec<f32>> = (0..10).map(|i| vec![i as f32 * 0.3, 0.0]).collect();
let dbscan = Dbscan::new(0.5, 2);
let labels = dbscan.fit_predict(&data).unwrap();
let cluster = labels[0];
for label in labels {
assert_eq!(label, cluster);
}
}
}