hossam 0.4.17__tar.gz → 0.4.19__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hossam
3
- Version: 0.4.17
3
+ Version: 0.4.19
4
4
  Summary: Hossam Data Helper
5
5
  Author-email: Lee Kwang-Ho <leekh4232@gmail.com>
6
6
  License-Expression: MIT
@@ -40,6 +40,7 @@ Requires-Dist: xgboost
40
40
  Requires-Dist: lightgbm
41
41
  Requires-Dist: catboost
42
42
  Requires-Dist: kneed
43
+ Requires-Dist: shap
43
44
  Dynamic: license-file
44
45
 
45
46
  ---
@@ -9,11 +9,19 @@ from . import hs_prep
9
9
  from . import hs_stats
10
10
  from . import hs_timeserise
11
11
  from . import hs_util
12
+ from . import hs_reg
12
13
  from . import hs_cluster
13
14
  from . import hs_study
14
15
  from .hs_util import load_info
15
16
  from .hs_util import _load_data_remote as load_data
16
17
  from .hs_plot import visualize_silhouette
18
+ from .hs_stats import ttest_ind as hs_ttest_ind
19
+ from .hs_stats import outlier_table as hs_outlier_table
20
+ from .hs_stats import oneway_anova as hs_oneway_anova
21
+ from .hs_reg import learning_cv as hs_learning_cv
22
+ from .hs_reg import get_scores as hs_get_scores
23
+ from .hs_reg import get_score_cv as hs_get_score_cv
24
+ from .hs_reg import VIFSelector
17
25
 
18
26
  # py-modules
19
27
  import sys
@@ -31,7 +39,29 @@ except Exception:
31
39
 
32
40
  my_dpi = hs_plot.config.dpi
33
41
 
34
- __all__ = ["my_dpi", "load_data", "load_info", "hs_classroom", "hs_gis", "hs_plot", "hs_prep", "hs_stats", "hs_timeserise", "hs_util", "hs_cluster", "hs_study", "visualize_silhouette"]
42
+ __all__ = [
43
+ "my_dpi",
44
+ "load_data",
45
+ "load_info",
46
+ "hs_classroom",
47
+ "hs_gis",
48
+ "hs_plot",
49
+ "hs_prep",
50
+ "hs_stats",
51
+ "hs_timeserise",
52
+ "hs_util",
53
+ "hs_cluster",
54
+ "hs_reg",
55
+ "hs_study",
56
+ "visualize_silhouette",
57
+ "hs_ttest_ind",
58
+ "hs_outlier_table",
59
+ "hs_oneway_anova",
60
+ "hs_learning_cv",
61
+ "hs_get_scores",
62
+ "hs_get_score_cv",
63
+ "VIFSelector",
64
+ ]
35
65
 
36
66
 
37
67
  def check_pypi_latest(package_name: str):
@@ -51,7 +81,7 @@ def check_pypi_latest(package_name: str):
51
81
  "package": package_name,
52
82
  "installed": installed,
53
83
  "latest": latest,
54
- "outdated": installed != latest
84
+ "outdated": installed != latest,
55
85
  }
56
86
 
57
87
 
@@ -67,21 +97,23 @@ def _init_korean_font():
67
97
  fprop = fm.FontProperties(fname=str(font_path))
68
98
  fname = fprop.get_name()
69
99
 
70
- plt.rcParams.update({
71
- "font.family": fname,
72
- "font.size": hs_plot.config.font_size,
73
- "font.weight": hs_plot.config.font_weight,
74
- "axes.unicode_minus": False,
75
- "text.antialiased": True,
76
- "lines.antialiased": True,
77
- "patch.antialiased": True,
78
- "figure.dpi": hs_plot.config.dpi,
79
- "savefig.dpi": hs_plot.config.dpi * 2,
80
- "text.hinting": "auto",
81
- "text.hinting_factor": 8,
82
- "pdf.fonttype": 42,
83
- "ps.fonttype": 42,
84
- })
100
+ plt.rcParams.update(
101
+ {
102
+ "font.family": fname,
103
+ "font.size": hs_plot.config.font_size,
104
+ "font.weight": hs_plot.config.font_weight,
105
+ "axes.unicode_minus": False,
106
+ "text.antialiased": True,
107
+ "lines.antialiased": True,
108
+ "patch.antialiased": True,
109
+ "figure.dpi": hs_plot.config.dpi,
110
+ "savefig.dpi": hs_plot.config.dpi * 2,
111
+ "text.hinting": "auto",
112
+ "text.hinting_factor": 8,
113
+ "pdf.fonttype": 42,
114
+ "ps.fonttype": 42,
115
+ }
116
+ )
85
117
 
