Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

clustering_methods.py

19_advanced_clustering/clustering_methods.py  ·  121 lines  ·  view on GitHub

"""
Advanced Clustering Methods
Simple implementations
"""
import numpy as np

def hierarchical_clustering(X: np.ndarray, n_clusters: int) -> np.ndarray:
    """
    Hierarchical Clustering (Agglomerative)
    
    Builds tree by merging closest clusters
    """
    n_samples = X.shape[0]
    clusters = [[i] for i in range(n_samples)]
    
    while len(clusters) > n_clusters:
        # Find two closest clusters
        min_dist = float('inf')
        merge_i, merge_j = 0, 1
        
        for i in range(len(clusters)):
            for j in range(i+1, len(clusters)):
                # Distance between centroids
                centroid_i = np.mean(X[clusters[i]], axis=0)
                centroid_j = np.mean(X[clusters[j]], axis=0)
                dist = np.linalg.norm(centroid_i - centroid_j)
                
                if dist < min_dist:
                    min_dist = dist
                    merge_i, merge_j = i, j
        
        # Merge
        clusters[merge_i].extend(clusters[merge_j])
        clusters.pop(merge_j)
    
    # Assign labels
    labels = np.zeros(n_samples)
    for label, cluster in enumerate(clusters):
        for idx in cluster:
            labels[idx] = label
    
    return labels

def dbscan(X: np.ndarray, eps: float, min_samples: int) -> np.ndarray:
    """
    DBSCAN: Density-Based Clustering
    
    Finds clusters of arbitrary shape
    -1 = noise/outlier
    """
    n_samples = X.shape[0]
    labels = np.full(n_samples, -1)
    cluster_id = 0
    
    def get_neighbors(point_idx: int) -> list:
        """Get neighbors within eps"""
        neighbors = []
        for i in range(n_samples):
            if np.linalg.norm(X[point_idx] - X[i]) <= eps:
                neighbors.append(i)
        return neighbors
    
    for i in range(n_samples):
        if labels[i] != -1:
            continue
        
        neighbors = get_neighbors(i)
        
        if len(neighbors) < min_samples:
            labels[i] = -1  # Noise
            continue
        
        # Start cluster
        labels[i] = cluster_id
        seed_set = neighbors.copy()
        
        # Expand cluster
        j = 0
        while j < len(seed_set):
            neighbor = seed_set[j]
            
            if labels[neighbor] == -1:
                labels[neighbor] = cluster_id
            
            neighbor_neighbors = get_neighbors(neighbor)
            if len(neighbor_neighbors) >= min_samples:
                for nn in neighbor_neighbors:
                    if labels[nn] == -1:
                        seed_set.append(nn)
            
            j += 1
        
        cluster_id += 1
    
    return labels


# Usage Example
if __name__ == "__main__":
    print("Advanced Clustering Methods")
    print("=" * 60)
    
    # Generate sample data
    np.random.seed(42)
    n_samples = 100
    
    # Two clusters
    cluster1 = np.random.randn(n_samples//2, 2) + np.array([0, 0])
    cluster2 = np.random.randn(n_samples//2, 2) + np.array([5, 5])
    X = np.vstack([cluster1, cluster2])
    
    # Hierarchical clustering
    labels_hier = hierarchical_clustering(X, n_clusters=2)
    print(f"Hierarchical: {len(np.unique(labels_hier))} clusters")
    
    # DBSCAN
    labels_dbscan = dbscan(X, eps=1.5, min_samples=5)
    n_clusters_dbscan = len(set(labels_dbscan)) - (1 if -1 in labels_dbscan else 0)
    n_noise = np.sum(labels_dbscan == -1)
    print(f"DBSCAN: {n_clusters_dbscan} clusters, {n_noise} noise points")