""" K-Means Clustering Module Mengelompokkan alumni berdasarkan kompetensi menggunakan algoritma K-Means Clustering. Modul ini mencakup wrapper class untuk scikit-learn KMeans, pencarian k optimal (Elbow Method & Silhouette), serta ringkasan statistik setiap kelompok (cluster). """ # Mengimpor modul Any untuk type hint fleksibel from typing import Any # Mengimpor modul KMeans dari pustaka scikit-learn (sklearn.cluster) untuk implementasi algoritma clustering berbasis centroid from sklearn.cluster import KMeans # Mengimpor fungsi silhouette_score untuk mengukur seberapa baik data terkelompok dalam cluster-nya dibandingkan cluster tetangga from sklearn.metrics import silhouette_score # Mengimpor pustaka numpy untuk operasi array numerik dan manipulasi matriks fitur import numpy as np # Mendefinisikan class KMeansClustering sebagai pembungkus (wrapper) model KMeans Scikit-Learn class KMeansClustering: """K-Means Clustering wrapper: Memudahkan inisialisasi, pelatihan (fit), dan prediksi klaster.""" # Metode konstruktor class yang dijalankan saat objek pertama kali diinisialisasi def __init__(self, n_clusters: int = 3, random_state: int = 42): """ Inisialisasi hyperparameter algoritma K-Means: Args: n_clusters: Jumlah cluster (k) yang diinginkan (default: 3) random_state: Seed acak untuk memastikan hasil inisialisasi centroid selalu sama/reproducible (default: 42) """ # Menyimpan nilai jumlah cluster ke dalam atribut instance objek self.n_clusters = n_clusters # Menyimpan nilai seed acak ke dalam atribut instance objek self.random_state = random_state # Menginstansiasi objek model KMeans dari Scikit-Learn # n_init='auto': Algoritma secara otomatis menentukan jumlah inisialisasi centroid awal berbasis heuristik Scikit-Learn modern self.model = KMeans( n_clusters=n_clusters, random_state=random_state, n_init='auto' ) # Menyiapkan atribut untuk menyimpan label cluster dari tiap data setelah model dilatih (default None) self.labels_ = None # Menyiapkan atribut untuk menyimpan nilai inertia (Within-Cluster Sum of Squares) setelah dilatih (default None) self.inertia_ = None # Metode fit untuk melatih model K-Means pada dataset masukan X def fit(self, X) -> 'KMeansClustering': """Melatih (fit) model K-Means terhadap matriks data/fitur X""" # Menjalankan proses clustering pada matriks fitur X hingga posisi centroid konvergen self.model.fit(X) # Menyimpan label cluster hasil fitting (0, 1, 2, ...) ke atribut labels_ self.labels_ = self.model.labels_ # Mengambil nilai inertia (jumlah kuadrat jarak titik ke centroid terdekat) dari model terlatih self.inertia_ = self.model.inertia_ # Mengembalikan instance objek diri sendiri (self) untuk chaining method jika diperlukan return self # Metode predict untuk memprediksi cluster data baru tanpa melatih ulang posisi centroid def predict(self, X) -> Any: """Memprediksi label cluster untuk data baru berdasarkan centroid yang telah terbentuk""" # Mengembalikan array numpy berisi label cluster hasil prediksi jarak Euclidean terdekat ke centroid return self.model.predict(X) # Metode fit_predict untuk melatih model sekaligus menghasilkan prediksi label cluster data dalam satu langkah def fit_predict(self, X) -> Any: """Melatih model sekaligus menghasilkan label cluster secara langsung""" # Menjalankan proses fitting dan langsung mengembalikan array label cluster self.labels_ = self.model.fit_predict(X) # Menyimpan nilai inersia akhir dari proses klastering self.inertia_ = self.model.inertia_ # Mengembalikan array label cluster numerik return self.labels_ # Fungsi untuk mencari nilai k optimal menggunakan kombinasi Elbow Method dan Silhouette Score def find_optimal_k(X, k_range: range = range(2, 11)) -> dict: """ Mencari jumlah cluster optimal (k) menggunakan Elbow Method dan Silhouette Analysis. Args: X: Matriks fitur (feature matrix) yang telah melalui tahap preprocessing dan feature engineering k_range: Rentang nilai k yang akan diuji (default: dari k=2 hingga k=10) Returns: Dictionary berisi daftar nilai k, inertia, dan silhouette score untuk setiap k yang diuji """ # Menyiapkan struktur data dictionary untuk menampung riwayat evaluasi results = { 'k': [], # Menyimpan nilai k yang diuji 'inertia': [], # Menyimpan nilai inertia untuk grafik Elbow 'silhouette': [] # Menyimpan nilai rata-rata Silhouette Coefficient } # Melakukan iterasi untuk setiap nilai k dalam rentang k_range for k in k_range: # Menginisialisasi objek KMeans dengan jumlah cluster k saat ini (n_init='auto' sesuai standar Scikit-Learn terbaru) kmeans = KMeans(n_clusters=k, random_state=42, n_init='auto') # Melatih model dan memperoleh label pengelompokan cluster untuk matriks data X labels = kmeans.fit_predict(X) # Menambahkan nilai k saat ini ke list hasil results['k'].append(k) # Menambahkan nilai inertia (WCSS - Within-Cluster Sum of Squares) ke list hasil results['inertia'].append(kmeans.inertia_) # Silhouette score hanya dapat dihitung jika k > 1 dan terdapat lebih dari 1 kelompok cluster unik yang terbentuk if k > 1 and len(set(labels)) > 1: # Menghitung skor silhouette dan menambahkannya ke list hasil results['silhouette'].append(silhouette_score(X, labels)) else: # Jika kondisi tidak terpenuhi, set nilai skor silhouette menjadi 0 results['silhouette'].append(0) # Mengembalikan dictionary lengkap berisi hasil pengujian seluruh nilai k return results # Fungsi untuk menghitung ringkasan statistik dan distribusi data di setiap kelompok klaster def get_cluster_summary(df, labels: np.ndarray) -> dict: """ Mendapatkan ringkasan profil statistik setiap cluster hasil pengelompokan. Args: df: DataFrame pandas berisi data asli alumni sebelum ditransformasi labels: Array numpy yang berisi label cluster dari setiap baris data Returns: Dictionary yang berisi jumlah data (count), persentase, dan contoh data per cluster """ # Membuat salinan (copy) DataFrame asli agar tidak memodifikasi data induk df_result = df.copy() # Menambahkan kolom baru 'cluster' yang berisi label klaster hasil prediksi K-Means df_result['cluster'] = labels # Menyiapkan dictionary untuk menampung ringkasan tiap cluster summary = {} # Melakukan perulangan untuk setiap ID klaster unik yang sudah diurutkan (0, 1, 2, ...) for cluster_id in sorted(df_result['cluster'].unique()): # Memfilter baris data yang masuk ke dalam ID cluster saat ini cluster_data = df_result[df_result['cluster'] == cluster_id] # Mengisi ringkasan informasi cluster (jumlah baris, persentase dari total data, dan 3 sampel data teratas) summary[f'Cluster_{cluster_id}'] = { 'count': len(cluster_data), # Jumlah anggota klaster 'percentage': round(len(cluster_data) / len(df_result) * 100, 2), # Persentase terhadap total populasi 'sample_data': cluster_data.head(3).to_dict('records') # 3 sampel baris data berformat list of dict } # Mengembalikan dictionary ringkasan klaster return summary # Blok pengetesan lokal yang hanya dijalankan ketika file ini dieksekusi langsung di terminal (bukan diimpor) if __name__ == "__main__": # Mengimpor modul make_blobs untuk membuat dataset sintesis tiruan untuk keperluan unit testing from sklearn.datasets import make_blobs # Membuat 100 sampel data tiruan dengan 3 pusat klaster untuk menguji fungsionalitas modul blob_data = make_blobs(n_samples=100, centers=3, random_state=42) # Mengambil matriks fitur X dari data sintesis X = blob_data[0] # Menjalankan fungsi pencarian k optimal results = find_optimal_k(X) print("Elbow Method Results:") # Menampilkan hasil evaluasi inertia dan silhouette score untuk setiap nilai k for i in range(len(results['k'])): print(f" K={results['k'][i]}: Inertia={results['inertia'][i]:.2f}, Silhouette={results['silhouette'][i]:.4f}") # Menguji proses klastering akhir dengan k=3 kmeans = KMeansClustering(n_clusters=3) # Melakukan fitting dan prediksi labels = kmeans.fit_predict(X) # Menampilkan label unik yang terbentuk print(f"\nClustering complete! Labels: {np.unique(labels)}")