Hierarchical Linkage & Density-Based DBSCAN
Derive Ward's minimum variance linkage equation, density-connectivity in DBSCAN, handling arbitrary non-convex geometries and noise, and zero-dependency implementations in Python and Rust.
Centroid-based algorithms like k-Means assume that clusters are convex, isotropic, and spherical. When data features elongated filaments, concentric rings, or substantial background noise, k-Means shatters natural groupings.
To model arbitrary topological manifolds, unsupervised learning relies on Hierarchical Clustering and Density-Based Spatial Clustering of Applications with Noise (DBSCAN).
DBSCAN identifies clusters of arbitrary non-convex shapes and isolates anomalous noise without pre-specifying K.
1. Hierarchical Linkage: Deriving Ward's Minimum Variance#
Agglomerative hierarchical clustering begins with individual singleton clusters and iteratively merges the closest pair of clusters until a single root remains.
Derivation of Ward's Distance Formula#
Ward's Linkage minimizes the total within-cluster variance increase resulting from merging two clusters and .
Let cluster have points with mean vector , and cluster have points with mean vector . The Error Sum of Squares (ESS) of cluster is:
The combined cluster has size and mean centroid:
Step 1: Formulate the Variance Increase
The increase in variance caused by merging and is:
Step 2: Expand $\text{ESS}_{A \cup B}$ Around Individual Means
For any point :
Summing over all , the cross-term vanishes because :
Similarly for cluster :
Step 3: Compute the Net Shift Difference
Subtracting :
Evaluate the centroid difference:
Step 4: Substitute and Factor
Substitute these differences back into the expression:
Canceling yields Ward's classic metric:
2. DBSCAN: Density Foundations#
DBSCAN (Density-Based Spatial Clustering of Applications with Noise) views clusters as contiguous dense regions bounded by regions of low point density.
Definitions & Topological Concepts#
Let be a dataset. Given radius parameter and density threshold :
- -Neighborhood:
- Core Point: A point is a core point if .
- Directly Density-Reachable: A point is directly density-reachable from point if and is a core point.
- Density-Reachable: A point is density-reachable from point if there exists a sequence of core points such that , , and is directly density-reachable from .
- Density-Connected: Two points and are density-connected if there exists an intermediate point such that both and are density-reachable from .
A DBSCAN Cluster is formally defined as a maximal set of density-connected points. Any sample not belonging to any cluster is designated as anomalous Noise (assigned label ).
3. From-Scratch Implementation: Zero Dependencies#
The following implementations construct the complete DBSCAN algorithm from scratch in Python and Rust using only built-in collections and standard math.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768import math from typing import List, Set class DBSCANFromScratch: def __init__(self, eps: float = 1.5, min_pts: int = 3): self.eps = eps self.min_pts = min_pts self.labels: List[int] = [] @staticmethod def _euclidean_dist(a: List[float], b: List[float]) -> float: return math.sqrt(sum((x_i - y_i) ** 2 for x_i, y_i in zip(a, b))) def _region_query(self, X: List[List[float]], point_idx: int) -> List[int]: neighbors = [] for i, sample in enumerate(X): if self._euclidean_dist(X[point_idx], sample) <= self.eps: neighbors.append(i) return neighbors def fit(self, X: List[List[float]]) -> List[int]: num_samples = len(X) # -1 represents Noise, 0 represents Unvisited self.labels = [0] * num_samples cluster_id = 0 for i in range(num_samples): if self.labels[i] != 0: continue neighbors = self._region_query(X, i) if len(neighbors) < self.min_pts: self.labels[i] = -1 # Mark as Noise else: cluster_id += 1 self._expand_cluster(X, i, neighbors, cluster_id) return self.labels def _expand_cluster(self, X: List[List[float]], core_idx: int, neighbors: List[int], cluster_id: int): self.labels[core_idx] = cluster_id # Use a queue for breadth-first density traversal queue = list(neighbors) while queue: current_idx = queue.pop(0) # If formerly labeled noise, it is a border point; reassign to cluster if self.labels[current_idx] == -1: self.labels[current_idx] = cluster_id if self.labels[current_idx] == 0: self.labels[current_idx] = cluster_id sub_neighbors = self._region_query(X, current_idx) if len(sub_neighbors) >= self.min_pts: queue.extend(sub_neighbors) if __name__ == "__main__": # Concentric clusters with an outlier X = [ [1.0, 1.2], [1.1, 1.0], [0.9, 1.1], [1.0, 0.9], # Dense Cluster 1 [6.0, 6.2], [6.1, 6.0], [5.9, 6.1], [6.0, 5.9], # Dense Cluster 2 [15.0, 15.0] # Outlier Noise ] dbscan = DBSCANFromScratch(eps=1.5, min_pts=3) labels = dbscan.fit(X) print("DBSCAN Cluster Labels (-1 is Noise):", labels)
4. Active Knowledge Checks#
In Ward's hierarchical linkage criterion, why does the merge distance Delta ESS equal (n_A * n_B / (n_A + n_B)) * ||mu_A - mu_B||^2?
What occurs when a data point p in DBSCAN has fewer than MinPts neighbors within epsilon, but is within the epsilon-neighborhood of an existing Core Point?