use std::collections::HashMap;
use kodama::linkage;
pub use kodama::Method;
use linfa::param_guard::TransformGuard;
use linfa::traits::Transformer;
use linfa::Float;
use linfa::{dataset::DatasetBase, ParamGuard};
use linfa_kernel::Kernel;
pub use error::{HierarchicalError, Result};
mod error;
#[derive(Clone, Debug, PartialEq)]
pub enum Criterion<F: Float> {
NumClusters(usize),
Distance(F),
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct HierarchicalCluster<T: Float>(ValidHierarchicalCluster<T>);
#[derive(Clone, Debug, PartialEq)]
pub struct ValidHierarchicalCluster<T: Float> {
method: Method,
stopping: Criterion<T>,
}
impl<F: Float> ParamGuard for HierarchicalCluster<F> {
type Checked = ValidHierarchicalCluster<F>;
type Error = HierarchicalError<F>;
fn check_ref(&self) -> std::result::Result<&Self::Checked, Self::Error> {
match self.0.stopping {
Criterion::NumClusters(0) => Err(HierarchicalError::InvalidStoppingCondition(
self.0.stopping.clone(),
)),
Criterion::Distance(x) if x.is_negative() || x.is_nan() || x.is_infinite() => Err(
HierarchicalError::InvalidStoppingCondition(self.0.stopping.clone()),
),
_ => Ok(&self.0),
}
}
fn check(self) -> std::result::Result<Self::Checked, Self::Error> {
self.check_ref()?;
Ok(self.0)
}
}
impl<F: Float> TransformGuard for HierarchicalCluster<F> {}
impl<F: Float> HierarchicalCluster<F> {
pub fn with_method(mut self, method: Method) -> HierarchicalCluster<F> {
self.0.method = method;
self
}
pub fn num_clusters(mut self, num_clusters: usize) -> HierarchicalCluster<F> {
self.0.stopping = Criterion::NumClusters(num_clusters);
self
}
pub fn max_distance(mut self, max_distance: F) -> HierarchicalCluster<F> {
self.0.stopping = Criterion::Distance(max_distance);
self
}
}
impl<F: Float> Transformer<Kernel<F>, DatasetBase<Kernel<F>, Vec<usize>>>
for ValidHierarchicalCluster<F>
{
fn transform(&self, kernel: Kernel<F>) -> DatasetBase<Kernel<F>, Vec<usize>> {
let threshold = F::cast(1e-6);
let mut distance = kernel
.to_upper_triangle()
.into_iter()
.map(|x| {
if x > threshold {
-x.ln()
} else {
-threshold.ln()
}
})
.collect::<Vec<_>>();
let num_observations = kernel.size();
let res = linkage(&mut distance, num_observations, self.method);
let mut clusters = (0..num_observations)
.map(|x| (x, vec![x]))
.collect::<HashMap<_, _>>();
let mut ct = num_observations;
for step in res.steps() {
let should_stop = match self.stopping {
Criterion::NumClusters(max_clusters) => clusters.len() <= max_clusters,
Criterion::Distance(dis) => step.dissimilarity >= dis,
};
if should_stop {
break;
}
let mut ids = Vec::with_capacity(2);
let mut cl = clusters.remove(&step.cluster1).unwrap();
ids.append(&mut cl);
let mut cl = clusters.remove(&step.cluster2).unwrap();
ids.append(&mut cl);
clusters.insert(ct, ids);
ct += 1;
}
let mut tmp = vec![0; num_observations];
for (i, (_, ids)) in clusters.into_iter().enumerate() {
for id in ids {
tmp[id] = i;
}
}
DatasetBase::new(kernel, tmp)
}
}
impl<F: Float, T> Transformer<DatasetBase<Kernel<F>, T>, DatasetBase<Kernel<F>, Vec<usize>>>
for ValidHierarchicalCluster<F>
{
fn transform(&self, dataset: DatasetBase<Kernel<F>, T>) -> DatasetBase<Kernel<F>, Vec<usize>> {
self.transform(dataset.records)
}
}
impl<T: Float> Default for ValidHierarchicalCluster<T> {
fn default() -> Self {
Self {
method: Method::Average,
stopping: Criterion::NumClusters(2),
}
}
}
#[cfg(test)]
mod tests {
use crate::HierarchicalError;
use linfa::traits::Transformer;
use linfa_kernel::{Kernel, KernelMethod};
use ndarray::{Array, Axis};
use ndarray_rand::{rand_distr::Normal, RandomExt};
use super::{Criterion, HierarchicalCluster, ValidHierarchicalCluster};
#[test]
fn autotraits() {
fn has_autotraits<T: Send + Sync + Sized + Unpin>() {}
has_autotraits::<Criterion<f64>>();
has_autotraits::<HierarchicalCluster<f64>>();
has_autotraits::<ValidHierarchicalCluster<f64>>();
has_autotraits::<HierarchicalError<f64>>();
}
#[test]
fn test_blobs() {
let npoints = 10;
let entries = ndarray::concatenate(
Axis(0),
&[
Array::random((npoints, 2), Normal::new(-1., 0.1).unwrap()).view(),
Array::random((npoints, 2), Normal::new(1., 0.1).unwrap()).view(),
],
)
.unwrap();
let kernel = Kernel::params()
.method(KernelMethod::Gaussian(5.0))
.transform(entries.view());
let kernel = HierarchicalCluster::default()
.max_distance(0.1)
.transform(kernel)
.unwrap();
let ids = kernel.targets();
let first_cluster_id = &ids[0];
assert!(ids
.iter()
.take(npoints)
.all(|item| item == first_cluster_id));
let second_cluster_id = &ids[npoints];
assert!(ids
.iter()
.skip(npoints)
.all(|item| item == second_cluster_id));
assert_ne!(first_cluster_id, second_cluster_id);
let kernel = HierarchicalCluster::default()
.num_clusters(2)
.transform(kernel)
.unwrap();
let ids = kernel.targets();
let first_cluster_id = &ids[0];
assert!(ids
.iter()
.take(npoints)
.all(|item| item == first_cluster_id));
let second_cluster_id = &ids[npoints];
assert!(ids
.iter()
.skip(npoints)
.all(|item| item == second_cluster_id));
assert_ne!(first_cluster_id, second_cluster_id);
}
#[test]
fn test_noise() {
let data = Array::random((100, 2), Normal::new(0., 1.0).unwrap());
let kernel = Kernel::params()
.method(KernelMethod::Linear)
.transform(data.view());
let predictions = HierarchicalCluster::default()
.max_distance(3.0)
.transform(kernel)
.unwrap();
dbg!(&predictions.targets());
}
}