hossam 0.4.18__py3-none-any.whl → 0.4.19__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.
@@ -1,1060 +0,0 @@
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
- )