hossam 0.4.16__py3-none-any.whl → 0.4.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hossam/__init__.py +19 -0
- hossam/hs_cluster copy.py +1060 -0
- hossam/hs_cluster.py +369 -128
- hossam/hs_plot.py +244 -13
- hossam/hs_prep.py +241 -56
- hossam/hs_stats.py +39 -2
- hossam/hs_util.py +20 -0
- {hossam-0.4.16.dist-info → hossam-0.4.18.dist-info}/METADATA +1 -1
- hossam-0.4.18.dist-info/RECORD +18 -0
- hossam-0.4.16.dist-info/RECORD +0 -17
- {hossam-0.4.16.dist-info → hossam-0.4.18.dist-info}/WHEEL +0 -0
- {hossam-0.4.16.dist-info → hossam-0.4.18.dist-info}/licenses/LICENSE +0 -0
- {hossam-0.4.16.dist-info → hossam-0.4.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# ===================================================================
|
|
3
|
+
# 파이썬 기본 패키지 참조
|
|
4
|
+
# ===================================================================
|
|
5
|
+
import numpy as np
|
|
6
|
+
import concurrent.futures as futures
|
|
7
|
+
from tqdm.auto import tqdm
|
|
8
|
+
from itertools import combinations
|
|
9
|
+
from typing import Literal, Callable
|
|
10
|
+
|
|
11
|
+
# ===================================================================
|
|
12
|
+
# 데이터 분석 패키지 참조
|
|
13
|
+
# ===================================================================
|
|
14
|
+
from kneed import KneeLocator
|
|
15
|
+
from pandas import Series, DataFrame, MultiIndex, concat
|
|
16
|
+
from matplotlib.pyplot import Axes # type: ignore
|
|
17
|
+
from scipy.stats import normaltest
|
|
18
|
+
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
|
|
19
|
+
from sklearn.neighbors import NearestNeighbors
|
|
20
|
+
from sklearn.metrics import silhouette_score, adjusted_rand_score
|
|
21
|
+
|
|
22
|
+
# ===================================================================
|
|
23
|
+
# hossam 패키지 참조
|
|
24
|
+
# ===================================================================
|
|
25
|
+
from . import hs_plot
|
|
26
|
+
from .hs_util import is_2d
|
|
27
|
+
|
|
28
|
+
RANDOM_STATE = 52
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ===================================================================
|
|
32
|
+
# K-평균 군집화 모델을 적합하는 함수.
|
|
33
|
+
# ===================================================================
|
|
34
|
+
def kmeans_fit(
|
|
35
|
+
data: DataFrame,
|
|
36
|
+
n_clusters: int | None = None,
|
|
37
|
+
k_range: list | tuple = [2, 11],
|
|
38
|
+
random_state: int = RANDOM_STATE,
|
|
39
|
+
plot: bool = False,
|
|
40
|
+
fields: list[str] | tuple[str] | tuple[tuple[str]] | list[list[str]] | None = None,
|
|
41
|
+
**params,
|
|
42
|
+
) -> tuple[KMeans, DataFrame, float]:
|
|
43
|
+
"""
|
|
44
|
+
K-평균 군집화 모델을 적합하는 함수.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
48
|
+
n_clusters (int | None): 군집 개수.
|
|
49
|
+
random_state (int, optional): 랜덤 시드. 기본값은 RANDOM_STATE.
|
|
50
|
+
plot (bool, optional): True면 결과를 시각화함. 기본값 False.
|
|
51
|
+
fields (list[list[str]] | None, optional): 시각화할 필드 쌍 리스트. 기본값 None이면 수치형 컬럼의 모든 조합 사용.
|
|
52
|
+
**params: KMeans에 전달할 추가 파라미터.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
KMeans: 적합된 KMeans 모델.
|
|
56
|
+
DataFrame: 클러스터 결과가 포함된 데이터 프레임
|
|
57
|
+
float: 실루엣 점수
|
|
58
|
+
"""
|
|
59
|
+
df = data.copy()
|
|
60
|
+
|
|
61
|
+
if n_clusters is None:
|
|
62
|
+
n_clusters = kmeans_best_k(data=df, k_range=k_range, random_state=random_state, plot=False)
|
|
63
|
+
print(f"Best k found: {n_clusters}")
|
|
64
|
+
|
|
65
|
+
kmeans = KMeans(n_clusters=n_clusters, random_state=random_state, **params)
|
|
66
|
+
kmeans.fit(data)
|
|
67
|
+
df["cluster"] = kmeans.predict(df)
|
|
68
|
+
score = float(silhouette_score(X=data, labels=df["cluster"]))
|
|
69
|
+
|
|
70
|
+
if plot:
|
|
71
|
+
|
|
72
|
+
if not is_2d(fields):
|
|
73
|
+
fields = [fields] # type: ignore
|
|
74
|
+
|
|
75
|
+
# cluster_plot(
|
|
76
|
+
# estimator=kmeans,
|
|
77
|
+
# data=data,
|
|
78
|
+
# fields=fields,
|
|
79
|
+
# title=f"K-Means Clustering (k={n_clusters})",
|
|
80
|
+
# )
|
|
81
|
+
for f in fields: # type: ignore
|
|
82
|
+
hs_plot.visualize_silhouette(
|
|
83
|
+
estimator=kmeans,
|
|
84
|
+
data=data,
|
|
85
|
+
xname=f[0], # type: ignore
|
|
86
|
+
yname=f[1], # type: ignore
|
|
87
|
+
title=f"K-Means Clustering (k={n_clusters})",
|
|
88
|
+
outline=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return kmeans, df, score
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ===================================================================
|
|
95
|
+
# K-평균 군집화에서 엘보우(Elbow) 기법을 활용해 최적의 K값을 탐지하는 함수.
|
|
96
|
+
# ===================================================================
|
|
97
|
+
def kmeans_elbow(
|
|
98
|
+
data: DataFrame,
|
|
99
|
+
k_range: list | tuple = [2, 11],
|
|
100
|
+
S: float = 0.1,
|
|
101
|
+
random_state: int = RANDOM_STATE,
|
|
102
|
+
plot: bool = True,
|
|
103
|
+
title: str = None,
|
|
104
|
+
marker: str = None,
|
|
105
|
+
width: int = hs_plot.config.width,
|
|
106
|
+
height: int = hs_plot.config.height,
|
|
107
|
+
dpi: int = hs_plot.config.dpi,
|
|
108
|
+
linewidth: int = hs_plot.config.line_width,
|
|
109
|
+
save_path: str | None = None,
|
|
110
|
+
ax: Axes | None = None,
|
|
111
|
+
callback: Callable | None = None,
|
|
112
|
+
**params,
|
|
113
|
+
) -> tuple:
|
|
114
|
+
"""
|
|
115
|
+
K-평균 군집화에서 엘보우(Elbow) 기법을 활용해 최적의 K값을 탐지하는 함수.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
119
|
+
k_range (list | tuple, optional): K값의 범위 지정. 기본값은 [2, 11].
|
|
120
|
+
S (float, optional): KneeLocator의 민감도 파라미터. 기본값 0.1.
|
|
121
|
+
random_state (int, optional): 랜덤 시드. 기본값은 RANDOM_STATE.
|
|
122
|
+
plot (bool, optional): True면 결과를 시각화함. 기본값 True.
|
|
123
|
+
title (str, optional): 플롯 제목.
|
|
124
|
+
marker (str, optional): 마커 스타일.
|
|
125
|
+
width (int, optional): 플롯 가로 크기.
|
|
126
|
+
height (int, optional): 플롯 세로 크기.
|
|
127
|
+
dpi (int, optional): 플롯 해상도.
|
|
128
|
+
linewidth (int, optional): 선 두께.
|
|
129
|
+
save_path (str | None, optional): 저장 경로 지정시 파일로 저장.
|
|
130
|
+
ax (Axes | None, optional): 기존 matplotlib Axes 객체. None이면 새로 생성.
|
|
131
|
+
callback (Callable | None, optional): 플롯 후 호출할 콜백 함수.
|
|
132
|
+
**params: lineplot에 전달할 추가 파라미터.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
tuple: (best_k, inertia_list)
|
|
136
|
+
- best_k: 최적의 K값
|
|
137
|
+
- inertia_list: 각 K값에 대한 inertia 리스트
|
|
138
|
+
|
|
139
|
+
Examples:
|
|
140
|
+
```python
|
|
141
|
+
from hossam import *
|
|
142
|
+
|
|
143
|
+
data = hs_util.load_data('iris')
|
|
144
|
+
best_k, inertia_list = hs_cluster.kmeans_elbow(data.iloc[:, :-1])
|
|
145
|
+
```
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
inertia_list = []
|
|
149
|
+
|
|
150
|
+
r = range(k_range[0], k_range[1])
|
|
151
|
+
|
|
152
|
+
for k in r:
|
|
153
|
+
kmeans, _, score = kmeans_fit(
|
|
154
|
+
data=data, n_clusters=k, random_state=random_state
|
|
155
|
+
)
|
|
156
|
+
inertia_list.append(kmeans.inertia_)
|
|
157
|
+
|
|
158
|
+
best_k, _ = elbow_point(
|
|
159
|
+
x=list(r),
|
|
160
|
+
y=inertia_list,
|
|
161
|
+
dir="left,down",
|
|
162
|
+
S=S,
|
|
163
|
+
plot=plot,
|
|
164
|
+
marker=marker,
|
|
165
|
+
width=width,
|
|
166
|
+
height=height,
|
|
167
|
+
dpi=dpi,
|
|
168
|
+
linewidth=linewidth,
|
|
169
|
+
save_path=save_path,
|
|
170
|
+
title=(
|
|
171
|
+
f"K-Means Elbow Method (k={k_range[0]}-{k_range[1]-1}, silhouette={score:.3f})"
|
|
172
|
+
if title is None
|
|
173
|
+
else title
|
|
174
|
+
),
|
|
175
|
+
ax=ax,
|
|
176
|
+
callback=callback,
|
|
177
|
+
**params,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return best_k, inertia_list
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ===================================================================
|
|
184
|
+
# K-평균 군집화에서 실루엣 점수를 계산하는 함수.
|
|
185
|
+
# ===================================================================
|
|
186
|
+
def kmeans_silhouette(
|
|
187
|
+
data: DataFrame,
|
|
188
|
+
k_range: list | tuple = [2, 11],
|
|
189
|
+
random_state: int = RANDOM_STATE,
|
|
190
|
+
plot: Literal[False, "silhouette", "cluster", "both"] = "both",
|
|
191
|
+
title: str = None,
|
|
192
|
+
xname: str = None,
|
|
193
|
+
yname: str = None,
|
|
194
|
+
width: int = hs_plot.config.width,
|
|
195
|
+
height: int = hs_plot.config.height,
|
|
196
|
+
linewidth: float = hs_plot.config.line_width,
|
|
197
|
+
dpi: int = hs_plot.config.dpi,
|
|
198
|
+
save_path: str | None = None,
|
|
199
|
+
**params,
|
|
200
|
+
) -> DataFrame:
|
|
201
|
+
"""
|
|
202
|
+
K-평균 군집화에서 실루엣 점수를 계산하는 함수.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
206
|
+
k_range (list | tuple, optional): K값의 범위 지정. 기본값은 [2, 11].
|
|
207
|
+
random_state (int, optional): 랜덤 시드. 기본값은 RANDOM_STATE.
|
|
208
|
+
plot (Literal[False, "silhouette", "cluster", "both"], optional):
|
|
209
|
+
플롯 옵션 지정. 기본값 "both".
|
|
210
|
+
title (str, optional): 플롯 제목.
|
|
211
|
+
xname (str, optional): 군집 산점도의 x축 컬럼명.
|
|
212
|
+
yname (str, optional): 군집 산점도의 y축 컬럼명.
|
|
213
|
+
width (int, optional): 플롯 가로 크기.
|
|
214
|
+
height (int, optional): 플롯 세로 크기.
|
|
215
|
+
linewidth (float, optional): 선 두께.
|
|
216
|
+
dpi (int, optional): 플롯 해상도.
|
|
217
|
+
save_path (str | None, optional): 저장 경로 지정시 파일로 저장.
|
|
218
|
+
**params: silhouette_plot에 전달할 추가 파라미터.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
DataFrame: 각 K값에 대한 실루엣 점수 데이터프레임.
|
|
222
|
+
|
|
223
|
+
Examples:
|
|
224
|
+
```python
|
|
225
|
+
from hossam import *
|
|
226
|
+
|
|
227
|
+
data = hs_util.load_data('iris')
|
|
228
|
+
silhouette_scores = hs_cluster.kmeans_silhouette(data.iloc[:, :-1], k=3)
|
|
229
|
+
```
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
klist = list(range(k_range[0], k_range[1]))
|
|
233
|
+
total = len(klist)
|
|
234
|
+
|
|
235
|
+
if plot is not False:
|
|
236
|
+
total *= 2
|
|
237
|
+
|
|
238
|
+
with tqdm(total=total) as pbar:
|
|
239
|
+
silhouettes = []
|
|
240
|
+
estimators = []
|
|
241
|
+
|
|
242
|
+
def __process_k(k):
|
|
243
|
+
estimator, cdf, score = kmeans_fit(
|
|
244
|
+
data=data, n_clusters=k, random_state=random_state
|
|
245
|
+
)
|
|
246
|
+
return score, estimator
|
|
247
|
+
|
|
248
|
+
with futures.ThreadPoolExecutor() as executor:
|
|
249
|
+
executed = []
|
|
250
|
+
for k in klist:
|
|
251
|
+
pbar.set_description(f"K-Means Silhouette: k={k}")
|
|
252
|
+
executed.append(executor.submit(__process_k, k))
|
|
253
|
+
|
|
254
|
+
for e in executed:
|
|
255
|
+
s_score, estimator = e.result()
|
|
256
|
+
silhouettes.append(s_score)
|
|
257
|
+
estimators.append(estimator)
|
|
258
|
+
pbar.update(1)
|
|
259
|
+
|
|
260
|
+
if plot is not False:
|
|
261
|
+
for estimator in estimators:
|
|
262
|
+
pbar.set_description(f"K-Means Plotting: k={estimator.n_clusters}")
|
|
263
|
+
|
|
264
|
+
if plot == "silhouette":
|
|
265
|
+
hs_plot.silhouette_plot(
|
|
266
|
+
estimator=estimator,
|
|
267
|
+
data=data,
|
|
268
|
+
title=title,
|
|
269
|
+
width=width,
|
|
270
|
+
height=height,
|
|
271
|
+
dpi=dpi,
|
|
272
|
+
linewidth=linewidth,
|
|
273
|
+
save_path=save_path,
|
|
274
|
+
**params,
|
|
275
|
+
)
|
|
276
|
+
elif plot == "cluster":
|
|
277
|
+
hs_plot.cluster_plot(
|
|
278
|
+
estimator=estimator,
|
|
279
|
+
data=data,
|
|
280
|
+
xname=xname,
|
|
281
|
+
yname=yname,
|
|
282
|
+
outline=True,
|
|
283
|
+
palette=None,
|
|
284
|
+
width=width,
|
|
285
|
+
height=height,
|
|
286
|
+
dpi=dpi,
|
|
287
|
+
title=title,
|
|
288
|
+
save_path=save_path,
|
|
289
|
+
)
|
|
290
|
+
elif plot == "both":
|
|
291
|
+
hs_plot.visualize_silhouette(
|
|
292
|
+
estimator=estimator,
|
|
293
|
+
data=data,
|
|
294
|
+
xname=xname,
|
|
295
|
+
yname=yname,
|
|
296
|
+
outline=True,
|
|
297
|
+
palette=None,
|
|
298
|
+
width=width,
|
|
299
|
+
height=height,
|
|
300
|
+
dpi=dpi,
|
|
301
|
+
title=title,
|
|
302
|
+
linewidth=linewidth,
|
|
303
|
+
save_path=save_path,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
pbar.update(1)
|
|
307
|
+
|
|
308
|
+
silhouette_df = DataFrame({"k": klist, "silhouette_score": silhouettes})
|
|
309
|
+
silhouette_df.sort_values(by="silhouette_score", ascending=False, inplace=True)
|
|
310
|
+
return silhouette_df
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ===================================================================
|
|
314
|
+
# 엘보우(Elbow) 포인트를 자동으로 탐지하는 함수.
|
|
315
|
+
# ===================================================================
|
|
316
|
+
def elbow_point(
|
|
317
|
+
x: Series | np.ndarray | list,
|
|
318
|
+
y: Series | np.ndarray | list,
|
|
319
|
+
dir: Literal["left,down", "left,up", "right,down", "right,up"] = "left,down",
|
|
320
|
+
S: float = 0.1,
|
|
321
|
+
plot: bool = True,
|
|
322
|
+
title: str = None,
|
|
323
|
+
marker: str = None,
|
|
324
|
+
width: int = hs_plot.config.width,
|
|
325
|
+
height: int = hs_plot.config.height,
|
|
326
|
+
dpi: int = hs_plot.config.dpi,
|
|
327
|
+
linewidth: int = hs_plot.config.line_width,
|
|
328
|
+
save_path: str | None = None,
|
|
329
|
+
ax: Axes | None = None,
|
|
330
|
+
callback: Callable | None = None,
|
|
331
|
+
**params,
|
|
332
|
+
) -> tuple:
|
|
333
|
+
"""
|
|
334
|
+
엘보우(Elbow) 포인트를 자동으로 탐지하는 함수.
|
|
335
|
+
|
|
336
|
+
주어진 x, y 값의 곡선에서 KneeLocator를 활용해 엘보우(혹은 니) 포인트를 탐지하고, 필요시 시각화까지 지원함.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
x (Series | np.ndarray | list): x축 값(일반적으로 K값 등).
|
|
340
|
+
y (Series | np.ndarray | list): y축 값(일반적으로 inertia, SSE 등).
|
|
341
|
+
dir (Literal["left,down", "left,up", "right,down", "right,up"], optional):
|
|
342
|
+
곡선의 방향 및 형태 지정. 기본값은 "left,down".
|
|
343
|
+
- "left,down": 왼쪽에서 오른쪽으로 감소(볼록)
|
|
344
|
+
- "left,up": 왼쪽에서 오른쪽으로 증가(오목)
|
|
345
|
+
- "right,down": 오른쪽에서 왼쪽으로 감소(볼록)
|
|
346
|
+
- "right,up": 오른쪽에서 왼쪽으로 증가(오목)
|
|
347
|
+
S (float, optional): KneeLocator의 민감도 파라미터. 기본값 0.1.
|
|
348
|
+
plot (bool, optional): True면 결과를 시각화함. 기본값 True.
|
|
349
|
+
title (str, optional): 플롯 제목.
|
|
350
|
+
marker (str, optional): 마커 스타일.
|
|
351
|
+
width (int, optional): 플롯 가로 크기.
|
|
352
|
+
height (int, optional): 플롯 세로 크기.
|
|
353
|
+
dpi (int, optional): 플롯 해상도.
|
|
354
|
+
linewidth (int, optional): 선 두께.
|
|
355
|
+
save_path (str | None, optional): 저장 경로 지정시 파일로 저장.
|
|
356
|
+
ax (Axes | None, optional): 기존 matplotlib Axes 객체. None이면 새로 생성.
|
|
357
|
+
callback (Callable | None, optional): 플롯 후 호출할 콜백 함수.
|
|
358
|
+
**params: lineplot에 전달할 추가 파라미터.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
tuple: (best_x, best_y)
|
|
362
|
+
- best_x: 엘보우 포인트의 x값(예: 최적 K)
|
|
363
|
+
- best_y: 엘보우 포인트의 y값
|
|
364
|
+
|
|
365
|
+
Examples:
|
|
366
|
+
```python
|
|
367
|
+
x = [1, 2, 3, 4, 5, 6]
|
|
368
|
+
y = [100, 80, 60, 45, 44, 43]
|
|
369
|
+
elbow_point(x, y)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Note:
|
|
373
|
+
- KneeLocator는 kneed 패키지의 클래스로, 곡선의 형태(curve)와 방향(direction)에 따라 엘보우 포인트를 탐지함.
|
|
374
|
+
- dir 파라미터에 따라 curve/direction이 자동 지정됨.
|
|
375
|
+
- plot=True일 때, 엘보우 포인트에 수직/수평선과 텍스트가 표시됨.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
if dir == "left,down":
|
|
379
|
+
curve = "convex"
|
|
380
|
+
direction = "decreasing"
|
|
381
|
+
elif dir == "left,up":
|
|
382
|
+
curve = "concave"
|
|
383
|
+
direction = "increasing"
|
|
384
|
+
elif dir == "right,down":
|
|
385
|
+
curve = "convex"
|
|
386
|
+
direction = "increasing"
|
|
387
|
+
else:
|
|
388
|
+
curve = "concave"
|
|
389
|
+
direction = "decreasing"
|
|
390
|
+
|
|
391
|
+
kn = KneeLocator(x=x, y=y, curve=curve, direction=direction, S=S)
|
|
392
|
+
|
|
393
|
+
best_x = kn.elbow
|
|
394
|
+
best_y = kn.elbow_y
|
|
395
|
+
|
|
396
|
+
if plot:
|
|
397
|
+
|
|
398
|
+
def hvline(ax):
|
|
399
|
+
ax.axvline(best_x, color="red", linestyle="--", linewidth=0.7)
|
|
400
|
+
ax.axhline(best_y, color="red", linestyle="--", linewidth=0.7)
|
|
401
|
+
ax.text(
|
|
402
|
+
best_x,
|
|
403
|
+
best_y + (best_y * 0.01),
|
|
404
|
+
"x=%.2f, y=%.2f" % (best_x, best_y),
|
|
405
|
+
fontsize=6,
|
|
406
|
+
ha="center",
|
|
407
|
+
va="bottom",
|
|
408
|
+
color="black",
|
|
409
|
+
fontweight="bold",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if callback is not None:
|
|
413
|
+
callback(ax)
|
|
414
|
+
|
|
415
|
+
hs_plot.lineplot(
|
|
416
|
+
df=None,
|
|
417
|
+
xname=x,
|
|
418
|
+
yname=y,
|
|
419
|
+
title=title,
|
|
420
|
+
marker=marker,
|
|
421
|
+
width=width,
|
|
422
|
+
height=height,
|
|
423
|
+
linewidth=linewidth,
|
|
424
|
+
dpi=dpi,
|
|
425
|
+
save_path=save_path,
|
|
426
|
+
callback=hvline,
|
|
427
|
+
ax=ax,
|
|
428
|
+
**params,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
return best_x, best_y
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ===================================================================
|
|
435
|
+
# 데이터프레임의 여러 필드 쌍에 대해 군집 산점도를 그리는 함수.
|
|
436
|
+
# ===================================================================
|
|
437
|
+
def cluster_plot(
|
|
438
|
+
estimator: KMeans | DBSCAN | AgglomerativeClustering,
|
|
439
|
+
data: DataFrame,
|
|
440
|
+
hue: str | None = None,
|
|
441
|
+
vector: str | None = None,
|
|
442
|
+
fields: list[list] = None,
|
|
443
|
+
title: str | None = None,
|
|
444
|
+
palette: str | None = None,
|
|
445
|
+
outline: bool = True,
|
|
446
|
+
width: int = hs_plot.config.width,
|
|
447
|
+
height: int = hs_plot.config.height,
|
|
448
|
+
linewidth: float = hs_plot.config.line_width,
|
|
449
|
+
dpi: int = hs_plot.config.dpi,
|
|
450
|
+
save_path: str | None = None,
|
|
451
|
+
ax: Axes | None = None,
|
|
452
|
+
):
|
|
453
|
+
"""
|
|
454
|
+
데이터프레임의 여러 필드 쌍에 대해 군집 산점도를 그리는 함수.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
estimator (KMeans): KMeans 군집화 모델.
|
|
458
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
459
|
+
hue (str | None, optional): 군집 레이블 컬럼명. 지정되지 않으면 estimator의 레이블 사용.
|
|
460
|
+
vector (str | None, optional): 벡터 종류를 의미하는 컬럼명(for DBSCAN)
|
|
461
|
+
fields (list[list], optional): 시각화할 필드 쌍 리스트. 기본값 None이면 수치형 컬럼의 모든 조합 사용.
|
|
462
|
+
title (str | None, optional): 플롯 제목.
|
|
463
|
+
palette (str | None, optional): 색상 팔레트 이름.
|
|
464
|
+
outline (bool, optional): True면 데이터 포인트 외곽선 표시. 기본값 False.
|
|
465
|
+
width (int, optional): 플롯 가로 크기.
|
|
466
|
+
height (int, optional): 플롯 세로 크기.
|
|
467
|
+
linewidth (float, optional): 선 두께.
|
|
468
|
+
dpi (int, optional): 플롯 해상도.
|
|
469
|
+
save_path (str | None, optional): 저장 경로 지정시 파일로 저장.
|
|
470
|
+
ax (Axes | None, optional): 기존 matplotlib Axes 객체. None이면 새로 생성.
|
|
471
|
+
|
|
472
|
+
Examples:
|
|
473
|
+
```python
|
|
474
|
+
from hossam import *
|
|
475
|
+
|
|
476
|
+
data = hs_util.load_data('iris')
|
|
477
|
+
estimator, cdf, score = hs_cluster.kmeans_fit(data.iloc[:, :-1], n_clusters=3)
|
|
478
|
+
hs_cluster.cluster_plot(cdf, hue='cluster')
|
|
479
|
+
```
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
if fields is None:
|
|
483
|
+
numeric_cols = data.select_dtypes(include=["number"]).columns.tolist()
|
|
484
|
+
if len(numeric_cols) < 2:
|
|
485
|
+
raise ValueError("데이터프레임에 수치형 컬럼이 2개 이상 필요합니다.")
|
|
486
|
+
|
|
487
|
+
# fields의 모든 조합 생성
|
|
488
|
+
fields = [list(pair) for pair in combinations(numeric_cols, 2)]
|
|
489
|
+
|
|
490
|
+
for field_pair in fields:
|
|
491
|
+
xname, yname = field_pair
|
|
492
|
+
|
|
493
|
+
hs_plot.cluster_plot(
|
|
494
|
+
estimator=estimator, # type: ignore
|
|
495
|
+
data=data,
|
|
496
|
+
xname=xname,
|
|
497
|
+
yname=yname,
|
|
498
|
+
hue=hue,
|
|
499
|
+
title=title,
|
|
500
|
+
vector=vector,
|
|
501
|
+
palette=palette,
|
|
502
|
+
outline=outline,
|
|
503
|
+
width=width,
|
|
504
|
+
height=height,
|
|
505
|
+
linewidth=linewidth,
|
|
506
|
+
dpi=dpi,
|
|
507
|
+
save_path=save_path,
|
|
508
|
+
ax=ax,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# ===================================================================
|
|
513
|
+
# 군집화된 데이터프레임에서 각 군집의 페르소나(특성 요약)를 생성하는 함수.
|
|
514
|
+
# ===================================================================
|
|
515
|
+
def persona(
|
|
516
|
+
data: DataFrame,
|
|
517
|
+
cluster: str | Series | np.ndarray | list | dict,
|
|
518
|
+
fields: list[str] | None = None,
|
|
519
|
+
full: bool = False,
|
|
520
|
+
) -> DataFrame:
|
|
521
|
+
"""
|
|
522
|
+
군집화된 데이터프레임에서 각 군집의 페르소나(특성 요약)를 생성하는 함수.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
data (DataFrame): 군집화된 데이터프레임.
|
|
526
|
+
cluster (str | Series | ndarray | list | dict): 군집 레이블 컬럼명 또는 배열.
|
|
527
|
+
fields (list[str] | None, optional): 페르소나 생성에 사용할 필드 리스트. 기본값 None이면 수치형 컬럼 전체 사용.
|
|
528
|
+
full (bool, optional): True면 모든 통계량을 포함. 기본값 False.
|
|
529
|
+
Returns:
|
|
530
|
+
DataFrame: 각 군집의 페르소나 요약 데이터프레임.
|
|
531
|
+
|
|
532
|
+
Examples:
|
|
533
|
+
```python
|
|
534
|
+
from hossam import *
|
|
535
|
+
|
|
536
|
+
data = hs_util.load_data('iris')
|
|
537
|
+
estimator, df, score = hs_cluster.kmeans_fit(data.iloc[:, :-1], n_clusters=3)
|
|
538
|
+
persona_df = hs_cluster.persona(df, hue='cluster')
|
|
539
|
+
print(persona_df)
|
|
540
|
+
```
|
|
541
|
+
"""
|
|
542
|
+
df = data.copy()
|
|
543
|
+
|
|
544
|
+
if fields is None:
|
|
545
|
+
fields = df.select_dtypes(include=["number"]).columns.tolist()
|
|
546
|
+
|
|
547
|
+
if isinstance(cluster, str):
|
|
548
|
+
if cluster not in df.columns:
|
|
549
|
+
raise ValueError(
|
|
550
|
+
f"cluster로 지정된 컬럼 '{cluster}'이(가) 데이터프레임에 존재하지 않습니다."
|
|
551
|
+
)
|
|
552
|
+
else:
|
|
553
|
+
df["cluster"] = cluster
|
|
554
|
+
cluster = "cluster"
|
|
555
|
+
fields.remove(cluster) if cluster in fields else None
|
|
556
|
+
|
|
557
|
+
persona_list = []
|
|
558
|
+
|
|
559
|
+
grouped = df.groupby(cluster)
|
|
560
|
+
for cluster_label, group in grouped:
|
|
561
|
+
persona_dict = {}
|
|
562
|
+
# 군집 레이블 및 카운트는 단일 인덱스 유지
|
|
563
|
+
persona_dict[(cluster, "")] = cluster_label
|
|
564
|
+
persona_dict[("", f"count")] = len(group)
|
|
565
|
+
|
|
566
|
+
for field in fields:
|
|
567
|
+
# 명목형일 경우 최빈값 사용
|
|
568
|
+
if df[field].dtype == "object" or df[field].dtype.name == "category":
|
|
569
|
+
persona_dict[(field, "mode")] = group[field].mode()[0]
|
|
570
|
+
else:
|
|
571
|
+
if full:
|
|
572
|
+
persona_dict[(field, "mean")] = group[field].mean()
|
|
573
|
+
persona_dict[(field, "median")] = group[field].median()
|
|
574
|
+
persona_dict[(field, "std")] = group[field].std()
|
|
575
|
+
persona_dict[(field, "min")] = group[field].min()
|
|
576
|
+
persona_dict[(field, "max")] = group[field].max()
|
|
577
|
+
persona_dict[(field, "25%")] = group[field].quantile(0.25)
|
|
578
|
+
persona_dict[(field, "50%")] = group[field].quantile(0.50)
|
|
579
|
+
persona_dict[(field, "75%")] = group[field].quantile(0.75)
|
|
580
|
+
else:
|
|
581
|
+
# normaltest를 사용해서 정규분포일 경우 평균/표준편차, 비정규분포일 경우 중앙값/IQR 사용
|
|
582
|
+
stat, p = normaltest(df[field])
|
|
583
|
+
alpha = 0.05
|
|
584
|
+
|
|
585
|
+
if p > alpha:
|
|
586
|
+
# 정규분포
|
|
587
|
+
persona_dict[(field, "mean")] = group[field].mean()
|
|
588
|
+
persona_dict[(field, "std")] = group[field].std()
|
|
589
|
+
else:
|
|
590
|
+
# 비정규분포
|
|
591
|
+
persona_dict[(field, "median")] = group[field].median()
|
|
592
|
+
persona_dict[(field, "IQR")] = group[field].quantile(
|
|
593
|
+
0.75
|
|
594
|
+
) - group[field].quantile(0.25)
|
|
595
|
+
|
|
596
|
+
persona_list.append(persona_dict)
|
|
597
|
+
|
|
598
|
+
persona_df = DataFrame(persona_list)
|
|
599
|
+
# 멀티인덱스로 변환 (단일 인덱스는 그대로)
|
|
600
|
+
persona_df.columns = MultiIndex.from_tuples(persona_df.columns) # type: ignore
|
|
601
|
+
# 군집 레이블(cluster)을 인덱스로 설정
|
|
602
|
+
persona_df.set_index((cluster, ""), inplace=True)
|
|
603
|
+
persona_df.index.name = cluster
|
|
604
|
+
return persona_df
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ===================================================================
|
|
608
|
+
# 엘보우 포인트와 실루엣 점수를 통해 최적의 K값을 결정하는 함수.
|
|
609
|
+
# ===================================================================
|
|
610
|
+
def kmeans_best_k(
|
|
611
|
+
data: DataFrame,
|
|
612
|
+
k_range: list | tuple = [2, 11],
|
|
613
|
+
S: float = 0.1,
|
|
614
|
+
random_state: int = RANDOM_STATE,
|
|
615
|
+
plot: bool = True,
|
|
616
|
+
) -> int:
|
|
617
|
+
"""
|
|
618
|
+
엘보우 포인트와 실루엣 점수를 통해 최적의 K값을 결정하는 함수.
|
|
619
|
+
Args:
|
|
620
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
621
|
+
k_range (list | tuple, optional): K값의 범위 지정. 기본값은 [2, 11].
|
|
622
|
+
S (float, optional): KneeLocator의 민감도 파라미터. 기본값 0.1.
|
|
623
|
+
random_state (int, optional): 랜덤 시드. 기본값은 RANDOM_STATE.
|
|
624
|
+
plot (bool, optional): True면 결과를 시각화함. 기본값 True.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
int: 최적의 K값.
|
|
628
|
+
|
|
629
|
+
Examples:
|
|
630
|
+
```python
|
|
631
|
+
from hossam import *
|
|
632
|
+
data = hs_util.load_data('iris')
|
|
633
|
+
best_k = hs_cluster.kmeans_best_k(data.iloc[:, :-1])
|
|
634
|
+
```
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
elbow_k, _ = kmeans_elbow(
|
|
638
|
+
data=data,
|
|
639
|
+
k_range=k_range,
|
|
640
|
+
S=S,
|
|
641
|
+
random_state=random_state,
|
|
642
|
+
plot=True if plot else False,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
silhouette_df = kmeans_silhouette(
|
|
646
|
+
data=data,
|
|
647
|
+
k_range=k_range,
|
|
648
|
+
random_state=random_state,
|
|
649
|
+
plot="both" if plot else False,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
silhouette_k = silhouette_df.sort_values(
|
|
653
|
+
by="silhouette_score", ascending=False
|
|
654
|
+
).iloc[0]["k"]
|
|
655
|
+
|
|
656
|
+
if elbow_k == silhouette_k:
|
|
657
|
+
best_k = elbow_k
|
|
658
|
+
else:
|
|
659
|
+
best_k = min(elbow_k, silhouette_k)
|
|
660
|
+
|
|
661
|
+
print(f"Elbow K: {elbow_k}, Silhouette K: {silhouette_k} => Best K: {best_k}")
|
|
662
|
+
return best_k
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
# ===================================================================
|
|
666
|
+
# DBSCAN 군집화 모델을 적합하는 함수.
|
|
667
|
+
# ===================================================================
|
|
668
|
+
def __dbscan_fit(
|
|
669
|
+
data: DataFrame, eps: float = 0.5, min_samples: int = 5, **params
|
|
670
|
+
) -> tuple[DBSCAN, DataFrame, DataFrame]:
|
|
671
|
+
"""
|
|
672
|
+
DBSCAN 군집화 모델을 적합하는 함수.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
676
|
+
eps (float, optional): 두 샘플이 같은 군집에 속하기 위한 최대 거리. 기본값 0.5.
|
|
677
|
+
min_samples (int, optional): 핵심점이 되기 위한 최소 샘플 수. 기본값 5.
|
|
678
|
+
**params: DBSCAN에 전달할 추가 파라미터.
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
tuple: (estimator, df)
|
|
682
|
+
- estimator: 적합된 DBSCAN 모델.
|
|
683
|
+
- df: 클러스터 및 벡터 유형이 포함된 데이터 프레임.
|
|
684
|
+
- result_df: 군집화 요약 통계 데이터 프레임.
|
|
685
|
+
|
|
686
|
+
"""
|
|
687
|
+
df = data.copy()
|
|
688
|
+
estimator = DBSCAN(eps=eps, min_samples=min_samples, n_jobs=-1, **params)
|
|
689
|
+
estimator.fit(df)
|
|
690
|
+
df["cluster"] = estimator.labels_
|
|
691
|
+
|
|
692
|
+
# 기본적으로 모두 외곽 벡터로 지정
|
|
693
|
+
df["vector"] = "border"
|
|
694
|
+
|
|
695
|
+
# 핵심 벡터인 경우 'core'로 지정
|
|
696
|
+
df.loc[estimator.core_sample_indices_, "vector"] = "core"
|
|
697
|
+
|
|
698
|
+
# 노이즈 분류
|
|
699
|
+
df.loc[df["cluster"] == -1, "vector"] = "noise"
|
|
700
|
+
|
|
701
|
+
labels = estimator.labels_
|
|
702
|
+
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
|
|
703
|
+
noise_ratio = np.mean(labels == -1)
|
|
704
|
+
|
|
705
|
+
result_df = DataFrame(
|
|
706
|
+
{
|
|
707
|
+
"eps": [eps],
|
|
708
|
+
"min_samples": [min_samples],
|
|
709
|
+
"n_clusters": [n_clusters],
|
|
710
|
+
"noise_ratio": [noise_ratio],
|
|
711
|
+
}
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
return estimator, df, result_df
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
# ===================================================================
|
|
718
|
+
# DBSCAN 군집화에서 최적의 eps 값을 탐지하는 함수.
|
|
719
|
+
# ===================================================================
|
|
720
|
+
def dbscan_eps(
|
|
721
|
+
data: DataFrame,
|
|
722
|
+
min_samples: int = 5,
|
|
723
|
+
delta_ratio: float = 0.3,
|
|
724
|
+
step_ratio: float = 0.05,
|
|
725
|
+
S: float = 0.1,
|
|
726
|
+
plot: bool = True,
|
|
727
|
+
title: str | None = None,
|
|
728
|
+
palette: str | None = None,
|
|
729
|
+
width: int = hs_plot.config.width,
|
|
730
|
+
height: int = hs_plot.config.height,
|
|
731
|
+
linewidth: int = hs_plot.config.line_width,
|
|
732
|
+
dpi: int = hs_plot.config.dpi,
|
|
733
|
+
save_path: str | None = None,
|
|
734
|
+
ax: Axes | None = None,
|
|
735
|
+
) -> tuple[float, np.ndarray]:
|
|
736
|
+
"""
|
|
737
|
+
DBSCAN 군집화에서 최적의 eps 값을 탐지하는 함수.
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
741
|
+
min_samples (int, optional): 핵심점이 되기 위한 최소 샘플 수. 기본값 5.
|
|
742
|
+
delta_ratio (float, optional): eps 탐색 범위 비율. 기본값 0.3.
|
|
743
|
+
step_ratio (float, optional): eps 탐색 스텝 비율. 기본값 0.05.
|
|
744
|
+
S (float, optional): KneeLocator의 민감도 파라미터. 기본값 0.1.
|
|
745
|
+
plot (bool, optional): True면 결과를 시각화함. 기본값 True.
|
|
746
|
+
title (str | None, optional): 플롯 제목.
|
|
747
|
+
palette (str | None, optional): 색상 팔레트 이름.
|
|
748
|
+
width (int, optional): 플롯 가로 크기.
|
|
749
|
+
height (int, optional): 플롯 세로 크기.
|
|
750
|
+
linewidth (float, optional): 선 두께.
|
|
751
|
+
dpi (int, optional): 플롯 해상도.
|
|
752
|
+
save_path (str | None, optional): 저장 경로 지정시 파일로 저장.
|
|
753
|
+
ax (Axes | None, optional): 기존 matplotlib Axes 객체. None이면 새로 생성.
|
|
754
|
+
|
|
755
|
+
Returns:
|
|
756
|
+
tuple: (best_eps, eps_grid)
|
|
757
|
+
- best_eps: 최적의 eps 값
|
|
758
|
+
- eps_grid: 탐색할 eps 값의 그리드 배열
|
|
759
|
+
|
|
760
|
+
Examples:
|
|
761
|
+
```python
|
|
762
|
+
from hossam import *
|
|
763
|
+
data = hs_util.load_data('iris')
|
|
764
|
+
best_eps, eps_grid = hs_cluster.dbscan_eps(data, plot=True)
|
|
765
|
+
```
|
|
766
|
+
"""
|
|
767
|
+
|
|
768
|
+
neigh = NearestNeighbors(n_neighbors=min_samples)
|
|
769
|
+
nbrs = neigh.fit(data)
|
|
770
|
+
distances, indices = nbrs.kneighbors(data)
|
|
771
|
+
|
|
772
|
+
# 각 포인트에 대해 k번째 최근접 이웃까지의 거리 추출
|
|
773
|
+
k_distances = distances[:, -1]
|
|
774
|
+
k_distances.sort()
|
|
775
|
+
|
|
776
|
+
# 엘보우 포인트 탐지
|
|
777
|
+
_, best_eps = elbow_point(
|
|
778
|
+
x=list(range(1, len(k_distances) + 1)),
|
|
779
|
+
y=k_distances,
|
|
780
|
+
dir="right,down",
|
|
781
|
+
S=S,
|
|
782
|
+
plot=plot,
|
|
783
|
+
title=title,
|
|
784
|
+
marker=None,
|
|
785
|
+
width=width,
|
|
786
|
+
height=height,
|
|
787
|
+
dpi=dpi,
|
|
788
|
+
linewidth=linewidth,
|
|
789
|
+
palette=palette,
|
|
790
|
+
save_path=save_path,
|
|
791
|
+
ax=ax,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
eps_min = best_eps * (1 - delta_ratio)
|
|
795
|
+
eps_max = best_eps * (1 + delta_ratio)
|
|
796
|
+
step = best_eps * step_ratio
|
|
797
|
+
|
|
798
|
+
eps_grid = np.arange(eps_min, eps_max + step, step)
|
|
799
|
+
|
|
800
|
+
return best_eps, eps_grid
|
|
801
|
+
|
|
802
|
+
# ===================================================================
|
|
803
|
+
# DBSCAN 군집화 모델을 적합하고 최적의 eps 값을 탐지하는 함수.
|
|
804
|
+
# ===================================================================
|
|
805
|
+
def dbscan_fit(
|
|
806
|
+
data: DataFrame,
|
|
807
|
+
eps: float | list | np.ndarray | None = None,
|
|
808
|
+
min_samples: int = 5,
|
|
809
|
+
ari_threshold: float = 0.9,
|
|
810
|
+
noise_diff_threshold: float = 0.05,
|
|
811
|
+
plot: bool = True,
|
|
812
|
+
**params,
|
|
813
|
+
) -> tuple[DBSCAN, DataFrame, DataFrame]:
|
|
814
|
+
"""
|
|
815
|
+
DBSCAN 군집화 모델을 적합하고 최적의 eps 값을 탐지하는 함수.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
819
|
+
eps (float | list | np.ndarray | None, optional): eps 값 또는 리스트.
|
|
820
|
+
None이면 최적의 eps 값을 탐지함. 기본값 None.
|
|
821
|
+
min_samples (int, optional): 핵심점이 되기 위한 최소 샘플수. 기본값 5.
|
|
822
|
+
ari_threshold (float, optional): 안정 구간 탐지를 위한 ARI 임계값. 기본값 0.9.
|
|
823
|
+
noise_diff_threshold (float, optional): 안정 구간 탐지를 위한 노이즈 비율 변화 임계값. 기본값 0.05.
|
|
824
|
+
plot (bool, optional): True면 결과를 시각화함. 기본값 True.
|
|
825
|
+
**params: DBSCAN에 전달할 추가 파라미터.
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
tuple: (estimator, cluster_df, result_df)
|
|
829
|
+
- estimator: 적합된 DBSCAN 모델 또는 모델 리스트(최적 eps가 여러 개인 경우).
|
|
830
|
+
- cluster_df: 클러스터 및 벡터 유형이 포함된 데이터 프레임 또는 데이터 프레임 리스트(최적 eps가 여러 개인 경우).
|
|
831
|
+
- result_df: eps 값에 따른 군집화 요약 통계 데이터 프레임.
|
|
832
|
+
"""
|
|
833
|
+
|
|
834
|
+
# eps 값이 지정되지 않은 경우 최적의 eps 탐지
|
|
835
|
+
if eps is None:
|
|
836
|
+
_, eps_grid = dbscan_eps(data=data, min_samples=min_samples, plot=plot)
|
|
837
|
+
eps = eps_grid
|
|
838
|
+
|
|
839
|
+
# eps가 단일 값인 경우 리스트로 변환
|
|
840
|
+
if not isinstance(eps, (list, np.ndarray)):
|
|
841
|
+
eps = [eps]
|
|
842
|
+
|
|
843
|
+
estimators = []
|
|
844
|
+
cluster_dfs = []
|
|
845
|
+
result_dfs: DataFrame | None = None
|
|
846
|
+
|
|
847
|
+
with tqdm(total=len(eps) + 2) as pbar:
|
|
848
|
+
pbar.set_description(f"DBSCAN Clustering")
|
|
849
|
+
|
|
850
|
+
with futures.ThreadPoolExecutor() as executor:
|
|
851
|
+
executers = []
|
|
852
|
+
for i, e in enumerate(eps):
|
|
853
|
+
executers.append(
|
|
854
|
+
executor.submit(
|
|
855
|
+
__dbscan_fit,
|
|
856
|
+
data=data,
|
|
857
|
+
eps=e,
|
|
858
|
+
min_samples=min_samples,
|
|
859
|
+
**params,
|
|
860
|
+
)
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
for i, e in enumerate(executers):
|
|
864
|
+
estimator, cluster_df, result_df = e.result()
|
|
865
|
+
estimators.append(estimator)
|
|
866
|
+
cluster_dfs.append(cluster_df)
|
|
867
|
+
|
|
868
|
+
if result_dfs is None:
|
|
869
|
+
result_df["ARI"] = np.nan
|
|
870
|
+
result_dfs = result_df
|
|
871
|
+
else:
|
|
872
|
+
result_df["ARI"] = adjusted_rand_score(cluster_dfs[i - 1]["cluster"], cluster_df["cluster"]) # type: ignore
|
|
873
|
+
result_dfs = concat([result_dfs, result_df], ignore_index=True)
|
|
874
|
+
|
|
875
|
+
pbar.update(1)
|
|
876
|
+
|
|
877
|
+
result_dfs["cluster_diff"] = result_dfs["n_clusters"].diff().abs() # type: ignore
|
|
878
|
+
result_dfs["noise_ratio_diff"] = result_dfs["noise_ratio"].diff().abs() # type: ignore
|
|
879
|
+
result_dfs["stable"] = ( # type: ignore
|
|
880
|
+
(result_dfs["ARI"] >= ari_threshold) # type: ignore
|
|
881
|
+
& (result_dfs["cluster_diff"] <= 0) # type: ignore
|
|
882
|
+
& (result_dfs["noise_ratio_diff"] <= noise_diff_threshold) # type: ignore
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
# 첫 행은 비교 불가
|
|
886
|
+
result_dfs.loc[0, "stable"] = False # type: ignore
|
|
887
|
+
pbar.update(1)
|
|
888
|
+
|
|
889
|
+
if len(eps) == 1:
|
|
890
|
+
result_dfs["group_id"] = 1 # type: ignore
|
|
891
|
+
result_dfs["recommand"] = "unknown" # type: ignore
|
|
892
|
+
else:
|
|
893
|
+
# 안정구간 도출하기
|
|
894
|
+
# stable 여부를 0/1로 변환
|
|
895
|
+
stable_flag = result_dfs["stable"].astype(int).values # type: ignore
|
|
896
|
+
|
|
897
|
+
# 연속 구간 구분용 그룹 id 생성
|
|
898
|
+
group_id = (stable_flag != np.roll(stable_flag, 1)).cumsum() # type: ignore
|
|
899
|
+
result_dfs["group_id"] = group_id # type: ignore
|
|
900
|
+
|
|
901
|
+
# 안정구간 중 가장 긴 구간 선택
|
|
902
|
+
stable_groups = result_dfs[result_dfs["stable"]].groupby("group_id") # type: ignore
|
|
903
|
+
|
|
904
|
+
# 각 구간의 길이 계산
|
|
905
|
+
group_sizes = stable_groups.size()
|
|
906
|
+
|
|
907
|
+
# 가장 긴 안정 구간 선택
|
|
908
|
+
best_group_id = group_sizes.idxmax()
|
|
909
|
+
|
|
910
|
+
result_dfs["recommand"] = "bad" # type: ignore
|
|
911
|
+
|
|
912
|
+
# 가장 긴 안정 구간에 해당하는 recommand 컬럼을 `best`로 변경
|
|
913
|
+
result_dfs.loc[result_dfs["group_id"] == best_group_id, "recommand"] = "best" # type: ignore
|
|
914
|
+
|
|
915
|
+
# result_dfs에서 recommand가 best에 해당하는 인덱스와 같은 위치의 추정기만 추출
|
|
916
|
+
best_indexes = list(result_dfs[result_dfs["recommand"] == "best"].index) # type: ignore
|
|
917
|
+
|
|
918
|
+
for i in range(len(estimators) - 1, -1, -1):
|
|
919
|
+
if i not in best_indexes:
|
|
920
|
+
del estimators[i]
|
|
921
|
+
del cluster_dfs[i]
|
|
922
|
+
|
|
923
|
+
pbar.update(1)
|
|
924
|
+
|
|
925
|
+
return (
|
|
926
|
+
estimators[0] if len(estimators) == 1 else estimators, # type: ignore
|
|
927
|
+
cluster_dfs[0] if len(cluster_dfs) == 1 else cluster_dfs,
|
|
928
|
+
result_dfs, # type: ignore
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
# ===================================================================
|
|
933
|
+
# 단일 계층적 군집화 모델을 적합하는 함수.
|
|
934
|
+
# ===================================================================
|
|
935
|
+
def __agg_fit(
|
|
936
|
+
data: DataFrame,
|
|
937
|
+
n_clusters: int = 3,
|
|
938
|
+
linkage: Literal["ward", "complete", "average", "single"] = "ward",
|
|
939
|
+
plot: bool = False,
|
|
940
|
+
compute_distances: bool = True,
|
|
941
|
+
**params,
|
|
942
|
+
) -> tuple[AgglomerativeClustering, DataFrame, float]:
|
|
943
|
+
"""
|
|
944
|
+
단일 계층적 군집화 모델을 적합하는 함수.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
948
|
+
n_clusters (int, optional): 군집 개수. 기본값 3.
|
|
949
|
+
linkage (str, optional): 병합 기준. 기본값 "ward".
|
|
950
|
+
compute_distances (bool, optional): 거리 행렬 계산 여부. 기본값 True.
|
|
951
|
+
plot (bool, optional): True면 결과를 시각화함. 기본값 False.
|
|
952
|
+
**params: AgglomerativeClustering에 전달할 추가 파라미터.
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
tuple: (estimator, df, score)
|
|
956
|
+
- estimator: 적합된 AgglomerativeClustering 모델.
|
|
957
|
+
- df: 클러스터 결과가 포함된 데이터 프레임.
|
|
958
|
+
- score: 실루엣 점수.
|
|
959
|
+
|
|
960
|
+
"""
|
|
961
|
+
df = data.copy()
|
|
962
|
+
estimator = AgglomerativeClustering(
|
|
963
|
+
n_clusters=n_clusters, compute_distances=compute_distances, linkage=linkage, **params
|
|
964
|
+
)
|
|
965
|
+
estimator.fit(data)
|
|
966
|
+
df["cluster"] = estimator.labels_
|
|
967
|
+
score = float(silhouette_score(X=data, labels=df["cluster"]))
|
|
968
|
+
|
|
969
|
+
if plot:
|
|
970
|
+
hs_plot.visualize_silhouette(estimator=estimator, data=data)
|
|
971
|
+
|
|
972
|
+
return estimator, df, score
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def agg_fit(
|
|
976
|
+
data: DataFrame,
|
|
977
|
+
n_clusters: int | list[int] | np.ndarray = 3,
|
|
978
|
+
linkage: Literal["ward", "complete", "average", "single"] = "ward",
|
|
979
|
+
plot: bool = False,
|
|
980
|
+
**params,
|
|
981
|
+
) -> tuple[AgglomerativeClustering | list[AgglomerativeClustering], DataFrame | list[DataFrame], DataFrame]:
|
|
982
|
+
"""
|
|
983
|
+
계층적 군집화 모델을 적합하는 함수.
|
|
984
|
+
|
|
985
|
+
Args:
|
|
986
|
+
data (DataFrame): 군집화할 데이터프레임.
|
|
987
|
+
n_clusters (int | list[int] | np.ndarray, optional): 군집 개수 또는 개수 리스트. 기본값 3.
|
|
988
|
+
linkage (str, optional): 병합 기준. 기본값 "ward".
|
|
989
|
+
plot (bool, optional): True면 결과를 시각화함. 기본값 False.
|
|
990
|
+
**params: AgglomerativeClustering에 전달할 추가 파라미터.
|
|
991
|
+
|
|
992
|
+
Returns:
|
|
993
|
+
tuple: (estimator(s), df(s), score_df)
|
|
994
|
+
- estimator(s): 적합된 AgglomerativeClustering 모델 또는 모델 리스트.
|
|
995
|
+
- df(s): 클러스터 결과가 포함된 데이터 프레임 또는 데이터 프레임 리스트.
|
|
996
|
+
- score_df: 각 군집 개수에 대한 실루엣 점수 데이터프레임.
|
|
997
|
+
|
|
998
|
+
Examples:
|
|
999
|
+
```python
|
|
1000
|
+
from hossam import *
|
|
1001
|
+
|
|
1002
|
+
data = hs_util.load_data('iris')
|
|
1003
|
+
estimators, cluster_dfs, score_df = hs_cluster.agg_fit(data.iloc[:, :-1], n_clusters=[2,3,4])
|
|
1004
|
+
```
|
|
1005
|
+
"""
|
|
1006
|
+
compute_distances = False
|
|
1007
|
+
|
|
1008
|
+
if isinstance(n_clusters, int):
|
|
1009
|
+
n_clusters = [n_clusters]
|
|
1010
|
+
compute_distances = True
|
|
1011
|
+
else:
|
|
1012
|
+
n_clusters = list(range(n_clusters[0], n_clusters[-1]))
|
|
1013
|
+
|
|
1014
|
+
estimators = []
|
|
1015
|
+
cluster_dfs = []
|
|
1016
|
+
scores = []
|
|
1017
|
+
|
|
1018
|
+
with tqdm(total=len(n_clusters)*2) as pbar:
|
|
1019
|
+
pbar.set_description(f"Agglomerative Clustering")
|
|
1020
|
+
|
|
1021
|
+
with futures.ThreadPoolExecutor() as executor:
|
|
1022
|
+
executers = []
|
|
1023
|
+
for k in n_clusters:
|
|
1024
|
+
executers.append(
|
|
1025
|
+
executor.submit(
|
|
1026
|
+
__agg_fit,
|
|
1027
|
+
data=data,
|
|
1028
|
+
n_clusters=k,
|
|
1029
|
+
linkage=linkage,
|
|
1030
|
+
plot=False,
|
|
1031
|
+
compute_distances=compute_distances,
|
|
1032
|
+
**params,
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
1035
|
+
pbar.update(1)
|
|
1036
|
+
|
|
1037
|
+
for e in executers:
|
|
1038
|
+
estimator, cluster_df, score = e.result()
|
|
1039
|
+
estimators.append(estimator)
|
|
1040
|
+
cluster_dfs.append(cluster_df)
|
|
1041
|
+
scores.append({"k": estimator.n_clusters, "silhouette_score": score})
|
|
1042
|
+
|
|
1043
|
+
if plot:
|
|
1044
|
+
hs_plot.visualize_silhouette(
|
|
1045
|
+
estimator=estimator,
|
|
1046
|
+
data=data,
|
|
1047
|
+
outline=True,
|
|
1048
|
+
title=f"Agglomerative Clustering Silhouette (k={estimator.n_clusters})",
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
pbar.update(1)
|
|
1052
|
+
|
|
1053
|
+
score_df = DataFrame(scores)
|
|
1054
|
+
score_df.sort_values(by="silhouette_score", ascending=False, inplace=True)
|
|
1055
|
+
|
|
1056
|
+
return (
|
|
1057
|
+
estimators[0] if len(estimators) == 1 else estimators, # type: ignore
|
|
1058
|
+
cluster_dfs[0] if len(cluster_dfs) == 1 else cluster_dfs,
|
|
1059
|
+
score_df, # type: ignore
|
|
1060
|
+
)
|