clustering_analysis.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. """
  2. Clustering analysis example with multiple algorithms, evaluation, and visualization.
  3. """
  4. import numpy as np
  5. import pandas as pd
  6. import matplotlib.pyplot as plt
  7. from sklearn.preprocessing import StandardScaler
  8. from sklearn.decomposition import PCA
  9. from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
  10. from sklearn.mixture import GaussianMixture
  11. from sklearn.metrics import (
  12. silhouette_score, calinski_harabasz_score, davies_bouldin_score
  13. )
  14. import warnings
  15. warnings.filterwarnings('ignore')
  16. def preprocess_for_clustering(X, scale=True, pca_components=None):
  17. """
  18. Preprocess data for clustering.
  19. Parameters:
  20. -----------
  21. X : array-like
  22. Feature matrix
  23. scale : bool
  24. Whether to standardize features
  25. pca_components : int or None
  26. Number of PCA components (None to skip PCA)
  27. Returns:
  28. --------
  29. array
  30. Preprocessed data
  31. """
  32. X_processed = X.copy()
  33. if scale:
  34. scaler = StandardScaler()
  35. X_processed = scaler.fit_transform(X_processed)
  36. if pca_components is not None:
  37. pca = PCA(n_components=pca_components)
  38. X_processed = pca.fit_transform(X_processed)
  39. print(f"PCA: Explained variance ratio = {pca.explained_variance_ratio_.sum():.3f}")
  40. return X_processed
  41. def find_optimal_k_kmeans(X, k_range=range(2, 11)):
  42. """
  43. Find optimal K for K-Means using elbow method and silhouette score.
  44. Parameters:
  45. -----------
  46. X : array-like
  47. Feature matrix (should be scaled)
  48. k_range : range
  49. Range of K values to test
  50. Returns:
  51. --------
  52. dict
  53. Dictionary with inertia and silhouette scores for each K
  54. """
  55. inertias = []
  56. silhouette_scores = []
  57. for k in k_range:
  58. kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
  59. labels = kmeans.fit_predict(X)
  60. inertias.append(kmeans.inertia_)
  61. silhouette_scores.append(silhouette_score(X, labels))
  62. # Plot results
  63. fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
  64. # Elbow plot
  65. ax1.plot(k_range, inertias, 'bo-')
  66. ax1.set_xlabel('Number of clusters (K)')
  67. ax1.set_ylabel('Inertia')
  68. ax1.set_title('Elbow Method')
  69. ax1.grid(True)
  70. # Silhouette plot
  71. ax2.plot(k_range, silhouette_scores, 'ro-')
  72. ax2.set_xlabel('Number of clusters (K)')
  73. ax2.set_ylabel('Silhouette Score')
  74. ax2.set_title('Silhouette Analysis')
  75. ax2.grid(True)
  76. plt.tight_layout()
  77. plt.savefig('clustering_optimization.png', dpi=300, bbox_inches='tight')
  78. print("Saved: clustering_optimization.png")
  79. plt.close()
  80. # Find best K based on silhouette score
  81. best_k = k_range[np.argmax(silhouette_scores)]
  82. print(f"\nRecommended K based on silhouette score: {best_k}")
  83. return {
  84. 'k_values': list(k_range),
  85. 'inertias': inertias,
  86. 'silhouette_scores': silhouette_scores,
  87. 'best_k': best_k
  88. }
  89. def compare_clustering_algorithms(X, n_clusters=3):
  90. """
  91. Compare different clustering algorithms.
  92. Parameters:
  93. -----------
  94. X : array-like
  95. Feature matrix (should be scaled)
  96. n_clusters : int
  97. Number of clusters
  98. Returns:
  99. --------
  100. dict
  101. Dictionary with results for each algorithm
  102. """
  103. print("="*60)
  104. print(f"Comparing Clustering Algorithms (n_clusters={n_clusters})")
  105. print("="*60)
  106. algorithms = {
  107. 'K-Means': KMeans(n_clusters=n_clusters, random_state=42, n_init=10),
  108. 'Agglomerative': AgglomerativeClustering(n_clusters=n_clusters, linkage='ward'),
  109. 'Gaussian Mixture': GaussianMixture(n_components=n_clusters, random_state=42)
  110. }
  111. # DBSCAN doesn't require n_clusters
  112. # We'll add it separately
  113. dbscan = DBSCAN(eps=0.5, min_samples=5)
  114. dbscan_labels = dbscan.fit_predict(X)
  115. results = {}
  116. for name, algorithm in algorithms.items():
  117. labels = algorithm.fit_predict(X)
  118. # Calculate metrics
  119. silhouette = silhouette_score(X, labels)
  120. calinski = calinski_harabasz_score(X, labels)
  121. davies = davies_bouldin_score(X, labels)
  122. results[name] = {
  123. 'labels': labels,
  124. 'n_clusters': n_clusters,
  125. 'silhouette': silhouette,
  126. 'calinski_harabasz': calinski,
  127. 'davies_bouldin': davies
  128. }
  129. print(f"\n{name}:")
  130. print(f" Silhouette Score: {silhouette:.4f} (higher is better)")
  131. print(f" Calinski-Harabasz: {calinski:.4f} (higher is better)")
  132. print(f" Davies-Bouldin: {davies:.4f} (lower is better)")
  133. # DBSCAN results
  134. n_clusters_dbscan = len(set(dbscan_labels)) - (1 if -1 in dbscan_labels else 0)
  135. n_noise = list(dbscan_labels).count(-1)
  136. if n_clusters_dbscan > 1:
  137. # Only calculate metrics if we have multiple clusters
  138. mask = dbscan_labels != -1 # Exclude noise
  139. if mask.sum() > 0:
  140. silhouette = silhouette_score(X[mask], dbscan_labels[mask])
  141. calinski = calinski_harabasz_score(X[mask], dbscan_labels[mask])
  142. davies = davies_bouldin_score(X[mask], dbscan_labels[mask])
  143. results['DBSCAN'] = {
  144. 'labels': dbscan_labels,
  145. 'n_clusters': n_clusters_dbscan,
  146. 'n_noise': n_noise,
  147. 'silhouette': silhouette,
  148. 'calinski_harabasz': calinski,
  149. 'davies_bouldin': davies
  150. }
  151. print(f"\nDBSCAN:")
  152. print(f" Clusters found: {n_clusters_dbscan}")
  153. print(f" Noise points: {n_noise}")
  154. print(f" Silhouette Score: {silhouette:.4f} (higher is better)")
  155. print(f" Calinski-Harabasz: {calinski:.4f} (higher is better)")
  156. print(f" Davies-Bouldin: {davies:.4f} (lower is better)")
  157. else:
  158. print(f"\nDBSCAN:")
  159. print(f" Clusters found: {n_clusters_dbscan}")
  160. print(f" Noise points: {n_noise}")
  161. print(" Note: Insufficient clusters for metric calculation")
  162. return results
  163. def visualize_clusters(X, results, true_labels=None):
  164. """
  165. Visualize clustering results using PCA for 2D projection.
  166. Parameters:
  167. -----------
  168. X : array-like
  169. Feature matrix
  170. results : dict
  171. Dictionary with clustering results
  172. true_labels : array-like or None
  173. True labels (if available) for comparison
  174. """
  175. # Reduce to 2D using PCA
  176. pca = PCA(n_components=2)
  177. X_2d = pca.fit_transform(X)
  178. # Determine number of subplots
  179. n_plots = len(results)
  180. if true_labels is not None:
  181. n_plots += 1
  182. n_cols = min(3, n_plots)
  183. n_rows = (n_plots + n_cols - 1) // n_cols
  184. fig, axes = plt.subplots(n_rows, n_cols, figsize=(5*n_cols, 4*n_rows))
  185. if n_plots == 1:
  186. axes = np.array([axes])
  187. axes = axes.flatten()
  188. plot_idx = 0
  189. # Plot true labels if available
  190. if true_labels is not None:
  191. ax = axes[plot_idx]
  192. scatter = ax.scatter(X_2d[:, 0], X_2d[:, 1], c=true_labels, cmap='viridis', alpha=0.6)
  193. ax.set_title('True Labels')
  194. ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%})')
  195. ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%})')
  196. plt.colorbar(scatter, ax=ax)
  197. plot_idx += 1
  198. # Plot clustering results
  199. for name, result in results.items():
  200. ax = axes[plot_idx]
  201. labels = result['labels']
  202. scatter = ax.scatter(X_2d[:, 0], X_2d[:, 1], c=labels, cmap='viridis', alpha=0.6)
  203. # Highlight noise points for DBSCAN
  204. if name == 'DBSCAN' and -1 in labels:
  205. noise_mask = labels == -1
  206. ax.scatter(X_2d[noise_mask, 0], X_2d[noise_mask, 1],
  207. c='red', marker='x', s=100, label='Noise', alpha=0.8)
  208. ax.legend()
  209. title = f"{name} (K={result['n_clusters']})"
  210. if 'silhouette' in result:
  211. title += f"\nSilhouette: {result['silhouette']:.3f}"
  212. ax.set_title(title)
  213. ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%})')
  214. ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%})')
  215. plt.colorbar(scatter, ax=ax)
  216. plot_idx += 1
  217. # Hide unused subplots
  218. for idx in range(plot_idx, len(axes)):
  219. axes[idx].axis('off')
  220. plt.tight_layout()
  221. plt.savefig('clustering_results.png', dpi=300, bbox_inches='tight')
  222. print("\nSaved: clustering_results.png")
  223. plt.close()
  224. def complete_clustering_analysis(X, true_labels=None, scale=True,
  225. find_k=True, k_range=range(2, 11), n_clusters=3):
  226. """
  227. Complete clustering analysis workflow.
  228. Parameters:
  229. -----------
  230. X : array-like
  231. Feature matrix
  232. true_labels : array-like or None
  233. True labels (for comparison only, not used in clustering)
  234. scale : bool
  235. Whether to scale features
  236. find_k : bool
  237. Whether to search for optimal K
  238. k_range : range
  239. Range of K values to test
  240. n_clusters : int
  241. Number of clusters to use in comparison
  242. Returns:
  243. --------
  244. dict
  245. Dictionary with all analysis results
  246. """
  247. print("="*60)
  248. print("Clustering Analysis")
  249. print("="*60)
  250. print(f"Data shape: {X.shape}")
  251. # Preprocess data
  252. X_processed = preprocess_for_clustering(X, scale=scale)
  253. # Find optimal K if requested
  254. optimization_results = None
  255. if find_k:
  256. print("\n" + "="*60)
  257. print("Finding Optimal Number of Clusters")
  258. print("="*60)
  259. optimization_results = find_optimal_k_kmeans(X_processed, k_range=k_range)
  260. # Use recommended K
  261. if optimization_results:
  262. n_clusters = optimization_results['best_k']
  263. # Compare clustering algorithms
  264. comparison_results = compare_clustering_algorithms(X_processed, n_clusters=n_clusters)
  265. # Visualize results
  266. print("\n" + "="*60)
  267. print("Visualizing Results")
  268. print("="*60)
  269. visualize_clusters(X_processed, comparison_results, true_labels=true_labels)
  270. return {
  271. 'X_processed': X_processed,
  272. 'optimization': optimization_results,
  273. 'comparison': comparison_results
  274. }
  275. # Example usage
  276. if __name__ == "__main__":
  277. from sklearn.datasets import load_iris, make_blobs
  278. print("="*60)
  279. print("Example 1: Iris Dataset")
  280. print("="*60)
  281. # Load Iris dataset
  282. iris = load_iris()
  283. X_iris = iris.data
  284. y_iris = iris.target
  285. results_iris = complete_clustering_analysis(
  286. X_iris,
  287. true_labels=y_iris,
  288. scale=True,
  289. find_k=True,
  290. k_range=range(2, 8),
  291. n_clusters=3
  292. )
  293. print("\n" + "="*60)
  294. print("Example 2: Synthetic Dataset with Noise")
  295. print("="*60)
  296. # Create synthetic dataset
  297. X_synth, y_synth = make_blobs(
  298. n_samples=500, n_features=2, centers=4,
  299. cluster_std=0.5, random_state=42
  300. )
  301. # Add noise points
  302. noise = np.random.randn(50, 2) * 3
  303. X_synth = np.vstack([X_synth, noise])
  304. y_synth_with_noise = np.concatenate([y_synth, np.full(50, -1)])
  305. results_synth = complete_clustering_analysis(
  306. X_synth,
  307. true_labels=y_synth_with_noise,
  308. scale=True,
  309. find_k=True,
  310. k_range=range(2, 8),
  311. n_clusters=4
  312. )
  313. print("\n" + "="*60)
  314. print("Analysis Complete!")
  315. print("="*60)