86
118
  print(
87
119
  "\n✅ 시각화를 위한 한글 글꼴(NotoSansKR-Regular)이 자동 적용되었습니다."
@@ -103,6 +135,8 @@ def _init():
103
135
  f"🔖 Version: {__version__}",
104
136
  ]
105
137
 
138
+
139
+
106
140
  for msg in messages:
107
141
  print(f"{msg}")
108
142
 
@@ -119,10 +153,36 @@ def _init():
119
153
 
120
154
  _init_korean_font()
121
155
 
156
+ # 각 열의 넓이 제한 없음
157
+ pd.set_option("display.max_colwidth", None)
158
+ # 출력 너비 제한 없음 (가로 스크롤될 수 있음)
159
+ pd.set_option("display.width", None)
122
160
  # 컬럼 생략 금지
123
161
  pd.set_option("display.max_columns", None)
124
162
  # 행 최대 출력 수 100개로 수정
125
163
  pd.set_option("display.max_rows", 100)
164
+ # 소수점 자리수 3자리로 설정
165
+ pd.options.display.float_format = "{:.3f}".format
166
+
167
+ from IPython.display import display, HTML
168
+
169
+ display(
170
+ HTML(
171
+ """
172
+ <style>
173
+ .dataframe tr:hover {
174
+ background-color: #ffff99 !important;
175
+ border: 1px solid #ffcc00;
176
+ }
177
+ </style>
178
+ """
179
+ )
180
+ )
181
+
182
+ import multiprocessing as mp
126
183
 
184
+ def is_parallel_worker():
185
+ return mp.current_process().name != "MainProcess"
127
186
 
128
- _init()
187
+ if not is_parallel_worker():
188
+ _init()
@@ -6,6 +6,7 @@ import math
6
6
  from pandas import DataFrame, qcut, concat, to_numeric
7
7
  from kmodes.kmodes import KModes
8
8
  from matplotlib import pyplot as plt
9
+ from prompt_toolkit.formatted_text.ansi import i
9
10
  import seaborn as sns
10
11
  from .hs_util import load_data, pretty_table
11
12
  from .hs_plot import config
@@ -19,6 +20,7 @@ def cluster_students(
19
20
  n_groups: int,
20
21
  score_cols: list | None = None,
21
22
  interest_col: str | None = None,
23
+ interest_ignore: str | None = None,
22
24
  max_iter: int = 200,
23
25
  score_metric: str = 'total'
24
26
  ) -> DataFrame:
