hossam 0.4.17__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.
hossam/hs_reg.py ADDED
@@ -0,0 +1,313 @@
1
+ from IPython.display import display
2
+
3
+ from pandas import DataFrame, merge
4
+ import seaborn as sb
5
+ import numpy as np
6
+
7
+ import statsmodels.api as sm
8
+ from statsmodels.stats.outliers_influence import variance_inflation_factor
9
+
10
+ from sklearn.base import BaseEstimator, TransformerMixin
11
+ from sklearn.model_selection import learning_curve
12
+
13
+ # 성능 평가 지표 모듈
14
+ from sklearn.metrics import (
15
+ r2_score,
16
+ mean_absolute_error,
17
+ mean_squared_error,
18
+ mean_absolute_percentage_error,
19
+ )
20
+
21
+ from .hs_plot import create_figure, finalize_plot
22
+
23
+
24
+ # --------------------------------------------------------
25
+ # VIF 기반 다중공선성 제거기
26
+ # --------------------------------------------------------
27
+ class VIFSelector(BaseEstimator, TransformerMixin):
28
+ """
29
+ VIF(Variance Inflation Factor) 기반 다중공선성 제거기
30
+
31
+ Args:
32
+ threshold (float): VIF 임계값 (기본값: 10.0
33
+ check_cols (list or None): VIF 계산에 사용할 열 목록 (기본값: None, 모든 열 사용)
34
+
35
+ Attributes:
36
+ drop_cols_ (list): 제거된 열 목록
37
+ vif_cols_ (list): VIF 계산에 사용된 열 목록
38
+
39
+ """
40
+
41
+ def __init__(self, threshold=10.0, check_cols=None):
42
+ self.threshold = threshold
43
+ self.check_cols = check_cols
44
+
45
+ def _compute_vifs(self, X: DataFrame):
46
+ exog = sm.add_constant(X, prepend=True)
47
+
48
+ vifs = {}
49
+ for i, col in enumerate(X.columns):
50
+ try:
51
+ vifs[col] = float(variance_inflation_factor(exog.values, i + 1))
52
+ except Exception:
53
+ vifs[col] = float("inf")
54
+
55
+ vdf = DataFrame(vifs.items(), columns=["Variable", "VIF"])
56
+ return vdf.sort_values("VIF", ascending=False)
57
+
58
+ def fit(self, X, y=None):
59
+ df = X.copy().dropna()
60
+
61
+ self.vif_cols_ = self.check_cols if self.check_cols else df.columns.tolist()
62
+ X_vif = df[self.vif_cols_].copy()
63
+
64
+ self.drop_cols_ = []
65
+ i = 0
66
+
67
+ while True:
68
+ if X_vif.shape[1] == 0:
69
+ break
70
+
71
+ vdf = self._compute_vifs(X_vif)
72
+ max_vif = vdf.iloc[0]["VIF"]
73
+ max_col = vdf.iloc[0]["Variable"]
74
+
75
+ if max_vif <= self.threshold:
76
+ # print(
77
+ # "모든 변수의 VIF가 임계값 이하가 되어 종료합니다. 제거된 변수 {0}개.".format(
78
+ # i
79
+ # )
80
+ # )
81
+ break
82
+
83
+ X_vif = X_vif.drop(columns=[max_col])
84
+ self.drop_cols_.append(max_col)
85
+ #print(f"제거된 변수: {max_col} (VIF={X_vif:.2f})")
86
+ i += 1
87
+
88
+ return self
89
+
90
+ def transform(self, X):
91
+ return X.drop(columns=self.drop_cols_, errors="ignore")
92
+
93
+
94
+ # --------------------------------------------------------
95
+ # 회귀 성능 평가 지표 함수
96
+ # --------------------------------------------------------
97
+ def get_scores(
98
+ estimator,
99
+ x_test: DataFrame,
100
+ y_test: DataFrame | np.ndarray
101
+ ) -> DataFrame:
102
+ """
103
+ 회귀 성능 평가 지표 함수
104
+
105
+ Args:
106
+ estimator: 학습된 사이킷런 회귀 모델
107
+ x_test: 테스트용 설명변수 데이터 (DataFrame)
108
+ y_test: 실제 목표변수 값 (DataFrame 또는 ndarray)
109
+
110
+ Returns:
111
+ DataFrame: 회귀 성능 평가 지표 (R2, MAE, MSE, RMSE, MAPE, MPE)
112
+ """
113
+ if hasattr(estimator, "named_steps"):
114
+ classname = estimator.named_steps["model"].__class__.__name__
115
+ else:
116
+ classname = estimator.__class__.__name__
117
+
118
+ y_pred = estimator.predict(x_test)
119
+
120
+ score_df = DataFrame(
121
+ {
122
+ "결정계수(R2)": r2_score(y_test, y_pred),
123
+ "평균절대오차(MAE)": mean_absolute_error(y_test, y_pred),
124
+ "평균제곱오차(MSE)": mean_squared_error(y_test, y_pred),
125
+ "평균오차(RMSE)": np.sqrt(mean_squared_error(y_test, y_pred)),
126
+ "평균 절대 백분오차 비율(MAPE)": mean_absolute_percentage_error(
127
+ y_test, y_pred
128
+ ),
129
+ "평균 비율 오차(MPE)": np.mean((y_test - y_pred) / y_test * 100),
130
+ },
131
+ index=[classname],
132
+ )
133
+
134
+ return score_df
135
+
136
+
137
+ # --------------------------------------------------------
138
+ # 학습곡선기반 과적합 판별 함수
139
+ # --------------------------------------------------------
140
+ def learning_cv(
141
+ estimator,
142
+ x,
143
+ y,
144
+ scoring="neg_root_mean_squared_error",
145
+ cv=5,
146
+ train_sizes=np.linspace(0.1, 1.0, 10),
147
+ n_jobs=-1,
148
+ ) -> DataFrame:
149
+ """학습곡선 기반 과적합 판별 함수
150
+
151
+ Args:
152
+ estimator: 사이킷런 Estimator (파이프라인 권장)
153
+ x: 설명변수 (DataFrame 또는 ndarray)
154
+ y: 목표변수 (Series 또는 ndarray)
155
+ scoring: 평가 지표 (기본값: neg_root_mean_squared_error)
156
+ cv: 교차검증 폴드 수 (기본값: 5)
157
+ train_sizes: 학습곡선 학습 데이터 비율 (기본값: np.linspace(0.1, 1.0, 10))
158
+ n_jobs: 병렬 처리 개수 (기본값: -1, 모든 CPU 사용)
159
+
160
+ Returns:
161
+ DataFrame: 과적합 판별 결과 표
162
+ """
163
+
164
+ train_sizes, train_scores, cv_scores = learning_curve( # type: ignore
165
+ estimator=estimator,
166
+ X=x,
167
+ y=y,
168
+ train_sizes=train_sizes,
169
+ cv=cv,
170
+ scoring=scoring,
171
+ n_jobs=n_jobs,
172
+ shuffle=True,
173
+ random_state=52,
174
+ )
175
+
176
+ if hasattr(estimator, "named_steps"):
177
+ classname = estimator.named_steps["model"].__class__.__name__
178
+ else:
179
+ classname = estimator.__class__.__name__
180
+
181
+ # neg RMSE → RMSE
182
+ train_rmse = -train_scores
183
+ cv_rmse = -cv_scores
184
+
185
+ # 평균 / 표준편차
186
+ train_mean = train_rmse.mean(axis=1)
187
+ cv_mean = cv_rmse.mean(axis=1)
188
+ cv_std = cv_rmse.std(axis=1)
189
+
190
+ # 마지막 지점 기준 정량 판정
191
+ final_train = train_mean[-1]
192
+ final_cv = cv_mean[-1]
193
+ final_std = cv_std[-1]
194
+ gap_ratio = final_train / final_cv
195
+ var_ratio = final_std / final_cv
196
+
197
+ # -----------------
198
+ # 과소적합 기준선 (some_threshold)
199
+ # -----------------
200
+ # 기준모형 RMSE (평균 예측)
201
+ y_mean = y.mean()
202
+ rmse_naive = np.sqrt(np.mean((y - y_mean) ** 2))
203
+
204
+ # 분산 기반
205
+ std_y = y.std()
206
+
207
+ # 최소 설명력(R²) 기반
208
+ min_r2 = 0.10
209
+ rmse_r2 = np.sqrt((1 - min_r2) * np.var(y))
210
+
211
+ # 최종 threshold (가장 관대한 기준)
212
+ # -> 원래 some_threshold는 도메인 지식 수준에서 이 모델은 최소 어느 정도의 성능은 내야 한다는 기준을 설정하는 것
213
+ some_threshold = min(rmse_naive, std_y, rmse_r2)
214
+
215
+ # -----------------
216
+ # 판정 로직
217
+ # -----------------
218
+ if gap_ratio >= 0.95 and final_cv > some_threshold:
219
+ status = "⚠️ 과소적합 (bias 큼)"
220
+ elif gap_ratio <= 0.8:
221
+ status = "⚠️ 과대적합 (variance 큼)"
222
+ elif gap_ratio <= 0.95 and var_ratio <= 0.10:
223
+ status = "✅ 일반화 양호"
224
+ elif var_ratio > 0.15:
225
+ status = "⚠️ 데이터 부족 / 분산 큼"
226
+ else:
227
+ status = "⚠️ 판단 유보"
228
+
229
+ # -----------------
230
+ # 정량 결과 표
231
+ # -----------------
232
+ result_df = DataFrame(
233
+ {
234
+ "Train RMSE": [final_train],
235
+ "CV RMSE 평균": [final_cv],
236
+ "CV RMSE 표준편차": [final_std],
237
+ "Train/CV 비율": [gap_ratio],
238
+ "CV 변동성 비율": [var_ratio],
239
+ "판정 결과": [status],
240
+ },
241
+ index=[classname],
242
+ )
243
+
244
+ # -----------------
245
+ # 학습곡선 시각화
246
+ # -----------------
247
+ fig, ax = create_figure()
248
+
249
+ sb.lineplot(
250
+ x=train_sizes,
251
+ y=train_mean,
252
+ marker="o",
253
+ markeredgecolor="#ffffff",
254
+ label="Train RMSE",
255
+ )
256
+ sb.lineplot(
257
+ x=train_sizes,
258
+ y=cv_mean,
259
+ marker="o",
260
+ markeredgecolor="#ffffff",
261
+ label="CV RMSE",
262
+ )
263
+
264
+ ax.set_xlabel("RMSE", fontsize=8, labelpad=5) # type : ignore
265
+ ax.set_ylabel("학습곡선 (Learning Curve)", fontsize=8, labelpad=5) # type : ignore
266
+ ax.grid(True, alpha=0.3) # type : ignore
267
+
268
+ finalize_plot(ax)
269
+
270
+ return result_df
271
+
272
+
273
+ def get_score_cv(
274
+ estimator,
275
+ x_test: DataFrame,
276
+ y_test: DataFrame | np.ndarray,
277
+ x_origin: DataFrame,
278
+ y_origin: DataFrame | np.ndarray,
279
+ scoring="neg_root_mean_squared_error",
280
+ cv=5,
281
+ train_sizes=np.linspace(0.1, 1.0, 10),
282
+ n_jobs=-1,
283
+ ) -> DataFrame:
284
+ """
285
+ 회귀 성능 평가 지표 함수
286
+
287
+ Args:
288
+ estimator: 학습된 사이킷런 회귀 모델
289
+ x_test: 테스트용 설명변수 데이터 (DataFrame)
290
+ y_test: 실제 목표변수 값 (DataFrame 또는 ndarray)
291
+ x_origin: 학습곡선용 전체 설명변수 데이터 (DataFrame, learning_curve=True일 때 필요)
292
+ y_origin: 학습곡선용 전체 목표변수 값 (DataFrame 또는 ndarray, learning_curve=True일 때 필요)
293
+ scoring: 학습곡선 평가 지표 (기본값: neg_root_mean_squared_error)
294
+ cv: 학습곡선 교차검증 폴드 수 (기본값: 5)
295
+ train_sizes: 학습곡선 학습 데이터 비율 (기본값: np.linspace(0.1, 1.0, 10))
296
+ n_jobs: 학습곡선 병렬 처리 개수 (기본값: -1, 모든 CPU 사용)
297
+
298
+ Returns:
299
+ DataFrame: 회귀 성능 평가 지표 + 과적합 판정 여부
300
+ """
301
+
302
+ score_df = get_scores(estimator, x_test, y_test)
303
+ cv_df = learning_cv(
304
+ estimator,
305
+ x_origin,
306
+ y_origin,
307
+ scoring=scoring,
308
+ cv=cv,
309
+ train_sizes=train_sizes,
310
+ n_jobs=n_jobs,
311
+ )
312
+
313
+ return merge(score_df, cv_df, left_index=True, right_index=True)