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/__init__.py +78 -18
- hossam/hs_classroom.py +27 -4
- hossam/hs_cluster.py +99 -21
- hossam/hs_plot.py +36 -40
- hossam/hs_reg.py +313 -0
- hossam/hs_stats.py +250 -221
- {hossam-0.4.17.dist-info → hossam-0.4.19.dist-info}/METADATA +2 -1
- hossam-0.4.19.dist-info/RECORD +18 -0
- hossam-0.4.17.dist-info/RECORD +0 -17
- {hossam-0.4.17.dist-info → hossam-0.4.19.dist-info}/WHEEL +0 -0
- {hossam-0.4.17.dist-info → hossam-0.4.19.dist-info}/licenses/LICENSE +0 -0
- {hossam-0.4.17.dist-info → hossam-0.4.19.dist-info}/top_level.txt +0 -0
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)
|