Data Science/Python
[파이썬] 실루엣 지수 점수(silhouette_score) DataFrame으로 단계별 계산
FDG
2024. 6. 26. 15:01
군집 후에 결과의 타당성을 확인하기 위해서는 적절한 평가 지수를 활용해야 하는데,
자기만의 지수를 개발한다고 하면 실루엣 지수 정도는 직접 코딩으로 구현이 가능해야 하겠다.
a(i) 계산할 때 자신을 뺀 점들로 평균을 구하기 때문에 클러스터 구성 점 갯수에서 1개 적게 평균을 구해야 한다.
클러스터에 1개의 점만 있는 경우는 a(i)=s(i)=0 이다.
실루엣 지수 직접 코딩_240628.ipynb
0.16MB
In [1]:
from sklearn.datasets import make_blobs
X, _ = make_blobs(n_samples=9, centers=3, cluster_std=1, random_state=1234)
In [2]:
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
from matplotlib import patches
from sklearn.metrics import pairwise_distances
n_list=[3] # [2,3,4,5,6]
n_kmeans_model={}
for n in n_list:
# KMeans 클러스터링
n_kmeans_model[n]=KMeans(n_clusters=n,random_state=1234)
n_kmeans_model[n].fit(X)
# 클러스터 중심점
cluster_centers = n_kmeans_model[n].cluster_centers_
# 클러스터 할당
point_labels = n_kmeans_model[n].labels_
# 데이터 포인트 시각화
plt.scatter(X[:, 0], X[:, 1], c=point_labels, marker='o')
# 위치별 좌표
for i, (x, y) in enumerate(X):
plt.text(x+2.5, y, f'#{i}: ({x:.2f}, {y:.2f})', fontsize=10, ha='right')
# 클러스터별 원 그리기
for i in range(len(cluster_centers)):
points_in_cluster=np.vstack([cluster_centers[i],X[np.where(point_labels==i)]])
just_dia =np.max(pairwise_distances(points_in_cluster)[0])
plt.gca().add_patch(
patches.Circle((cluster_centers[i][0],cluster_centers[i][1]),
color='green',radius=just_dia, fill=False)
)
# 클러스터 센터 시각화
plt.scatter(cluster_centers[:, 0], cluster_centers[:, 1], c='red', marker='x', s=200)
# 그래프 제목 및 레이블 설정
plt.title('K-means Clustering')
plt.xlabel('X')
plt.ylabel('Y')
# 그래프 표시
plt.show()
In [3]:
import pandas as pd
# 출력 폭 조정
pd.set_option('display.max_columns', 12)
df=pd.DataFrame(pairwise_distances(X))
df
Out[3]:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|---|
0 | 0.000000 | 14.367599 | 6.127185 | 0.793259 | 12.819788 | 7.863864 | 4.902396 | 11.992301 | 2.374419 |
1 | 14.367599 | 0.000000 | 14.560601 | 14.546335 | 1.765148 | 14.510898 | 15.276607 | 2.383954 | 12.152851 |
2 | 6.127185 | 14.560601 | 0.000000 | 5.399539 | 12.797438 | 1.814362 | 1.654168 | 12.526768 | 5.358613 |
3 | 0.793259 | 14.546335 | 5.399539 | 0.000000 | 12.952703 | 7.161212 | 4.124361 | 12.186910 | 2.398216 |
4 | 12.819788 | 1.765148 | 12.797438 | 12.952703 | 0.000000 | 12.752239 | 13.532239 | 1.272611 | 10.554493 |
5 | 7.863864 | 14.510898 | 1.814362 | 7.161212 | 12.752239 | 0.000000 | 3.427764 | 12.661698 | 6.820249 |
6 | 4.902396 | 15.276607 | 1.654168 | 4.124361 | 13.532239 | 3.427764 | 0.000000 | 13.118611 | 4.734155 |
7 | 11.992301 | 2.383954 | 12.526768 | 12.186910 | 1.272611 | 12.661698 | 13.118611 | 0.000000 | 9.798977 |
8 | 2.374419 | 12.152851 | 5.358613 | 2.398216 | 10.554493 | 6.820249 | 4.734155 | 9.798977 | 0.000000 |
In [4]:
# 인덱스(점)을 클러스터 라벨로 묶음
kmeans=n_kmeans_model[n]
labels_index=pd.MultiIndex.from_tuples(list(zip(kmeans.labels_,range(X.shape[0]))),names=['label','point'])
df.index=labels_index
df=df.sort_index()
df
Out[4]:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ||
---|---|---|---|---|---|---|---|---|---|---|
label | point | |||||||||
0 | 0 | 0.000000 | 14.367599 | 6.127185 | 0.793259 | 12.819788 | 7.863864 | 4.902396 | 11.992301 | 2.374419 |
3 | 0.793259 | 14.546335 | 5.399539 | 0.000000 | 12.952703 | 7.161212 | 4.124361 | 12.186910 | 2.398216 | |
8 | 2.374419 | 12.152851 | 5.358613 | 2.398216 | 10.554493 | 6.820249 | 4.734155 | 9.798977 | 0.000000 | |
1 | 1 | 14.367599 | 0.000000 | 14.560601 | 14.546335 | 1.765148 | 14.510898 | 15.276607 | 2.383954 | 12.152851 |
4 | 12.819788 | 1.765148 | 12.797438 | 12.952703 | 0.000000 | 12.752239 | 13.532239 | 1.272611 | 10.554493 | |
7 | 11.992301 | 2.383954 | 12.526768 | 12.186910 | 1.272611 | 12.661698 | 13.118611 | 0.000000 | 9.798977 | |
2 | 2 | 6.127185 | 14.560601 | 0.000000 | 5.399539 | 12.797438 | 1.814362 | 1.654168 | 12.526768 | 5.358613 |
5 | 7.863864 | 14.510898 | 1.814362 | 7.161212 | 12.752239 | 0.000000 | 3.427764 | 12.661698 | 6.820249 | |
6 | 4.902396 | 15.276607 | 1.654168 | 4.124361 | 13.532239 | 3.427764 | 0.000000 | 13.118611 | 4.734155 |
In [5]:
# 컬럼(점)을 클러스터 라벨로 묶음
column_labels_index=pd.MultiIndex.from_tuples(list(zip(kmeans.labels_,range(X.shape[0]))),names=['label','point'])
df.columns=column_labels_index
df=df.sort_index(axis=1, level=0)
df.insert(0,'XY',[str(i) for i in X])
df
Out[5]:
label | XY | 0 | 1 | 2 | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|
point | 0 | 3 | 8 | 1 | 4 | 7 | 2 | 5 | 6 | ||
label | point | ||||||||||
0 | 0 | [-0.83999181 5.99626362] | 0.000000 | 0.793259 | 2.374419 | 14.367599 | 12.819788 | 11.992301 | 6.127185 | 7.863864 | 4.902396 |
3 | [ 6.92067435 -6.09505345] | 0.793259 | 0.000000 | 2.398216 | 14.546335 | 12.952703 | 12.186910 | 5.399539 | 7.161212 | 4.124361 | |
8 | [-5.31002258 1.80565192] | 2.374419 | 2.398216 | 0.000000 | 12.152851 | 10.554493 | 9.798977 | 5.358613 | 6.820249 | 4.734155 | |
1 | 1 | [-1.57952259 5.70929004] | 14.367599 | 14.546335 | 12.152851 | 0.000000 | 1.765148 | 2.383954 | 14.560601 | 14.510898 | 15.276607 |
4 | [ 5.39686984 -5.20411724] | 12.819788 | 12.952703 | 10.554493 | 1.765148 | 0.000000 | 1.272611 | 12.797438 | 12.752239 | 13.532239 | |
7 | [-6.15391462 0.19949047] | 11.992301 | 12.186910 | 9.798977 | 2.383954 | 1.272611 | 0.000000 | 12.526768 | 12.661698 | 13.118611 | |
2 | 2 | [-5.01957527 3.43412144] | 6.127185 | 5.399539 | 5.358613 | 14.560601 | 12.797438 | 12.526768 | 0.000000 | 1.814362 | 1.654168 |
5 | [ 5.79293754 -3.99470898] | 7.863864 | 7.161212 | 6.820249 | 14.510898 | 12.752239 | 12.661698 | 1.814362 | 0.000000 | 3.427764 | |
6 | [-0.29212109 3.68591685] | 4.902396 | 4.124361 | 4.734155 | 15.276607 | 13.532239 | 13.118611 | 1.654168 | 3.427764 | 0.000000 |
In [6]:
unique_labels=np.unique(point_labels)
for i in range(len(unique_labels)):
temp_df=df.loc[df.index.get_level_values(0) == i, df.columns.get_level_values(0) == i]\
.replace(0,np.nan).copy() # 평균구할 때 n-1 해주는 효과
temp_df[('','ai')]=temp_df.mean(axis=1)
display(temp_df)
df.loc[df.index.get_level_values(0) == i,('result','ai')]=temp_df.iloc[:,:-1].apply(np.mean,axis=1)
label | 0 | ||||
---|---|---|---|---|---|
point | 0 | 3 | 8 | ai | |
label | point | ||||
0 | 0 | NaN | 0.793259 | 2.374419 | 1.583839 |
3 | 0.793259 | NaN | 2.398216 | 1.595738 | |
8 | 2.374419 | 2.398216 | NaN | 2.386318 |
label | 1 | ||||
---|---|---|---|---|---|
point | 1 | 4 | 7 | ai | |
label | point | ||||
1 | 1 | NaN | 1.765148 | 2.383954 | 2.074551 |
4 | 1.765148 | NaN | 1.272611 | 1.518879 | |
7 | 2.383954 | 1.272611 | NaN | 1.828282 |
label | 2 | ||||
---|---|---|---|---|---|
point | 2 | 5 | 6 | ai | |
label | point | ||||
2 | 2 | NaN | 1.814362 | 1.654168 | 1.734265 |
5 | 1.814362 | NaN | 3.427764 | 2.621063 | |
6 | 1.654168 | 3.427764 | NaN | 2.540966 |
b(i) : i번째 점이 속하지 않은 클러스들에 있는 점들과의 평균거리를 구하고 가까운 클러스터의 평균거리를 택함¶
In [7]:
for i in df.index:
class_n, index=i
other_class_n = unique_labels[unique_labels != class_n]
distances=[]
for j in other_class_n:
temp_df=df.loc[[i],j] # 데이터 프레임으로 만들기 위해 [i]
temp_df['mean']=temp_df.mean(axis=1)
display(temp_df)
bi=temp_df.iloc[[0],:-1].mean(axis=1)
distances+=[bi.values[0]]
min_distance=np.min(distances)
print(f'b({i[1]}) = {min_distance}','in',distances)
df.loc[i,('result','bi')]=min_distance
point | 1 | 4 | 7 | mean | |
---|---|---|---|---|---|
label | point | ||||
0 | 0 | 14.367599 | 12.819788 | 11.992301 | 13.059896 |
point | 2 | 5 | 6 | mean | |
---|---|---|---|---|---|
label | point | ||||
0 | 0 | 6.127185 | 7.863864 | 4.902396 | 6.297815 |
b(0) = 6.297815228952985 in [13.059896002203851, 6.297815228952985]
point | 1 | 4 | 7 | mean | |
---|---|---|---|---|---|
label | point | ||||
0 | 3 | 14.546335 | 12.952703 | 12.18691 | 13.228649 |
point | 2 | 5 | 6 | mean | |
---|---|---|---|---|---|
label | point | ||||
0 | 3 | 5.399539 | 7.161212 | 4.124361 | 5.561704 |
b(3) = 5.561703937284574 in [13.228649255919471, 5.561703937284574]
point | 1 | 4 | 7 | mean | |
---|---|---|---|---|---|
label | point | ||||
0 | 8 | 12.152851 | 10.554493 | 9.798977 | 10.835441 |
point | 2 | 5 | 6 | mean | |
---|---|---|---|---|---|
label | point | ||||
0 | 8 | 5.358613 | 6.820249 | 4.734155 | 5.637672 |
b(8) = 5.6376721922637545 in [10.83544053054826, 5.6376721922637545]
point | 0 | 3 | 8 | mean | |
---|---|---|---|---|---|
label | point | ||||
1 | 1 | 14.367599 | 14.546335 | 12.152851 | 13.688928 |
point | 2 | 5 | 6 | mean | |
---|---|---|---|---|---|
label | point | ||||
1 | 1 | 14.560601 | 14.510898 | 15.276607 | 14.782702 |
b(1) = 13.688928454016683 in [13.688928454016683, 14.782702039847793]
point | 0 | 3 | 8 | mean | |
---|---|---|---|---|---|
label | point | ||||
1 | 4 | 12.819788 | 12.952703 | 10.554493 | 12.108995 |
point | 2 | 5 | 6 | mean | |
---|---|---|---|---|---|
label | point | ||||
1 | 4 | 12.797438 | 12.752239 | 13.532239 | 13.027305 |
b(4) = 12.108994752287325 in [12.108994752287325, 13.0273052208372]
point | 0 | 3 | 8 | mean | |
---|---|---|---|---|---|
label | point | ||||
1 | 7 | 11.992301 | 12.18691 | 9.798977 | 11.326063 |
point | 2 | 5 | 6 | mean | |
---|---|---|---|---|---|
label | point | ||||
1 | 7 | 12.526768 | 12.661698 | 13.118611 | 12.769025 |
b(7) = 11.326062582367575 in [11.326062582367575, 12.76902543977152]
point | 0 | 3 | 8 | mean | |
---|---|---|---|---|---|
label | point | ||||
2 | 2 | 6.127185 | 5.399539 | 5.358613 | 5.628446 |
point | 1 | 4 | 7 | mean | |
---|---|---|---|---|---|
label | point | ||||
2 | 2 | 14.560601 | 12.797438 | 12.526768 | 13.294935 |
b(2) = 5.6284457430411985 in [5.6284457430411985, 13.294935341461871]
point | 0 | 3 | 8 | mean | |
---|---|---|---|---|---|
label | point | ||||
2 | 5 | 7.863864 | 7.161212 | 6.820249 | 7.281775 |
point | 1 | 4 | 7 | mean | |
---|---|---|---|---|---|
label | point | ||||
2 | 5 | 14.510898 | 12.752239 | 12.661698 | 13.308278 |
b(5) = 7.2817747672629025 in [7.2817747672629025, 13.308278160997176]
point | 0 | 3 | 8 | mean | |
---|---|---|---|---|---|
label | point | ||||
2 | 6 | 4.902396 | 4.124361 | 4.734155 | 4.586971 |
point | 1 | 4 | 7 | mean | |
---|---|---|---|---|---|
label | point | ||||
2 | 6 | 15.276607 | 13.532239 | 13.118611 | 13.975819 |
b(6) = 4.586970848197212 in [4.586970848197212, 13.975819197997469]
ai와 bi를 구했으니 si를 구할 수 있다.¶
In [8]:
df
Out[8]:
label | XY | 0 | 1 | 2 | result | ||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
point | 0 | 3 | 8 | 1 | 4 | 7 | 2 | 5 | 6 | ai | bi | ||
label | point | ||||||||||||
0 | 0 | [-0.83999181 5.99626362] | 0.000000 | 0.793259 | 2.374419 | 14.367599 | 12.819788 | 11.992301 | 6.127185 | 7.863864 | 4.902396 | 1.583839 | 6.297815 |
3 | [ 6.92067435 -6.09505345] | 0.793259 | 0.000000 | 2.398216 | 14.546335 | 12.952703 | 12.186910 | 5.399539 | 7.161212 | 4.124361 | 1.595738 | 5.561704 | |
8 | [-5.31002258 1.80565192] | 2.374419 | 2.398216 | 0.000000 | 12.152851 | 10.554493 | 9.798977 | 5.358613 | 6.820249 | 4.734155 | 2.386318 | 5.637672 | |
1 | 1 | [-1.57952259 5.70929004] | 14.367599 | 14.546335 | 12.152851 | 0.000000 | 1.765148 | 2.383954 | 14.560601 | 14.510898 | 15.276607 | 2.074551 | 13.688928 |
4 | [ 5.39686984 -5.20411724] | 12.819788 | 12.952703 | 10.554493 | 1.765148 | 0.000000 | 1.272611 | 12.797438 | 12.752239 | 13.532239 | 1.518879 | 12.108995 | |
7 | [-6.15391462 0.19949047] | 11.992301 | 12.186910 | 9.798977 | 2.383954 | 1.272611 | 0.000000 | 12.526768 | 12.661698 | 13.118611 | 1.828282 | 11.326063 | |
2 | 2 | [-5.01957527 3.43412144] | 6.127185 | 5.399539 | 5.358613 | 14.560601 | 12.797438 | 12.526768 | 0.000000 | 1.814362 | 1.654168 | 1.734265 | 5.628446 |
5 | [ 5.79293754 -3.99470898] | 7.863864 | 7.161212 | 6.820249 | 14.510898 | 12.752239 | 12.661698 | 1.814362 | 0.000000 | 3.427764 | 2.621063 | 7.281775 | |
6 | [-0.29212109 3.68591685] | 4.902396 | 4.124361 | 4.734155 | 15.276607 | 13.532239 | 13.118611 | 1.654168 | 3.427764 | 0.000000 | 2.540966 | 4.586971 |
In [9]:
# 컬럼명에서 level 0 삭제. df.index편하게 하기 위함
df.columns=df.columns.droplevel(level=0)
In [10]:
df['si']=(df.bi-df.ai)/df.loc[:,['ai','bi']].max(axis=1)
# 클러스터에 1개 점만 있으면 a(i), b(i)가 게산이 안 된다.
# np.nan이 되는데 0으로 대체한다.
df=df.replace(np.nan,0)
df.loc[:,'ai':'si']
Out[10]:
point | ai | bi | si | |
---|---|---|---|---|
label | point | |||
0 | 0 | 1.583839 | 6.297815 | 0.748510 |
3 | 1.595738 | 5.561704 | 0.713085 | |
8 | 2.386318 | 5.637672 | 0.576719 | |
1 | 1 | 2.074551 | 13.688928 | 0.848450 |
4 | 1.518879 | 12.108995 | 0.874566 | |
7 | 1.828282 | 11.326063 | 0.838577 | |
2 | 2 | 1.734265 | 5.628446 | 0.691875 |
5 | 2.621063 | 7.281775 | 0.640052 | |
6 | 2.540966 | 4.586971 | 0.446047 |
종합 실루엣 지수¶
In [11]:
silhouette_score=df.si.mean()
print(f"Overall Silhouette Score: {silhouette_score:.4f}")
Overall Silhouette Score: 0.7087
위에서 구한 거를 numpy로 구현¶
In [12]:
import numpy as np
from sklearn.metrics import pairwise_distances
# 각 데이터 포인트 간의 거리 계산
distances = pairwise_distances(X)
# 변수 초기화
a_i = np.zeros(X.shape[0])
b_i = np.zeros(X.shape[0])
s_i = np.zeros(X.shape[0])
# 점들마다 거리 계산
unique_labels=np.unique(point_labels)
클러스터를 기준으로 for 계산¶
In [13]:
for label in unique_labels:
# 클러스터 라벨과 아닌 클러스터들
mask = point_labels == label
other_mask = ~mask
# a(i)
cluster_distances = distances[mask][:, mask]
a_i[mask] = np.sum(cluster_distances, axis=1) / (mask.sum() - 1)
# b(i)
b_i[mask] = np.min([np.mean(distances[mask][:, point_labels == other_label], axis=1)
for other_label in unique_labels if other_label != label], axis=0)
# s(i)
s_i = (b_i - a_i) / np.maximum(a_i, b_i)
# 클러스터에 데이터가 1개만 있는 경우 a(i), s(i)는 0 처리
nan_index=np.isnan(s_i)# 결측치가 있는 index 확인
s_i[nan_index]=0 # 결측치는 0으로 대체
# 결과 출력
# print("a_i values:", a_i)
# print("b_i values:", b_i)
# print("s_i values:", s_i)
In [14]:
silhouette_score=np.mean(s_i)
print(f"Overall Silhouette Score: {silhouette_score:.4f}")
Overall Silhouette Score: 0.7087
점을 기준으로 for 계산¶
In [15]:
for i in range(X.shape[0]):
# 같은 클러스터에 속한 점들의 인덱스
mask = point_labels == point_labels[i]
# 같은 클러스터에 속한 점들과의 평균 거리 (a_i)
temp=distances[i, mask]
a_i[i] = np.sum(temp)/(mask.sum()-1)
# 다른 클러스터에 속한 포인트들과의 평균 거리 (b_i)
b_i[i] = np.min([np.mean(distances[i, point_labels == label]) for label in unique_labels if label != point_labels[i]])
s_i[i] = (b_i[i]-a_i[i])/np.max([b_i[i],a_i[i]])
# 클러스터에 데이터가 1개만 있는 경우 a(i), s(i)는 0 처리
nan_index=np.isnan(s_i)# 결측치가 있는 index 확인
s_i[nan_index]=0 # 결측치는 0으로 대체
# 결과 출력
# print("a_i values:", a_i)
# print("b_i values:", b_i)
# print("s_i values:", s_i)
In [16]:
silhouette_score=np.mean(s_i)
print(f"Overall Silhouette Score: {silhouette_score:.4f}")
Overall Silhouette Score: 0.7087
In [17]:
from sklearn.metrics import silhouette_samples, silhouette_score
silhouette_avg = silhouette_score(X, point_labels)
print(f'Average silhouette score: {silhouette_avg:.4f}')
silhouette_avg = np.mean(silhouette_samples(X,point_labels))
print(f'Average silhouette score: {silhouette_avg:.4f}')
Average silhouette score: 0.7087 Average silhouette score: 0.7087
그래프 그리기¶
In [18]:
# 각 샘플의 실루엣 점수 계산
sample_silhouette_values = silhouette_samples(X, point_labels)
n_clusters=len(cluster_centers)
y_lower = 10
for i in range(n_clusters):
# 각 클러스터의 실루엣 점수 추출 및 정렬
ith_cluster_silhouette_values = sample_silhouette_values[point_labels == i]
# ith_cluster_silhouette_values.sort()
size_cluster_i = ith_cluster_silhouette_values.shape[0]
y_upper = y_lower + size_cluster_i
# 실루엣 그래프 그리기
plt.barh(range(y_lower, y_upper), ith_cluster_silhouette_values, height=1.0)
# 클러스터 번호 표시
plt.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
y_lower = y_upper + 10
# 실루엣 그래프의 설정
plt.title(f"Silhouette plot for the various clusters")
plt.xlabel("Silhouette coefficient values")
plt.ylabel("Cluster label")
# 실루엣 지수 0 위치
plt.axvline(x=0, color="black", linestyle="-")
# 평균 실루엣 점수에 대한 수직선 그리기
plt.axvline(x=silhouette_avg, color="red", linestyle="--")
plt.yticks([])
plt.xticks(np.arange(-1.1, 1.2, 0.2))
plt.show()