@@ -39,6 +41,8 @@ def cluster_students(
39
41
  None일 경우 점수 기반 균형 조정을 하지 않습니다. 기본값: None
40
42
  interest_col: 관심사 정보가 있는 컬럼명.
41
43
  None일 경우 관심사 기반 군집화를 하지 않습니다. 기본값: None
44
+ interest_ignore: 관심사 군집화에서 제외할 값.
45
+ 지정된 값은 별도 군집에서 제외됩니다. 기본값: None
42
46
  max_iter: 균형 조정 최대 반복 횟수. 기본값: 200
43
47
  score_metric: 점수 기준 선택 ('total' 또는 'average').
44
48
  'total'이면 총점, 'average'이면 평균점수 기준. 기본값: 'total'
@@ -151,8 +155,18 @@ def cluster_students(
151
155
  if actual_n_groups < 2:
152
156
  actual_n_groups = 2
153
157
 
158
+ df_ignore = None
159
+
154
160
  # ===== 3단계: 관심사 기반 1차 군집 =====
155
161
  if interest_col is not None:
162
+ df_main[interest_col] = df_main[interest_col].fillna('미정')
163
+
164
+ if interest_ignore is not None:
165
+ df_ignore = df_main[df_main[interest_col] == interest_ignore].copy()
166
+ df_main = df_main[df_main[interest_col] != interest_ignore].copy()
167
+
168
+ print(df_ignore)
169
+
156
170
  X_interest = df_main[[interest_col]].to_numpy()
157
171
 
158
172
  kmodes_interest = KModes(
@@ -184,12 +198,18 @@ def cluster_students(
184
198
  df_main = _balance_group_sizes_only(df_main, actual_n_groups, min_size, max_size)
185
199
 
186
200
  # ===== 5단계: 극단값 포함 병합 =====
187
- if df_outlier is not None and len(df_outlier) > 0:
201
+ result = df_main
202
+
203
+ if (df_outlier is not None and len(df_outlier) > 0):
188
204
  # '조'는 숫자형 유지: 극단값은 0으로 표시
189
205
  df_outlier['조'] = 0
190
- result = concat([df_main, df_outlier], ignore_index=True)
191
- else:
192
- result = df_main
206
+ result = concat([result, df_outlier], ignore_index=True)
207
+
208
+ if (df_ignore is not None and len(df_ignore) > 0):
209
+ # '조'는 숫자형 유지: 제외된 학생은 -1로 표시
210
+ df_ignore['조'] = -1
211
+ result = concat([result, df_ignore], ignore_index=True)
212
+
193
213
 
194
214
  # 평균점수는 이미 계산됨 (score_cols 있을 때)
195
215
 
@@ -694,6 +714,7 @@ def analyze_classroom(
694
714
  n_groups: int,
695
715
  score_cols: list | None = None,
696
716
  interest_col: str | None = None,
717
+ interest_ignore: str | None = None,
697
718
  max_iter: int = 200,
698
719
  score_metric: str = 'average',
699
720
  name_col: str = '학생이름',
@@ -713,6 +734,7 @@ def analyze_classroom(
713
734
  n_groups: 목표 조의 개수.
714
735
  score_cols: 성적 계산에 사용할 점수 컬럼명 리스트. 기본값: None
715
736
  interest_col: 관심사 정보가 있는 컬럼명. 기본값: None
737
+ interest_ignore: 관심사 군집화에서 제외할 값. 기본값: None
716
738
  max_iter: 균형 조정 최대 반복 횟수. 기본값: 200
717
739
  score_metric: 점수 기준 선택 ('total' 또는 'average'). 기본값: 'average'
718
740
  name_col: 학생 이름 컬럼명. 기본값: '학생이름'
@@ -740,6 +762,7 @@ def analyze_classroom(
740
762
  n_groups=n_groups,
741
763
  score_cols=score_cols,
742
764
  interest_col=interest_col,
765
+ interest_ignore=interest_ignore,
743
766
  max_iter=max_iter,
744
767
  score_metric=score_metric
745
768
  )
@@ -23,6 +23,7 @@ from sklearn.metrics import silhouette_score, adjusted_rand_score
23
23
  # hossam 패키지 참조
24
24
  # ===================================================================
25
25
  from . import hs_plot
26
+ from .hs_util import is_2d
26
27
 
27
28
  RANDOM_STATE = 52
28
29
 
@@ -32,10 +33,11 @@ RANDOM_STATE = 52
32
33
  # ===================================================================
33
34
  def kmeans_fit(
34
35
  data: DataFrame,
35
- n_clusters: int,
36
+ n_clusters: int | None = None,
37
+ k_range: list | tuple = [2, 11],
36
38
  random_state: int = RANDOM_STATE,
37
39
  plot: bool = False,
38
- fields: list[list[str]] | None = None,
40
+ fields: list[str] | tuple[str] | tuple[tuple[str]] | list[list[str]] | None = None,
39
41
  **params,
40
42
  ) -> tuple[KMeans, DataFrame, float]:
41
43
  """
@@ -43,7 +45,7 @@ def kmeans_fit(
43
45
 
44
46
  Args:
45
47
  data (DataFrame): 군집화할 데이터프레임.
46
- n_clusters (int): 군집 개수.
48
+ n_clusters (int | None): 군집 개수.
47
49
  random_state (int, optional): 랜덤 시드. 기본값은 RANDOM_STATE.
48
50
  plot (bool, optional): True면 결과를 시각화함. 기본값 False.
49
51
  fields (list[list[str]] | None, optional): 시각화할 필드 쌍 리스트. 기본값 None이면 수치형 컬럼의 모든 조합 사용.
@@ -55,18 +57,36 @@ def kmeans_fit(
55
57
  float: 실루엣 점수
56
58
  """
57
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
+
58
65
  kmeans = KMeans(n_clusters=n_clusters, random_state=random_state, **params)
59
66
  kmeans.fit(data)
60
67
  df["cluster"] = kmeans.predict(df)
61
68
  score = float(silhouette_score(X=data, labels=df["cluster"]))
62
69
 
63
70
  if plot:
64
- cluster_plot(
65
- estimator=kmeans,
66
- data=data,
67
- fields=fields,
68
- title=f"K-Means Clustering (k={n_clusters})",
69
- )
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
+ )
70
90
 
71
91
  return kmeans, df, score
72
92
 
@@ -544,6 +564,9 @@ def persona(
544
564
  persona_dict[("", f"count")] = len(group)
545
565
 
546
566
  for field in fields:
567
+ if field == cluster:
568
+ continue
569
+
547
570
  # 명목형일 경우 최빈값 사용
548
571
  if df[field].dtype == "object" or df[field].dtype.name == "category":
549
572
  persona_dict[(field, "mode")] = group[field].mode()[0]
@@ -779,7 +802,9 @@ def dbscan_eps(
779
802
 
780
803
  return best_eps, eps_grid
781
804
 
782
-
805
+ # ===================================================================
806
+ # DBSCAN 군집화 모델을 적합하고 최적의 eps 값을 탐지하는 함수.
807
+ # ===================================================================
783
808
  def dbscan_fit(
784
809
  data: DataFrame,
785
810
  eps: float | list | np.ndarray | None = None,
@@ -789,6 +814,25 @@ def dbscan_fit(
789
814
  plot: bool = True,
790
815
  **params,
791
816
  ) -> tuple[DBSCAN, DataFrame, DataFrame]:
817
+ """
818
+ DBSCAN 군집화 모델을 적합하고 최적의 eps 값을 탐지하는 함수.
819
+
820
+ Args:
821
+ data (DataFrame): 군집화할 데이터프레임.
822
+ eps (float | list | np.ndarray | None, optional): eps 값 또는 리스트.
823
+ None이면 최적의 eps 값을 탐지함. 기본값 None.
824
+ min_samples (int, optional): 핵심점이 되기 위한 최소 샘플수. 기본값 5.
825
+ ari_threshold (float, optional): 안정 구간 탐지를 위한 ARI 임계값. 기본값 0.9.
826
+ noise_diff_threshold (float, optional): 안정 구간 탐지를 위한 노이즈 비율 변화 임계값. 기본값 0.05.
827
+ plot (bool, optional): True면 결과를 시각화함. 기본값 True.
828
+ **params: DBSCAN에 전달할 추가 파라미터.
829
+
830
+ Returns:
831
+ tuple: (estimator, cluster_df, result_df)
832
+ - estimator: 적합된 DBSCAN 모델 또는 모델 리스트(최적 eps가 여러 개인 경우).
833
+ - cluster_df: 클러스터 및 벡터 유형이 포함된 데이터 프레임 또는 데이터 프레임 리스트(최적 eps가 여러 개인 경우).
834
+ - result_df: eps 값에 따른 군집화 요약 통계 데이터 프레임.
835
+ """
792
836
 
793
837
  # eps 값이 지정되지 않은 경우 최적의 eps 탐지
794
838
  if eps is None:
@@ -874,18 +918,52 @@ def dbscan_fit(
874
918
  # result_dfs에서 recommand가 best에 해당하는 인덱스와 같은 위치의 추정기만 추출
875
919
  best_indexes = list(result_dfs[result_dfs["recommand"] == "best"].index) # type: ignore
876
920
 
877
- for i in range(len(estimators) - 1, -1, -1):
878
- if i not in best_indexes:
879
- del estimators[i]
880
- del cluster_dfs[i]
921
+ # for i in range(len(estimators) - 1, -1, -1):
922
+ # if i not in best_indexes:
923
+ # del estimators[i]
924
+ # del cluster_dfs[i]
881
925
 
882
926
  pbar.update(1)
883
927
 
884
- return (
885
- estimators[0] if len(estimators) == 1 else estimators, # type: ignore
886
- cluster_dfs[0] if len(cluster_dfs) == 1 else cluster_dfs,
887
- result_dfs, # type: ignore
888
- )
928
+ # best 모델 선정: recommand=='best'인 인덱스의 estimator/cluster_df만 반환
929
+ if len(estimators) == 1:
930
+
931
+ if plot:
932
+ hs_plot.scatterplot(
933
+ df=cluster_dfs[0],
934
+ xname=cluster_dfs[0].columns[0],
935
+ yname=cluster_dfs[0].columns[1],
936
+ hue="cluster",
937
+ vector="vector",
938
+ title=f"DBSCAN Clustering (eps={estimators[0].eps}, min_samples={estimators[0].min_samples})",
939
+ outline=True
940
+ )
941
+
942
+ return estimators[0], cluster_dfs[0], result_dfs # type: ignore
943
+
944
+ # recommand=='best'인 인덱스 추출 (여러 개면 첫 번째)
945
+ best_indexes = list(result_dfs[result_dfs["recommand"] == "best"].index) # type: ignore
946
+ if not best_indexes:
947
+ # fallback: 첫 번째
948
+ best_index = 0
949
+ else:
950
+ best_index = best_indexes[0]
951
+
952
+ best_estimator = estimators[best_index]
953
+ best_cluster_df = cluster_dfs[best_index]
954
+
955
+ if plot:
956
+ hs_plot.scatterplot(
957
+ df=best_cluster_df,
958
+ xname=best_cluster_df.columns[0],
959
+ yname=best_cluster_df.columns[1],
960
+ hue="cluster",
961
+ vector="vector",
962
+ title=f"DBSCAN Clustering (eps={best_estimator.eps}, min_samples={best_estimator.min_samples})",
963
+ outline=True
964
+ )
965
+
966
+ return best_estimator, best_cluster_df, result_dfs # type: ignore
889
967
 
890
968
 
891
969
  # ===================================================================
@@ -950,8 +1028,8 @@ def agg_fit(
950
1028
 
951
1029
  Returns:
952
1030
  tuple: (estimator(s), df(s), score_df)
953
- - estimator(s): 적합된 AgglomerativeClustering 모델 또는 모델 리스트.
954
- - df(s): 클러스터 결과가 포함된 데이터 프레임 또는 데이터 프레임 리스트.
1031
+ - estimator(s): 적합된 AgglomerativeClustering 모델 또는 모델 리스트 (n_clusters가 리스트일 때 리턴도 리스트로 처리됨).
1032
+ - df(s): 클러스터 결과가 포함된 데이터 프레임 또는 데이터 프레임 리스트(n_cluseters가 리스트일 때 리턴되 리스트로 처리됨).
955
1033
  - score_df: 각 군집 개수에 대한 실루엣 점수 데이터프레임.
956
1034
 
957
1035
  Examples:
@@ -8,6 +8,7 @@ from itertools import combinations
8
8
  import numpy as np
9
9
  import seaborn as sb
10
10
  import matplotlib.pyplot as plt
11
+ from matplotlib.figure import Figure # type: ignore
11
12
  from matplotlib.pyplot import Axes # type: ignore
12
13
  from pandas import Series, DataFrame
13
14
  from math import sqrt
@@ -132,7 +133,7 @@ def create_figure(
132
133
  ws: int | None = None,
133
134
  hs: int | None = None,
134
135
  title: str | None = None,
135
- ):
136
+ ) -> tuple[Figure, Axes]:
136
137
  """기본 크기의 Figure와 Axes를 생성한다. get_default_ax의 래퍼 함수.
137
138
 
138
139
  Args:
@@ -309,7 +310,7 @@ def lineplot(
309
310
  # 상자그림(boxplot)을 그린다
310
311
  # ===================================================================
311
312
  def boxplot(
312
- df: DataFrame,
313
+ df: DataFrame | None = None,
313
314
  xname: str | None = None,
314
315
  yname: str | None = None,
315
316
  title: str | None = None,
@@ -331,7 +332,7 @@ def boxplot(
331
332
  """상자그림(boxplot)을 그린다.
332
333
 
333
334
  Args:
334
- df (DataFrame): 시각화할 데이터.
335
+ df (DataFrame|None): 시각화할 데이터.
335
336
  xname (str|None): x축 범주 컬럼명.
336
337
  yname (str|None): y축 값 컬럼명.
337
338
  title (str|None): 그래프 제목.
@@ -359,13 +360,20 @@ def boxplot(
359
360
  fig, ax = get_default_ax(width, height, 1, 1, dpi) # type: ignore
360
361
  outparams = True
361
362
 
362
- if xname is not None and yname is not None:
363
+ if xname is not None or yname is not None:
364
+ if xname is not None and yname is None:
365
+ orient = "h"
366
+ elif xname is None and yname is not None:
367
+ orient = "v"
368
+
369
+
363
370
  boxplot_kwargs = {
364
371
  "data": df,
365
372
  "x": xname,
366
373
  "y": yname,
367
374
  "orient": orient,
368
375
  "ax": ax,
376
+ "linewidth": linewidth,
369
377
  }
370
378
 
371
379
  # hue 파라미터 확인 (params에 있을 수 있음)
@@ -377,12 +385,12 @@ def boxplot(
377
385
  boxplot_kwargs["color"] = sb.color_palette(palette)[0]
378
386
 
379
387
  boxplot_kwargs.update(params)
380
- sb.boxplot(**boxplot_kwargs, linewidth=linewidth)
388
+ sb.boxplot(**boxplot_kwargs)
381
389
 
382
390
  # 통계 검정 추가
383
391
  if stat_test is not None:
384
392
  if stat_pairs is None:
385
- stat_pairs = [df[xname].dropna().unique().tolist()]
393
+ stat_pairs = [df[xname].dropna().unique().tolist()] # type: ignore
386
394
 
387
395
  annotator = Annotator(
388
396
  ax, data=df, x=xname, y=yname, pairs=stat_pairs, orient=orient
@@ -847,15 +855,15 @@ def scatterplot(
847
855
  else:
848
856
  # 핵심벡터
849
857
  scatterplot_kwargs["edgecolor"] = "#ffffff"
850
- sb.scatterplot(data=df[df[vector] == "core"], **scatterplot_kwargs)
858
+ sb.scatterplot(data=df[df[vector] == "core"], **scatterplot_kwargs) # type: ignore
851
859
 
852
860
  # 외곽백터
853
861
  scatterplot_kwargs["edgecolor"] = "#000000"
854
862
  scatterplot_kwargs["s"] = 25
855
863
  scatterplot_kwargs["marker"] = "^"
856
864
  scatterplot_kwargs["linewidth"] = 0.8
857
- sb.scatterplot(data=df[df[vector] == "border"], **scatterplot_kwargs)
858
-
865
+ sb.scatterplot(data=df[df[vector] == "border"], **scatterplot_kwargs) # type: ignore
866
+
859
867
  # 노이즈벡터
860
868
  scatterplot_kwargs["edgecolor"] = None
861
869
  scatterplot_kwargs["s"] = 25
@@ -1096,14 +1104,9 @@ def pairplot(
1096
1104
  g.fig.suptitle(title, fontsize=config.font_size * 1.5, fontweight="bold")
1097
1105
 
1098
1106
  g.map_lower(
1099
- func=sb.kdeplot, fill=True, alpha=config.fill_alpha, linewidth=linewidth
1107
+ func=sb.kdeplot, fill=True, alpha=config.fill_alpha
1100
1108
  )
1101
- g.map_upper(func=sb.scatterplot, linewidth=linewidth)
1102
-
1103
- # KDE 대각선에도 linewidth 적용
1104
- for ax in g.axes.diag: # type: ignore
1105
- for line in ax.get_lines():
1106
- line.set_linewidth(linewidth)
1109
+ g.map_upper(func=sb.scatterplot)
1107
1110
 
1108
1111
  plt.tight_layout()
1109
1112
 
@@ -1761,25 +1764,14 @@ def ols_residplot(
1761
1764
  fig, ax = get_default_ax(width + 150 if mse else width, height, 1, 1, dpi) # type: ignore
1762
1765
  outparams = True
1763
1766
 
1764
- # 산점도 seaborn으로 그리기
1765
- sb.scatterplot(x=y_pred, y=resid, ax=ax, s=20, edgecolor="white", **params)
1766
-
1767
- # 기준선 (잔차 = 0)
1768
- ax.axhline(0, color="gray", linestyle="--", linewidth=linewidth * 0.7) # type: ignore
1769
-
1770
- # LOWESS 스무딩 (선택적)
1771
- if lowess:
1772
- lowess_result = sm_lowess(resid, y_pred, frac=0.6667)
1773
- ax.plot( # type: ignore
1774
- lowess_result[:, 0],
1775
- lowess_result[:, 1], # type: ignore
1776
- color="red",
1777
- linewidth=linewidth,
1778
- label="LOWESS",
1779
- ) # type: ignore
1780
-
1781
- ax.set_xlabel("Fitted values") # type: ignore
1782
- ax.set_ylabel("Residuals") # type: ignore
1767
+ sb.residplot(
1768
+ x=y_pred,
1769
+ y=resid,
1770
+ lowess=True, # 잔차의 추세선 표시
1771
+ line_kws={"color": "red", "linewidth": linewidth * 0.7}, # 추세선 스타일
1772
+ scatter_kws={"edgecolor": "white", "alpha": config.alpha},
1773
+ **params
1774
+ )
1783
1775
 
1784
1776
  if mse:
1785
1777
  mse_val = mean_squared_error(y, y_pred)
@@ -1909,8 +1901,7 @@ def ols_qqplot(
1909
1901
 
1910
1902
  # 선 굵기 조정
1911
1903
  for line in ax.get_lines(): # type: ignore
1912
- if line.get_linestyle() == "--" or line.get_color() == "r": # type: ignore
1913
- line.set_linewidth(linewidth) # type: ignore
1904
+ line.set_linewidth(linewidth) # type: ignore
1914
1905
 
1915
1906
  finalize_plot(ax, callback, outparams, save_path, True, title) # type: ignore
1916
1907
 
@@ -3022,10 +3013,15 @@ def pca_plot(
3022
3013
  if field_group is not None:
3023
3014
  title += " - " + ", ".join(field_group)
3024
3015
 
3016
+ tdf = DataFrame({
3017
+ field_group[0]: xs * scalex,
3018
+ field_group[1]: ys * scaley,
3019
+ })
3020
+
3025
3021
  scatterplot(
3026
- df=None,
3027
- xname=xs * scalex,
3028
- yname=ys * scaley,
3022
+ df=tdf,
3023
+ xname=field_group[0],
3024
+ yname=field_group[1],
3029
3025
  hue=pca_df[hue] if hue is not None else None,
3030
3026
  outline=False,
3031
3027
  palette=palette,