collablearn-int396 / src /clusterers.py
Cyril-36's picture
Deploy CollabLearn Streamlit demo via Docker
d81f51d verified
Raw
History Blame Contribute Delete
4.19 kB
"""Clusterer implementations for the 12-configuration sweep."""
from __future__ import annotations
import warnings
from typing import Any
import numpy as np
from sklearn.cluster import AgglomerativeClustering, KMeans
from sklearn.metrics import silhouette_score
from sklearn.mixture import GaussianMixture
from .config import HDBSCAN_MIN_CLUSTER_SIZE, HDBSCAN_MIN_SAMPLES, K_SWEEP, SEED
def effective_k(labels: np.ndarray) -> int:
unique = set(np.asarray(labels).tolist())
return len(unique - {-1})
def _can_score(labels: np.ndarray, n_samples: int) -> bool:
k = len(set(np.asarray(labels).tolist()))
return 1 < k < n_samples
def _pick_k_silhouette(X: np.ndarray, fit_fn, k_sweep: list[int] = K_SWEEP):
best_k, best_score, best_labels = None, -np.inf, None
for k in k_sweep:
if k >= X.shape[0]:
continue
labels = fit_fn(X, k)
if not _can_score(labels, X.shape[0]):
continue
score = silhouette_score(X, labels)
if score > best_score or (np.isclose(score, best_score) and (best_k is None or k < best_k)):
best_k, best_score, best_labels = k, score, labels
if best_labels is None:
raise ValueError("Could not find a valid k for silhouette selection")
return best_k, best_labels
def cluster_kmeans(X: np.ndarray, k_sweep: list[int] | None = None):
def fit(data, k):
return KMeans(n_clusters=k, n_init=10, random_state=SEED).fit_predict(data)
k, labels = _pick_k_silhouette(X, fit, k_sweep=k_sweep or K_SWEEP)
return labels.astype(int), {"k": k, "k_effective": effective_k(labels), "noise_ratio": 0.0}
def cluster_gmm(X: np.ndarray, k_sweep: list[int] | None = None):
best_k, best_bic, best_labels = None, np.inf, None
for k in (k_sweep or K_SWEEP):
if k >= X.shape[0]:
continue
model = GaussianMixture(n_components=k, covariance_type="full", random_state=SEED)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
model.fit(X)
bic = model.bic(X)
if bic < best_bic or (np.isclose(bic, best_bic) and (best_k is None or k < best_k)):
best_k, best_bic, best_labels = k, bic, model.predict(X)
if best_labels is None:
raise ValueError("Could not fit a valid GMM")
return best_labels.astype(int), {
"k": best_k,
"k_effective": effective_k(best_labels),
"bic": float(best_bic),
"noise_ratio": 0.0,
}
def cluster_agglo(X: np.ndarray, k_sweep: list[int] | None = None):
def fit(data, k):
return AgglomerativeClustering(n_clusters=k, linkage="ward").fit_predict(data)
k, labels = _pick_k_silhouette(X, fit, k_sweep=k_sweep or K_SWEEP)
return labels.astype(int), {"k": k, "k_effective": effective_k(labels), "noise_ratio": 0.0}
def _fit_external_hdbscan(X: np.ndarray):
import hdbscan
clusterer = hdbscan.HDBSCAN(
min_cluster_size=min(HDBSCAN_MIN_CLUSTER_SIZE, max(2, X.shape[0] // 5)),
min_samples=min(HDBSCAN_MIN_SAMPLES, max(1, X.shape[0] // 10)),
cluster_selection_method="eom",
)
return clusterer.fit_predict(X), clusterer, "hdbscan"
def _fit_sklearn_hdbscan(X: np.ndarray):
from sklearn.cluster import HDBSCAN
clusterer = HDBSCAN(
min_cluster_size=min(HDBSCAN_MIN_CLUSTER_SIZE, max(2, X.shape[0] // 5)),
min_samples=min(HDBSCAN_MIN_SAMPLES, max(1, X.shape[0] // 10)),
)
return clusterer.fit_predict(X), clusterer, "sklearn"
def cluster_hdbscan(X: np.ndarray):
try:
labels, clusterer, implementation = _fit_external_hdbscan(X)
except ImportError:
labels, clusterer, implementation = _fit_sklearn_hdbscan(X)
labels = np.asarray(labels, dtype=int)
noise_ratio = float(np.mean(labels == -1))
return labels, {
"k": effective_k(labels),
"k_effective": effective_k(labels),
"noise_ratio": noise_ratio,
"clusterer": clusterer,
"hdbscan_impl": implementation,
}
CLUSTERERS: dict[str, Any] = {
"kmeans": cluster_kmeans,
"gmm": cluster_gmm,
"agglo": cluster_agglo,
"hdbscan": cluster_hdbscan,
}