177 lines
8.7 KiB
Python
177 lines
8.7 KiB
Python
"""
|
|
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)}")
|
|
|