hossam 0.4.5__py3-none-any.whl → 0.4.6__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 +2 -1
- hossam/hs_classroom.py +30 -30
- hossam/hs_plot.py +137 -147
- hossam/hs_prep.py +7 -1
- hossam/hs_stats.py +1570 -1459
- hossam/hs_timeserise.py +38 -39
- hossam/hs_util.py +198 -1
- {hossam-0.4.5.dist-info → hossam-0.4.6.dist-info}/METADATA +1 -1
- hossam-0.4.6.dist-info/RECORD +15 -0
- hossam/data_loader.py +0 -203
- hossam-0.4.5.dist-info/RECORD +0 -16
- {hossam-0.4.5.dist-info → hossam-0.4.6.dist-info}/WHEEL +0 -0
- {hossam-0.4.5.dist-info → hossam-0.4.6.dist-info}/licenses/LICENSE +0 -0
- {hossam-0.4.5.dist-info → hossam-0.4.6.dist-info}/top_level.txt +0 -0
hossam/hs_stats.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import overload, Tuple, Literal, Union, Any
|
|
4
|
+
from typing import overload, Tuple, Literal, LiteralString, Union, Any
|
|
5
5
|
|
|
6
6
|
# -------------------------------------------------------------
|
|
7
7
|
import numpy as np
|
|
@@ -44,7 +44,8 @@ from statsmodels.discrete.discrete_model import BinaryResults
|
|
|
44
44
|
|
|
45
45
|
from pingouin import anova, pairwise_tukey, welch_anova, pairwise_gameshowell
|
|
46
46
|
|
|
47
|
-
from .hs_plot import ols_residplot, ols_qqplot
|
|
47
|
+
from .hs_plot import ols_residplot, ols_qqplot, get_default_ax, finalize_plot
|
|
48
|
+
from .hs_prep import unmelt
|
|
48
49
|
|
|
49
50
|
# ===================================================================
|
|
50
51
|
# MCAR(결측치 무작위성) 검정
|
|
@@ -867,15 +868,24 @@ def ttest_1samp(data, mean_value: float = 0.0) -> DataFrame:
|
|
|
867
868
|
# ===================================================================
|
|
868
869
|
# 독립표본 t-검정 또는 Welch's t-test
|
|
869
870
|
# ===================================================================
|
|
870
|
-
def ttest_ind(
|
|
871
|
+
def ttest_ind(
|
|
872
|
+
data: DataFrame | None = None,
|
|
873
|
+
x : Series | list | np.ndarray | str | None = None,
|
|
874
|
+
y : Series | list | np.ndarray | str | None = None,
|
|
875
|
+
equal_var: bool | None = None
|
|
876
|
+
) -> DataFrame:
|
|
871
877
|
"""두 독립 집단의 평균 차이를 검정한다 (독립표본 t-검정 또는 Welch's t-test).
|
|
872
878
|
|
|
873
879
|
독립표본 t-검정은 두 독립된 집단의 평균이 같은지를 검정한다.
|
|
874
880
|
귀무가설(H0): μ1 = μ2 (두 집단의 평균이 같다)
|
|
875
881
|
|
|
876
882
|
Args:
|
|
877
|
-
|
|
878
|
-
|
|
883
|
+
data (DataFrame | None, optional): x와 y가 컬럼명인 경우 사용할 데이터프레임.
|
|
884
|
+
기본값은 None.
|
|
885
|
+
x (Series | list | np.ndarray | str | None, optional): 첫 번째 집단의 데이터 또는
|
|
886
|
+
data가 주어진 경우 연속형 변수의 컬럼명. 기본값은 None.
|
|
887
|
+
y (Series | list | np.ndarray | str | None, optional): 두 번째 집단의 데이터 또는
|
|
888
|
+
data가 주어진 경우 명목형 변수의 컬럼명. 기본값은 None.
|
|
879
889
|
equal_var (bool | None, optional): 등분산성 가정 여부.
|
|
880
890
|
- True: 독립표본 t-검정 (등분산 가정)
|
|
881
891
|
- False: Welch's t-test (등분산 가정하지 않음, 더 강건함)
|
|
@@ -909,6 +919,12 @@ def ttest_ind(x, y, equal_var: bool | None = None) -> DataFrame:
|
|
|
909
919
|
result = hs_stats.ttest_ind(s1, s2, equal_var=False)
|
|
910
920
|
```
|
|
911
921
|
"""
|
|
922
|
+
# data가 주어지고 x, y가 컬럼명인 경우 데이터 추출
|
|
923
|
+
if data is not None and isinstance(x, str) and isinstance(y, str):
|
|
924
|
+
df = unmelt(data=data, value_vars=x, id_vars=y)
|
|
925
|
+
x = df[df.columns[0]]
|
|
926
|
+
y = df[df.columns[1]]
|
|
927
|
+
|
|
912
928
|
# 데이터를 Series로 변환
|
|
913
929
|
if isinstance(x, Series):
|
|
914
930
|
x_data = x.dropna()
|
|
@@ -955,12 +971,12 @@ def ttest_ind(x, y, equal_var: bool | None = None) -> DataFrame:
|
|
|
955
971
|
result.append({
|
|
956
972
|
"test": n,
|
|
957
973
|
"alternative": a,
|
|
974
|
+
"interpretation": itp,
|
|
975
|
+
"equal_var_checked": var_checked,
|
|
958
976
|
"statistic": round(s, 3), # type: ignore
|
|
959
977
|
"p-value": round(p, 4), # type: ignore
|
|
960
978
|
"H0": p > 0.05, # type: ignore
|
|
961
979
|
"H1": p <= 0.05, # type: ignore
|
|
962
|
-
"interpretation": itp,
|
|
963
|
-
"equal_var_checked": var_checked
|
|
964
980
|
})
|
|
965
981
|
except Exception as e:
|
|
966
982
|
result.append({
|
|
@@ -1112,1652 +1128,1865 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
|
|
|
1112
1128
|
return rdf
|
|
1113
1129
|
|
|
1114
1130
|
|
|
1131
|
+
|
|
1132
|
+
|
|
1115
1133
|
# ===================================================================
|
|
1116
|
-
#
|
|
1134
|
+
# 일원 분산분석 (One-way ANOVA)
|
|
1117
1135
|
# ===================================================================
|
|
1118
|
-
def
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1136
|
+
def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) -> tuple[DataFrame, str, DataFrame | None, str]:
|
|
1137
|
+
"""일원분산분석(One-way ANOVA)을 일괄 처리한다.
|
|
1138
|
+
|
|
1139
|
+
정규성 및 등분산성 검정을 자동으로 수행한 후,
|
|
1140
|
+
그 결과에 따라 적절한 ANOVA 방식을 선택하여 분산분석을 수행한다.
|
|
1141
|
+
ANOVA 결과가 유의하면 자동으로 사후검정을 실시한다.
|
|
1142
|
+
|
|
1143
|
+
분석 흐름:
|
|
1144
|
+
1. 정규성 검정 (각 그룹별로 normaltest 수행)
|
|
1145
|
+
2. 등분산성 검정 (정규성 만족 시 Bartlett, 불만족 시 Levene)
|
|
1146
|
+
3. ANOVA 수행 (등분산 만족 시 parametric ANOVA, 불만족 시 Welch's ANOVA)
|
|
1147
|
+
4. ANOVA p-value ≤ alpha 일 때 사후검정 (등분산 만족 시 Tukey HSD, 불만족 시 Games-Howell)
|
|
1126
1148
|
|
|
1127
1149
|
Args:
|
|
1128
|
-
data (DataFrame):
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
verbose (bool, optional): True일 경우 각 단계의 VIF를 출력한다. Defaults to False.
|
|
1150
|
+
data (DataFrame): 분석 대상 데이터프레임. 종속변수와 그룹 변수를 포함해야 함.
|
|
1151
|
+
dv (str): 종속변수(Dependent Variable) 컬럼명.
|
|
1152
|
+
between (str): 그룹 구분 변수 컬럼명.
|
|
1153
|
+
alpha (float, optional): 유의수준. 기본값 0.05.
|
|
1133
1154
|
|
|
1134
1155
|
Returns:
|
|
1135
|
-
|
|
1156
|
+
tuple:
|
|
1157
|
+
- anova_df (DataFrame): ANOVA 또는 Welch 결과 테이블(Source, ddof1, ddof2, F, p-unc, np2 등 포함).
|
|
1158
|
+
- anova_report (str): 정규성/등분산 여부와 F, p값, 효과크기를 요약한 보고 문장.
|
|
1159
|
+
- posthoc_df (DataFrame|None): 사후검정 결과(Tukey HSD 또는 Games-Howell). ANOVA가 유의할 때만 생성.
|
|
1160
|
+
- posthoc_report (str): 사후검정 유무와 유의한 쌍 정보를 요약한 보고 문장.
|
|
1136
1161
|
|
|
1137
1162
|
Examples:
|
|
1138
1163
|
```python
|
|
1139
|
-
# 기본 사용 예
|
|
1140
1164
|
from hossam import *
|
|
1141
|
-
|
|
1142
|
-
```
|
|
1143
|
-
"""
|
|
1144
|
-
|
|
1145
|
-
df = data.copy()
|
|
1165
|
+
from pandas import DataFrame
|
|
1146
1166
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
df = df.drop(columns=[yname])
|
|
1167
|
+
df = DataFrame({
|
|
1168
|
+
'score': [5.1, 4.9, 5.3, 5.0, 4.8, 5.5, 5.2, 5.7, 5.3, 5.1],
|
|
1169
|
+
'group': ['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B']
|
|
1170
|
+
})
|
|
1152
1171
|
|
|
1153
|
-
|
|
1154
|
-
ignore = ignore or []
|
|
1155
|
-
ignore_cols_present = [c for c in ignore if c in df.columns]
|
|
1172
|
+
anova_df, anova_report, posthoc_df, posthoc_report = hs_stats.oneway_anova(df, dv='score', between='group')
|
|
1156
1173
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1174
|
+
# 사후검정결과는 ANOVA가 유의할 때만 생성됨
|
|
1175
|
+
if posthoc_df is not None:
|
|
1176
|
+
print(posthoc_report)
|
|
1177
|
+
print(posthoc_df.head())
|
|
1178
|
+
```
|
|
1160
1179
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1180
|
+
Raises:
|
|
1181
|
+
ValueError: dv 또는 between 컬럼이 데이터프레임에 없을 경우.
|
|
1182
|
+
"""
|
|
1183
|
+
# 컬럼 유효성 검사
|
|
1184
|
+
if dv not in data.columns:
|
|
1185
|
+
raise ValueError(f"'{dv}' 컬럼이 데이터프레임에 없습니다.")
|
|
1186
|
+
if between not in data.columns:
|
|
1187
|
+
raise ValueError(f"'{between}' 컬럼이 데이터프레임에 없습니다.")
|
|
1165
1188
|
|
|
1166
|
-
|
|
1167
|
-
if X.shape[1] == 0:
|
|
1168
|
-
result = data.copy()
|
|
1169
|
-
return result
|
|
1189
|
+
df_filtered = data[[dv, between]].dropna()
|
|
1170
1190
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
return {col: np.nan for col in X_.columns}
|
|
1177
|
-
if X_clean.shape[1] == 1:
|
|
1178
|
-
# 단일 예측변수의 경우 다른 설명변수가 없으므로 VIF는 1로 간주
|
|
1179
|
-
return {col: 1.0 for col in X_clean.columns}
|
|
1180
|
-
exog = sm.add_constant(X_clean, prepend=True)
|
|
1181
|
-
vifs = {}
|
|
1182
|
-
for i, col in enumerate(X_clean.columns, start=0):
|
|
1183
|
-
# exog의 첫 열은 상수항이므로 변수 인덱스는 +1
|
|
1184
|
-
try:
|
|
1185
|
-
vifs[col] = float(variance_inflation_factor(exog.values, i + 1))# type: ignore
|
|
1186
|
-
except Exception:
|
|
1187
|
-
# 계산 실패 시 무한대로 처리하여 우선 제거 대상으로
|
|
1188
|
-
vifs[col] = float("inf")
|
|
1189
|
-
return vifs
|
|
1191
|
+
# ============================================
|
|
1192
|
+
# 1. 정규성 검정 (각 그룹별로 수행)
|
|
1193
|
+
# ============================================
|
|
1194
|
+
group_names = sorted(df_filtered[between].unique())
|
|
1195
|
+
normality_satisfied = True
|
|
1190
1196
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
if
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
# 모든 변수가 임계값 이하이면 종료
|
|
1199
|
-
max_key = max(vifs, key=lambda k: (vifs[k] if not np.isnan(vifs[k]) else -np.inf))
|
|
1200
|
-
max_vif = vifs[max_key]
|
|
1201
|
-
if np.isnan(max_vif) or max_vif <= threshold:
|
|
1202
|
-
break
|
|
1203
|
-
# 가장 큰 VIF 변수 제거
|
|
1204
|
-
X = X.drop(columns=[max_key])
|
|
1197
|
+
for group in group_names:
|
|
1198
|
+
group_values = df_filtered[df_filtered[between] == group][dv].dropna()
|
|
1199
|
+
if len(group_values) > 0:
|
|
1200
|
+
s, p = normaltest(group_values)
|
|
1201
|
+
if p <= alpha:
|
|
1202
|
+
normality_satisfied = False
|
|
1203
|
+
break
|
|
1205
1204
|
|
|
1206
|
-
#
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1205
|
+
# ============================================
|
|
1206
|
+
# 2. 등분산성 검정 (그룹별로 수행)
|
|
1207
|
+
# ============================================
|
|
1208
|
+
# 각 그룹별로 데이터 분리
|
|
1209
|
+
group_data_dict = {}
|
|
1210
|
+
for group in group_names:
|
|
1211
|
+
group_data_dict[group] = df_filtered[df_filtered[between] == group][dv].dropna().values
|
|
1210
1212
|
|
|
1211
|
-
#
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1213
|
+
# 등분산 검정 수행
|
|
1214
|
+
if len(group_names) > 1:
|
|
1215
|
+
if normality_satisfied:
|
|
1216
|
+
# 정규성을 만족하면 Bartlett 검정
|
|
1217
|
+
s, p = bartlett(*group_data_dict.values())
|
|
1218
|
+
else:
|
|
1219
|
+
# 정규성을 만족하지 않으면 Levene 검정
|
|
1220
|
+
s, p = levene(*group_data_dict.values())
|
|
1221
|
+
equal_var_satisfied = p > alpha
|
|
1222
|
+
else:
|
|
1223
|
+
# 그룹이 1개인 경우 등분산성 검정 불가능
|
|
1224
|
+
equal_var_satisfied = True
|
|
1215
1225
|
|
|
1216
|
-
|
|
1226
|
+
# ============================================
|
|
1227
|
+
# 3. ANOVA 수행
|
|
1228
|
+
# ============================================
|
|
1229
|
+
if equal_var_satisfied:
|
|
1230
|
+
# 등분산을 만족할 때 일반적인 ANOVA 사용
|
|
1231
|
+
anova_method = "ANOVA"
|
|
1232
|
+
anova_df = anova(data=df_filtered, dv=dv, between=between)
|
|
1233
|
+
else:
|
|
1234
|
+
# 등분산을 만족하지 않을 때 Welch's ANOVA 사용
|
|
1235
|
+
anova_method = "Welch"
|
|
1236
|
+
anova_df = welch_anova(data=df_filtered, dv=dv, between=between)
|
|
1217
1237
|
|
|
1238
|
+
# ANOVA 결과에 메타정보 추가
|
|
1239
|
+
anova_df.insert(1, 'normality', normality_satisfied)
|
|
1240
|
+
anova_df.insert(2, 'equal_var', equal_var_satisfied)
|
|
1241
|
+
anova_df.insert(3, 'method', anova_method)
|
|
1218
1242
|
|
|
1243
|
+
# 유의성 여부 컬럼 추가
|
|
1244
|
+
if 'p-unc' in anova_df.columns:
|
|
1245
|
+
anova_df['significant'] = anova_df['p-unc'] <= alpha
|
|
1219
1246
|
|
|
1220
|
-
#
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
def trend(x: Any, y: Any, degree: int = 1, value_count: int = 100) -> Tuple[np.ndarray, np.ndarray]:
|
|
1224
|
-
"""x, y 데이터에 대한 추세선을 구한다.
|
|
1247
|
+
# ANOVA 결과가 유의한지 확인
|
|
1248
|
+
p_unc = float(anova_df.loc[0, 'p-unc']) # type: ignore
|
|
1249
|
+
anova_significant = p_unc <= alpha
|
|
1225
1250
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1251
|
+
# ANOVA 보고 문장 생성
|
|
1252
|
+
def _safe_get(col: str, default: float = np.nan) -> float:
|
|
1253
|
+
try:
|
|
1254
|
+
return float(anova_df.loc[0, col]) if col in anova_df.columns else default # type: ignore
|
|
1255
|
+
except Exception:
|
|
1256
|
+
return default
|
|
1231
1257
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1258
|
+
df1 = _safe_get('ddof1')
|
|
1259
|
+
df2 = _safe_get('ddof2')
|
|
1260
|
+
fval = _safe_get('F')
|
|
1261
|
+
eta2 = _safe_get('np2')
|
|
1234
1262
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
# 2차 다항 회귀 추세선
|
|
1238
|
-
from hossam import *
|
|
1239
|
-
vx, vy = hs_stats.trend(x, y, degree=2, value_count=200)
|
|
1240
|
-
print(len(vx), len(vy)) # 200, 200
|
|
1241
|
-
```
|
|
1242
|
-
"""
|
|
1243
|
-
# [ a, b, c ] ==> ax^2 + bx + c
|
|
1244
|
-
x_arr = np.asarray(x)
|
|
1245
|
-
y_arr = np.asarray(y)
|
|
1263
|
+
anova_sig_text = "그룹별 평균이 다를 가능성이 높습니다." if anova_significant else "그룹별 평균 차이에 대한 근거가 부족합니다."
|
|
1264
|
+
assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않았고'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않았다'}고 판단됩니다."
|
|
1246
1265
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1266
|
+
anova_report = (
|
|
1267
|
+
f"{between}별로 {dv} 평균을 비교한 {anova_method} 결과: F({df1:.3f}, {df2:.3f}) = {fval:.3f}, p = {p_unc:.4f}. "
|
|
1268
|
+
f"해석: {anova_sig_text} {assumption_text}"
|
|
1269
|
+
)
|
|
1249
1270
|
|
|
1250
|
-
|
|
1271
|
+
if not np.isnan(eta2):
|
|
1272
|
+
anova_report += f" 효과 크기(η²p) ≈ {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
|
|
1251
1273
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1274
|
+
# ============================================
|
|
1275
|
+
# 4. 사후검정 (ANOVA 유의할 때만)
|
|
1276
|
+
# ============================================
|
|
1277
|
+
posthoc_df = None
|
|
1278
|
+
posthoc_method = 'None'
|
|
1279
|
+
posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
|
|
1255
1280
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1281
|
+
if anova_significant:
|
|
1282
|
+
if equal_var_satisfied:
|
|
1283
|
+
# 등분산을 만족하면 Tukey HSD 사용
|
|
1284
|
+
posthoc_method = "Tukey HSD"
|
|
1285
|
+
posthoc_df = pairwise_tukey(data=df_filtered, dv=dv, between=between)
|
|
1286
|
+
else:
|
|
1287
|
+
# 등분산을 만족하지 않으면 Games-Howell 사용
|
|
1288
|
+
posthoc_method = "Games-Howell"
|
|
1289
|
+
posthoc_df = pairwise_gameshowell(df_filtered, dv=dv, between=between)
|
|
1258
1290
|
|
|
1259
|
-
|
|
1291
|
+
# 사후검정 결과에 메타정보 추가
|
|
1292
|
+
# posthoc_df.insert(0, 'normality', normality_satisfied)
|
|
1293
|
+
# posthoc_df.insert(1, 'equal_var', equal_var_satisfied)
|
|
1294
|
+
posthoc_df.insert(0, 'method', posthoc_method)
|
|
1295
|
+
|
|
1296
|
+
# p-value 컬럼 탐색
|
|
1297
|
+
p_cols = [c for c in ["p-tukey", "pval", "p-adjust", "p_adj", "p-corr", "p", "p-unc", "pvalue", "p_value"] if c in posthoc_df.columns]
|
|
1298
|
+
p_col = p_cols[0] if p_cols else None
|
|
1299
|
+
|
|
1300
|
+
if p_col:
|
|
1301
|
+
# 유의성 여부 컬럼 추가
|
|
1302
|
+
posthoc_df['significant'] = posthoc_df[p_col] <= alpha
|
|
1303
|
+
|
|
1304
|
+
sig_pairs_df = posthoc_df[posthoc_df[p_col] <= alpha]
|
|
1305
|
+
sig_count = len(sig_pairs_df)
|
|
1306
|
+
total_count = len(posthoc_df)
|
|
1307
|
+
pair_samples = []
|
|
1308
|
+
if not sig_pairs_df.empty and {'A', 'B'}.issubset(sig_pairs_df.columns):
|
|
1309
|
+
pair_samples = [f"{row['A']} vs {row['B']}" for _, row in sig_pairs_df.head(3).iterrows()]
|
|
1310
|
+
|
|
1311
|
+
if sig_count > 0:
|
|
1312
|
+
posthoc_report = (
|
|
1313
|
+
f"{posthoc_method} 사후검정에서 {sig_count}/{total_count}쌍이 의미 있는 차이를 보였습니다 (alpha={alpha})."
|
|
1314
|
+
)
|
|
1315
|
+
if pair_samples:
|
|
1316
|
+
posthoc_report += " 예: " + ", ".join(pair_samples) + " 등."
|
|
1317
|
+
else:
|
|
1318
|
+
posthoc_report = f"{posthoc_method} 사후검정에서 추가로 유의한 쌍은 발견되지 않았습니다."
|
|
1319
|
+
else:
|
|
1320
|
+
posthoc_report = f"{posthoc_method} 결과는 생성했지만 p-value 정보를 찾지 못해 유의성을 확인할 수 없습니다."
|
|
1321
|
+
|
|
1322
|
+
# ============================================
|
|
1323
|
+
# 5. 결과 반환
|
|
1324
|
+
# ============================================
|
|
1325
|
+
return anova_df, anova_report, posthoc_df, posthoc_report
|
|
1260
1326
|
|
|
1261
1327
|
|
|
1262
1328
|
# ===================================================================
|
|
1263
|
-
#
|
|
1329
|
+
# 이원 분산분석 (Two-way ANOVA: 두 범주형 독립변수)
|
|
1264
1330
|
# ===================================================================
|
|
1265
|
-
def
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1331
|
+
def twoway_anova(
|
|
1332
|
+
data: DataFrame,
|
|
1333
|
+
dv: str,
|
|
1334
|
+
factor_a: str,
|
|
1335
|
+
factor_b: str,
|
|
1336
|
+
alpha: float = 0.05,
|
|
1337
|
+
) -> tuple[DataFrame, str, DataFrame | None, str]:
|
|
1338
|
+
"""두 범주형 요인에 대한 이원분산분석을 수행하고 해석용 보고문을 반환한다.
|
|
1339
|
+
|
|
1340
|
+
분석 흐름:
|
|
1341
|
+
1) 각 셀(요인 조합)별 정규성 검정
|
|
1342
|
+
2) 전체 셀을 대상으로 등분산성 검정 (정규성 충족 시 Bartlett, 불충족 시 Levene)
|
|
1343
|
+
3) 두 요인 및 교호작용을 포함한 2원 ANOVA 수행
|
|
1344
|
+
4) 유의한 요인에 대해 Tukey HSD 사후검정(요인별) 실행
|
|
1270
1345
|
|
|
1271
1346
|
Args:
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1347
|
+
data (DataFrame): 종속변수와 두 개의 범주형 요인을 포함한 데이터프레임.
|
|
1348
|
+
dv (str): 종속변수 컬럼명.
|
|
1349
|
+
factor_a (str): 첫 번째 요인 컬럼명.
|
|
1350
|
+
factor_b (str): 두 번째 요인 컬럼명.
|
|
1351
|
+
alpha (float, optional): 유의수준. 기본 0.05.
|
|
1276
1352
|
|
|
1277
1353
|
Returns:
|
|
1278
|
-
tuple:
|
|
1279
|
-
-
|
|
1280
|
-
-
|
|
1281
|
-
-
|
|
1282
|
-
-
|
|
1283
|
-
- 변수별 보고 리스트 (`variable_reports`, list[str]): 각 예측변수에 대한 서술형 문장.
|
|
1284
|
-
- 회귀식 문자열 (`equation_text`, str): 상수항과 계수를 포함한 회귀식 표현.
|
|
1354
|
+
tuple:
|
|
1355
|
+
- anova_df (DataFrame): 2원 ANOVA 결과(각 요인과 상호작용의 F, p, η²p 포함).
|
|
1356
|
+
- anova_report (str): 두 요인 및 상호작용의 유의성/가정 충족 여부를 요약한 문장.
|
|
1357
|
+
- posthoc_df (DataFrame|None): 유의한 요인에 대한 Tukey 사후검정 결과(요인명, A, B, p 포함). 없으면 None.
|
|
1358
|
+
- posthoc_report (str): 사후검정 유무 및 유의 쌍 요약 문장.
|
|
1285
1359
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1360
|
+
Raises:
|
|
1361
|
+
ValueError: 입력 컬럼이 데이터프레임에 없을 때.
|
|
1362
|
+
"""
|
|
1363
|
+
# 컬럼 유효성 검사
|
|
1364
|
+
for col in [dv, factor_a, factor_b]:
|
|
1365
|
+
if col not in data.columns:
|
|
1366
|
+
raise ValueError(f"'{col}' 컬럼이 데이터프레임에 없습니다.")
|
|
1289
1367
|
|
|
1290
|
-
|
|
1291
|
-
```python
|
|
1292
|
-
from hossam import *
|
|
1368
|
+
df_filtered = data[[dv, factor_a, factor_b]].dropna()
|
|
1293
1369
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1370
|
+
# 1) 셀별 정규성 검정
|
|
1371
|
+
normality_satisfied = True
|
|
1372
|
+
for (a, b), subset in df_filtered.groupby([factor_a, factor_b], observed=False):
|
|
1373
|
+
vals = subset[dv].dropna()
|
|
1374
|
+
if len(vals) > 0:
|
|
1375
|
+
_, p = normaltest(vals)
|
|
1376
|
+
if p <= alpha:
|
|
1377
|
+
normality_satisfied = False
|
|
1378
|
+
break
|
|
1296
1379
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1380
|
+
# 2) 등분산성 검정 (셀 단위)
|
|
1381
|
+
cell_values = [g[dv].dropna().values for _, g in df_filtered.groupby([factor_a, factor_b], observed=False)]
|
|
1382
|
+
if len(cell_values) > 1:
|
|
1383
|
+
if normality_satisfied:
|
|
1384
|
+
_, p_var = bartlett(*cell_values)
|
|
1385
|
+
else:
|
|
1386
|
+
_, p_var = levene(*cell_values)
|
|
1387
|
+
equal_var_satisfied = p_var > alpha
|
|
1388
|
+
else:
|
|
1389
|
+
equal_var_satisfied = True
|
|
1299
1390
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
""
|
|
1391
|
+
# 3) 2원 ANOVA 수행 (pingouin anova with between factors)
|
|
1392
|
+
anova_df = anova(data=df_filtered, dv=dv, between=[factor_a, factor_b], effsize="np2")
|
|
1393
|
+
anova_df.insert(0, "normality", normality_satisfied)
|
|
1394
|
+
anova_df.insert(1, "equal_var", equal_var_satisfied)
|
|
1395
|
+
if 'p-unc' in anova_df.columns:
|
|
1396
|
+
anova_df['significant'] = anova_df['p-unc'] <= alpha
|
|
1304
1397
|
|
|
1305
|
-
#
|
|
1306
|
-
|
|
1307
|
-
|
|
1398
|
+
# 보고문 생성
|
|
1399
|
+
def _safe(row, col, default=np.nan):
|
|
1400
|
+
try:
|
|
1401
|
+
return float(row[col])
|
|
1402
|
+
except Exception:
|
|
1403
|
+
return default
|
|
1308
1404
|
|
|
1309
|
-
#
|
|
1310
|
-
|
|
1405
|
+
# 요인별 문장
|
|
1406
|
+
reports = []
|
|
1407
|
+
sig_flags = {}
|
|
1408
|
+
for _, row in anova_df.iterrows():
|
|
1409
|
+
term = row.get("Source", "")
|
|
1410
|
+
fval = _safe(row, "F")
|
|
1411
|
+
pval = _safe(row, "p-unc")
|
|
1412
|
+
eta2 = _safe(row, "np2")
|
|
1413
|
+
sig = pval <= alpha
|
|
1414
|
+
sig_flags[term] = sig
|
|
1415
|
+
if term.lower() == "residual":
|
|
1416
|
+
continue
|
|
1417
|
+
effect_name = term.replace("*", "와 ")
|
|
1418
|
+
msg = f"{effect_name}: F={fval:.3f}, p={pval:.4f}. 해석: "
|
|
1419
|
+
msg += "유의한 차이가 있습니다." if sig else "유의한 차이를 찾지 못했습니다."
|
|
1420
|
+
if not np.isnan(eta2):
|
|
1421
|
+
msg += f" 효과 크기(η²p)≈{eta2:.3f}."
|
|
1422
|
+
reports.append(msg)
|
|
1311
1423
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1424
|
+
assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않음'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않음'}으로 판단했습니다."
|
|
1425
|
+
anova_report = " ".join(reports) + " " + assumption_text
|
|
1314
1426
|
|
|
1315
|
-
#
|
|
1316
|
-
|
|
1427
|
+
# 4) 사후검정: 유의한 요인(교호작용 제외) 대상, 수준이 2 초과일 때만 실행
|
|
1428
|
+
posthoc_df_list = []
|
|
1429
|
+
interaction_name = f"{factor_a}*{factor_b}".lower()
|
|
1430
|
+
interaction_name_spaced = f"{factor_a} * {factor_b}".lower()
|
|
1317
1431
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1432
|
+
for factor, sig in sig_flags.items():
|
|
1433
|
+
if factor is None:
|
|
1434
|
+
continue
|
|
1435
|
+
factor_lower = str(factor).lower()
|
|
1320
1436
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
for i, col in enumerate(indi_df.columns, start=1): # 상수항이 0이므로 1부터 시작
|
|
1325
|
-
try:
|
|
1326
|
-
with np.errstate(divide='ignore', invalid='ignore'):
|
|
1327
|
-
vif_value = variance_inflation_factor(indi_df_const.values, i) # type: ignore
|
|
1328
|
-
# inf나 매우 큰 값 처리
|
|
1329
|
-
if np.isinf(vif_value) or vif_value > 1e10:
|
|
1330
|
-
vif_dict[col] = np.inf
|
|
1331
|
-
else:
|
|
1332
|
-
vif_dict[col] = vif_value
|
|
1333
|
-
except:
|
|
1334
|
-
vif_dict[col] = np.inf
|
|
1437
|
+
# 교호작용(residual 포함) 혹은 비유의 항은 건너뛴다
|
|
1438
|
+
if factor_lower in ["residual", interaction_name, interaction_name_spaced] or not sig:
|
|
1439
|
+
continue
|
|
1335
1440
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if name not in xnames:
|
|
1441
|
+
# 실제 컬럼이 아니면 건너뛴다 (ex: "A * B" 같은 교호작용 이름)
|
|
1442
|
+
if factor not in df_filtered.columns:
|
|
1339
1443
|
continue
|
|
1340
1444
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1445
|
+
levels = df_filtered[factor].unique()
|
|
1446
|
+
if len(levels) <= 2:
|
|
1447
|
+
continue
|
|
1448
|
+
tukey_df = pairwise_tukey(data=df_filtered, dv=dv, between=factor)
|
|
1449
|
+
tukey_df.insert(0, "factor", factor)
|
|
1450
|
+
posthoc_df_list.append(tukey_df)
|
|
1345
1451
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1452
|
+
posthoc_df = None
|
|
1453
|
+
posthoc_report = "사후검정이 필요하지 않거나 유의한 요인이 없습니다."
|
|
1454
|
+
if posthoc_df_list:
|
|
1455
|
+
posthoc_df = concat(posthoc_df_list, ignore_index=True)
|
|
1456
|
+
p_cols = [c for c in ["p-tukey", "pval", "p-adjust", "p_adj", "p-corr", "p", "p-unc", "pvalue", "p_value"] if c in posthoc_df.columns]
|
|
1457
|
+
p_col = p_cols[0] if p_cols else None
|
|
1458
|
+
if p_col:
|
|
1459
|
+
posthoc_df['significant'] = posthoc_df[p_col] <= alpha
|
|
1460
|
+
sig_df = posthoc_df[posthoc_df[p_col] <= alpha]
|
|
1461
|
+
sig_count = len(sig_df)
|
|
1462
|
+
total_count = len(posthoc_df)
|
|
1463
|
+
examples = []
|
|
1464
|
+
if not sig_df.empty and {"A", "B"}.issubset(sig_df.columns):
|
|
1465
|
+
examples = [f"{row['A']} vs {row['B']}" for _, row in sig_df.head(3).iterrows()]
|
|
1466
|
+
if sig_count > 0:
|
|
1467
|
+
posthoc_report = f"사후검정(Tukey)에서 {sig_count}/{total_count}쌍이 의미 있는 차이를 보였습니다."
|
|
1468
|
+
if examples:
|
|
1469
|
+
posthoc_report += " 예: " + ", ".join(examples) + " 등."
|
|
1470
|
+
else:
|
|
1471
|
+
posthoc_report = "사후검정 결과 추가로 유의한 쌍은 없었습니다."
|
|
1472
|
+
else:
|
|
1473
|
+
posthoc_report = "사후검정 결과를 생성했으나 p-value 정보를 찾지 못했습니다."
|
|
1348
1474
|
|
|
1349
|
-
|
|
1350
|
-
vif = vif_dict.get(name, np.nan)
|
|
1475
|
+
return anova_df, anova_report, posthoc_df, posthoc_report
|
|
1351
1476
|
|
|
1352
|
-
# 유의확률과 별표 표시
|
|
1353
|
-
stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
|
|
1354
1477
|
|
|
1355
|
-
# 한 변수에 대한 보고 정보 추가
|
|
1356
|
-
variables.append(
|
|
1357
|
-
{
|
|
1358
|
-
"종속변수": yname, # 종속변수 이름
|
|
1359
|
-
"독립변수": name, # 독립변수 이름
|
|
1360
|
-
"B": f"{b:.6f}", # 비표준화 회귀계수(B)
|
|
1361
|
-
"표준오차": f"{se:.6f}", # 계수 표준오차
|
|
1362
|
-
"Beta": beta, # 표준화 회귀계수(β)
|
|
1363
|
-
"t": f"{t:.3f}{stars}", # t-통계량(+별표)
|
|
1364
|
-
"p-value": p, # 계수 유의확률
|
|
1365
|
-
"significant": p <= alpha, # 유의성 여부 (boolean)
|
|
1366
|
-
"공차": 1 / vif, # 공차(Tolerance = 1/VIF)
|
|
1367
|
-
"vif": vif, # 분산팽창계수
|
|
1368
|
-
}
|
|
1369
|
-
)
|
|
1370
1478
|
|
|
1371
|
-
|
|
1479
|
+
# ===================================================================
|
|
1480
|
+
# 종속변수에 대한 편상관계수 및 효과크기 분석 (Correlation & Effect Size)
|
|
1481
|
+
# ===================================================================
|
|
1482
|
+
def corr_effect_size(data: DataFrame, dv: str, *fields: str, alpha: float = 0.05) -> DataFrame:
|
|
1483
|
+
"""종속변수와의 편상관계수 및 효과크기를 계산한다.
|
|
1372
1484
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
for i in [0, 2]:
|
|
1377
|
-
for item in summary_main.tables[i].data:
|
|
1378
|
-
n = len(item)
|
|
1379
|
-
for i in range(0, n, 2):
|
|
1380
|
-
key = item[i].strip()[:-1]
|
|
1381
|
-
value = item[i + 1].strip()
|
|
1382
|
-
if not key or not value:
|
|
1383
|
-
continue
|
|
1384
|
-
result_dict[key] = value
|
|
1485
|
+
각 독립변수와 종속변수 간의 상관계수를 계산하되, 정규성과 선형성을 검사하여
|
|
1486
|
+
Pearson 또는 Spearman 상관계수를 적절히 선택한다.
|
|
1487
|
+
Cohen's d (효과크기)를 계산하여 상관 강도를 정량화한다.
|
|
1385
1488
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1489
|
+
Args:
|
|
1490
|
+
data (DataFrame): 분석 대상 데이터프레임.
|
|
1491
|
+
dv (str): 종속변수 컬럼 이름.
|
|
1492
|
+
*fields (str): 독립변수 컬럼 이름들. 지정하지 않으면 수치형 컬럼 중 dv 제외 모두 사용.
|
|
1493
|
+
alpha (float, optional): 유의수준. 기본 0.05.
|
|
1388
1494
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
else "유의하지 않다"
|
|
1398
|
-
),
|
|
1399
|
-
result_dict["Df Model"],
|
|
1400
|
-
result_dict["Df Residuals"],
|
|
1401
|
-
result_dict["F-statistic"],
|
|
1402
|
-
"<=" if float(result_dict["Prob (F-statistic)"]) <= 0.05 else ">",
|
|
1403
|
-
)
|
|
1495
|
+
Returns:
|
|
1496
|
+
DataFrame: 다음 컬럼을 포함한 데이터프레임:
|
|
1497
|
+
- Variable (str): 독립변수 이름
|
|
1498
|
+
- Correlation (float): 상관계수 (Pearson 또는 Spearman)
|
|
1499
|
+
- Corr_Type (str): 선택된 상관계수 종류 ('Pearson' 또는 'Spearman')
|
|
1500
|
+
- P-value (float): 상관계수의 유의확률
|
|
1501
|
+
- Cohens_d (float): 표준화된 효과크기
|
|
1502
|
+
- Effect_Size (str): 효과크기 분류 ('Large', 'Medium', 'Small', 'Negligible')
|
|
1404
1503
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1504
|
+
Examples:
|
|
1505
|
+
```python
|
|
1506
|
+
from hossam import *
|
|
1507
|
+
from pandas import DataFrame
|
|
1408
1508
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
s
|
|
1413
|
-
% (
|
|
1414
|
-
row["독립변수"],
|
|
1415
|
-
row["B"],
|
|
1416
|
-
"<=" if float(row["p-value"]) < 0.05 else ">",
|
|
1417
|
-
row["종속변수"],
|
|
1418
|
-
"유의미한" if float(row["p-value"]) < 0.05 else "유의하지 않은",
|
|
1419
|
-
)
|
|
1420
|
-
)
|
|
1509
|
+
df = DataFrame({'age': [20, 30, 40, 50],
|
|
1510
|
+
'bmi': [22, 25, 28, 30],
|
|
1511
|
+
'charges': [1000, 2000, 3000, 4000]})
|
|
1421
1512
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
intercept = fit.params["const"]
|
|
1426
|
-
terms = []
|
|
1513
|
+
result = hs_stats.corr_effect_size(df, 'charges', 'age', 'bmi')
|
|
1514
|
+
```
|
|
1515
|
+
"""
|
|
1427
1516
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
terms.append(f" {sign} {abs(coef):.3f}·{name}")
|
|
1517
|
+
# fields가 지정되지 않으면 수치형 컬럼 중 dv 제외 모두 사용
|
|
1518
|
+
if not fields:
|
|
1519
|
+
fields = [col for col in data.columns if is_numeric_dtype(data[col]) and col != dv] # type: ignore
|
|
1432
1520
|
|
|
1433
|
-
|
|
1521
|
+
# dv가 수치형인지 확인
|
|
1522
|
+
if not is_numeric_dtype(data[dv]):
|
|
1523
|
+
raise ValueError(f"Dependent variable '{dv}' must be numeric type")
|
|
1434
1524
|
|
|
1435
|
-
|
|
1436
|
-
pdf = DataFrame(
|
|
1437
|
-
{
|
|
1438
|
-
"R": [float(result_dict.get('R-squared', np.nan))],
|
|
1439
|
-
"R²": [float(result_dict.get('Adj. R-squared', np.nan))],
|
|
1440
|
-
"F": [float(result_dict.get('F-statistic', np.nan))],
|
|
1441
|
-
"p-value": [float(result_dict.get('Prob (F-statistic)', np.nan))],
|
|
1442
|
-
"Durbin-Watson": [float(result_dict.get('Durbin-Watson', np.nan))],
|
|
1443
|
-
}
|
|
1444
|
-
)
|
|
1525
|
+
results = []
|
|
1445
1526
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
return pdf, rdf
|
|
1527
|
+
for var in fields:
|
|
1528
|
+
if not is_numeric_dtype(data[var]):
|
|
1529
|
+
continue
|
|
1450
1530
|
|
|
1531
|
+
# 결측치 제거
|
|
1532
|
+
valid_idx = data[[var, dv]].notna().all(axis=1)
|
|
1533
|
+
x = data.loc[valid_idx, var].values
|
|
1534
|
+
y = data.loc[valid_idx, dv].values
|
|
1451
1535
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
# ===================================================================
|
|
1455
|
-
def ols(df: DataFrame, yname: str, report: bool | str | int = False) -> Union[
|
|
1456
|
-
RegressionResultsWrapper,
|
|
1457
|
-
Tuple[RegressionResultsWrapper, DataFrame, DataFrame],
|
|
1458
|
-
Tuple[
|
|
1459
|
-
RegressionResultsWrapper,
|
|
1460
|
-
DataFrame,
|
|
1461
|
-
DataFrame,
|
|
1462
|
-
str,
|
|
1463
|
-
str,
|
|
1464
|
-
list[str],
|
|
1465
|
-
str
|
|
1466
|
-
]
|
|
1467
|
-
]:
|
|
1468
|
-
"""선형회귀분석을 수행하고 적합 결과를 반환한다.
|
|
1536
|
+
if len(x) < 3:
|
|
1537
|
+
continue
|
|
1469
1538
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1539
|
+
# 정규성 검사 (Shapiro-Wilk: n <= 5000 권장, 그 외 D'Agostino)
|
|
1540
|
+
method_x = 's' if len(x) <= 5000 else 'n'
|
|
1541
|
+
method_y = 's' if len(y) <= 5000 else 'n'
|
|
1472
1542
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
yname (str): 종속변수 컬럼명.
|
|
1476
|
-
report (bool | str | int): 리포트 모드 설정. 다음 값 중 하나:
|
|
1477
|
-
- False (기본값): 리포트 미사용. fit 객체만 반환.
|
|
1478
|
-
- 1 또는 'summary': 요약 리포트 반환 (full=False).
|
|
1479
|
-
- 2 또는 'full': 풀 리포트 반환 (full=True).
|
|
1480
|
-
- True: 풀 리포트 반환 (2와 동일).
|
|
1543
|
+
normal_x_result = normal_test(data[[var]], columns=[var], method=method_x)
|
|
1544
|
+
normal_y_result = normal_test(data[[dv]], columns=[dv], method=method_y)
|
|
1481
1545
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1546
|
+
# 정규성 판정 (p > alpha면 정규분포 가정)
|
|
1547
|
+
normal_x = normal_x_result.loc[var, 'p-val'] > alpha if var in normal_x_result.index else False # type: ignore
|
|
1548
|
+
normal_y = normal_y_result.loc[dv, 'p-val'] > alpha if dv in normal_y_result.index else False # type: ignore
|
|
1485
1549
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1550
|
+
# Pearson (모두 정규) vs Spearman (하나라도 비정규)
|
|
1551
|
+
if normal_x and normal_y:
|
|
1552
|
+
r, p = pearsonr(x, y)
|
|
1553
|
+
corr_type = 'Pearson'
|
|
1554
|
+
else:
|
|
1555
|
+
r, p = spearmanr(x, y)
|
|
1556
|
+
corr_type = 'Spearman'
|
|
1488
1557
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
- model_report: 모형 보고 문장 (str)
|
|
1496
|
-
- variable_reports: 변수별 보고 문장 리스트 (list[str])
|
|
1497
|
-
- equation_text: 회귀식 문자열 (str)
|
|
1558
|
+
# Cohen's d 계산 (상관계수에서 효과크기로 변환)
|
|
1559
|
+
# d = 2*r / sqrt(1-r^2)
|
|
1560
|
+
if r ** 2 < 1: # type: ignore
|
|
1561
|
+
d = (2 * r) / np.sqrt(1 - r ** 2) # type: ignore
|
|
1562
|
+
else:
|
|
1563
|
+
d = 0
|
|
1498
1564
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1565
|
+
# 효과크기 분류 (Cohen's d 기준)
|
|
1566
|
+
# Small: 0.2 < |d| <= 0.5
|
|
1567
|
+
# Medium: 0.5 < |d| <= 0.8
|
|
1568
|
+
# Large: |d| > 0.8
|
|
1569
|
+
abs_d = abs(d)
|
|
1570
|
+
if abs_d > 0.8:
|
|
1571
|
+
effect_size = 'Large'
|
|
1572
|
+
elif abs_d > 0.5:
|
|
1573
|
+
effect_size = 'Medium'
|
|
1574
|
+
elif abs_d > 0.2:
|
|
1575
|
+
effect_size = 'Small'
|
|
1576
|
+
else:
|
|
1577
|
+
effect_size = 'Negligible'
|
|
1504
1578
|
|
|
1505
|
-
|
|
1506
|
-
'
|
|
1507
|
-
'
|
|
1508
|
-
'
|
|
1579
|
+
results.append({
|
|
1580
|
+
'Variable': var,
|
|
1581
|
+
'Correlation': r,
|
|
1582
|
+
'Corr_Type': corr_type,
|
|
1583
|
+
'P-value': p,
|
|
1584
|
+
'Cohens_d': d,
|
|
1585
|
+
'Effect_Size': effect_size
|
|
1509
1586
|
})
|
|
1510
1587
|
|
|
1511
|
-
|
|
1512
|
-
fit = hs_stats.ols(df, 'target')
|
|
1513
|
-
|
|
1514
|
-
# 요약 리포트 반환
|
|
1515
|
-
fit, pdf, rdf = hs_stats.ols(df, 'target', report='summary')
|
|
1516
|
-
|
|
1517
|
-
# 풀 리포트 반환
|
|
1518
|
-
fit, pdf, rdf, result_report, model_report, var_reports, eq = hs_stats.ols(df, 'target', report='full')
|
|
1519
|
-
```
|
|
1520
|
-
"""
|
|
1521
|
-
x = df.drop(yname, axis=1)
|
|
1522
|
-
y = df[yname]
|
|
1588
|
+
result_df = DataFrame(results)
|
|
1523
1589
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1590
|
+
# 상관계수로 정렬 (절댓값 기준 내림차순)
|
|
1591
|
+
if len(result_df) > 0:
|
|
1592
|
+
result_df = result_df.sort_values('Correlation', key=lambda x: x.abs(), ascending=False).reset_index(drop=True)
|
|
1527
1593
|
|
|
1528
|
-
|
|
1529
|
-
if not report or report is False:
|
|
1530
|
-
# 리포트 미사용
|
|
1531
|
-
return linear_fit
|
|
1532
|
-
elif report == 1 or report == 'summary':
|
|
1533
|
-
# 요약 리포트 (full=False)
|
|
1534
|
-
pdf, rdf = ols_report(linear_fit, df, full=False, alpha=0.05) # type: ignore
|
|
1535
|
-
return linear_fit, pdf, rdf
|
|
1536
|
-
elif report == 2 or report == 'full' or report is True:
|
|
1537
|
-
# 풀 리포트 (full=True)
|
|
1538
|
-
pdf, rdf, result_report, model_report, variable_reports, equation_text = ols_report(linear_fit, df, full=True, alpha=0.05) # type: ignore
|
|
1539
|
-
return linear_fit, pdf, rdf, result_report, model_report, variable_reports, equation_text
|
|
1540
|
-
else:
|
|
1541
|
-
# 기본값: 리포트 미사용
|
|
1542
|
-
return linear_fit
|
|
1594
|
+
return result_df
|
|
1543
1595
|
|
|
1544
1596
|
|
|
1545
1597
|
# ===================================================================
|
|
1546
|
-
#
|
|
1598
|
+
# 쌍별 상관분석 (선형성/이상치 점검 후 Pearson/Spearman 자동 선택)
|
|
1547
1599
|
# ===================================================================
|
|
1548
|
-
def
|
|
1549
|
-
fit: BinaryResultsWrapper,
|
|
1600
|
+
def corr_pairwise(
|
|
1550
1601
|
data: DataFrame,
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
str,
|
|
1560
|
-
str,
|
|
1561
|
-
list[str],
|
|
1562
|
-
np.ndarray
|
|
1563
|
-
]
|
|
1564
|
-
]:
|
|
1565
|
-
"""로지스틱 회귀 적합 결과를 상세 리포트로 변환한다.
|
|
1602
|
+
fields: list[str] | None = None,
|
|
1603
|
+
alpha: float = 0.05,
|
|
1604
|
+
z_thresh: float = 3.0,
|
|
1605
|
+
min_n: int = 8,
|
|
1606
|
+
linearity_power: tuple[int, ...] = (2,),
|
|
1607
|
+
p_adjust: str = "none",
|
|
1608
|
+
) -> tuple[DataFrame, DataFrame]:
|
|
1609
|
+
"""각 변수 쌍에 대해 선형성·이상치 여부를 점검한 뒤 Pearson/Spearman을 자동 선택해 상관을 요약한다.
|
|
1566
1610
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1611
|
+
절차:
|
|
1612
|
+
1) z-score 기준(|z|>z_thresh)으로 각 변수의 이상치 존재 여부를 파악
|
|
1613
|
+
2) 단순회귀 y~x에 대해 Ramsey RESET(linearity_power)로 선형성 검정 (모든 p>alpha → 선형성 충족)
|
|
1614
|
+
3) 선형성 충족이고 양쪽 변수에서 |z|>z_thresh 이상치가 없으면 Pearson, 그 외엔 Spearman 선택
|
|
1615
|
+
4) 상관계수/유의확률, 유의성 여부, 강도(strong/medium/weak/no correlation) 기록
|
|
1616
|
+
5) 선택적으로 다중비교 보정(p_adjust="fdr_bh" 등) 적용하여 pval_adj와 significant_adj 추가
|
|
1573
1617
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1618
|
+
Args:
|
|
1619
|
+
data (DataFrame): 분석 대상 데이터프레임.
|
|
1620
|
+
fields (list[str]|None): 분석할 숫자형 컬럼 이름 리스트. None이면 모든 숫자형 컬럼 사용. 기본값 None.
|
|
1621
|
+
alpha (float, optional): 유의수준. 기본 0.05.
|
|
1622
|
+
z_thresh (float, optional): 이상치 판단 임계값(|z| 기준). 기본 3.0.
|
|
1623
|
+
min_n (int, optional): 쌍별 최소 표본 크기. 미만이면 계산 생략. 기본 8.
|
|
1624
|
+
linearity_power (tuple[int,...], optional): RESET 검정에서 포함할 차수 집합. 기본 (2,).
|
|
1625
|
+
p_adjust (str, optional): 다중비교 보정 방법. "none" 또는 statsmodels.multipletests 지원값 중 하나(e.g., "fdr_bh"). 기본 "none".
|
|
1582
1626
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1627
|
+
Returns:
|
|
1628
|
+
tuple[DataFrame, DataFrame]: 두 개의 데이터프레임을 반환.
|
|
1629
|
+
[0] result_df: 각 변수쌍별 결과 테이블. 컬럼:
|
|
1630
|
+
var_a, var_b, n, linearity(bool), outlier_flag(bool), chosen('pearson'|'spearman'),
|
|
1631
|
+
corr, pval, significant(bool), strength(str), (보정 사용 시) pval_adj, significant_adj
|
|
1632
|
+
[1] corr_matrix: 상관계수 행렬 (행과 열에 변수명, 값에 상관계수)
|
|
1586
1633
|
|
|
1587
1634
|
Examples:
|
|
1588
1635
|
```python
|
|
1589
1636
|
from hossam import *
|
|
1590
1637
|
from pandas import DataFrame
|
|
1591
|
-
import numpy as np
|
|
1592
|
-
|
|
1593
|
-
df = DataFrame({
|
|
1594
|
-
'target': np.random.binomial(1, 0.5, 100),
|
|
1595
|
-
'x1': np.random.normal(0, 1, 100),
|
|
1596
|
-
'x2': np.random.normal(0, 1, 100)
|
|
1597
|
-
})
|
|
1598
|
-
|
|
1599
|
-
# 로지스틱 회귀 적합
|
|
1600
|
-
fit = hs_stats.logit(df, yname="target")
|
|
1601
|
-
|
|
1602
|
-
# 전체 리포트
|
|
1603
|
-
cdf, rdf, result_report, model_report, variable_reports, cm = hs_stats.logit_report(fit, df, full=True)
|
|
1604
1638
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1639
|
+
df = DataFrame({'x1': [1,2,3,4,5], 'x2': [2,4,5,4,6], 'x3': [10,20,25,24,30]})
|
|
1640
|
+
# 전체 숫자형 컬럼에 대해 상관분석
|
|
1641
|
+
result_df, corr_matrix = hs_stats.corr_pairwise(df)
|
|
1642
|
+
# 특정 컬럼만 분석
|
|
1643
|
+
result_df, corr_matrix = hs_stats.corr_pairwise(df, fields=['x1', 'x2'])
|
|
1607
1644
|
```
|
|
1608
1645
|
"""
|
|
1609
1646
|
|
|
1610
|
-
#
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
# 혼동행렬
|
|
1619
|
-
cm = confusion_matrix(y_true, y_pred_fix)
|
|
1620
|
-
tn, fp, fn, tp = cm.ravel()
|
|
1621
|
-
|
|
1622
|
-
acc = accuracy_score(y_true, y_pred_fix) # 정확도
|
|
1623
|
-
pre = precision_score(y_true, y_pred_fix) # 정밀도
|
|
1624
|
-
tpr = recall_score(y_true, y_pred_fix) # 재현율
|
|
1625
|
-
fpr = fp / (fp + tn) # 위양성율
|
|
1626
|
-
tnr = 1 - fpr # 특이성
|
|
1627
|
-
f1 = f1_score(y_true, y_pred_fix) # f1-score
|
|
1628
|
-
ras = roc_auc_score(y_true, y_pred) # auc score
|
|
1629
|
-
|
|
1630
|
-
cdf = DataFrame(
|
|
1631
|
-
{
|
|
1632
|
-
"설명력(P-Rsqe)": [fit.prsquared],
|
|
1633
|
-
"정확도(Accuracy)": [acc],
|
|
1634
|
-
"정밀도(Precision)": [pre],
|
|
1635
|
-
"재현율(Recall,TPR)": [tpr],
|
|
1636
|
-
"위양성율(Fallout,FPR)": [fpr],
|
|
1637
|
-
"특이성(Specif city,TNR)": [tnr],
|
|
1638
|
-
"RAS(auc score)": [ras],
|
|
1639
|
-
"F1": [f1],
|
|
1640
|
-
}
|
|
1641
|
-
)
|
|
1647
|
+
# 0) 컬럼 선정 (숫자형만)
|
|
1648
|
+
if fields is None:
|
|
1649
|
+
# None이면 모든 숫자형 컬럼 사용
|
|
1650
|
+
cols = data.select_dtypes(include=[np.number]).columns.tolist()
|
|
1651
|
+
else:
|
|
1652
|
+
# fields 리스트에서 데이터에 있는 것만 선택하되, 숫자형만 필터링
|
|
1653
|
+
cols = [c for c in fields if c in data.columns and is_numeric_dtype(data[c])]
|
|
1642
1654
|
|
|
1643
|
-
#
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1655
|
+
# 사용 가능한 컬럼이 2개 미만이면 상관분석 불가능
|
|
1656
|
+
if len(cols) < 2:
|
|
1657
|
+
empty_df = DataFrame(columns=["var_a", "var_b", "n", "linearity", "outlier_flag", "chosen", "corr", "pval", "significant", "strength"])
|
|
1658
|
+
return empty_df, DataFrame()
|
|
1647
1659
|
|
|
1648
|
-
#
|
|
1649
|
-
|
|
1660
|
+
# z-score 기반 이상치 유무 계산
|
|
1661
|
+
z_outlier_flags = {}
|
|
1662
|
+
for c in cols:
|
|
1663
|
+
col = data[c].dropna()
|
|
1664
|
+
if col.std(ddof=1) == 0:
|
|
1665
|
+
z_outlier_flags[c] = False
|
|
1666
|
+
continue
|
|
1667
|
+
z = (col - col.mean()) / col.std(ddof=1)
|
|
1668
|
+
z_outlier_flags[c] = (z.abs() > z_thresh).any()
|
|
1650
1669
|
|
|
1651
|
-
|
|
1652
|
-
x = data[xnames]
|
|
1670
|
+
rows = []
|
|
1653
1671
|
|
|
1654
|
-
|
|
1672
|
+
for a, b in combinations(cols, 2):
|
|
1673
|
+
# 공통 관측치 사용
|
|
1674
|
+
pair_df = data[[a, b]].dropna()
|
|
1675
|
+
if len(pair_df) < max(3, min_n):
|
|
1676
|
+
# 표본이 너무 적으면 계산하지 않음
|
|
1677
|
+
rows.append(
|
|
1678
|
+
{
|
|
1679
|
+
"var_a": a,
|
|
1680
|
+
"var_b": b,
|
|
1681
|
+
"n": len(pair_df),
|
|
1682
|
+
"linearity": False,
|
|
1683
|
+
"outlier_flag": True,
|
|
1684
|
+
"chosen": None,
|
|
1685
|
+
"corr": np.nan,
|
|
1686
|
+
"pval": np.nan,
|
|
1687
|
+
"significant": False,
|
|
1688
|
+
"strength": "no correlation",
|
|
1689
|
+
}
|
|
1690
|
+
)
|
|
1691
|
+
continue
|
|
1655
1692
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
x_const = sm.add_constant(x, has_constant="add")
|
|
1659
|
-
for i, col in enumerate(x.columns, start=1): # 상수항이 0이므로 1부터 시작
|
|
1660
|
-
vif_dict[col] = variance_inflation_factor(x_const.values, i) # type: ignore
|
|
1693
|
+
x = pair_df[a]
|
|
1694
|
+
y = pair_df[b]
|
|
1661
1695
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1696
|
+
# 상수열/분산 0 체크 → 상관계수 계산 불가
|
|
1697
|
+
if x.nunique(dropna=True) <= 1 or y.nunique(dropna=True) <= 1:
|
|
1698
|
+
rows.append(
|
|
1699
|
+
{
|
|
1700
|
+
"var_a": a,
|
|
1701
|
+
"var_b": b,
|
|
1702
|
+
"n": len(pair_df),
|
|
1703
|
+
"linearity": False,
|
|
1704
|
+
"outlier_flag": True,
|
|
1705
|
+
"chosen": None,
|
|
1706
|
+
"corr": np.nan,
|
|
1707
|
+
"pval": np.nan,
|
|
1708
|
+
"significant": False,
|
|
1709
|
+
"strength": "no correlation",
|
|
1710
|
+
}
|
|
1711
|
+
)
|
|
1665
1712
|
continue
|
|
1666
1713
|
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1714
|
+
# 1) 선형성: Ramsey RESET (지정 차수 전부 p>alpha 여야 통과)
|
|
1715
|
+
linearity_ok = False
|
|
1716
|
+
try:
|
|
1717
|
+
X_const = sm.add_constant(x)
|
|
1718
|
+
model = sm.OLS(y, X_const).fit()
|
|
1719
|
+
pvals = []
|
|
1720
|
+
for pwr in linearity_power:
|
|
1721
|
+
reset = linear_reset(model, power=pwr, use_f=True)
|
|
1722
|
+
pvals.append(reset.pvalue)
|
|
1723
|
+
# 모든 차수에서 유의하지 않을 때 선형성 충족으로 간주
|
|
1724
|
+
if len(pvals) > 0:
|
|
1725
|
+
linearity_ok = all([pv > alpha for pv in pvals])
|
|
1726
|
+
except Exception:
|
|
1727
|
+
linearity_ok = False
|
|
1671
1728
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
ci_high = np.exp(beta + 1.96 * se)
|
|
1729
|
+
# 2) 이상치 플래그 (두 변수 중 하나라도 z-outlier 있으면 True)
|
|
1730
|
+
outlier_flag = bool(z_outlier_flags.get(a, False) or z_outlier_flags.get(b, False))
|
|
1675
1731
|
|
|
1676
|
-
|
|
1732
|
+
# 3) 상관 계산: 선형·무이상치면 Pearson, 아니면 Spearman
|
|
1733
|
+
try:
|
|
1734
|
+
if linearity_ok and not outlier_flag:
|
|
1735
|
+
chosen = "pearson"
|
|
1736
|
+
corr_val, pval = pearsonr(x, y)
|
|
1737
|
+
else:
|
|
1738
|
+
chosen = "spearman"
|
|
1739
|
+
corr_val, pval = spearmanr(x, y)
|
|
1740
|
+
except Exception:
|
|
1741
|
+
chosen = None
|
|
1742
|
+
corr_val, pval = np.nan, np.nan
|
|
1677
1743
|
|
|
1678
|
-
|
|
1744
|
+
# 4) 유의성, 강도
|
|
1745
|
+
significant = False if np.isnan(pval) else pval <= alpha # type: ignore
|
|
1746
|
+
abs_r = abs(corr_val) if not np.isnan(corr_val) else 0 # type: ignore
|
|
1747
|
+
if abs_r > 0.7:
|
|
1748
|
+
strength = "strong"
|
|
1749
|
+
elif abs_r > 0.3:
|
|
1750
|
+
strength = "medium"
|
|
1751
|
+
elif abs_r > 0:
|
|
1752
|
+
strength = "weak"
|
|
1753
|
+
else:
|
|
1754
|
+
strength = "no correlation"
|
|
1755
|
+
|
|
1756
|
+
rows.append(
|
|
1679
1757
|
{
|
|
1680
|
-
"
|
|
1681
|
-
"
|
|
1682
|
-
"
|
|
1683
|
-
"
|
|
1684
|
-
"
|
|
1685
|
-
"
|
|
1686
|
-
"
|
|
1687
|
-
"
|
|
1688
|
-
"
|
|
1689
|
-
"
|
|
1690
|
-
"VIF": vif_dict.get(name, np.nan),
|
|
1758
|
+
"var_a": a,
|
|
1759
|
+
"var_b": b,
|
|
1760
|
+
"n": len(pair_df),
|
|
1761
|
+
"linearity": linearity_ok,
|
|
1762
|
+
"outlier_flag": outlier_flag,
|
|
1763
|
+
"chosen": chosen,
|
|
1764
|
+
"corr": corr_val,
|
|
1765
|
+
"pval": pval,
|
|
1766
|
+
"significant": significant,
|
|
1767
|
+
"strength": strength,
|
|
1691
1768
|
}
|
|
1692
1769
|
)
|
|
1693
1770
|
|
|
1694
|
-
|
|
1771
|
+
result_df = DataFrame(rows)
|
|
1695
1772
|
|
|
1696
|
-
#
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1773
|
+
# 5) 다중비교 보정 (선택)
|
|
1774
|
+
if p_adjust.lower() != "none" and not result_df.empty:
|
|
1775
|
+
# 유효한 p만 보정
|
|
1776
|
+
mask = result_df["pval"].notna()
|
|
1777
|
+
if mask.any():
|
|
1778
|
+
_, p_adj, _, _ = multipletests(result_df.loc[mask, "pval"], alpha=alpha, method=p_adjust)
|
|
1779
|
+
result_df.loc[mask, "pval_adj"] = p_adj
|
|
1780
|
+
result_df["significant_adj"] = result_df["pval_adj"] <= alpha
|
|
1700
1781
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
)
|
|
1782
|
+
# 6) 상관행렬 생성 (result_df 기반)
|
|
1783
|
+
# 모든 변수를 행과 열로 하는 대칭 행렬 생성
|
|
1784
|
+
corr_matrix = DataFrame(np.nan, index=cols, columns=cols)
|
|
1785
|
+
# 대각선: 1 (자기상관)
|
|
1786
|
+
for c in cols:
|
|
1787
|
+
corr_matrix.loc[c, c] = 1.0
|
|
1788
|
+
# 쌍별 상관계수 채우기 (대칭성 유지)
|
|
1789
|
+
if not result_df.empty:
|
|
1790
|
+
for _, row in result_df.iterrows():
|
|
1791
|
+
a, b, corr_val = row["var_a"], row["var_b"], row["corr"]
|
|
1792
|
+
corr_matrix.loc[a, b] = corr_val
|
|
1793
|
+
corr_matrix.loc[b, a] = corr_val # 대칭성
|
|
1708
1794
|
|
|
1709
|
-
|
|
1710
|
-
# 모형 보고 문장
|
|
1711
|
-
# -----------------------------
|
|
1712
|
-
tpl = (
|
|
1713
|
-
"%s에 대하여 %s로 예측하는 로지스틱 회귀분석을 실시한 결과, "
|
|
1714
|
-
"모형은 통계적으로 %s(χ²(%s) = %.3f, p %s 0.05)하였다."
|
|
1715
|
-
)
|
|
1716
|
-
|
|
1717
|
-
model_report = tpl % (
|
|
1718
|
-
yname,
|
|
1719
|
-
", ".join(xnames),
|
|
1720
|
-
"유의" if fit.llr_pvalue <= 0.05 else "유의하지 않음",
|
|
1721
|
-
int(fit.df_model),
|
|
1722
|
-
fit.llr,
|
|
1723
|
-
"<=" if fit.llr_pvalue <= 0.05 else ">",
|
|
1724
|
-
)
|
|
1725
|
-
|
|
1726
|
-
# -----------------------------
|
|
1727
|
-
# 변수별 보고 문장
|
|
1728
|
-
# -----------------------------
|
|
1729
|
-
variable_reports = []
|
|
1730
|
-
|
|
1731
|
-
s = (
|
|
1732
|
-
"%s의 오즈비는 %.3f(p %s 0.05)로, "
|
|
1733
|
-
"%s 발생 odds에 %s 영향을 미치는 것으로 나타났다."
|
|
1734
|
-
)
|
|
1735
|
-
|
|
1736
|
-
for _, row in rdf.iterrows():
|
|
1737
|
-
variable_reports.append(
|
|
1738
|
-
s
|
|
1739
|
-
% (
|
|
1740
|
-
row["독립변수"],
|
|
1741
|
-
row["OR"],
|
|
1742
|
-
"<=" if row["p-value"] < 0.05 else ">",
|
|
1743
|
-
row["종속변수"],
|
|
1744
|
-
"유의미한" if row["p-value"] < 0.05 else "유의하지 않은",
|
|
1745
|
-
)
|
|
1746
|
-
)
|
|
1795
|
+
return result_df, corr_matrix
|
|
1747
1796
|
|
|
1748
|
-
if full:
|
|
1749
|
-
return cdf, rdf, result_report, model_report, variable_reports, cm
|
|
1750
|
-
else:
|
|
1751
|
-
return cdf, rdf
|
|
1752
1797
|
|
|
1753
1798
|
|
|
1754
1799
|
# ===================================================================
|
|
1755
|
-
#
|
|
1800
|
+
# 독립변수간 다중공선성 제거
|
|
1756
1801
|
# ===================================================================
|
|
1757
|
-
def
|
|
1758
|
-
|
|
1759
|
-
yname: str,
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
DataFrame
|
|
1766
|
-
],
|
|
1767
|
-
Tuple[
|
|
1768
|
-
BinaryResultsWrapper,
|
|
1769
|
-
DataFrame,
|
|
1770
|
-
DataFrame,
|
|
1771
|
-
str,
|
|
1772
|
-
str,
|
|
1773
|
-
list[str]
|
|
1774
|
-
]
|
|
1775
|
-
]:
|
|
1776
|
-
"""로지스틱 회귀분석을 수행하고 적합 결과를 반환한다.
|
|
1777
|
-
|
|
1778
|
-
종속변수가 이항(binary) 형태일 때 로지스틱 회귀분석을 실시한다.
|
|
1779
|
-
필요시 상세한 통계 보고서를 함께 제공한다.
|
|
1802
|
+
def vif_filter(
|
|
1803
|
+
data: DataFrame,
|
|
1804
|
+
yname: str | None = None,
|
|
1805
|
+
ignore: list | None = None,
|
|
1806
|
+
threshold: float = 10.0,
|
|
1807
|
+
verbose: bool = False,
|
|
1808
|
+
) -> DataFrame:
|
|
1809
|
+
"""독립변수 간 다중공선성을 검사하여 VIF가 threshold 이상인 변수를 반복적으로 제거한다.
|
|
1780
1810
|
|
|
1781
1811
|
Args:
|
|
1782
|
-
|
|
1783
|
-
yname (str): 종속변수 컬럼명.
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
- 2 또는 'full': 풀 리포트 반환 (full=True).
|
|
1788
|
-
- True: 풀 리포트 반환 (2와 동일).
|
|
1812
|
+
data (DataFrame): 데이터프레임
|
|
1813
|
+
yname (str, optional): 종속변수 컬럼명. Defaults to None.
|
|
1814
|
+
ignore (list | None, optional): 제외할 컬럼 목록. Defaults to None.
|
|
1815
|
+
threshold (float, optional): VIF 임계값. Defaults to 10.0.
|
|
1816
|
+
verbose (bool, optional): True일 경우 각 단계의 VIF를 출력한다. Defaults to False.
|
|
1789
1817
|
|
|
1790
1818
|
Returns:
|
|
1791
|
-
|
|
1792
|
-
로지스틱 회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
|
|
1793
|
-
|
|
1794
|
-
tuple (4개): report=1 또는 'summary'일 때.
|
|
1795
|
-
(fit, rdf, result_report, variable_reports) 형태로 (cdf 제외).
|
|
1796
|
-
|
|
1797
|
-
tuple (6개): report=2, 'full' 또는 True일 때.
|
|
1798
|
-
(fit, cdf, rdf, result_report, model_report, variable_reports) 형태로:
|
|
1799
|
-
- fit: 로지스틱 회귀 적합 결과 객체
|
|
1800
|
-
- cdf: 성능 지표 표 (DataFrame)
|
|
1801
|
-
- rdf: 회귀계수 표 (DataFrame)
|
|
1802
|
-
- result_report: 적합도 및 예측 성능 요약 (str)
|
|
1803
|
-
- model_report: 모형 보고 문장 (str)
|
|
1804
|
-
- variable_reports: 변수별 보고 문장 리스트 (list[str])
|
|
1819
|
+
DataFrame: VIF가 threshold 이하인 변수만 남은 데이터프레임 (원본 컬럼 순서 유지)
|
|
1805
1820
|
|
|
1806
1821
|
Examples:
|
|
1807
1822
|
```python
|
|
1823
|
+
# 기본 사용 예
|
|
1808
1824
|
from hossam import *
|
|
1809
|
-
|
|
1810
|
-
|
|
1825
|
+
filtered = hs_stats.vif_filter(df, yname="target", ignore=["id"], threshold=10.0)
|
|
1826
|
+
```
|
|
1827
|
+
"""
|
|
1811
1828
|
|
|
1812
|
-
|
|
1813
|
-
'target': np.random.binomial(1, 0.5, 100),
|
|
1814
|
-
'x1': np.random.normal(0, 1, 100),
|
|
1815
|
-
'x2': np.random.normal(0, 1, 100)
|
|
1816
|
-
})
|
|
1829
|
+
df = data.copy()
|
|
1817
1830
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1831
|
+
# y 분리 (있다면)
|
|
1832
|
+
y = None
|
|
1833
|
+
if yname and yname in df.columns:
|
|
1834
|
+
y = df[yname]
|
|
1835
|
+
df = df.drop(columns=[yname])
|
|
1820
1836
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1837
|
+
# 제외할 목록 정리
|
|
1838
|
+
ignore = ignore or []
|
|
1839
|
+
ignore_cols_present = [c for c in ignore if c in df.columns]
|
|
1823
1840
|
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
"""
|
|
1828
|
-
x = df.drop(yname, axis=1)
|
|
1829
|
-
y = df[yname]
|
|
1841
|
+
# VIF 대상 수치형 컬럼 선택 (bool은 연속형이 아니므로 제외)
|
|
1842
|
+
numeric_df = df.select_dtypes(include=[np.number])
|
|
1843
|
+
numeric_cols = [c for c in numeric_df.columns if not is_bool_dtype(numeric_df[c])]
|
|
1830
1844
|
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1845
|
+
# VIF 대상 X 구성 (수치형에서 제외 목록 제거)
|
|
1846
|
+
X = df[numeric_cols]
|
|
1847
|
+
if ignore_cols_present:
|
|
1848
|
+
X = X.drop(columns=ignore_cols_present, errors="ignore")
|
|
1849
|
+
|
|
1850
|
+
# 수치형 변수가 없으면 바로 반환
|
|
1851
|
+
if X.shape[1] == 0:
|
|
1852
|
+
result = data.copy()
|
|
1853
|
+
return result
|
|
1854
|
+
|
|
1855
|
+
def _compute_vifs(X_: DataFrame) -> dict:
|
|
1856
|
+
# NA 제거 후 상수항 추가
|
|
1857
|
+
X_clean = X_.dropna()
|
|
1858
|
+
if X_clean.shape[0] == 0:
|
|
1859
|
+
# 데이터가 모두 NA인 경우 VIF 계산 불가: NaN 반환
|
|
1860
|
+
return {col: np.nan for col in X_.columns}
|
|
1861
|
+
if X_clean.shape[1] == 1:
|
|
1862
|
+
# 단일 예측변수의 경우 다른 설명변수가 없으므로 VIF는 1로 간주
|
|
1863
|
+
return {col: 1.0 for col in X_clean.columns}
|
|
1864
|
+
exog = sm.add_constant(X_clean, prepend=True)
|
|
1865
|
+
vifs = {}
|
|
1866
|
+
for i, col in enumerate(X_clean.columns, start=0):
|
|
1867
|
+
# exog의 첫 열은 상수항이므로 변수 인덱스는 +1
|
|
1868
|
+
try:
|
|
1869
|
+
vifs[col] = float(variance_inflation_factor(exog.values, i + 1))# type: ignore
|
|
1870
|
+
except Exception:
|
|
1871
|
+
# 계산 실패 시 무한대로 처리하여 우선 제거 대상으로
|
|
1872
|
+
vifs[col] = float("inf")
|
|
1873
|
+
return vifs
|
|
1874
|
+
|
|
1875
|
+
# 반복 제거 루프
|
|
1876
|
+
while True:
|
|
1877
|
+
if X.shape[1] == 0:
|
|
1878
|
+
break
|
|
1879
|
+
vifs = _compute_vifs(X)
|
|
1880
|
+
if verbose:
|
|
1881
|
+
print(vifs)
|
|
1882
|
+
# 모든 변수가 임계값 이하이면 종료
|
|
1883
|
+
max_key = max(vifs, key=lambda k: (vifs[k] if not np.isnan(vifs[k]) else -np.inf))
|
|
1884
|
+
max_vif = vifs[max_key]
|
|
1885
|
+
if np.isnan(max_vif) or max_vif <= threshold:
|
|
1886
|
+
break
|
|
1887
|
+
# 가장 큰 VIF 변수 제거
|
|
1888
|
+
X = X.drop(columns=[max_key])
|
|
1889
|
+
|
|
1890
|
+
# 출력 옵션이 False일 경우 최종 값만 출력
|
|
1891
|
+
if not verbose:
|
|
1892
|
+
final_vifs = _compute_vifs(X) if X.shape[1] > 0 else {}
|
|
1893
|
+
print(final_vifs)
|
|
1894
|
+
|
|
1895
|
+
# 원본 컬럼 순서 유지하며 제거된 수치형 컬럼만 제외
|
|
1896
|
+
kept_numeric_cols = list(X.columns)
|
|
1897
|
+
removed_numeric_cols = [c for c in numeric_cols if c not in kept_numeric_cols]
|
|
1898
|
+
result = data.drop(columns=removed_numeric_cols, errors="ignore")
|
|
1899
|
+
|
|
1900
|
+
return result
|
|
1834
1901
|
|
|
1835
|
-
# report 파라미터에 따른 처리
|
|
1836
|
-
if not report or report is False:
|
|
1837
|
-
# 리포트 미사용
|
|
1838
|
-
return logit_fit
|
|
1839
|
-
elif report == 1 or report == 'summary':
|
|
1840
|
-
# 요약 리포트 (full=False)
|
|
1841
|
-
cdf, rdf = logit_report(logit_fit, df, threshold=0.5, full=False, alpha=0.05) # type: ignore
|
|
1842
|
-
# 요약에서는 result_report와 variable_reports만 포함
|
|
1843
|
-
# 간단한 버전으로 result와 variable_reports만 생성
|
|
1844
|
-
return logit_fit, rdf
|
|
1845
|
-
elif report == 2 or report == 'full' or report is True:
|
|
1846
|
-
# 풀 리포트 (full=True)
|
|
1847
|
-
cdf, rdf, result_report, model_report, variable_reports, cm = logit_report(logit_fit, df, threshold=0.5, full=True, alpha=0.05) # type: ignore
|
|
1848
|
-
return logit_fit, cdf, rdf, result_report, model_report, variable_reports
|
|
1849
|
-
else:
|
|
1850
|
-
# 기본값: 리포트 미사용
|
|
1851
|
-
return logit_fit
|
|
1852
1902
|
|
|
1853
1903
|
|
|
1854
1904
|
# ===================================================================
|
|
1855
|
-
#
|
|
1905
|
+
# x, y 데이터에 대한 추세선을 구한다.
|
|
1856
1906
|
# ===================================================================
|
|
1857
|
-
def
|
|
1858
|
-
"""
|
|
1859
|
-
|
|
1860
|
-
적합된 회귀모형에 대해 Ramsey RESET(Regression Specification Error Test) 검정을 수행하여
|
|
1861
|
-
모형의 선형성 가정이 타당한지를 검정한다. 귀무가설은 '모형이 선형이다'이다.
|
|
1907
|
+
def trend(x: Any, y: Any, degree: int = 1, value_count: int = 100) -> Tuple[np.ndarray, np.ndarray]:
|
|
1908
|
+
"""x, y 데이터에 대한 추세선을 구한다.
|
|
1862
1909
|
|
|
1863
1910
|
Args:
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
power가 클수록 더 높은 차수의 비선형성을 감지.
|
|
1869
|
-
alpha (float, optional): 유의수준. 기본값 0.05.
|
|
1870
|
-
plot (bool, optional): True이면 잔차 플롯을 출력. 기본값 False.
|
|
1871
|
-
title (str, optional): 플롯 제목. 기본값 None.
|
|
1872
|
-
save_path (str, optional): 플롯을 저장할 경로. 기본값 None
|
|
1911
|
+
x (_type_): 산점도 그래프에 대한 x 데이터
|
|
1912
|
+
y (_type_): 산점도 그래프에 대한 y 데이터
|
|
1913
|
+
degree (int, optional): 추세선 방정식의 차수. Defaults to 1.
|
|
1914
|
+
value_count (int, optional): x 데이터의 범위 안에서 간격 수. Defaults to 100.
|
|
1873
1915
|
|
|
1874
1916
|
Returns:
|
|
1875
|
-
|
|
1876
|
-
- 검정통계량: F-statistic
|
|
1877
|
-
- p-value: 검정의 p값
|
|
1878
|
-
- 유의성: alpha 기준 결과 (bool)
|
|
1879
|
-
- 해석: 선형성 판정 (문자열)
|
|
1917
|
+
tuple: (v_trend, t_trend)
|
|
1880
1918
|
|
|
1881
1919
|
Examples:
|
|
1882
1920
|
```python
|
|
1921
|
+
# 2차 다항 회귀 추세선
|
|
1883
1922
|
from hossam import *
|
|
1884
|
-
|
|
1885
|
-
|
|
1923
|
+
vx, vy = hs_stats.trend(x, y, degree=2, value_count=200)
|
|
1924
|
+
print(len(vx), len(vy)) # 200, 200
|
|
1886
1925
|
```
|
|
1887
|
-
|
|
1888
|
-
Notes:
|
|
1889
|
-
- p-value > alpha: 선형성 가정을 만족 (귀무가설 채택)
|
|
1890
|
-
- p-value <= alpha: 선형성 가정 위반 가능 (귀무가설 기각)
|
|
1891
1926
|
"""
|
|
1892
|
-
|
|
1927
|
+
# [ a, b, c ] ==> ax^2 + bx + c
|
|
1928
|
+
x_arr = np.asarray(x)
|
|
1929
|
+
y_arr = np.asarray(y)
|
|
1893
1930
|
|
|
1894
|
-
|
|
1895
|
-
|
|
1931
|
+
if x_arr.ndim == 0 or y_arr.ndim == 0:
|
|
1932
|
+
raise ValueError("x, y는 1차원 이상의 배열이어야 합니다.")
|
|
1896
1933
|
|
|
1897
|
-
|
|
1898
|
-
test_stat = None
|
|
1899
|
-
p_value = None
|
|
1934
|
+
coeff = np.polyfit(x_arr, y_arr, degree)
|
|
1900
1935
|
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
# 정규식으로 숫자값들 추출 (F-statistic과 p-value)
|
|
1906
|
-
numbers = re.findall(r'\d+\.?\d*[eE]?-?\d*', result_str)
|
|
1907
|
-
|
|
1908
|
-
if len(numbers) >= 2:
|
|
1909
|
-
# 일반적으로 첫 번째는 F-statistic, 마지막은 p-value
|
|
1910
|
-
test_stat = float(numbers[0])
|
|
1911
|
-
p_value = float(numbers[-1])
|
|
1912
|
-
except (ValueError, IndexError, AttributeError):
|
|
1913
|
-
pass
|
|
1914
|
-
|
|
1915
|
-
# 정규식 실패 시 직접 속성 접근
|
|
1916
|
-
if test_stat is None or p_value is None:
|
|
1917
|
-
attr_pairs = [
|
|
1918
|
-
('statistic', 'pvalue'),
|
|
1919
|
-
('test_stat', 'pvalue'),
|
|
1920
|
-
('fvalue', 'pvalue'),
|
|
1921
|
-
]
|
|
1922
|
-
|
|
1923
|
-
for attr_stat, attr_pval in attr_pairs:
|
|
1924
|
-
if hasattr(reset_result, attr_stat) and hasattr(reset_result, attr_pval):
|
|
1925
|
-
try:
|
|
1926
|
-
test_stat = float(getattr(reset_result, attr_stat))
|
|
1927
|
-
p_value = float(getattr(reset_result, attr_pval))
|
|
1928
|
-
break
|
|
1929
|
-
except (ValueError, TypeError):
|
|
1930
|
-
continue
|
|
1931
|
-
|
|
1932
|
-
# 여전히 값을 못 찾으면 에러
|
|
1933
|
-
if test_stat is None or p_value is None:
|
|
1934
|
-
raise ValueError(f"linear_reset 결과를 해석할 수 없습니다. 반환값: {reset_result}")
|
|
1935
|
-
|
|
1936
|
-
# 유의성 판정
|
|
1937
|
-
significant = p_value <= alpha
|
|
1938
|
-
|
|
1939
|
-
# 해석 문구
|
|
1940
|
-
if significant:
|
|
1941
|
-
interpretation = f"선형성 가정 위반 (p={p_value:.4f} <= {alpha})"
|
|
1942
|
-
else:
|
|
1943
|
-
interpretation = f"선형성 가정 만족 (p={p_value:.4f} > {alpha})"
|
|
1944
|
-
|
|
1945
|
-
# 결과를 DataFrame으로 반환
|
|
1946
|
-
result_df = DataFrame({
|
|
1947
|
-
"검정": ["Ramsey RESET"],
|
|
1948
|
-
"검정통계량 (F)": [f"{test_stat:.4f}"],
|
|
1949
|
-
"p-value": [f"{p_value:.4f}"],
|
|
1950
|
-
"유의수준": [alpha],
|
|
1951
|
-
"선형성_위반": [significant], # True: 선형성 위반, False: 선형성 만족
|
|
1952
|
-
"해석": [interpretation]
|
|
1953
|
-
})
|
|
1936
|
+
minx = np.min(x_arr)
|
|
1937
|
+
maxx = np.max(x_arr)
|
|
1938
|
+
v_trend = np.linspace(minx, maxx, value_count)
|
|
1954
1939
|
|
|
1955
|
-
|
|
1956
|
-
|
|
1940
|
+
# np.polyval 사용으로 간결하게 추세선 계산
|
|
1941
|
+
t_trend = np.polyval(coeff, v_trend)
|
|
1957
1942
|
|
|
1958
|
-
return
|
|
1943
|
+
return (v_trend, t_trend)
|
|
1959
1944
|
|
|
1960
1945
|
|
|
1961
1946
|
# ===================================================================
|
|
1962
|
-
#
|
|
1947
|
+
# 선형회귀 요약 리포트
|
|
1963
1948
|
# ===================================================================
|
|
1964
|
-
|
|
1965
|
-
|
|
1949
|
+
@overload
|
|
1950
|
+
def ols_report(
|
|
1951
|
+
fit: RegressionResultsWrapper,
|
|
1952
|
+
data: DataFrame,
|
|
1953
|
+
full: Literal[False],
|
|
1954
|
+
alpha: float = 0.05
|
|
1955
|
+
) -> tuple[DataFrame, DataFrame]: ...
|
|
1966
1956
|
|
|
1967
|
-
|
|
1968
|
-
|
|
1957
|
+
@overload
|
|
1958
|
+
def ols_report(
|
|
1959
|
+
fit: RegressionResultsWrapper,
|
|
1960
|
+
data: DataFrame,
|
|
1961
|
+
full: Literal[True],
|
|
1962
|
+
alpha: float = 0.05
|
|
1963
|
+
) -> tuple[
|
|
1964
|
+
DataFrame,
|
|
1965
|
+
DataFrame,
|
|
1966
|
+
str,
|
|
1967
|
+
LiteralString,
|
|
1968
|
+
list[str],
|
|
1969
|
+
str
|
|
1970
|
+
]: ...
|
|
1971
|
+
|
|
1972
|
+
def ols_report(
|
|
1973
|
+
fit: RegressionResultsWrapper,
|
|
1974
|
+
data: DataFrame,
|
|
1975
|
+
full: bool = False,
|
|
1976
|
+
alpha: float = 0.05
|
|
1977
|
+
):
|
|
1978
|
+
"""선형회귀 적합 결과를 요약 리포트로 변환한다.
|
|
1969
1979
|
|
|
1970
1980
|
Args:
|
|
1971
|
-
fit:
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
save_path (str, optional): 플롯을 저장할 경로. 기본값 None
|
|
1981
|
+
fit: statsmodels OLS 등 선형회귀 결과 객체 (`fit.summary()`를 지원해야 함).
|
|
1982
|
+
data: 종속변수와 독립변수를 모두 포함한 DataFrame.
|
|
1983
|
+
full: True이면 6개 값 반환, False이면 회귀계수 테이블(rdf)만 반환. 기본값 True.
|
|
1984
|
+
alpha: 유의수준. 기본값 0.05.
|
|
1976
1985
|
|
|
1977
1986
|
Returns:
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1987
|
+
tuple: full=True일 때 다음 요소를 포함한다.
|
|
1988
|
+
- 성능 지표 표 (`pdf`, DataFrame): R, R², Adj. R², F, p-value, Durbin-Watson.
|
|
1989
|
+
- 회귀계수 표 (`rdf`, DataFrame): 변수별 B, 표준오차, Beta, t, p-value, significant, 공차, VIF.
|
|
1990
|
+
- 적합도 요약 (`result_report`, str): R, R², F, p-value, Durbin-Watson 등 핵심 지표 문자열.
|
|
1991
|
+
- 모형 보고 문장 (`model_report`, str): F-검정 유의성에 기반한 서술형 문장.
|
|
1992
|
+
- 변수별 보고 리스트 (`variable_reports`, list[str]): 각 예측변수에 대한 서술형 문장.
|
|
1993
|
+
- 회귀식 문자열 (`equation_text`, str): 상수항과 계수를 포함한 회귀식 표현.
|
|
1994
|
+
|
|
1995
|
+
full=False일 때:
|
|
1996
|
+
- 성능 지표 표 (`pdf`, DataFrame): R, R², Adj. R², F, p-value, Durbin-Watson.
|
|
1997
|
+
- 회귀계수 표 (`rdf`, DataFrame)
|
|
1985
1998
|
|
|
1986
1999
|
Examples:
|
|
1987
2000
|
```python
|
|
1988
2001
|
from hossam import *
|
|
1989
|
-
fit = hs_stats.logit(df, 'target')
|
|
1990
|
-
result = hs_stats.ols_normality_test(fit)
|
|
1991
|
-
```
|
|
1992
2002
|
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
- Jarque-Bera: 왜도(skewness)와 첨도(kurtosis) 기반 검정
|
|
1996
|
-
- p-value > alpha: 정규성 가정 만족 (귀무가설 채택)
|
|
1997
|
-
- p-value <= alpha: 정규성 가정 위반 (귀무가설 기각)
|
|
1998
|
-
"""
|
|
1999
|
-
from scipy.stats import jarque_bera
|
|
2003
|
+
df = hs_util.load_data("some_data.csv")
|
|
2004
|
+
fit = hs_stats.ols(df, yname="target")
|
|
2000
2005
|
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
n = len(residuals)
|
|
2006
|
+
# 전체 리포트
|
|
2007
|
+
pdf, rdf, result_report, model_report, variable_reports, eq = hs_stats.ols_report(fit, data, full=True)
|
|
2004
2008
|
|
|
2005
|
-
|
|
2009
|
+
# 간단한 버전 (성능지표, 회귀계수 테이블만)
|
|
2010
|
+
pdf, rdf = hs_stats.ols_report(fit, data)
|
|
2011
|
+
```
|
|
2012
|
+
"""
|
|
2006
2013
|
|
|
2007
|
-
#
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
stat_sw, p_sw = shapiro(residuals)
|
|
2011
|
-
significant_sw = p_sw <= alpha
|
|
2014
|
+
# summary2() 결과에서 실제 회귀계수 DataFrame 추출
|
|
2015
|
+
summary_obj = fit.summary2()
|
|
2016
|
+
tbl = summary_obj.tables[1] # 회귀계수 테이블은 tables[1]에 위치
|
|
2012
2017
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
else:
|
|
2016
|
-
interpretation_sw = f"정규성 만족 (p={p_sw:.4f} > {alpha})"
|
|
2018
|
+
# 종속변수 이름
|
|
2019
|
+
yname = fit.model.endog_names
|
|
2017
2020
|
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
"검정통계량": f"{stat_sw:.4f}",
|
|
2021
|
-
"p-value": f"{p_sw:.4f}",
|
|
2022
|
-
"유의수준": alpha,
|
|
2023
|
-
"정규성_위반": significant_sw,
|
|
2024
|
-
"해석": interpretation_sw
|
|
2025
|
-
})
|
|
2026
|
-
except Exception as e:
|
|
2027
|
-
pass
|
|
2021
|
+
# 독립변수 이름(상수항 제외)
|
|
2022
|
+
xnames = [n for n in fit.model.exog_names if n != "const"]
|
|
2028
2023
|
|
|
2029
|
-
#
|
|
2030
|
-
|
|
2031
|
-
stat_jb, p_jb = jarque_bera(residuals)
|
|
2032
|
-
significant_jb = p_jb <= alpha # type: ignore
|
|
2024
|
+
# 독립변수 부분 데이터 (VIF 계산용)
|
|
2025
|
+
indi_df = data.filter(xnames)
|
|
2033
2026
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
else:
|
|
2037
|
-
interpretation_jb = f"정규성 만족 (p={p_jb:.4f} > {alpha})"
|
|
2027
|
+
# 독립변수 결과를 누적
|
|
2028
|
+
variables = []
|
|
2038
2029
|
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2030
|
+
# VIF 계산 (상수항 포함 설계행렬 사용)
|
|
2031
|
+
vif_dict = {}
|
|
2032
|
+
indi_df_const = sm.add_constant(indi_df, has_constant="add")
|
|
2033
|
+
for i, col in enumerate(indi_df.columns, start=1): # 상수항이 0이므로 1부터 시작
|
|
2034
|
+
try:
|
|
2035
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
2036
|
+
vif_value = variance_inflation_factor(indi_df_const.values, i) # type: ignore
|
|
2037
|
+
# inf나 매우 큰 값 처리
|
|
2038
|
+
if np.isinf(vif_value) or vif_value > 1e10:
|
|
2039
|
+
vif_dict[col] = np.inf
|
|
2040
|
+
else:
|
|
2041
|
+
vif_dict[col] = vif_value
|
|
2042
|
+
except:
|
|
2043
|
+
vif_dict[col] = np.inf
|
|
2049
2044
|
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2045
|
+
for idx, row in tbl.iterrows():
|
|
2046
|
+
name = idx
|
|
2047
|
+
if name not in xnames:
|
|
2048
|
+
continue
|
|
2053
2049
|
|
|
2050
|
+
b = float(row['Coef.'])
|
|
2051
|
+
se = float(row['Std.Err.'])
|
|
2052
|
+
t = float(row['t'])
|
|
2053
|
+
p = float(row['P>|t|'])
|
|
2054
2054
|
|
|
2055
|
-
|
|
2056
|
-
|
|
2055
|
+
# 표준화 회귀계수(β) 계산
|
|
2056
|
+
beta = b * (data[name].std(ddof=1) / data[yname].std(ddof=1))
|
|
2057
2057
|
|
|
2058
|
-
|
|
2059
|
-
|
|
2058
|
+
# VIF 값
|
|
2059
|
+
vif = vif_dict.get(name, np.nan)
|
|
2060
2060
|
|
|
2061
|
+
# 유의확률과 별표 표시
|
|
2062
|
+
stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
|
|
2061
2063
|
|
|
2062
|
-
#
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2064
|
+
# 한 변수에 대한 보고 정보 추가
|
|
2065
|
+
variables.append(
|
|
2066
|
+
{
|
|
2067
|
+
"종속변수": yname, # 종속변수 이름
|
|
2068
|
+
"독립변수": name, # 독립변수 이름
|
|
2069
|
+
"B": f"{b:.6f}", # 비표준화 회귀계수(B)
|
|
2070
|
+
"표준오차": f"{se:.6f}", # 계수 표준오차
|
|
2071
|
+
"Beta": beta, # 표준화 회귀계수(β)
|
|
2072
|
+
"t": f"{t:.3f}{stars}", # t-통계량(+별표)
|
|
2073
|
+
"p-value": p, # 계수 유의확률
|
|
2074
|
+
"significant": p <= alpha, # 유의성 여부 (boolean)
|
|
2075
|
+
"공차": 1 / vif, # 공차(Tolerance = 1/VIF)
|
|
2076
|
+
"vif": vif, # 분산팽창계수
|
|
2077
|
+
}
|
|
2078
|
+
)
|
|
2067
2079
|
|
|
2068
|
-
|
|
2069
|
-
등분산성 가정은 회귀분석의 추론(표준오차, 검정통계량)이 정확하기 위한 중요한 가정이다.
|
|
2080
|
+
rdf = DataFrame(variables)
|
|
2070
2081
|
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2082
|
+
# summary 표에서 적합도 정보를 key-value로 추출
|
|
2083
|
+
result_dict = {}
|
|
2084
|
+
summary_main = fit.summary()
|
|
2085
|
+
for i in [0, 2]:
|
|
2086
|
+
for item in summary_main.tables[i].data:
|
|
2087
|
+
n = len(item)
|
|
2088
|
+
for i in range(0, n, 2):
|
|
2089
|
+
key = item[i].strip()[:-1]
|
|
2090
|
+
value = item[i + 1].strip()
|
|
2091
|
+
if not key or not value:
|
|
2092
|
+
continue
|
|
2093
|
+
result_dict[key] = value
|
|
2074
2094
|
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
- 검정명: 사용된 검정 방법
|
|
2078
|
-
- 검정통계량: 검정 통계량 값 (LM 또는 F)
|
|
2079
|
-
- p-value: 검정의 p값
|
|
2080
|
-
- 유의수준: 설정된 유의수준
|
|
2081
|
-
- 등분산성_위반: alpha 기준 결과 (bool)
|
|
2082
|
-
- 해석: 등분산성 판정 (문자열)
|
|
2095
|
+
# 적합도 보고 문자열 구성
|
|
2096
|
+
result_report = f"𝑅({result_dict['R-squared']}), 𝑅^2({result_dict['Adj. R-squared']}), 𝐹({result_dict['F-statistic']}), 유의확률({result_dict['Prob (F-statistic)']}), Durbin-Watson({result_dict['Durbin-Watson']})"
|
|
2083
2097
|
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2098
|
+
# 모형 보고 문장 구성
|
|
2099
|
+
tpl = "%s에 대하여 %s로 예측하는 회귀분석을 실시한 결과, 이 회귀모형은 통계적으로 %s(F(%s,%s) = %s, p %s 0.05)."
|
|
2100
|
+
model_report = tpl % (
|
|
2101
|
+
rdf["종속변수"][0],
|
|
2102
|
+
",".join(list(rdf["독립변수"])),
|
|
2103
|
+
(
|
|
2104
|
+
"유의하다"
|
|
2105
|
+
if float(result_dict["Prob (F-statistic)"]) <= 0.05
|
|
2106
|
+
else "유의하지 않다"
|
|
2107
|
+
),
|
|
2108
|
+
result_dict["Df Model"],
|
|
2109
|
+
result_dict["Df Residuals"],
|
|
2110
|
+
result_dict["F-statistic"],
|
|
2111
|
+
"<=" if float(result_dict["Prob (F-statistic)"]) <= 0.05 else ">",
|
|
2112
|
+
)
|
|
2090
2113
|
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
- p-value > alpha: 등분산성 가정 만족 (귀무가설 채택)
|
|
2095
|
-
- p-value <= alpha: 이분산성 존재 (귀무가설 기각)
|
|
2096
|
-
"""
|
|
2114
|
+
# 변수별 보고 문장 리스트 구성
|
|
2115
|
+
variable_reports = []
|
|
2116
|
+
s = "%s의 회귀계수는 %s(p %s 0.05)로, %s에 대하여 %s 예측변인인 것으로 나타났다."
|
|
2097
2117
|
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2118
|
+
for i in rdf.index:
|
|
2119
|
+
row = rdf.iloc[i]
|
|
2120
|
+
variable_reports.append(
|
|
2121
|
+
s
|
|
2122
|
+
% (
|
|
2123
|
+
row["독립변수"],
|
|
2124
|
+
row["B"],
|
|
2125
|
+
"<=" if float(row["p-value"]) < 0.05 else ">",
|
|
2126
|
+
row["종속변수"],
|
|
2127
|
+
"유의미한" if float(row["p-value"]) < 0.05 else "유의하지 않은",
|
|
2128
|
+
)
|
|
2129
|
+
)
|
|
2101
2130
|
|
|
2102
|
-
|
|
2131
|
+
# -----------------------------
|
|
2132
|
+
# 회귀식 자동 출력
|
|
2133
|
+
# -----------------------------
|
|
2134
|
+
intercept = fit.params["const"]
|
|
2135
|
+
terms = []
|
|
2103
2136
|
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2137
|
+
for name in xnames:
|
|
2138
|
+
coef = fit.params[name]
|
|
2139
|
+
sign = "+" if coef >= 0 else "-"
|
|
2140
|
+
terms.append(f" {sign} {abs(coef):.3f}·{name}")
|
|
2108
2141
|
|
|
2109
|
-
|
|
2110
|
-
interpretation_bp = f"등분산성 위반 (p={lm_pvalue:.4f} <= {alpha})"
|
|
2111
|
-
else:
|
|
2112
|
-
interpretation_bp = f"등분산성 만족 (p={lm_pvalue:.4f} > {alpha})"
|
|
2142
|
+
equation_text = f"{yname} = {intercept:.3f}" + "".join(terms)
|
|
2113
2143
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
"
|
|
2118
|
-
"
|
|
2119
|
-
"
|
|
2120
|
-
"
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2144
|
+
# 성능 지표 표 생성 (pdf)
|
|
2145
|
+
pdf = DataFrame(
|
|
2146
|
+
{
|
|
2147
|
+
"R": [float(result_dict.get('R-squared', np.nan))],
|
|
2148
|
+
"R²": [float(result_dict.get('Adj. R-squared', np.nan))],
|
|
2149
|
+
"F": [float(result_dict.get('F-statistic', np.nan))],
|
|
2150
|
+
"p-value": [float(result_dict.get('Prob (F-statistic)', np.nan))],
|
|
2151
|
+
"Durbin-Watson": [float(result_dict.get('Durbin-Watson', np.nan))],
|
|
2152
|
+
}
|
|
2153
|
+
)
|
|
2124
2154
|
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2155
|
+
if full:
|
|
2156
|
+
return pdf, rdf, result_report, model_report, variable_reports, equation_text
|
|
2157
|
+
else:
|
|
2158
|
+
return pdf, rdf
|
|
2129
2159
|
|
|
2130
|
-
if significant_white:
|
|
2131
|
-
interpretation_white = f"등분산성 위반 (p={lm_pvalue:.4f} <= {alpha})"
|
|
2132
|
-
else:
|
|
2133
|
-
interpretation_white = f"등분산성 만족 (p={lm_pvalue:.4f} > {alpha})"
|
|
2134
2160
|
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2161
|
+
# ===================================================================
|
|
2162
|
+
# 선형회귀
|
|
2163
|
+
# ===================================================================
|
|
2164
|
+
@overload
|
|
2165
|
+
def ols(
|
|
2166
|
+
df: DataFrame,
|
|
2167
|
+
yname: str,
|
|
2168
|
+
report: Literal[False]
|
|
2169
|
+
) -> RegressionResultsWrapper: ...
|
|
2170
|
+
|
|
2171
|
+
@overload
|
|
2172
|
+
def ols(
|
|
2173
|
+
df: DataFrame,
|
|
2174
|
+
yname: str,
|
|
2175
|
+
report: Literal["summary"]
|
|
2176
|
+
) -> tuple[RegressionResultsWrapper, DataFrame, DataFrame]: ...
|
|
2177
|
+
|
|
2178
|
+
@overload
|
|
2179
|
+
def ols(
|
|
2180
|
+
df: DataFrame,
|
|
2181
|
+
yname: str,
|
|
2182
|
+
report: Literal["full"]
|
|
2183
|
+
) -> tuple[
|
|
2184
|
+
RegressionResultsWrapper,
|
|
2185
|
+
DataFrame,
|
|
2186
|
+
DataFrame,
|
|
2187
|
+
str,
|
|
2188
|
+
str,
|
|
2189
|
+
list[str],
|
|
2190
|
+
str
|
|
2191
|
+
]: ...
|
|
2192
|
+
|
|
2193
|
+
def ols(df: DataFrame, yname: str, report: Literal[False, "summary", "full"] = "summary") -> Union[
|
|
2194
|
+
Tuple[RegressionResultsWrapper, DataFrame, DataFrame],
|
|
2195
|
+
Tuple[
|
|
2196
|
+
RegressionResultsWrapper,
|
|
2197
|
+
DataFrame,
|
|
2198
|
+
DataFrame,
|
|
2199
|
+
str,
|
|
2200
|
+
str,
|
|
2201
|
+
list[str],
|
|
2202
|
+
str
|
|
2203
|
+
],
|
|
2204
|
+
RegressionResultsWrapper
|
|
2205
|
+
]:
|
|
2206
|
+
"""선형회귀분석을 수행하고 적합 결과를 반환한다.
|
|
2207
|
+
|
|
2208
|
+
OLS(Ordinary Least Squares) 선형회귀분석을 실시한다.
|
|
2209
|
+
필요시 상세한 통계 보고서를 함께 제공한다.
|
|
2210
|
+
|
|
2211
|
+
Args:
|
|
2212
|
+
df (DataFrame): 종속변수와 독립변수를 모두 포함한 데이터프레임.
|
|
2213
|
+
yname (str): 종속변수 컬럼명.
|
|
2214
|
+
report (bool | str): 리포트 모드 설정. 다음 값 중 하나:
|
|
2215
|
+
- False (기본값): 리포트 미사용. fit 객체만 반환.
|
|
2216
|
+
- 1 또는 'summary': 요약 리포트 반환 (full=False).
|
|
2217
|
+
- 2 또는 'full': 풀 리포트 반환 (full=True).
|
|
2218
|
+
- True: 풀 리포트 반환 (2와 동일).
|
|
2219
|
+
|
|
2220
|
+
Returns:
|
|
2221
|
+
statsmodels.regression.linear_model.RegressionResultsWrapper: report=False일 때.
|
|
2222
|
+
선형회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
|
|
2223
|
+
|
|
2224
|
+
tuple (6개): report=1 또는 'summary'일 때.
|
|
2225
|
+
(fit, rdf, result_report, model_report, variable_reports, equation_text) 형태로 (pdf 제외).
|
|
2226
|
+
|
|
2227
|
+
tuple (7개): report=2, 'full' 또는 True일 때.
|
|
2228
|
+
(fit, pdf, rdf, result_report, model_report, variable_reports, equation_text) 형태로:
|
|
2229
|
+
- fit: 선형회귀 적합 결과 객체
|
|
2230
|
+
- pdf: 성능 지표 표 (DataFrame): R, R², F, p-value, Durbin-Watson
|
|
2231
|
+
- rdf: 회귀계수 표 (DataFrame)
|
|
2232
|
+
- result_report: 적합도 요약 (str)
|
|
2233
|
+
- model_report: 모형 보고 문장 (str)
|
|
2234
|
+
- variable_reports: 변수별 보고 문장 리스트 (list[str])
|
|
2235
|
+
- equation_text: 회귀식 문자열 (str)
|
|
2236
|
+
|
|
2237
|
+
Examples:
|
|
2238
|
+
```python
|
|
2239
|
+
from hossam import *
|
|
2240
|
+
from pandas import DataFrame
|
|
2241
|
+
import numpy as np
|
|
2242
|
+
|
|
2243
|
+
df = DataFrame({
|
|
2244
|
+
'target': np.random.normal(100, 10, 100),
|
|
2245
|
+
'x1': np.random.normal(0, 1, 100),
|
|
2246
|
+
'x2': np.random.normal(0, 1, 100)
|
|
2142
2247
|
})
|
|
2143
|
-
except Exception as e:
|
|
2144
|
-
pass
|
|
2145
2248
|
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
raise ValueError("등분산성 검정을 수행할 수 없습니다.")
|
|
2249
|
+
# 적합 결과만 반환
|
|
2250
|
+
fit = hs_stats.ols(df, 'target')
|
|
2149
2251
|
|
|
2150
|
-
|
|
2151
|
-
|
|
2252
|
+
# 요약 리포트 반환
|
|
2253
|
+
fit, pdf, rdf = hs_stats.ols(df, 'target', report='summary')
|
|
2254
|
+
|
|
2255
|
+
# 풀 리포트 반환
|
|
2256
|
+
fit, pdf, rdf, result_report, model_report, var_reports, eq = hs_stats.ols(df, 'target', report='full')
|
|
2257
|
+
```
|
|
2258
|
+
"""
|
|
2259
|
+
x = df.drop(yname, axis=1)
|
|
2260
|
+
y = df[yname]
|
|
2261
|
+
|
|
2262
|
+
X_const = sm.add_constant(x)
|
|
2263
|
+
linear_model = sm.OLS(y, X_const)
|
|
2264
|
+
linear_fit = linear_model.fit()
|
|
2265
|
+
|
|
2266
|
+
# report 파라미터에 따른 처리
|
|
2267
|
+
if not report or report is False:
|
|
2268
|
+
# 리포트 미사용
|
|
2269
|
+
return linear_fit
|
|
2270
|
+
elif report == 'full':
|
|
2271
|
+
# 풀 리포트 (full=True)
|
|
2272
|
+
pdf, rdf, result_report, model_report, variable_reports, equation_text = ols_report(linear_fit, df, full=True, alpha=0.05) # type: ignore
|
|
2273
|
+
return linear_fit, pdf, rdf, result_report, model_report, variable_reports, equation_text
|
|
2274
|
+
else:
|
|
2275
|
+
# 요약 리포트 (full=False) -> report == 1 or report == 'summary':
|
|
2276
|
+
pdf, rdf = ols_report(linear_fit, df, full=False, alpha=0.05) # type: ignore
|
|
2277
|
+
return linear_fit, pdf, rdf
|
|
2152
2278
|
|
|
2153
2279
|
|
|
2154
2280
|
# ===================================================================
|
|
2155
|
-
#
|
|
2281
|
+
# 선형성 검정 (Linearity Test)
|
|
2156
2282
|
# ===================================================================
|
|
2157
|
-
def
|
|
2158
|
-
"""회귀모형의
|
|
2283
|
+
def ols_linearity_test(fit: RegressionResultsWrapper, power: int = 2, alpha: float = 0.05) -> DataFrame:
|
|
2284
|
+
"""회귀모형의 선형성을 Ramsey RESET 검정으로 평가한다.
|
|
2159
2285
|
|
|
2160
|
-
|
|
2161
|
-
|
|
2286
|
+
적합된 회귀모형에 대해 Ramsey RESET(Regression Specification Error Test) 검정을 수행하여
|
|
2287
|
+
모형의 선형성 가정이 타당한지를 검정한다. 귀무가설은 '모형이 선형이다'이다.
|
|
2162
2288
|
|
|
2163
2289
|
Args:
|
|
2164
|
-
fit:
|
|
2165
|
-
|
|
2290
|
+
fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
|
|
2291
|
+
OLS 또는 WLS 모형이어야 한다.
|
|
2292
|
+
power (int, optional): RESET 검정에 사용할 거듭제곱 수. 기본값 2.
|
|
2293
|
+
power=2일 때 예측값의 제곱항이 추가됨.
|
|
2294
|
+
power가 클수록 더 높은 차수의 비선형성을 감지.
|
|
2295
|
+
alpha (float, optional): 유의수준. 기본값 0.05
|
|
2166
2296
|
|
|
2167
2297
|
Returns:
|
|
2168
|
-
DataFrame: 검정
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2298
|
+
DataFrame: 선형성 검정 결과를 포함한 데이터프레임.
|
|
2299
|
+
- 검정통계량: F-statistic
|
|
2300
|
+
- p-value: 검정의 p값
|
|
2301
|
+
- 유의성: alpha 기준 결과 (bool)
|
|
2302
|
+
- 해석: 선형성 판정 (문자열)
|
|
2173
2303
|
|
|
2174
2304
|
Examples:
|
|
2175
2305
|
```python
|
|
2176
2306
|
from hossam import *
|
|
2177
2307
|
fit = hs_stats.logit(df, 'target')
|
|
2178
|
-
result = hs_stats.
|
|
2308
|
+
result = hs_stats.ols_linearity_test(fit)
|
|
2179
2309
|
```
|
|
2180
2310
|
|
|
2181
2311
|
Notes:
|
|
2182
|
-
-
|
|
2183
|
-
|
|
2184
|
-
* 0에 가까우면: 양의 자기상관 (독립성 위반)
|
|
2185
|
-
* 4에 가까우면: 음의 자기상관 (독립성 위반)
|
|
2186
|
-
- 일반적으로 1.5~2.5 범위를 자기상관 없음으로 판단
|
|
2187
|
-
- 시계열 데이터나 관측치에 순서가 있는 경우 중요한 검정
|
|
2312
|
+
- p-value > alpha: 선형성 가정을 만족 (귀무가설 채택)
|
|
2313
|
+
- p-value <= alpha: 선형성 가정 위반 가능 (귀무가설 기각)
|
|
2188
2314
|
"""
|
|
2189
|
-
|
|
2315
|
+
import re
|
|
2190
2316
|
|
|
2191
|
-
#
|
|
2192
|
-
|
|
2317
|
+
# Ramsey RESET 검정 수행
|
|
2318
|
+
reset_result = linear_reset(fit, power=power)
|
|
2193
2319
|
|
|
2194
|
-
#
|
|
2195
|
-
|
|
2320
|
+
# ContrastResults 객체에서 결과 추출
|
|
2321
|
+
test_stat = None
|
|
2322
|
+
p_value = None
|
|
2196
2323
|
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
elif dw_stat > 2.5:
|
|
2201
|
-
interpretation = f"DW={dw_stat:.4f} > 2.5 (음의 자기상관)"
|
|
2202
|
-
else:
|
|
2203
|
-
interpretation = f"DW={dw_stat:.4f} (독립성 가정 만족)"
|
|
2324
|
+
try:
|
|
2325
|
+
# 문자열 표현에서 숫자 추출 시도
|
|
2326
|
+
result_str = str(reset_result)
|
|
2204
2327
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2328
|
+
# 정규식으로 숫자값들 추출 (F-statistic과 p-value)
|
|
2329
|
+
numbers = re.findall(r'\d+\.?\d*[eE]?-?\d*', result_str)
|
|
2330
|
+
|
|
2331
|
+
if len(numbers) >= 2:
|
|
2332
|
+
# 일반적으로 첫 번째는 F-statistic, 마지막은 p-value
|
|
2333
|
+
test_stat = float(numbers[0])
|
|
2334
|
+
p_value = float(numbers[-1])
|
|
2335
|
+
except (ValueError, IndexError, AttributeError):
|
|
2336
|
+
pass
|
|
2337
|
+
|
|
2338
|
+
# 정규식 실패 시 직접 속성 접근
|
|
2339
|
+
if test_stat is None or p_value is None:
|
|
2340
|
+
attr_pairs = [
|
|
2341
|
+
('statistic', 'pvalue'),
|
|
2342
|
+
('test_stat', 'pvalue'),
|
|
2343
|
+
('fvalue', 'pvalue'),
|
|
2344
|
+
]
|
|
2345
|
+
|
|
2346
|
+
for attr_stat, attr_pval in attr_pairs:
|
|
2347
|
+
if hasattr(reset_result, attr_stat) and hasattr(reset_result, attr_pval):
|
|
2348
|
+
try:
|
|
2349
|
+
test_stat = float(getattr(reset_result, attr_stat))
|
|
2350
|
+
p_value = float(getattr(reset_result, attr_pval))
|
|
2351
|
+
break
|
|
2352
|
+
except (ValueError, TypeError):
|
|
2353
|
+
continue
|
|
2354
|
+
|
|
2355
|
+
# 여전히 값을 못 찾으면 에러
|
|
2356
|
+
if test_stat is None or p_value is None:
|
|
2357
|
+
raise ValueError(f"linear_reset 결과를 해석할 수 없습니다. 반환값: {reset_result}")
|
|
2358
|
+
|
|
2359
|
+
# 유의성 판정
|
|
2360
|
+
significant = p_value <= alpha
|
|
2361
|
+
|
|
2362
|
+
# 해석 문구
|
|
2363
|
+
if significant:
|
|
2364
|
+
interpretation = f"선형성 가정 위반 (p={p_value:.4f} <= {alpha})"
|
|
2365
|
+
else:
|
|
2366
|
+
interpretation = f"선형성 가정 만족 (p={p_value:.4f} > {alpha})"
|
|
2367
|
+
|
|
2368
|
+
# 결과를 DataFrame으로 반환
|
|
2369
|
+
result_df = DataFrame({
|
|
2370
|
+
"검정": ["Ramsey RESET"],
|
|
2371
|
+
"검정통계량 (F)": [f"{test_stat:.4f}"],
|
|
2372
|
+
"p-value": [f"{p_value:.4f}"],
|
|
2373
|
+
"유의수준": [alpha],
|
|
2374
|
+
"선형성_위반": [significant], # True: 선형성 위반, False: 선형성 만족
|
|
2375
|
+
"해석": [interpretation]
|
|
2376
|
+
})
|
|
2214
2377
|
|
|
2215
2378
|
return result_df
|
|
2216
2379
|
|
|
2380
|
+
|
|
2217
2381
|
# ===================================================================
|
|
2218
|
-
#
|
|
2382
|
+
# 정규성 검정 (Normality Test)
|
|
2219
2383
|
# ===================================================================
|
|
2220
|
-
def
|
|
2221
|
-
|
|
2222
|
-
fields: list[str] | None = None,
|
|
2223
|
-
alpha: float = 0.05,
|
|
2224
|
-
z_thresh: float = 3.0,
|
|
2225
|
-
min_n: int = 8,
|
|
2226
|
-
linearity_power: tuple[int, ...] = (2,),
|
|
2227
|
-
p_adjust: str = "none",
|
|
2228
|
-
) -> tuple[DataFrame, DataFrame]:
|
|
2229
|
-
"""각 변수 쌍에 대해 선형성·이상치 여부를 점검한 뒤 Pearson/Spearman을 자동 선택해 상관을 요약한다.
|
|
2384
|
+
def ols_normality_test(fit: RegressionResultsWrapper, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
|
|
2385
|
+
"""회귀모형 잔차의 정규성을 검정한다.
|
|
2230
2386
|
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
2) 단순회귀 y~x에 대해 Ramsey RESET(linearity_power)로 선형성 검정 (모든 p>alpha → 선형성 충족)
|
|
2234
|
-
3) 선형성 충족이고 양쪽 변수에서 |z|>z_thresh 이상치가 없으면 Pearson, 그 외엔 Spearman 선택
|
|
2235
|
-
4) 상관계수/유의확률, 유의성 여부, 강도(strong/medium/weak/no correlation) 기록
|
|
2236
|
-
5) 선택적으로 다중비교 보정(p_adjust="fdr_bh" 등) 적용하여 pval_adj와 significant_adj 추가
|
|
2387
|
+
회귀모형의 잔차가 정규분포를 따르는지 Shapiro-Wilk 검정과 Jarque-Bera 검정으로 평가한다.
|
|
2388
|
+
정규성 가정은 회귀분석의 추론(신뢰구간, 가설검정)이 타당하기 위한 중요한 가정이다.
|
|
2237
2389
|
|
|
2238
2390
|
Args:
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
linearity_power (tuple[int,...], optional): RESET 검정에서 포함할 차수 집합. 기본 (2,).
|
|
2245
|
-
p_adjust (str, optional): 다중비교 보정 방법. "none" 또는 statsmodels.multipletests 지원값 중 하나(e.g., "fdr_bh"). 기본 "none".
|
|
2391
|
+
fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
|
|
2392
|
+
alpha (float, optional): 유의수준. 기본값 0.05.
|
|
2393
|
+
plot (bool, optional): True이면 Q-Q 플롯을 출력. 기본값 False.
|
|
2394
|
+
title (str, optional): 플롯 제목. 기본값 None.
|
|
2395
|
+
save_path (str, optional): 플롯을 저장할 경로. 기본값 None
|
|
2246
2396
|
|
|
2247
2397
|
Returns:
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2398
|
+
DataFrame: 정규성 검정 결과를 포함한 데이터프레임.
|
|
2399
|
+
- 검정명: 사용된 검정 방법
|
|
2400
|
+
- 검정통계량: 검정 통계량 값
|
|
2401
|
+
- p-value: 검정의 p값
|
|
2402
|
+
- 유의수준: 설정된 유의수준
|
|
2403
|
+
- 정규성_위반: alpha 기준 결과 (bool)
|
|
2404
|
+
- 해석: 정규성 판정 (문자열)
|
|
2253
2405
|
|
|
2254
2406
|
Examples:
|
|
2255
2407
|
```python
|
|
2256
2408
|
from hossam import *
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
df = DataFrame({'x1': [1,2,3,4,5], 'x2': [2,4,5,4,6], 'x3': [10,20,25,24,30]})
|
|
2260
|
-
# 전체 숫자형 컬럼에 대해 상관분석
|
|
2261
|
-
result_df, corr_matrix = hs_stats.corr_pairwise(df)
|
|
2262
|
-
# 특정 컬럼만 분석
|
|
2263
|
-
result_df, corr_matrix = hs_stats.corr_pairwise(df, fields=['x1', 'x2'])
|
|
2409
|
+
fit = hs_stats.logit(df, 'target')
|
|
2410
|
+
result = hs_stats.ols_normality_test(fit)
|
|
2264
2411
|
```
|
|
2265
|
-
"""
|
|
2266
2412
|
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2413
|
+
Notes:
|
|
2414
|
+
- Shapiro-Wilk: 샘플 크기가 작을 때 (< 5000) 강력한 검정
|
|
2415
|
+
- Jarque-Bera: 왜도(skewness)와 첨도(kurtosis) 기반 검정
|
|
2416
|
+
- p-value > alpha: 정규성 가정 만족 (귀무가설 채택)
|
|
2417
|
+
- p-value <= alpha: 정규성 가정 위반 (귀무가설 기각)
|
|
2418
|
+
"""
|
|
2419
|
+
from scipy.stats import jarque_bera
|
|
2274
2420
|
|
|
2275
|
-
#
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
return empty_df, DataFrame()
|
|
2421
|
+
# fit 객체에서 잔차 추출
|
|
2422
|
+
residuals = fit.resid
|
|
2423
|
+
n = len(residuals)
|
|
2279
2424
|
|
|
2280
|
-
|
|
2281
|
-
z_outlier_flags = {}
|
|
2282
|
-
for c in cols:
|
|
2283
|
-
col = data[c].dropna()
|
|
2284
|
-
if col.std(ddof=1) == 0:
|
|
2285
|
-
z_outlier_flags[c] = False
|
|
2286
|
-
continue
|
|
2287
|
-
z = (col - col.mean()) / col.std(ddof=1)
|
|
2288
|
-
z_outlier_flags[c] = (z.abs() > z_thresh).any()
|
|
2425
|
+
results = []
|
|
2289
2426
|
|
|
2290
|
-
|
|
2427
|
+
# 1. Shapiro-Wilk 검정 (샘플 크기 < 5000일 때 권장)
|
|
2428
|
+
if n < 5000:
|
|
2429
|
+
try:
|
|
2430
|
+
stat_sw, p_sw = shapiro(residuals)
|
|
2431
|
+
significant_sw = p_sw <= alpha
|
|
2291
2432
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
# 표본이 너무 적으면 계산하지 않음
|
|
2297
|
-
rows.append(
|
|
2298
|
-
{
|
|
2299
|
-
"var_a": a,
|
|
2300
|
-
"var_b": b,
|
|
2301
|
-
"n": len(pair_df),
|
|
2302
|
-
"linearity": False,
|
|
2303
|
-
"outlier_flag": True,
|
|
2304
|
-
"chosen": None,
|
|
2305
|
-
"corr": np.nan,
|
|
2306
|
-
"pval": np.nan,
|
|
2307
|
-
"significant": False,
|
|
2308
|
-
"strength": "no correlation",
|
|
2309
|
-
}
|
|
2310
|
-
)
|
|
2311
|
-
continue
|
|
2433
|
+
if significant_sw:
|
|
2434
|
+
interpretation_sw = f"정규성 위반 (p={p_sw:.4f} <= {alpha})"
|
|
2435
|
+
else:
|
|
2436
|
+
interpretation_sw = f"정규성 만족 (p={p_sw:.4f} > {alpha})"
|
|
2312
2437
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2438
|
+
results.append({
|
|
2439
|
+
"검정": "Shapiro-Wilk",
|
|
2440
|
+
"검정통계량": f"{stat_sw:.4f}",
|
|
2441
|
+
"p-value": f"{p_sw:.4f}",
|
|
2442
|
+
"유의수준": alpha,
|
|
2443
|
+
"정규성_위반": significant_sw,
|
|
2444
|
+
"해석": interpretation_sw
|
|
2445
|
+
})
|
|
2446
|
+
except Exception as e:
|
|
2447
|
+
pass
|
|
2315
2448
|
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
"var_a": a,
|
|
2321
|
-
"var_b": b,
|
|
2322
|
-
"n": len(pair_df),
|
|
2323
|
-
"linearity": False,
|
|
2324
|
-
"outlier_flag": True,
|
|
2325
|
-
"chosen": None,
|
|
2326
|
-
"corr": np.nan,
|
|
2327
|
-
"pval": np.nan,
|
|
2328
|
-
"significant": False,
|
|
2329
|
-
"strength": "no correlation",
|
|
2330
|
-
}
|
|
2331
|
-
)
|
|
2332
|
-
continue
|
|
2449
|
+
# 2. Jarque-Bera 검정 (항상 수행)
|
|
2450
|
+
try:
|
|
2451
|
+
stat_jb, p_jb = jarque_bera(residuals)
|
|
2452
|
+
significant_jb = p_jb <= alpha # type: ignore
|
|
2333
2453
|
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
model = sm.OLS(y, X_const).fit()
|
|
2339
|
-
pvals = []
|
|
2340
|
-
for pwr in linearity_power:
|
|
2341
|
-
reset = linear_reset(model, power=pwr, use_f=True)
|
|
2342
|
-
pvals.append(reset.pvalue)
|
|
2343
|
-
# 모든 차수에서 유의하지 않을 때 선형성 충족으로 간주
|
|
2344
|
-
if len(pvals) > 0:
|
|
2345
|
-
linearity_ok = all([pv > alpha for pv in pvals])
|
|
2346
|
-
except Exception:
|
|
2347
|
-
linearity_ok = False
|
|
2454
|
+
if significant_jb:
|
|
2455
|
+
interpretation_jb = f"정규성 위반 (p={p_jb:.4f} <= {alpha})"
|
|
2456
|
+
else:
|
|
2457
|
+
interpretation_jb = f"정규성 만족 (p={p_jb:.4f} > {alpha})"
|
|
2348
2458
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2459
|
+
results.append({
|
|
2460
|
+
"검정": "Jarque-Bera",
|
|
2461
|
+
"검정통계량": f"{stat_jb:.4f}",
|
|
2462
|
+
"p-value": f"{p_jb:.4f}",
|
|
2463
|
+
"유의수준": alpha,
|
|
2464
|
+
"정규성_위반": significant_jb,
|
|
2465
|
+
"해석": interpretation_jb
|
|
2466
|
+
})
|
|
2467
|
+
except Exception as e:
|
|
2468
|
+
pass
|
|
2351
2469
|
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
chosen = "pearson"
|
|
2356
|
-
corr_val, pval = pearsonr(x, y)
|
|
2357
|
-
else:
|
|
2358
|
-
chosen = "spearman"
|
|
2359
|
-
corr_val, pval = spearmanr(x, y)
|
|
2360
|
-
except Exception:
|
|
2361
|
-
chosen = None
|
|
2362
|
-
corr_val, pval = np.nan, np.nan
|
|
2470
|
+
# 결과를 DataFrame으로 반환
|
|
2471
|
+
if not results:
|
|
2472
|
+
raise ValueError("정규성 검정을 수행할 수 없습니다.")
|
|
2363
2473
|
|
|
2364
|
-
# 4) 유의성, 강도
|
|
2365
|
-
significant = False if np.isnan(pval) else pval <= alpha # type: ignore
|
|
2366
|
-
abs_r = abs(corr_val) if not np.isnan(corr_val) else 0 # type: ignore
|
|
2367
|
-
if abs_r > 0.7:
|
|
2368
|
-
strength = "strong"
|
|
2369
|
-
elif abs_r > 0.3:
|
|
2370
|
-
strength = "medium"
|
|
2371
|
-
elif abs_r > 0:
|
|
2372
|
-
strength = "weak"
|
|
2373
|
-
else:
|
|
2374
|
-
strength = "no correlation"
|
|
2375
2474
|
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
"var_a": a,
|
|
2379
|
-
"var_b": b,
|
|
2380
|
-
"n": len(pair_df),
|
|
2381
|
-
"linearity": linearity_ok,
|
|
2382
|
-
"outlier_flag": outlier_flag,
|
|
2383
|
-
"chosen": chosen,
|
|
2384
|
-
"corr": corr_val,
|
|
2385
|
-
"pval": pval,
|
|
2386
|
-
"significant": significant,
|
|
2387
|
-
"strength": strength,
|
|
2388
|
-
}
|
|
2389
|
-
)
|
|
2390
|
-
|
|
2391
|
-
result_df = DataFrame(rows)
|
|
2392
|
-
|
|
2393
|
-
# 5) 다중비교 보정 (선택)
|
|
2394
|
-
if p_adjust.lower() != "none" and not result_df.empty:
|
|
2395
|
-
# 유효한 p만 보정
|
|
2396
|
-
mask = result_df["pval"].notna()
|
|
2397
|
-
if mask.any():
|
|
2398
|
-
_, p_adj, _, _ = multipletests(result_df.loc[mask, "pval"], alpha=alpha, method=p_adjust)
|
|
2399
|
-
result_df.loc[mask, "pval_adj"] = p_adj
|
|
2400
|
-
result_df["significant_adj"] = result_df["pval_adj"] <= alpha
|
|
2401
|
-
|
|
2402
|
-
# 6) 상관행렬 생성 (result_df 기반)
|
|
2403
|
-
# 모든 변수를 행과 열로 하는 대칭 행렬 생성
|
|
2404
|
-
corr_matrix = DataFrame(np.nan, index=cols, columns=cols)
|
|
2405
|
-
# 대각선: 1 (자기상관)
|
|
2406
|
-
for c in cols:
|
|
2407
|
-
corr_matrix.loc[c, c] = 1.0
|
|
2408
|
-
# 쌍별 상관계수 채우기 (대칭성 유지)
|
|
2409
|
-
if not result_df.empty:
|
|
2410
|
-
for _, row in result_df.iterrows():
|
|
2411
|
-
a, b, corr_val = row["var_a"], row["var_b"], row["corr"]
|
|
2412
|
-
corr_matrix.loc[a, b] = corr_val
|
|
2413
|
-
corr_matrix.loc[b, a] = corr_val # 대칭성
|
|
2475
|
+
if plot:
|
|
2476
|
+
ols_qqplot(fit, title=title, save_path=save_path)
|
|
2414
2477
|
|
|
2415
|
-
|
|
2478
|
+
result_df = DataFrame(results)
|
|
2479
|
+
return result_df
|
|
2416
2480
|
|
|
2417
2481
|
|
|
2418
2482
|
# ===================================================================
|
|
2419
|
-
#
|
|
2483
|
+
# 등분산성 검정 (Homoscedasticity Test)
|
|
2420
2484
|
# ===================================================================
|
|
2421
|
-
def
|
|
2422
|
-
"""
|
|
2423
|
-
|
|
2424
|
-
정규성 및 등분산성 검정을 자동으로 수행한 후,
|
|
2425
|
-
그 결과에 따라 적절한 ANOVA 방식을 선택하여 분산분석을 수행한다.
|
|
2426
|
-
ANOVA 결과가 유의하면 자동으로 사후검정을 실시한다.
|
|
2485
|
+
def ols_variance_test(fit: RegressionResultsWrapper, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
|
|
2486
|
+
"""회귀모형의 등분산성 가정을 검정한다.
|
|
2427
2487
|
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
2. 등분산성 검정 (정규성 만족 시 Bartlett, 불만족 시 Levene)
|
|
2431
|
-
3. ANOVA 수행 (등분산 만족 시 parametric ANOVA, 불만족 시 Welch's ANOVA)
|
|
2432
|
-
4. ANOVA p-value ≤ alpha 일 때 사후검정 (등분산 만족 시 Tukey HSD, 불만족 시 Games-Howell)
|
|
2488
|
+
잔차의 분산이 예측값의 수준에 관계없이 일정한지 Breusch-Pagan 검정과 White 검정으로 평가한다.
|
|
2489
|
+
등분산성 가정은 회귀분석의 추론(표준오차, 검정통계량)이 정확하기 위한 중요한 가정이다.
|
|
2433
2490
|
|
|
2434
2491
|
Args:
|
|
2435
|
-
|
|
2436
|
-
dv (str): 종속변수(Dependent Variable) 컬럼명.
|
|
2437
|
-
between (str): 그룹 구분 변수 컬럼명.
|
|
2492
|
+
fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
|
|
2438
2493
|
alpha (float, optional): 유의수준. 기본값 0.05.
|
|
2494
|
+
plot (bool, optional): True이면 Q-Q 플롯을 출력. 기본값 False.
|
|
2495
|
+
title (str, optional): 플롯 제목. 기본값 None.
|
|
2496
|
+
save_path (str, optional): 플롯을 저장할 경로. 기본값 None
|
|
2439
2497
|
|
|
2440
2498
|
Returns:
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2499
|
+
DataFrame: 등분산성 검정 결과를 포함한 데이터프레임.
|
|
2500
|
+
- 검정명: 사용된 검정 방법
|
|
2501
|
+
- 검정통계량: 검정 통계량 값 (LM 또는 F)
|
|
2502
|
+
- p-value: 검정의 p값
|
|
2503
|
+
- 유의수준: 설정된 유의수준
|
|
2504
|
+
- 등분산성_위반: alpha 기준 결과 (bool)
|
|
2505
|
+
- 해석: 등분산성 판정 (문자열)
|
|
2446
2506
|
|
|
2447
2507
|
Examples:
|
|
2448
2508
|
```python
|
|
2449
2509
|
from hossam import *
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
df = DataFrame({
|
|
2453
|
-
'score': [5.1, 4.9, 5.3, 5.0, 4.8, 5.5, 5.2, 5.7, 5.3, 5.1],
|
|
2454
|
-
'group': ['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B']
|
|
2455
|
-
})
|
|
2456
|
-
|
|
2457
|
-
anova_df, anova_report, posthoc_df, posthoc_report = hs_stats.oneway_anova(df, dv='score', between='group')
|
|
2458
|
-
|
|
2459
|
-
# 사후검정결과는 ANOVA가 유의할 때만 생성됨
|
|
2460
|
-
if posthoc_df is not None:
|
|
2461
|
-
print(posthoc_report)
|
|
2462
|
-
print(posthoc_df.head())
|
|
2510
|
+
fit = hs_stats.logit(df, 'target')
|
|
2511
|
+
result = hs_stats.ols_variance_test(fit)
|
|
2463
2512
|
```
|
|
2464
2513
|
|
|
2465
|
-
|
|
2466
|
-
|
|
2514
|
+
Notes:
|
|
2515
|
+
- Breusch-Pagan: 잔차 제곱과 독립변수의 선형관계 검정
|
|
2516
|
+
- White: 잔차 제곱과 독립변수 및 그 제곱, 교차항의 관계 검정
|
|
2517
|
+
- p-value > alpha: 등분산성 가정 만족 (귀무가설 채택)
|
|
2518
|
+
- p-value <= alpha: 이분산성 존재 (귀무가설 기각)
|
|
2467
2519
|
"""
|
|
2468
|
-
# 컬럼 유효성 검사
|
|
2469
|
-
if dv not in data.columns:
|
|
2470
|
-
raise ValueError(f"'{dv}' 컬럼이 데이터프레임에 없습니다.")
|
|
2471
|
-
if between not in data.columns:
|
|
2472
|
-
raise ValueError(f"'{between}' 컬럼이 데이터프레임에 없습니다.")
|
|
2473
|
-
|
|
2474
|
-
df_filtered = data[[dv, between]].dropna()
|
|
2475
2520
|
|
|
2476
|
-
#
|
|
2477
|
-
|
|
2478
|
-
#
|
|
2479
|
-
group_names = sorted(df_filtered[between].unique())
|
|
2480
|
-
normality_satisfied = True
|
|
2521
|
+
# fit 객체에서 필요한 정보 추출
|
|
2522
|
+
exog = fit.model.exog # 설명변수 (상수항 포함)
|
|
2523
|
+
resid = fit.resid # 잔차
|
|
2481
2524
|
|
|
2482
|
-
|
|
2483
|
-
group_values = df_filtered[df_filtered[between] == group][dv].dropna()
|
|
2484
|
-
if len(group_values) > 0:
|
|
2485
|
-
s, p = normaltest(group_values)
|
|
2486
|
-
if p <= alpha:
|
|
2487
|
-
normality_satisfied = False
|
|
2488
|
-
break
|
|
2525
|
+
results = []
|
|
2489
2526
|
|
|
2490
|
-
#
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
group_data_dict = {}
|
|
2495
|
-
for group in group_names:
|
|
2496
|
-
group_data_dict[group] = df_filtered[df_filtered[between] == group][dv].dropna().values
|
|
2527
|
+
# 1. Breusch-Pagan 검정
|
|
2528
|
+
try:
|
|
2529
|
+
lm, lm_pvalue, fvalue, f_pvalue = het_breuschpagan(resid, exog)
|
|
2530
|
+
significant_bp = lm_pvalue <= alpha
|
|
2497
2531
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
if normality_satisfied:
|
|
2501
|
-
# 정규성을 만족하면 Bartlett 검정
|
|
2502
|
-
s, p = bartlett(*group_data_dict.values())
|
|
2532
|
+
if significant_bp:
|
|
2533
|
+
interpretation_bp = f"등분산성 위반 (p={lm_pvalue:.4f} <= {alpha})"
|
|
2503
2534
|
else:
|
|
2504
|
-
|
|
2505
|
-
s, p = levene(*group_data_dict.values())
|
|
2506
|
-
equal_var_satisfied = p > alpha
|
|
2507
|
-
else:
|
|
2508
|
-
# 그룹이 1개인 경우 등분산성 검정 불가능
|
|
2509
|
-
equal_var_satisfied = True
|
|
2535
|
+
interpretation_bp = f"등분산성 만족 (p={lm_pvalue:.4f} > {alpha})"
|
|
2510
2536
|
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
anova_df = welch_anova(data=df_filtered, dv=dv, between=between)
|
|
2537
|
+
results.append({
|
|
2538
|
+
"검정": "Breusch-Pagan",
|
|
2539
|
+
"검정통계량 (LM)": f"{lm:.4f}",
|
|
2540
|
+
"p-value": f"{lm_pvalue:.4f}",
|
|
2541
|
+
"유의수준": alpha,
|
|
2542
|
+
"등분산성_위반": significant_bp,
|
|
2543
|
+
"해석": interpretation_bp
|
|
2544
|
+
})
|
|
2545
|
+
except Exception as e:
|
|
2546
|
+
pass
|
|
2522
2547
|
|
|
2523
|
-
#
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2548
|
+
# 2. White 검정
|
|
2549
|
+
try:
|
|
2550
|
+
lm, lm_pvalue, fvalue, f_pvalue = het_white(resid, exog)
|
|
2551
|
+
significant_white = lm_pvalue <= alpha
|
|
2527
2552
|
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2553
|
+
if significant_white:
|
|
2554
|
+
interpretation_white = f"등분산성 위반 (p={lm_pvalue:.4f} <= {alpha})"
|
|
2555
|
+
else:
|
|
2556
|
+
interpretation_white = f"등분산성 만족 (p={lm_pvalue:.4f} > {alpha})"
|
|
2531
2557
|
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2558
|
+
results.append({
|
|
2559
|
+
"검정": "White",
|
|
2560
|
+
"검정통계량 (LM)": f"{lm:.4f}",
|
|
2561
|
+
"p-value": f"{lm_pvalue:.4f}",
|
|
2562
|
+
"유의수준": alpha,
|
|
2563
|
+
"등분산성_위반": significant_white,
|
|
2564
|
+
"해석": interpretation_white
|
|
2565
|
+
})
|
|
2566
|
+
except Exception as e:
|
|
2567
|
+
pass
|
|
2535
2568
|
|
|
2536
|
-
#
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
return float(anova_df.loc[0, col]) if col in anova_df.columns else default # type: ignore
|
|
2540
|
-
except Exception:
|
|
2541
|
-
return default
|
|
2569
|
+
# 결과를 DataFrame으로 반환
|
|
2570
|
+
if not results:
|
|
2571
|
+
raise ValueError("등분산성 검정을 수행할 수 없습니다.")
|
|
2542
2572
|
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
fval = _safe_get('F')
|
|
2546
|
-
eta2 = _safe_get('np2')
|
|
2573
|
+
if plot:
|
|
2574
|
+
ols_residplot(fit, lowess=True, mse=True, title=title, save_path=save_path)
|
|
2547
2575
|
|
|
2548
|
-
|
|
2549
|
-
|
|
2576
|
+
result_df = DataFrame(results)
|
|
2577
|
+
return result_df
|
|
2550
2578
|
|
|
2551
|
-
anova_report = (
|
|
2552
|
-
f"{between}별로 {dv} 평균을 비교한 {anova_method} 결과: F({df1:.3f}, {df2:.3f}) = {fval:.3f}, p = {p_unc:.4f}. "
|
|
2553
|
-
f"해석: {anova_sig_text} {assumption_text}"
|
|
2554
|
-
)
|
|
2555
2579
|
|
|
2556
|
-
|
|
2557
|
-
|
|
2580
|
+
# ===================================================================
|
|
2581
|
+
# 독립성 검정 (Independence Test - Durbin-Watson)
|
|
2582
|
+
# ===================================================================
|
|
2583
|
+
def ols_independence_test(fit: RegressionResultsWrapper, alpha: float = 0.05) -> DataFrame:
|
|
2584
|
+
"""회귀모형의 독립성 가정(자기상관 없음)을 검정한다.
|
|
2558
2585
|
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
# ============================================
|
|
2562
|
-
posthoc_df = None
|
|
2563
|
-
posthoc_method = 'None'
|
|
2564
|
-
posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
|
|
2586
|
+
Durbin-Watson 검정을 사용하여 잔차의 1차 자기상관 여부를 검정한다.
|
|
2587
|
+
시계열 데이터나 순서가 있는 데이터에서 주로 사용된다.
|
|
2565
2588
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
posthoc_method = "Tukey HSD"
|
|
2570
|
-
posthoc_df = pairwise_tukey(data=df_filtered, dv=dv, between=between)
|
|
2571
|
-
else:
|
|
2572
|
-
# 등분산을 만족하지 않으면 Games-Howell 사용
|
|
2573
|
-
posthoc_method = "Games-Howell"
|
|
2574
|
-
posthoc_df = pairwise_gameshowell(df_filtered, dv=dv, between=between)
|
|
2589
|
+
Args:
|
|
2590
|
+
fit: statsmodels 회귀분석 결과 객체 (RegressionResultsWrapper).
|
|
2591
|
+
alpha (float, optional): 유의수준. 기본값은 0.05.
|
|
2575
2592
|
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2593
|
+
Returns:
|
|
2594
|
+
DataFrame: 검정 결과 데이터프레임.
|
|
2595
|
+
- 검정: 검정 방법명
|
|
2596
|
+
- 검정통계량(DW): Durbin-Watson 통계량 (0~4 범위, 2에 가까울수록 자기상관 없음)
|
|
2597
|
+
- 독립성_위반: 자기상관 의심 여부 (True/False)
|
|
2598
|
+
- 해석: 검정 결과 해석
|
|
2580
2599
|
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2600
|
+
Examples:
|
|
2601
|
+
```python
|
|
2602
|
+
from hossam import *
|
|
2603
|
+
fit = hs_stats.logit(df, 'target')
|
|
2604
|
+
result = hs_stats.ols_independence_test(fit)
|
|
2605
|
+
```
|
|
2584
2606
|
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2607
|
+
Notes:
|
|
2608
|
+
- Durbin-Watson 통계량 해석:
|
|
2609
|
+
* 2에 가까우면: 자기상관 없음 (독립성 만족)
|
|
2610
|
+
* 0에 가까우면: 양의 자기상관 (독립성 위반)
|
|
2611
|
+
* 4에 가까우면: 음의 자기상관 (독립성 위반)
|
|
2612
|
+
- 일반적으로 1.5~2.5 범위를 자기상관 없음으로 판단
|
|
2613
|
+
- 시계열 데이터나 관측치에 순서가 있는 경우 중요한 검정
|
|
2614
|
+
"""
|
|
2615
|
+
from pandas import DataFrame
|
|
2588
2616
|
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2617
|
+
# Durbin-Watson 통계량 계산
|
|
2618
|
+
dw_stat = durbin_watson(fit.resid)
|
|
2619
|
+
|
|
2620
|
+
# 자기상관 판단 (1.5 < DW < 2.5 범위를 독립성 만족으로 판단)
|
|
2621
|
+
is_autocorrelated = dw_stat < 1.5 or dw_stat > 2.5
|
|
2622
|
+
|
|
2623
|
+
# 해석 메시지 생성
|
|
2624
|
+
if dw_stat < 1.5:
|
|
2625
|
+
interpretation = f"DW={dw_stat:.4f} < 1.5 (양의 자기상관)"
|
|
2626
|
+
elif dw_stat > 2.5:
|
|
2627
|
+
interpretation = f"DW={dw_stat:.4f} > 2.5 (음의 자기상관)"
|
|
2628
|
+
else:
|
|
2629
|
+
interpretation = f"DW={dw_stat:.4f} (독립성 가정 만족)"
|
|
2630
|
+
|
|
2631
|
+
# 결과 데이터프레임 생성
|
|
2632
|
+
result_df = DataFrame(
|
|
2633
|
+
{
|
|
2634
|
+
"검정": ["Durbin-Watson"],
|
|
2635
|
+
"검정통계량(DW)": [dw_stat],
|
|
2636
|
+
"독립성_위반": [is_autocorrelated],
|
|
2637
|
+
"해석": [interpretation],
|
|
2638
|
+
}
|
|
2639
|
+
)
|
|
2640
|
+
|
|
2641
|
+
return result_df
|
|
2595
2642
|
|
|
2596
|
-
if sig_count > 0:
|
|
2597
|
-
posthoc_report = (
|
|
2598
|
-
f"{posthoc_method} 사후검정에서 {sig_count}/{total_count}쌍이 의미 있는 차이를 보였습니다 (alpha={alpha})."
|
|
2599
|
-
)
|
|
2600
|
-
if pair_samples:
|
|
2601
|
-
posthoc_report += " 예: " + ", ".join(pair_samples) + " 등."
|
|
2602
|
-
else:
|
|
2603
|
-
posthoc_report = f"{posthoc_method} 사후검정에서 추가로 유의한 쌍은 발견되지 않았습니다."
|
|
2604
|
-
else:
|
|
2605
|
-
posthoc_report = f"{posthoc_method} 결과는 생성했지만 p-value 정보를 찾지 못해 유의성을 확인할 수 없습니다."
|
|
2606
2643
|
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2644
|
+
|
|
2645
|
+
def ols_tests(fit: RegressionResultsWrapper, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> None:
|
|
2646
|
+
"""회귀모형의 가정 검정을 종합적으로 수행한다.
|
|
2647
|
+
|
|
2648
|
+
선형성, 정규성, 등분산성, 독립성 검정을 순차적으로 실시하고 결과를 하나의 데이터프레임으로 반환한다.
|
|
2649
|
+
|
|
2650
|
+
Args:
|
|
2651
|
+
fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
|
|
2652
|
+
alpha (float, optional): 유의수준. 기본값 0.05.
|
|
2653
|
+
plot (bool, optional): True이면 정규성 검정 시 Q-Q 플롯을 출력. 기본값 False.
|
|
2654
|
+
title (str, optional): 플롯 제목. 기본값 None.
|
|
2655
|
+
save_path (str, optional): 플롯을 저장할 경로. 기본값 None.
|
|
2656
|
+
|
|
2657
|
+
Returns:
|
|
2658
|
+
None
|
|
2659
|
+
|
|
2660
|
+
Examples:
|
|
2661
|
+
```python
|
|
2662
|
+
from hossam import *
|
|
2663
|
+
fit = hs_stats.ols(df, 'target')
|
|
2664
|
+
hs_stats.ols_tests(fit)
|
|
2665
|
+
```
|
|
2666
|
+
"""
|
|
2667
|
+
# 각 검정 함수 호출
|
|
2668
|
+
linearity_df = ols_linearity_test(fit, alpha=alpha)
|
|
2669
|
+
normality_df = ols_normality_test(fit, alpha=alpha, plot=False)
|
|
2670
|
+
variance_df = ols_variance_test(fit, alpha=alpha, plot=False)
|
|
2671
|
+
independence_df = ols_independence_test(fit, alpha=alpha)
|
|
2672
|
+
|
|
2673
|
+
from IPython.display import display
|
|
2674
|
+
display(linearity_df)
|
|
2675
|
+
display(normality_df)
|
|
2676
|
+
display(variance_df)
|
|
2677
|
+
display(independence_df)
|
|
2678
|
+
|
|
2679
|
+
if plot:
|
|
2680
|
+
fig, ax = get_default_ax(rows=1, cols=2, title=title)
|
|
2681
|
+
ols_qqplot(fit, ax=ax[0])
|
|
2682
|
+
ols_residplot(fit, lowess=True, mse=True, ax=ax[1])
|
|
2683
|
+
finalize_plot(ax)
|
|
2611
2684
|
|
|
2612
2685
|
|
|
2613
2686
|
# ===================================================================
|
|
2614
|
-
#
|
|
2687
|
+
# 로지스틱 회귀 요약 리포트
|
|
2615
2688
|
# ===================================================================
|
|
2616
|
-
def
|
|
2689
|
+
def logit_report(
|
|
2690
|
+
fit: BinaryResultsWrapper,
|
|
2617
2691
|
data: DataFrame,
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2692
|
+
threshold: float = 0.5,
|
|
2693
|
+
full: Union[bool, str, int] = False,
|
|
2694
|
+
alpha: float = 0.05
|
|
2695
|
+
) -> Union[
|
|
2696
|
+
Tuple[DataFrame, DataFrame],
|
|
2697
|
+
Tuple[
|
|
2698
|
+
DataFrame,
|
|
2699
|
+
DataFrame,
|
|
2700
|
+
str,
|
|
2701
|
+
str,
|
|
2702
|
+
list[str],
|
|
2703
|
+
np.ndarray
|
|
2704
|
+
]
|
|
2705
|
+
]:
|
|
2706
|
+
"""로지스틱 회귀 적합 결과를 상세 리포트로 변환한다.
|
|
2630
2707
|
|
|
2631
2708
|
Args:
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
alpha (float
|
|
2709
|
+
fit: statsmodels Logit 결과 객체 (`fit.summary()`와 예측 확률을 지원해야 함).
|
|
2710
|
+
data (DataFrame): 종속변수와 독립변수를 모두 포함한 DataFrame.
|
|
2711
|
+
threshold (float): 예측 확률을 이진 분류로 변환할 임계값. 기본값 0.5.
|
|
2712
|
+
full (bool | str | int): True이면 6개 값 반환, False이면 주요 2개(cdf, rdf)만 반환. 기본값 False.
|
|
2713
|
+
alpha (float): 유의수준. 기본값 0.05.
|
|
2637
2714
|
|
|
2638
2715
|
Returns:
|
|
2639
|
-
tuple:
|
|
2640
|
-
-
|
|
2641
|
-
-
|
|
2642
|
-
-
|
|
2643
|
-
-
|
|
2716
|
+
tuple: full=True일 때 다음 요소를 포함한다.
|
|
2717
|
+
- 성능 지표 표 (`cdf`, DataFrame): McFadden Pseudo R², Accuracy, Precision, Recall, FPR, TNR, AUC, F1.
|
|
2718
|
+
- 회귀계수 표 (`rdf`, DataFrame): B, 표준오차, z, p-value, significant, OR, 95% CI, VIF 등.
|
|
2719
|
+
- 적합도 및 예측 성능 요약 (`result_report`, str): Pseudo R², LLR χ², p-value, Accuracy, AUC.
|
|
2720
|
+
- 모형 보고 문장 (`model_report`, str): LLR p-value에 기반한 서술형 문장.
|
|
2721
|
+
- 변수별 보고 리스트 (`variable_reports`, list[str]): 각 예측변수의 오즈비 해석 문장.
|
|
2722
|
+
- 혼동행렬 (`cm`, ndarray): 예측 결과와 실제값의 혼동행렬 [[TN, FP], [FN, TP]].
|
|
2644
2723
|
|
|
2645
|
-
|
|
2646
|
-
|
|
2724
|
+
full=False일 때:
|
|
2725
|
+
- 성능 지표 표 (`cdf`, DataFrame)
|
|
2726
|
+
- 회귀계수 표 (`rdf`, DataFrame)
|
|
2727
|
+
|
|
2728
|
+
Examples:
|
|
2729
|
+
```python
|
|
2730
|
+
from hossam import *
|
|
2731
|
+
from pandas import DataFrame
|
|
2732
|
+
import numpy as np
|
|
2733
|
+
|
|
2734
|
+
df = DataFrame({
|
|
2735
|
+
'target': np.random.binomial(1, 0.5, 100),
|
|
2736
|
+
'x1': np.random.normal(0, 1, 100),
|
|
2737
|
+
'x2': np.random.normal(0, 1, 100)
|
|
2738
|
+
})
|
|
2739
|
+
|
|
2740
|
+
# 로지스틱 회귀 적합
|
|
2741
|
+
fit = hs_stats.logit(df, yname="target")
|
|
2742
|
+
|
|
2743
|
+
# 전체 리포트
|
|
2744
|
+
cdf, rdf, result_report, model_report, variable_reports, cm = hs_stats.logit_report(fit, df, full=True)
|
|
2745
|
+
|
|
2746
|
+
# 간단한 버전 (주요 테이블만)
|
|
2747
|
+
cdf, rdf = hs_stats.logit_report(fit, df)
|
|
2748
|
+
```
|
|
2647
2749
|
"""
|
|
2648
|
-
# 컬럼 유효성 검사
|
|
2649
|
-
for col in [dv, factor_a, factor_b]:
|
|
2650
|
-
if col not in data.columns:
|
|
2651
|
-
raise ValueError(f"'{col}' 컬럼이 데이터프레임에 없습니다.")
|
|
2652
2750
|
|
|
2653
|
-
|
|
2751
|
+
# -----------------------------
|
|
2752
|
+
# 성능평가지표
|
|
2753
|
+
# -----------------------------
|
|
2754
|
+
yname = fit.model.endog_names
|
|
2755
|
+
y_true = data[yname]
|
|
2756
|
+
y_pred = fit.predict(fit.model.exog)
|
|
2757
|
+
y_pred_fix = (y_pred >= threshold).astype(int)
|
|
2654
2758
|
|
|
2655
|
-
#
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
vals = subset[dv].dropna()
|
|
2659
|
-
if len(vals) > 0:
|
|
2660
|
-
_, p = normaltest(vals)
|
|
2661
|
-
if p <= alpha:
|
|
2662
|
-
normality_satisfied = False
|
|
2663
|
-
break
|
|
2759
|
+
# 혼동행렬
|
|
2760
|
+
cm = confusion_matrix(y_true, y_pred_fix)
|
|
2761
|
+
tn, fp, fn, tp = cm.ravel()
|
|
2664
2762
|
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2763
|
+
acc = accuracy_score(y_true, y_pred_fix) # 정확도
|
|
2764
|
+
pre = precision_score(y_true, y_pred_fix) # 정밀도
|
|
2765
|
+
tpr = recall_score(y_true, y_pred_fix) # 재현율
|
|
2766
|
+
fpr = fp / (fp + tn) # 위양성율
|
|
2767
|
+
tnr = 1 - fpr # 특이성
|
|
2768
|
+
f1 = f1_score(y_true, y_pred_fix) # f1-score
|
|
2769
|
+
ras = roc_auc_score(y_true, y_pred) # auc score
|
|
2770
|
+
|
|
2771
|
+
cdf = DataFrame(
|
|
2772
|
+
{
|
|
2773
|
+
"설명력(P-Rsqe)": [fit.prsquared],
|
|
2774
|
+
"정확도(Accuracy)": [acc],
|
|
2775
|
+
"정밀도(Precision)": [pre],
|
|
2776
|
+
"재현율(Recall,TPR)": [tpr],
|
|
2777
|
+
"위양성율(Fallout,FPR)": [fpr],
|
|
2778
|
+
"특이성(Specif city,TNR)": [tnr],
|
|
2779
|
+
"RAS(auc score)": [ras],
|
|
2780
|
+
"F1": [f1],
|
|
2781
|
+
}
|
|
2782
|
+
)
|
|
2783
|
+
|
|
2784
|
+
# -----------------------------
|
|
2785
|
+
# 회귀계수 표 구성 (OR 중심)
|
|
2786
|
+
# -----------------------------
|
|
2787
|
+
tbl = fit.summary2().tables[1]
|
|
2788
|
+
|
|
2789
|
+
# 독립변수 이름(상수항 제외)
|
|
2790
|
+
xnames = [n for n in fit.model.exog_names if n != "const"]
|
|
2791
|
+
|
|
2792
|
+
# 독립변수
|
|
2793
|
+
x = data[xnames]
|
|
2794
|
+
|
|
2795
|
+
variables = []
|
|
2796
|
+
|
|
2797
|
+
# VIF 계산 (상수항 포함 설계행렬 사용)
|
|
2798
|
+
vif_dict = {}
|
|
2799
|
+
x_const = sm.add_constant(x, has_constant="add")
|
|
2800
|
+
for i, col in enumerate(x.columns, start=1): # 상수항이 0이므로 1부터 시작
|
|
2801
|
+
vif_dict[col] = variance_inflation_factor(x_const.values, i) # type: ignore
|
|
2802
|
+
|
|
2803
|
+
for idx, row in tbl.iterrows():
|
|
2804
|
+
name = idx
|
|
2805
|
+
if name not in xnames:
|
|
2806
|
+
continue
|
|
2807
|
+
|
|
2808
|
+
beta = float(row['Coef.'])
|
|
2809
|
+
se = float(row['Std.Err.'])
|
|
2810
|
+
z = float(row['z'])
|
|
2811
|
+
p = float(row['P>|z|'])
|
|
2812
|
+
|
|
2813
|
+
or_val = np.exp(beta)
|
|
2814
|
+
ci_low = np.exp(beta - 1.96 * se)
|
|
2815
|
+
ci_high = np.exp(beta + 1.96 * se)
|
|
2816
|
+
|
|
2817
|
+
stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
|
|
2818
|
+
|
|
2819
|
+
variables.append(
|
|
2820
|
+
{
|
|
2821
|
+
"종속변수": yname,
|
|
2822
|
+
"독립변수": name,
|
|
2823
|
+
"B(β)": beta,
|
|
2824
|
+
"표준오차": se,
|
|
2825
|
+
"z": f"{z:.3f}{stars}",
|
|
2826
|
+
"p-value": p,
|
|
2827
|
+
"significant": p <= alpha,
|
|
2828
|
+
"OR": or_val,
|
|
2829
|
+
"CI_lower": ci_low,
|
|
2830
|
+
"CI_upper": ci_high,
|
|
2831
|
+
"VIF": vif_dict.get(name, np.nan),
|
|
2832
|
+
}
|
|
2833
|
+
)
|
|
2834
|
+
|
|
2835
|
+
rdf = DataFrame(variables)
|
|
2836
|
+
|
|
2837
|
+
# ---------------------------------
|
|
2838
|
+
# 모델 적합도 + 예측 성능 지표
|
|
2839
|
+
# ---------------------------------
|
|
2840
|
+
auc = roc_auc_score(y_true, y_pred)
|
|
2841
|
+
|
|
2842
|
+
result_report = (
|
|
2843
|
+
f"Pseudo R²(McFadden) = {fit.prsquared:.3f}, "
|
|
2844
|
+
f"LLR χ²({int(fit.df_model)}) = {fit.llr:.3f}, "
|
|
2845
|
+
f"p-value = {fit.llr_pvalue:.4f}, "
|
|
2846
|
+
f"Accuracy = {acc:.3f}, "
|
|
2847
|
+
f"AUC = {auc:.3f}"
|
|
2848
|
+
)
|
|
2849
|
+
|
|
2850
|
+
# -----------------------------
|
|
2851
|
+
# 모형 보고 문장
|
|
2852
|
+
# -----------------------------
|
|
2853
|
+
tpl = (
|
|
2854
|
+
"%s에 대하여 %s로 예측하는 로지스틱 회귀분석을 실시한 결과, "
|
|
2855
|
+
"모형은 통계적으로 %s(χ²(%s) = %.3f, p %s 0.05)하였다."
|
|
2856
|
+
)
|
|
2857
|
+
|
|
2858
|
+
model_report = tpl % (
|
|
2859
|
+
yname,
|
|
2860
|
+
", ".join(xnames),
|
|
2861
|
+
"유의" if fit.llr_pvalue <= 0.05 else "유의하지 않음",
|
|
2862
|
+
int(fit.df_model),
|
|
2863
|
+
fit.llr,
|
|
2864
|
+
"<=" if fit.llr_pvalue <= 0.05 else ">",
|
|
2865
|
+
)
|
|
2866
|
+
|
|
2867
|
+
# -----------------------------
|
|
2868
|
+
# 변수별 보고 문장
|
|
2869
|
+
# -----------------------------
|
|
2870
|
+
variable_reports = []
|
|
2871
|
+
|
|
2872
|
+
s = (
|
|
2873
|
+
"%s의 오즈비는 %.3f(p %s 0.05)로, "
|
|
2874
|
+
"%s 발생 odds에 %s 영향을 미치는 것으로 나타났다."
|
|
2875
|
+
)
|
|
2876
|
+
|
|
2877
|
+
for _, row in rdf.iterrows():
|
|
2878
|
+
variable_reports.append(
|
|
2879
|
+
s
|
|
2880
|
+
% (
|
|
2881
|
+
row["독립변수"],
|
|
2882
|
+
row["OR"],
|
|
2883
|
+
"<=" if row["p-value"] < 0.05 else ">",
|
|
2884
|
+
row["종속변수"],
|
|
2885
|
+
"유의미한" if row["p-value"] < 0.05 else "유의하지 않은",
|
|
2886
|
+
)
|
|
2887
|
+
)
|
|
2888
|
+
|
|
2889
|
+
if full:
|
|
2890
|
+
return cdf, rdf, result_report, model_report, variable_reports, cm
|
|
2673
2891
|
else:
|
|
2674
|
-
|
|
2892
|
+
return cdf, rdf
|
|
2893
|
+
|
|
2894
|
+
|
|
2895
|
+
# ===================================================================
|
|
2896
|
+
# 로지스틱 회귀
|
|
2897
|
+
# ===================================================================
|
|
2898
|
+
def logit(
|
|
2899
|
+
df: DataFrame,
|
|
2900
|
+
yname: str,
|
|
2901
|
+
report: bool | str = 'summary'
|
|
2902
|
+
) -> Union[
|
|
2903
|
+
BinaryResultsWrapper,
|
|
2904
|
+
Tuple[
|
|
2905
|
+
BinaryResultsWrapper,
|
|
2906
|
+
DataFrame
|
|
2907
|
+
],
|
|
2908
|
+
Tuple[
|
|
2909
|
+
BinaryResultsWrapper,
|
|
2910
|
+
DataFrame,
|
|
2911
|
+
DataFrame,
|
|
2912
|
+
str,
|
|
2913
|
+
str,
|
|
2914
|
+
list[str]
|
|
2915
|
+
]
|
|
2916
|
+
]:
|
|
2917
|
+
"""로지스틱 회귀분석을 수행하고 적합 결과를 반환한다.
|
|
2675
2918
|
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
anova_df.insert(0, "normality", normality_satisfied)
|
|
2679
|
-
anova_df.insert(1, "equal_var", equal_var_satisfied)
|
|
2680
|
-
if 'p-unc' in anova_df.columns:
|
|
2681
|
-
anova_df['significant'] = anova_df['p-unc'] <= alpha
|
|
2919
|
+
종속변수가 이항(binary) 형태일 때 로지스틱 회귀분석을 실시한다.
|
|
2920
|
+
필요시 상세한 통계 보고서를 함께 제공한다.
|
|
2682
2921
|
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2922
|
+
Args:
|
|
2923
|
+
df (DataFrame): 종속변수와 독립변수를 모두 포함한 데이터프레임.
|
|
2924
|
+
yname (str): 종속변수 컬럼명. 이항 변수여야 한다.
|
|
2925
|
+
report: 리포트 모드 설정. 다음 값 중 하나:
|
|
2926
|
+
- False (기본값): 리포트 미사용. fit 객체만 반환.
|
|
2927
|
+
- 1 또는 'summary': 요약 리포트 반환 (full=False).
|
|
2928
|
+
- 2 또는 'full': 풀 리포트 반환 (full=True).
|
|
2929
|
+
- True: 풀 리포트 반환 (2와 동일).
|
|
2689
2930
|
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
for _, row in anova_df.iterrows():
|
|
2694
|
-
term = row.get("Source", "")
|
|
2695
|
-
fval = _safe(row, "F")
|
|
2696
|
-
pval = _safe(row, "p-unc")
|
|
2697
|
-
eta2 = _safe(row, "np2")
|
|
2698
|
-
sig = pval <= alpha
|
|
2699
|
-
sig_flags[term] = sig
|
|
2700
|
-
if term.lower() == "residual":
|
|
2701
|
-
continue
|
|
2702
|
-
effect_name = term.replace("*", "와 ")
|
|
2703
|
-
msg = f"{effect_name}: F={fval:.3f}, p={pval:.4f}. 해석: "
|
|
2704
|
-
msg += "유의한 차이가 있습니다." if sig else "유의한 차이를 찾지 못했습니다."
|
|
2705
|
-
if not np.isnan(eta2):
|
|
2706
|
-
msg += f" 효과 크기(η²p)≈{eta2:.3f}."
|
|
2707
|
-
reports.append(msg)
|
|
2931
|
+
Returns:
|
|
2932
|
+
statsmodels.genmod.generalized_linear_model.BinomialResults: report=False일 때.
|
|
2933
|
+
로지스틱 회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
|
|
2708
2934
|
|
|
2709
|
-
|
|
2710
|
-
|
|
2935
|
+
tuple (4개): report=1 또는 'summary'일 때.
|
|
2936
|
+
(fit, rdf, result_report, variable_reports) 형태로 (cdf 제외).
|
|
2711
2937
|
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2938
|
+
tuple (6개): report=2, 'full' 또는 True일 때.
|
|
2939
|
+
(fit, cdf, rdf, result_report, model_report, variable_reports) 형태로:
|
|
2940
|
+
- fit: 로지스틱 회귀 적합 결과 객체
|
|
2941
|
+
- cdf: 성능 지표 표 (DataFrame)
|
|
2942
|
+
- rdf: 회귀계수 표 (DataFrame)
|
|
2943
|
+
- result_report: 적합도 및 예측 성능 요약 (str)
|
|
2944
|
+
- model_report: 모형 보고 문장 (str)
|
|
2945
|
+
- variable_reports: 변수별 보고 문장 리스트 (list[str])
|
|
2716
2946
|
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2947
|
+
Examples:
|
|
2948
|
+
```python
|
|
2949
|
+
from hossam import *
|
|
2950
|
+
from pandas import DataFrame
|
|
2951
|
+
import numpy as np
|
|
2721
2952
|
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2953
|
+
df = DataFrame({
|
|
2954
|
+
'target': np.random.binomial(1, 0.5, 100),
|
|
2955
|
+
'x1': np.random.normal(0, 1, 100),
|
|
2956
|
+
'x2': np.random.normal(0, 1, 100)
|
|
2957
|
+
})
|
|
2725
2958
|
|
|
2726
|
-
#
|
|
2727
|
-
|
|
2728
|
-
continue
|
|
2959
|
+
# 적합 결과만 반환
|
|
2960
|
+
fit = hs_stats.logit(df, 'target')
|
|
2729
2961
|
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
continue
|
|
2733
|
-
tukey_df = pairwise_tukey(data=df_filtered, dv=dv, between=factor)
|
|
2734
|
-
tukey_df.insert(0, "factor", factor)
|
|
2735
|
-
posthoc_df_list.append(tukey_df)
|
|
2962
|
+
# 요약 리포트 반환
|
|
2963
|
+
fit, rdf, result_report, var_reports = hs_stats.logit(df, 'target', report='summary')
|
|
2736
2964
|
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
if p_col:
|
|
2744
|
-
posthoc_df['significant'] = posthoc_df[p_col] <= alpha
|
|
2745
|
-
sig_df = posthoc_df[posthoc_df[p_col] <= alpha]
|
|
2746
|
-
sig_count = len(sig_df)
|
|
2747
|
-
total_count = len(posthoc_df)
|
|
2748
|
-
examples = []
|
|
2749
|
-
if not sig_df.empty and {"A", "B"}.issubset(sig_df.columns):
|
|
2750
|
-
examples = [f"{row['A']} vs {row['B']}" for _, row in sig_df.head(3).iterrows()]
|
|
2751
|
-
if sig_count > 0:
|
|
2752
|
-
posthoc_report = f"사후검정(Tukey)에서 {sig_count}/{total_count}쌍이 의미 있는 차이를 보였습니다."
|
|
2753
|
-
if examples:
|
|
2754
|
-
posthoc_report += " 예: " + ", ".join(examples) + " 등."
|
|
2755
|
-
else:
|
|
2756
|
-
posthoc_report = "사후검정 결과 추가로 유의한 쌍은 없었습니다."
|
|
2757
|
-
else:
|
|
2758
|
-
posthoc_report = "사후검정 결과를 생성했으나 p-value 정보를 찾지 못했습니다."
|
|
2965
|
+
# 풀 리포트 반환
|
|
2966
|
+
fit, cdf, rdf, result_report, model_report, var_reports = hs_stats.logit(df, 'target', report='full')
|
|
2967
|
+
```
|
|
2968
|
+
"""
|
|
2969
|
+
x = df.drop(yname, axis=1)
|
|
2970
|
+
y = df[yname]
|
|
2759
2971
|
|
|
2760
|
-
|
|
2972
|
+
X_const = sm.add_constant(x)
|
|
2973
|
+
logit_model = sm.Logit(y, X_const)
|
|
2974
|
+
logit_fit = logit_model.fit(disp=False)
|
|
2975
|
+
|
|
2976
|
+
# report 파라미터에 따른 처리
|
|
2977
|
+
if not report or report is False:
|
|
2978
|
+
# 리포트 미사용
|
|
2979
|
+
return logit_fit
|
|
2980
|
+
elif report == 'full':
|
|
2981
|
+
# 풀 리포트 (full=True)
|
|
2982
|
+
cdf, rdf, result_report, model_report, variable_reports, cm = logit_report(logit_fit, df, threshold=0.5, full=True, alpha=0.05) # type: ignore
|
|
2983
|
+
return logit_fit, cdf, rdf, result_report, model_report, variable_reports
|
|
2984
|
+
else:
|
|
2985
|
+
# 요약 리포트 (report == 'summary')
|
|
2986
|
+
cdf, rdf = logit_report(logit_fit, df, threshold=0.5, full=False, alpha=0.05) # type: ignore
|
|
2987
|
+
# 요약에서는 result_report와 variable_reports만 포함
|
|
2988
|
+
# 간단한 버전으로 result와 variable_reports만 생성
|
|
2989
|
+
return logit_fit, rdf
|
|
2761
2990
|
|
|
2762
2991
|
|
|
2763
2992
|
# ===================================================================
|
|
@@ -2881,122 +3110,4 @@ def predict(fit, data: DataFrame | Series) -> DataFrame | Series | float:
|
|
|
2881
3110
|
f"예측 과정에서 오류가 발생했습니다.\n"
|
|
2882
3111
|
f"모형 학습 시 사용한 특성과 입력 데이터의 특성이 일치하는지 확인하세요.\n"
|
|
2883
3112
|
f"원본 오류: {str(e)}"
|
|
2884
|
-
)
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
# ===================================================================
|
|
2888
|
-
# 상관계수 및 효과크기 분석 (Correlation & Effect Size)
|
|
2889
|
-
# ===================================================================
|
|
2890
|
-
def corr_effect_size(data: DataFrame, dv: str, *fields: str, alpha: float = 0.05) -> DataFrame:
|
|
2891
|
-
"""종속변수와의 편상관계수 및 효과크기를 계산한다.
|
|
2892
|
-
|
|
2893
|
-
각 독립변수와 종속변수 간의 상관계수를 계산하되, 정규성과 선형성을 검사하여
|
|
2894
|
-
Pearson 또는 Spearman 상관계수를 적절히 선택한다.
|
|
2895
|
-
Cohen's d (효과크기)를 계산하여 상관 강도를 정량화한다.
|
|
2896
|
-
|
|
2897
|
-
Args:
|
|
2898
|
-
data (DataFrame): 분석 대상 데이터프레임.
|
|
2899
|
-
dv (str): 종속변수 컬럼 이름.
|
|
2900
|
-
*fields (str): 독립변수 컬럼 이름들. 지정하지 않으면 수치형 컬럼 중 dv 제외 모두 사용.
|
|
2901
|
-
alpha (float, optional): 유의수준. 기본 0.05.
|
|
2902
|
-
|
|
2903
|
-
Returns:
|
|
2904
|
-
DataFrame: 다음 컬럼을 포함한 데이터프레임:
|
|
2905
|
-
- Variable (str): 독립변수 이름
|
|
2906
|
-
- Correlation (float): 상관계수 (Pearson 또는 Spearman)
|
|
2907
|
-
- Corr_Type (str): 선택된 상관계수 종류 ('Pearson' 또는 'Spearman')
|
|
2908
|
-
- P-value (float): 상관계수의 유의확률
|
|
2909
|
-
- Cohens_d (float): 표준화된 효과크기
|
|
2910
|
-
- Effect_Size (str): 효과크기 분류 ('Large', 'Medium', 'Small', 'Negligible')
|
|
2911
|
-
|
|
2912
|
-
Examples:
|
|
2913
|
-
```python
|
|
2914
|
-
from hossam import *
|
|
2915
|
-
from pandas import DataFrame
|
|
2916
|
-
|
|
2917
|
-
df = DataFrame({'age': [20, 30, 40, 50],
|
|
2918
|
-
'bmi': [22, 25, 28, 30],
|
|
2919
|
-
'charges': [1000, 2000, 3000, 4000]})
|
|
2920
|
-
|
|
2921
|
-
result = hs_stats.corr_effect_size(df, 'charges', 'age', 'bmi')
|
|
2922
|
-
```
|
|
2923
|
-
"""
|
|
2924
|
-
|
|
2925
|
-
# fields가 지정되지 않으면 수치형 컬럼 중 dv 제외 모두 사용
|
|
2926
|
-
if not fields:
|
|
2927
|
-
fields = [col for col in data.columns if is_numeric_dtype(data[col]) and col != dv] # type: ignore
|
|
2928
|
-
|
|
2929
|
-
# dv가 수치형인지 확인
|
|
2930
|
-
if not is_numeric_dtype(data[dv]):
|
|
2931
|
-
raise ValueError(f"Dependent variable '{dv}' must be numeric type")
|
|
2932
|
-
|
|
2933
|
-
results = []
|
|
2934
|
-
|
|
2935
|
-
for var in fields:
|
|
2936
|
-
if not is_numeric_dtype(data[var]):
|
|
2937
|
-
continue
|
|
2938
|
-
|
|
2939
|
-
# 결측치 제거
|
|
2940
|
-
valid_idx = data[[var, dv]].notna().all(axis=1)
|
|
2941
|
-
x = data.loc[valid_idx, var].values
|
|
2942
|
-
y = data.loc[valid_idx, dv].values
|
|
2943
|
-
|
|
2944
|
-
if len(x) < 3:
|
|
2945
|
-
continue
|
|
2946
|
-
|
|
2947
|
-
# 정규성 검사 (Shapiro-Wilk: n <= 5000 권장, 그 외 D'Agostino)
|
|
2948
|
-
method_x = 's' if len(x) <= 5000 else 'n'
|
|
2949
|
-
method_y = 's' if len(y) <= 5000 else 'n'
|
|
2950
|
-
|
|
2951
|
-
normal_x_result = normal_test(data[[var]], columns=[var], method=method_x)
|
|
2952
|
-
normal_y_result = normal_test(data[[dv]], columns=[dv], method=method_y)
|
|
2953
|
-
|
|
2954
|
-
# 정규성 판정 (p > alpha면 정규분포 가정)
|
|
2955
|
-
normal_x = normal_x_result.loc[var, 'p-val'] > alpha if var in normal_x_result.index else False # type: ignore
|
|
2956
|
-
normal_y = normal_y_result.loc[dv, 'p-val'] > alpha if dv in normal_y_result.index else False # type: ignore
|
|
2957
|
-
|
|
2958
|
-
# Pearson (모두 정규) vs Spearman (하나라도 비정규)
|
|
2959
|
-
if normal_x and normal_y:
|
|
2960
|
-
r, p = pearsonr(x, y)
|
|
2961
|
-
corr_type = 'Pearson'
|
|
2962
|
-
else:
|
|
2963
|
-
r, p = spearmanr(x, y)
|
|
2964
|
-
corr_type = 'Spearman'
|
|
2965
|
-
|
|
2966
|
-
# Cohen's d 계산 (상관계수에서 효과크기로 변환)
|
|
2967
|
-
# d = 2*r / sqrt(1-r^2)
|
|
2968
|
-
if r ** 2 < 1: # type: ignore
|
|
2969
|
-
d = (2 * r) / np.sqrt(1 - r ** 2) # type: ignore
|
|
2970
|
-
else:
|
|
2971
|
-
d = 0
|
|
2972
|
-
|
|
2973
|
-
# 효과크기 분류 (Cohen's d 기준)
|
|
2974
|
-
# Small: 0.2 < |d| <= 0.5
|
|
2975
|
-
# Medium: 0.5 < |d| <= 0.8
|
|
2976
|
-
# Large: |d| > 0.8
|
|
2977
|
-
abs_d = abs(d)
|
|
2978
|
-
if abs_d > 0.8:
|
|
2979
|
-
effect_size = 'Large'
|
|
2980
|
-
elif abs_d > 0.5:
|
|
2981
|
-
effect_size = 'Medium'
|
|
2982
|
-
elif abs_d > 0.2:
|
|
2983
|
-
effect_size = 'Small'
|
|
2984
|
-
else:
|
|
2985
|
-
effect_size = 'Negligible'
|
|
2986
|
-
|
|
2987
|
-
results.append({
|
|
2988
|
-
'Variable': var,
|
|
2989
|
-
'Correlation': r,
|
|
2990
|
-
'Corr_Type': corr_type,
|
|
2991
|
-
'P-value': p,
|
|
2992
|
-
'Cohens_d': d,
|
|
2993
|
-
'Effect_Size': effect_size
|
|
2994
|
-
})
|
|
2995
|
-
|
|
2996
|
-
result_df = DataFrame(results)
|
|
2997
|
-
|
|
2998
|
-
# 상관계수로 정렬 (절댓값 기준 내림차순)
|
|
2999
|
-
if len(result_df) > 0:
|
|
3000
|
-
result_df = result_df.sort_values('Correlation', key=lambda x: x.abs(), ascending=False).reset_index(drop=True)
|
|
3001
|
-
|
|
3002
|
-
return result_df
|
|
3113
|
+
)
|