hossam 0.4.4__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_gis.py +17 -18
- hossam/hs_plot.py +334 -347
- hossam/hs_prep.py +36 -31
- hossam/hs_stats.py +1360 -1250
- hossam/hs_timeserise.py +38 -39
- hossam/hs_util.py +202 -7
- {hossam-0.4.4.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 -205
- hossam-0.4.4.dist-info/RECORD +0 -16
- {hossam-0.4.4.dist-info → hossam-0.4.6.dist-info}/WHEEL +0 -0
- {hossam-0.4.4.dist-info → hossam-0.4.6.dist-info}/licenses/LICENSE +0 -0
- {hossam-0.4.4.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
|
|
4
|
+
from typing import overload, Tuple, Literal, LiteralString, Union, Any
|
|
5
5
|
|
|
6
6
|
# -------------------------------------------------------------
|
|
7
7
|
import numpy as np
|
|
@@ -24,9 +24,9 @@ from scipy.stats import (
|
|
|
24
24
|
normaltest,
|
|
25
25
|
bartlett,
|
|
26
26
|
levene,
|
|
27
|
-
ttest_1samp,
|
|
27
|
+
ttest_1samp, # type: ignore
|
|
28
28
|
ttest_ind as scipy_ttest_ind,
|
|
29
|
-
ttest_rel,
|
|
29
|
+
ttest_rel, # type: ignore
|
|
30
30
|
wilcoxon,
|
|
31
31
|
pearsonr,
|
|
32
32
|
spearmanr,
|
|
@@ -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(결측치 무작위성) 검정
|
|
@@ -375,29 +376,29 @@ def describe(data: DataFrame, *fields: str, columns: list | None = None):
|
|
|
375
376
|
outlier_rate = (outlier_count / len(data)) * 100
|
|
376
377
|
|
|
377
378
|
# 분포 특성 판정 (왜도 기준)
|
|
378
|
-
abs_skew = abs(skew)
|
|
379
|
-
if abs_skew < 0.5:
|
|
379
|
+
abs_skew = abs(skew) # type: ignore
|
|
380
|
+
if abs_skew < 0.5: # type: ignore
|
|
380
381
|
dist = "거의 대칭"
|
|
381
|
-
elif abs_skew < 1.0:
|
|
382
|
-
if skew > 0:
|
|
382
|
+
elif abs_skew < 1.0: # type: ignore
|
|
383
|
+
if skew > 0: # type: ignore
|
|
383
384
|
dist = "약한 우측 꼬리"
|
|
384
385
|
else:
|
|
385
386
|
dist = "약한 좌측 꼬리"
|
|
386
|
-
elif abs_skew < 2.0:
|
|
387
|
-
if skew > 0:
|
|
387
|
+
elif abs_skew < 2.0: # type: ignore
|
|
388
|
+
if skew > 0: # type: ignore
|
|
388
389
|
dist = "중간 우측 꼬리"
|
|
389
390
|
else:
|
|
390
391
|
dist = "중간 좌측 꼬리"
|
|
391
392
|
else:
|
|
392
|
-
if skew > 0:
|
|
393
|
+
if skew > 0: # type: ignore
|
|
393
394
|
dist = "극단 우측 꼬리"
|
|
394
395
|
else:
|
|
395
396
|
dist = "극단 좌측 꼬리"
|
|
396
397
|
|
|
397
398
|
# 로그변환 필요성 판정
|
|
398
|
-
if abs_skew < 0.5:
|
|
399
|
+
if abs_skew < 0.5: # type: ignore
|
|
399
400
|
log_need = "낮음"
|
|
400
|
-
elif abs_skew < 1.0:
|
|
401
|
+
elif abs_skew < 1.0: # type: ignore
|
|
401
402
|
log_need = "중간"
|
|
402
403
|
else:
|
|
403
404
|
log_need = "높음"
|
|
@@ -473,7 +474,7 @@ def category_describe(data: DataFrame, *fields: str):
|
|
|
473
474
|
"""
|
|
474
475
|
if not fields:
|
|
475
476
|
# 명목형(범주형) 컬럼 선택: object, category, bool 타입
|
|
476
|
-
fields = data.select_dtypes(include=['object', 'category', 'bool']).columns
|
|
477
|
+
fields = data.select_dtypes(include=['object', 'category', 'bool']).columns # type: ignore
|
|
477
478
|
|
|
478
479
|
result = []
|
|
479
480
|
summary = []
|
|
@@ -730,7 +731,7 @@ def equal_var_test(data: DataFrame, columns: list | str | None = None, normal_di
|
|
|
730
731
|
normality_result = normal_test(data[numeric_cols], method="n")
|
|
731
732
|
# 모든 컬럼이 정규분포를 따르는지 확인
|
|
732
733
|
all_normal = normality_result["is_normal"].all()
|
|
733
|
-
normal_dist = all_normal
|
|
734
|
+
normal_dist = all_normal # type: ignore
|
|
734
735
|
|
|
735
736
|
try:
|
|
736
737
|
if normal_dist:
|
|
@@ -829,7 +830,7 @@ def ttest_1samp(data, mean_value: float = 0.0) -> DataFrame:
|
|
|
829
830
|
else:
|
|
830
831
|
for a in alternative:
|
|
831
832
|
try:
|
|
832
|
-
s, p = ttest_1samp(col_data, mean_value, alternative=a)
|
|
833
|
+
s, p = ttest_1samp(col_data, mean_value, alternative=a) # type: ignore
|
|
833
834
|
|
|
834
835
|
itp = None
|
|
835
836
|
|
|
@@ -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()
|
|
@@ -939,28 +955,28 @@ def ttest_ind(x, y, equal_var: bool | None = None) -> DataFrame:
|
|
|
939
955
|
|
|
940
956
|
for a in alternative:
|
|
941
957
|
try:
|
|
942
|
-
s, p = scipy_ttest_ind(x_data, y_data, equal_var=equal_var, alternative=a)
|
|
958
|
+
s, p = scipy_ttest_ind(x_data, y_data, equal_var=equal_var, alternative=a) # type: ignore
|
|
943
959
|
n = "t-test_ind" if equal_var else "Welch's t-test"
|
|
944
960
|
|
|
945
961
|
# 검정 결과 해석
|
|
946
962
|
itp = None
|
|
947
963
|
|
|
948
964
|
if a == "two-sided":
|
|
949
|
-
itp = fmt.format("==" if p > 0.05 else "!=")
|
|
965
|
+
itp = fmt.format("==" if p > 0.05 else "!=") # type: ignore
|
|
950
966
|
elif a == "less":
|
|
951
|
-
itp = fmt.format(">=" if p > 0.05 else "<")
|
|
967
|
+
itp = fmt.format(">=" if p > 0.05 else "<") # type: ignore
|
|
952
968
|
else:
|
|
953
|
-
itp = fmt.format("<=" if p > 0.05 else ">")
|
|
969
|
+
itp = fmt.format("<=" if p > 0.05 else ">") # type: ignore
|
|
954
970
|
|
|
955
971
|
result.append({
|
|
956
972
|
"test": n,
|
|
957
973
|
"alternative": a,
|
|
958
|
-
"statistic": round(s, 3),
|
|
959
|
-
"p-value": round(p, 4),
|
|
960
|
-
"H0": p > 0.05,
|
|
961
|
-
"H1": p <= 0.05,
|
|
962
974
|
"interpretation": itp,
|
|
963
|
-
"equal_var_checked": var_checked
|
|
975
|
+
"equal_var_checked": var_checked,
|
|
976
|
+
"statistic": round(s, 3), # type: ignore
|
|
977
|
+
"p-value": round(p, 4), # type: ignore
|
|
978
|
+
"H0": p > 0.05, # type: ignore
|
|
979
|
+
"H1": p <= 0.05, # type: ignore
|
|
964
980
|
})
|
|
965
981
|
except Exception as e:
|
|
966
982
|
result.append({
|
|
@@ -1068,7 +1084,7 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
|
|
|
1068
1084
|
for a in alternative:
|
|
1069
1085
|
try:
|
|
1070
1086
|
if parametric:
|
|
1071
|
-
s, p = ttest_rel(x_data, y_data, alternative=a)
|
|
1087
|
+
s, p = ttest_rel(x_data, y_data, alternative=a) # type: ignore
|
|
1072
1088
|
n = "t-test_paired"
|
|
1073
1089
|
else:
|
|
1074
1090
|
# Wilcoxon signed-rank test (대응표본용 비모수 검정)
|
|
@@ -1078,19 +1094,19 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
|
|
|
1078
1094
|
itp = None
|
|
1079
1095
|
|
|
1080
1096
|
if a == "two-sided":
|
|
1081
|
-
itp = fmt.format("==" if p > 0.05 else "!=")
|
|
1097
|
+
itp = fmt.format("==" if p > 0.05 else "!=") # type: ignore
|
|
1082
1098
|
elif a == "less":
|
|
1083
|
-
itp = fmt.format(">=" if p > 0.05 else "<")
|
|
1099
|
+
itp = fmt.format(">=" if p > 0.05 else "<") # type: ignore
|
|
1084
1100
|
else:
|
|
1085
|
-
itp = fmt.format("<=" if p > 0.05 else ">")
|
|
1101
|
+
itp = fmt.format("<=" if p > 0.05 else ">") # type: ignore
|
|
1086
1102
|
|
|
1087
1103
|
result.append({
|
|
1088
1104
|
"test": n,
|
|
1089
1105
|
"alternative": a,
|
|
1090
|
-
"statistic": round(s, 3) if not np.isnan(s) else s,
|
|
1091
|
-
"p-value": round(p, 4) if not np.isnan(p) else p,
|
|
1092
|
-
"H0": p > 0.05,
|
|
1093
|
-
"H1": p <= 0.05,
|
|
1106
|
+
"statistic": round(s, 3) if not np.isnan(s) else s, # type: ignore
|
|
1107
|
+
"p-value": round(p, 4) if not np.isnan(p) else p, # type: ignore
|
|
1108
|
+
"H0": p > 0.05, # type: ignore
|
|
1109
|
+
"H1": p <= 0.05, # type: ignore
|
|
1094
1110
|
"interpretation": itp,
|
|
1095
1111
|
"normality_checked": var_checked
|
|
1096
1112
|
})
|
|
@@ -1112,696 +1128,1111 @@ 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
|
-
"""
|
|
1165
|
+
from pandas import DataFrame
|
|
1144
1166
|
|
|
1145
|
-
|
|
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
|
+
})
|
|
1146
1171
|
|
|
1147
|
-
|
|
1148
|
-
y = None
|
|
1149
|
-
if yname and yname in df.columns:
|
|
1150
|
-
y = df[yname]
|
|
1151
|
-
df = df.drop(columns=[yname])
|
|
1172
|
+
anova_df, anova_report, posthoc_df, posthoc_report = hs_stats.oneway_anova(df, dv='score', between='group')
|
|
1152
1173
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1174
|
+
# 사후검정결과는 ANOVA가 유의할 때만 생성됨
|
|
1175
|
+
if posthoc_df is not None:
|
|
1176
|
+
print(posthoc_report)
|
|
1177
|
+
print(posthoc_df.head())
|
|
1178
|
+
```
|
|
1156
1179
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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}' 컬럼이 데이터프레임에 없습니다.")
|
|
1160
1188
|
|
|
1161
|
-
|
|
1162
|
-
X = df[numeric_cols]
|
|
1163
|
-
if ignore_cols_present:
|
|
1164
|
-
X = X.drop(columns=ignore_cols_present, errors="ignore")
|
|
1189
|
+
df_filtered = data[[dv, between]].dropna()
|
|
1165
1190
|
|
|
1166
|
-
#
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1191
|
+
# ============================================
|
|
1192
|
+
# 1. 정규성 검정 (각 그룹별로 수행)
|
|
1193
|
+
# ============================================
|
|
1194
|
+
group_names = sorted(df_filtered[between].unique())
|
|
1195
|
+
normality_satisfied = True
|
|
1170
1196
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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))
|
|
1186
|
-
except Exception:
|
|
1187
|
-
# 계산 실패 시 무한대로 처리하여 우선 제거 대상으로
|
|
1188
|
-
vifs[col] = float("inf")
|
|
1189
|
-
return vifs
|
|
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
|
|
1190
1204
|
|
|
1191
|
-
#
|
|
1192
|
-
|
|
1193
|
-
|
|
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])
|
|
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
|
|
1205
1212
|
|
|
1206
|
-
#
|
|
1207
|
-
if
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
|
1210
1225
|
|
|
1211
|
-
#
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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)
|
|
1215
1237
|
|
|
1216
|
-
|
|
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)
|
|
1217
1242
|
|
|
1243
|
+
# 유의성 여부 컬럼 추가
|
|
1244
|
+
if 'p-unc' in anova_df.columns:
|
|
1245
|
+
anova_df['significant'] = anova_df['p-unc'] <= alpha
|
|
1218
1246
|
|
|
1247
|
+
# ANOVA 결과가 유의한지 확인
|
|
1248
|
+
p_unc = float(anova_df.loc[0, 'p-unc']) # type: ignore
|
|
1249
|
+
anova_significant = p_unc <= alpha
|
|
1219
1250
|
|
|
1220
|
-
#
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
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
|
|
1225
1257
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
value_count (int, optional): x 데이터의 범위 안에서 간격 수. Defaults to 100.
|
|
1258
|
+
df1 = _safe_get('ddof1')
|
|
1259
|
+
df2 = _safe_get('ddof2')
|
|
1260
|
+
fval = _safe_get('F')
|
|
1261
|
+
eta2 = _safe_get('np2')
|
|
1231
1262
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1263
|
+
anova_sig_text = "그룹별 평균이 다를 가능성이 높습니다." if anova_significant else "그룹별 평균 차이에 대한 근거가 부족합니다."
|
|
1264
|
+
assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않았고'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않았다'}고 판단됩니다."
|
|
1234
1265
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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)
|
|
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
|
+
)
|
|
1246
1270
|
|
|
1247
|
-
if
|
|
1248
|
-
|
|
1271
|
+
if not np.isnan(eta2):
|
|
1272
|
+
anova_report += f" 효과 크기(η²p) ≈ {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
|
|
1249
1273
|
|
|
1250
|
-
|
|
1274
|
+
# ============================================
|
|
1275
|
+
# 4. 사후검정 (ANOVA 유의할 때만)
|
|
1276
|
+
# ============================================
|
|
1277
|
+
posthoc_df = None
|
|
1278
|
+
posthoc_method = 'None'
|
|
1279
|
+
posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
|
|
1251
1280
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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)
|
|
1255
1290
|
|
|
1256
|
-
|
|
1257
|
-
|
|
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)
|
|
1258
1295
|
|
|
1259
|
-
|
|
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
|
|
1260
1299
|
|
|
1300
|
+
if p_col:
|
|
1301
|
+
# 유의성 여부 컬럼 추가
|
|
1302
|
+
posthoc_df['significant'] = posthoc_df[p_col] <= alpha
|
|
1261
1303
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
]:
|
|
1269
|
-
"""선형회귀 적합 결과를 요약 리포트로 변환한다.
|
|
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()]
|
|
1270
1310
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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 정보를 찾지 못해 유의성을 확인할 수 없습니다."
|
|
1276
1321
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
- 적합도 요약 (`result_report`, str): R, R², F, p-value, Durbin-Watson 등 핵심 지표 문자열.
|
|
1282
|
-
- 모형 보고 문장 (`model_report`, str): F-검정 유의성에 기반한 서술형 문장.
|
|
1283
|
-
- 변수별 보고 리스트 (`variable_reports`, list[str]): 각 예측변수에 대한 서술형 문장.
|
|
1284
|
-
- 회귀식 문자열 (`equation_text`, str): 상수항과 계수를 포함한 회귀식 표현.
|
|
1322
|
+
# ============================================
|
|
1323
|
+
# 5. 결과 반환
|
|
1324
|
+
# ============================================
|
|
1325
|
+
return anova_df, anova_report, posthoc_df, posthoc_report
|
|
1285
1326
|
|
|
1286
|
-
full=False일 때:
|
|
1287
|
-
- 성능 지표 표 (`pdf`, DataFrame): R, R², Adj. R², F, p-value, Durbin-Watson.
|
|
1288
|
-
- 회귀계수 표 (`rdf`, DataFrame)
|
|
1289
1327
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1328
|
+
# ===================================================================
|
|
1329
|
+
# 이원 분산분석 (Two-way ANOVA: 두 범주형 독립변수)
|
|
1330
|
+
# ===================================================================
|
|
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
|
+
"""두 범주형 요인에 대한 이원분산분석을 수행하고 해석용 보고문을 반환한다.
|
|
1293
1339
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1340
|
+
분석 흐름:
|
|
1341
|
+
1) 각 셀(요인 조합)별 정규성 검정
|
|
1342
|
+
2) 전체 셀을 대상으로 등분산성 검정 (정규성 충족 시 Bartlett, 불충족 시 Levene)
|
|
1343
|
+
3) 두 요인 및 교호작용을 포함한 2원 ANOVA 수행
|
|
1344
|
+
4) 유의한 요인에 대해 Tukey HSD 사후검정(요인별) 실행
|
|
1296
1345
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1346
|
+
Args:
|
|
1347
|
+
data (DataFrame): 종속변수와 두 개의 범주형 요인을 포함한 데이터프레임.
|
|
1348
|
+
dv (str): 종속변수 컬럼명.
|
|
1349
|
+
factor_a (str): 첫 번째 요인 컬럼명.
|
|
1350
|
+
factor_b (str): 두 번째 요인 컬럼명.
|
|
1351
|
+
alpha (float, optional): 유의수준. 기본 0.05.
|
|
1299
1352
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1353
|
+
Returns:
|
|
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): 사후검정 유무 및 유의 쌍 요약 문장.
|
|
1304
1359
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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}' 컬럼이 데이터프레임에 없습니다.")
|
|
1308
1367
|
|
|
1309
|
-
|
|
1310
|
-
yname = fit.model.endog_names
|
|
1368
|
+
df_filtered = data[[dv, factor_a, factor_b]].dropna()
|
|
1311
1369
|
|
|
1312
|
-
#
|
|
1313
|
-
|
|
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
|
|
1314
1379
|
|
|
1315
|
-
#
|
|
1316
|
-
|
|
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
|
|
1317
1390
|
|
|
1318
|
-
#
|
|
1319
|
-
|
|
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
|
|
1320
1397
|
|
|
1321
|
-
#
|
|
1322
|
-
|
|
1323
|
-
indi_df_const = sm.add_constant(indi_df, has_constant="add")
|
|
1324
|
-
for i, col in enumerate(indi_df.columns, start=1): # 상수항이 0이므로 1부터 시작
|
|
1398
|
+
# 보고문 생성
|
|
1399
|
+
def _safe(row, col, default=np.nan):
|
|
1325
1400
|
try:
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
|
1401
|
+
return float(row[col])
|
|
1402
|
+
except Exception:
|
|
1403
|
+
return default
|
|
1335
1404
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
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":
|
|
1339
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)
|
|
1340
1423
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
t = float(row['t'])
|
|
1344
|
-
p = float(row['P>|t|'])
|
|
1424
|
+
assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않음'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않음'}으로 판단했습니다."
|
|
1425
|
+
anova_report = " ".join(reports) + " " + assumption_text
|
|
1345
1426
|
|
|
1346
|
-
|
|
1347
|
-
|
|
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()
|
|
1348
1431
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1432
|
+
for factor, sig in sig_flags.items():
|
|
1433
|
+
if factor is None:
|
|
1434
|
+
continue
|
|
1435
|
+
factor_lower = str(factor).lower()
|
|
1351
1436
|
|
|
1352
|
-
#
|
|
1353
|
-
|
|
1437
|
+
# 교호작용(residual 포함) 혹은 비유의 항은 건너뛴다
|
|
1438
|
+
if factor_lower in ["residual", interaction_name, interaction_name_spaced] or not sig:
|
|
1439
|
+
continue
|
|
1354
1440
|
|
|
1355
|
-
#
|
|
1356
|
-
|
|
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
|
-
)
|
|
1441
|
+
# 실제 컬럼이 아니면 건너뛴다 (ex: "A * B" 같은 교호작용 이름)
|
|
1442
|
+
if factor not in df_filtered.columns:
|
|
1443
|
+
continue
|
|
1370
1444
|
|
|
1371
|
-
|
|
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)
|
|
1372
1451
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
for
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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 정보를 찾지 못했습니다."
|
|
1385
1474
|
|
|
1386
|
-
|
|
1387
|
-
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']})"
|
|
1475
|
+
return anova_df, anova_report, posthoc_df, posthoc_report
|
|
1388
1476
|
|
|
1389
|
-
# 모형 보고 문장 구성
|
|
1390
|
-
tpl = "%s에 대하여 %s로 예측하는 회귀분석을 실시한 결과, 이 회귀모형은 통계적으로 %s(F(%s,%s) = %s, p %s 0.05)."
|
|
1391
|
-
model_report = tpl % (
|
|
1392
|
-
rdf["종속변수"][0],
|
|
1393
|
-
",".join(list(rdf["독립변수"])),
|
|
1394
|
-
(
|
|
1395
|
-
"유의하다"
|
|
1396
|
-
if float(result_dict["Prob (F-statistic)"]) <= 0.05
|
|
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
|
-
)
|
|
1404
|
-
|
|
1405
|
-
# 변수별 보고 문장 리스트 구성
|
|
1406
|
-
variable_reports = []
|
|
1407
|
-
s = "%s의 회귀계수는 %s(p %s 0.05)로, %s에 대하여 %s 예측변인인 것으로 나타났다."
|
|
1408
|
-
|
|
1409
|
-
for i in rdf.index:
|
|
1410
|
-
row = rdf.iloc[i]
|
|
1411
|
-
variable_reports.append(
|
|
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
|
-
)
|
|
1421
|
-
|
|
1422
|
-
# -----------------------------
|
|
1423
|
-
# 회귀식 자동 출력
|
|
1424
|
-
# -----------------------------
|
|
1425
|
-
intercept = fit.params["const"]
|
|
1426
|
-
terms = []
|
|
1427
|
-
|
|
1428
|
-
for name in xnames:
|
|
1429
|
-
coef = fit.params[name]
|
|
1430
|
-
sign = "+" if coef >= 0 else "-"
|
|
1431
|
-
terms.append(f" {sign} {abs(coef):.3f}·{name}")
|
|
1432
|
-
|
|
1433
|
-
equation_text = f"{yname} = {intercept:.3f}" + "".join(terms)
|
|
1434
|
-
|
|
1435
|
-
# 성능 지표 표 생성 (pdf)
|
|
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
|
-
)
|
|
1445
|
-
|
|
1446
|
-
if full:
|
|
1447
|
-
return pdf, rdf, result_report, model_report, variable_reports, equation_text
|
|
1448
|
-
else:
|
|
1449
|
-
return pdf, rdf
|
|
1450
1477
|
|
|
1451
1478
|
|
|
1452
1479
|
# ===================================================================
|
|
1453
|
-
#
|
|
1480
|
+
# 종속변수에 대한 편상관계수 및 효과크기 분석 (Correlation & Effect Size)
|
|
1454
1481
|
# ===================================================================
|
|
1455
|
-
def
|
|
1456
|
-
|
|
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
|
-
"""선형회귀분석을 수행하고 적합 결과를 반환한다.
|
|
1482
|
+
def corr_effect_size(data: DataFrame, dv: str, *fields: str, alpha: float = 0.05) -> DataFrame:
|
|
1483
|
+
"""종속변수와의 편상관계수 및 효과크기를 계산한다.
|
|
1469
1484
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1485
|
+
각 독립변수와 종속변수 간의 상관계수를 계산하되, 정규성과 선형성을 검사하여
|
|
1486
|
+
Pearson 또는 Spearman 상관계수를 적절히 선택한다.
|
|
1487
|
+
Cohen's d (효과크기)를 계산하여 상관 강도를 정량화한다.
|
|
1472
1488
|
|
|
1473
1489
|
Args:
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
- 1 또는 'summary': 요약 리포트 반환 (full=False).
|
|
1479
|
-
- 2 또는 'full': 풀 리포트 반환 (full=True).
|
|
1480
|
-
- True: 풀 리포트 반환 (2와 동일).
|
|
1490
|
+
data (DataFrame): 분석 대상 데이터프레임.
|
|
1491
|
+
dv (str): 종속변수 컬럼 이름.
|
|
1492
|
+
*fields (str): 독립변수 컬럼 이름들. 지정하지 않으면 수치형 컬럼 중 dv 제외 모두 사용.
|
|
1493
|
+
alpha (float, optional): 유의수준. 기본 0.05.
|
|
1481
1494
|
|
|
1482
1495
|
Returns:
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
(fit, pdf, rdf, result_report, model_report, variable_reports, equation_text) 형태로:
|
|
1491
|
-
- fit: 선형회귀 적합 결과 객체
|
|
1492
|
-
- pdf: 성능 지표 표 (DataFrame): R, R², F, p-value, Durbin-Watson
|
|
1493
|
-
- rdf: 회귀계수 표 (DataFrame)
|
|
1494
|
-
- result_report: 적합도 요약 (str)
|
|
1495
|
-
- model_report: 모형 보고 문장 (str)
|
|
1496
|
-
- variable_reports: 변수별 보고 문장 리스트 (list[str])
|
|
1497
|
-
- equation_text: 회귀식 문자열 (str)
|
|
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')
|
|
1498
1503
|
|
|
1499
1504
|
Examples:
|
|
1500
1505
|
```python
|
|
1501
1506
|
from hossam import *
|
|
1502
1507
|
from pandas import DataFrame
|
|
1503
|
-
import numpy as np
|
|
1504
|
-
|
|
1505
|
-
df = DataFrame({
|
|
1506
|
-
'target': np.random.normal(100, 10, 100),
|
|
1507
|
-
'x1': np.random.normal(0, 1, 100),
|
|
1508
|
-
'x2': np.random.normal(0, 1, 100)
|
|
1509
|
-
})
|
|
1510
|
-
|
|
1511
|
-
# 적합 결과만 반환
|
|
1512
|
-
fit = hs_stats.ols(df, 'target')
|
|
1513
1508
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1509
|
+
df = DataFrame({'age': [20, 30, 40, 50],
|
|
1510
|
+
'bmi': [22, 25, 28, 30],
|
|
1511
|
+
'charges': [1000, 2000, 3000, 4000]})
|
|
1516
1512
|
|
|
1517
|
-
|
|
1518
|
-
fit, pdf, rdf, result_report, model_report, var_reports, eq = hs_stats.ols(df, 'target', report='full')
|
|
1513
|
+
result = hs_stats.corr_effect_size(df, 'charges', 'age', 'bmi')
|
|
1519
1514
|
```
|
|
1520
1515
|
"""
|
|
1521
|
-
x = df.drop(yname, axis=1)
|
|
1522
|
-
y = df[yname]
|
|
1523
1516
|
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
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
|
|
1527
1520
|
|
|
1528
|
-
#
|
|
1529
|
-
if not
|
|
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)
|
|
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)
|
|
1539
|
-
return linear_fit, pdf, rdf, result_report, model_report, variable_reports, equation_text
|
|
1540
|
-
else:
|
|
1541
|
-
# 기본값: 리포트 미사용
|
|
1542
|
-
return linear_fit
|
|
1521
|
+
# dv가 수치형인지 확인
|
|
1522
|
+
if not is_numeric_dtype(data[dv]):
|
|
1523
|
+
raise ValueError(f"Dependent variable '{dv}' must be numeric type")
|
|
1543
1524
|
|
|
1525
|
+
results = []
|
|
1544
1526
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
def logit_report(
|
|
1549
|
-
fit: BinaryResultsWrapper,
|
|
1550
|
-
data: DataFrame,
|
|
1551
|
-
threshold: float = 0.5,
|
|
1552
|
-
full: Union[bool, str, int] = False,
|
|
1553
|
-
alpha: float = 0.05
|
|
1554
|
-
) -> Union[
|
|
1555
|
-
Tuple[DataFrame, DataFrame],
|
|
1556
|
-
Tuple[
|
|
1557
|
-
DataFrame,
|
|
1558
|
-
DataFrame,
|
|
1559
|
-
str,
|
|
1560
|
-
str,
|
|
1561
|
-
list[str],
|
|
1562
|
-
np.ndarray
|
|
1563
|
-
]
|
|
1564
|
-
]:
|
|
1565
|
-
"""로지스틱 회귀 적합 결과를 상세 리포트로 변환한다.
|
|
1527
|
+
for var in fields:
|
|
1528
|
+
if not is_numeric_dtype(data[var]):
|
|
1529
|
+
continue
|
|
1566
1530
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
full (bool | str | int): True이면 6개 값 반환, False이면 주요 2개(cdf, rdf)만 반환. 기본값 False.
|
|
1572
|
-
alpha (float): 유의수준. 기본값 0.05.
|
|
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
|
|
1573
1535
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
- 성능 지표 표 (`cdf`, DataFrame): McFadden Pseudo R², Accuracy, Precision, Recall, FPR, TNR, AUC, F1.
|
|
1577
|
-
- 회귀계수 표 (`rdf`, DataFrame): B, 표준오차, z, p-value, significant, OR, 95% CI, VIF 등.
|
|
1578
|
-
- 적합도 및 예측 성능 요약 (`result_report`, str): Pseudo R², LLR χ², p-value, Accuracy, AUC.
|
|
1579
|
-
- 모형 보고 문장 (`model_report`, str): LLR p-value에 기반한 서술형 문장.
|
|
1580
|
-
- 변수별 보고 리스트 (`variable_reports`, list[str]): 각 예측변수의 오즈비 해석 문장.
|
|
1581
|
-
- 혼동행렬 (`cm`, ndarray): 예측 결과와 실제값의 혼동행렬 [[TN, FP], [FN, TP]].
|
|
1536
|
+
if len(x) < 3:
|
|
1537
|
+
continue
|
|
1582
1538
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
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'
|
|
1586
1542
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
from hossam import *
|
|
1590
|
-
from pandas import DataFrame
|
|
1591
|
-
import numpy as np
|
|
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)
|
|
1592
1545
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
'x2': np.random.normal(0, 1, 100)
|
|
1597
|
-
})
|
|
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
|
|
1598
1549
|
|
|
1599
|
-
#
|
|
1600
|
-
|
|
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'
|
|
1601
1557
|
|
|
1602
|
-
#
|
|
1603
|
-
|
|
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
|
|
1604
1564
|
|
|
1605
|
-
#
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
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'
|
|
1609
1578
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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
|
|
1586
|
+
})
|
|
1617
1587
|
|
|
1618
|
-
|
|
1619
|
-
cm = confusion_matrix(y_true, y_pred_fix)
|
|
1620
|
-
tn, fp, fn, tp = cm.ravel()
|
|
1588
|
+
result_df = DataFrame(results)
|
|
1621
1589
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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)
|
|
1593
|
+
|
|
1594
|
+
return result_df
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
# ===================================================================
|
|
1598
|
+
# 쌍별 상관분석 (선형성/이상치 점검 후 Pearson/Spearman 자동 선택)
|
|
1599
|
+
# ===================================================================
|
|
1600
|
+
def corr_pairwise(
|
|
1601
|
+
data: DataFrame,
|
|
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을 자동 선택해 상관을 요약한다.
|
|
1610
|
+
|
|
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 추가
|
|
1617
|
+
|
|
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".
|
|
1626
|
+
|
|
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: 상관계수 행렬 (행과 열에 변수명, 값에 상관계수)
|
|
1633
|
+
|
|
1634
|
+
Examples:
|
|
1635
|
+
```python
|
|
1636
|
+
from hossam import *
|
|
1637
|
+
from pandas import DataFrame
|
|
1638
|
+
|
|
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'])
|
|
1644
|
+
```
|
|
1645
|
+
"""
|
|
1646
|
+
|
|
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])]
|
|
1654
|
+
|
|
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()
|
|
1659
|
+
|
|
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()
|
|
1669
|
+
|
|
1670
|
+
rows = []
|
|
1671
|
+
|
|
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
|
|
1692
|
+
|
|
1693
|
+
x = pair_df[a]
|
|
1694
|
+
y = pair_df[b]
|
|
1695
|
+
|
|
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
|
+
)
|
|
1712
|
+
continue
|
|
1713
|
+
|
|
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
|
|
1728
|
+
|
|
1729
|
+
# 2) 이상치 플래그 (두 변수 중 하나라도 z-outlier 있으면 True)
|
|
1730
|
+
outlier_flag = bool(z_outlier_flags.get(a, False) or z_outlier_flags.get(b, False))
|
|
1731
|
+
|
|
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
|
|
1743
|
+
|
|
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(
|
|
1757
|
+
{
|
|
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,
|
|
1768
|
+
}
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
result_df = DataFrame(rows)
|
|
1772
|
+
|
|
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
|
|
1781
|
+
|
|
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 # 대칭성
|
|
1794
|
+
|
|
1795
|
+
return result_df, corr_matrix
|
|
1796
|
+
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
# ===================================================================
|
|
1800
|
+
# 독립변수간 다중공선성 제거
|
|
1801
|
+
# ===================================================================
|
|
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 이상인 변수를 반복적으로 제거한다.
|
|
1810
|
+
|
|
1811
|
+
Args:
|
|
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.
|
|
1817
|
+
|
|
1818
|
+
Returns:
|
|
1819
|
+
DataFrame: VIF가 threshold 이하인 변수만 남은 데이터프레임 (원본 컬럼 순서 유지)
|
|
1820
|
+
|
|
1821
|
+
Examples:
|
|
1822
|
+
```python
|
|
1823
|
+
# 기본 사용 예
|
|
1824
|
+
from hossam import *
|
|
1825
|
+
filtered = hs_stats.vif_filter(df, yname="target", ignore=["id"], threshold=10.0)
|
|
1826
|
+
```
|
|
1827
|
+
"""
|
|
1828
|
+
|
|
1829
|
+
df = data.copy()
|
|
1830
|
+
|
|
1831
|
+
# y 분리 (있다면)
|
|
1832
|
+
y = None
|
|
1833
|
+
if yname and yname in df.columns:
|
|
1834
|
+
y = df[yname]
|
|
1835
|
+
df = df.drop(columns=[yname])
|
|
1836
|
+
|
|
1837
|
+
# 제외할 목록 정리
|
|
1838
|
+
ignore = ignore or []
|
|
1839
|
+
ignore_cols_present = [c for c in ignore if c in df.columns]
|
|
1840
|
+
|
|
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])]
|
|
1844
|
+
|
|
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
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
# ===================================================================
|
|
1905
|
+
# x, y 데이터에 대한 추세선을 구한다.
|
|
1906
|
+
# ===================================================================
|
|
1907
|
+
def trend(x: Any, y: Any, degree: int = 1, value_count: int = 100) -> Tuple[np.ndarray, np.ndarray]:
|
|
1908
|
+
"""x, y 데이터에 대한 추세선을 구한다.
|
|
1909
|
+
|
|
1910
|
+
Args:
|
|
1911
|
+
x (_type_): 산점도 그래프에 대한 x 데이터
|
|
1912
|
+
y (_type_): 산점도 그래프에 대한 y 데이터
|
|
1913
|
+
degree (int, optional): 추세선 방정식의 차수. Defaults to 1.
|
|
1914
|
+
value_count (int, optional): x 데이터의 범위 안에서 간격 수. Defaults to 100.
|
|
1915
|
+
|
|
1916
|
+
Returns:
|
|
1917
|
+
tuple: (v_trend, t_trend)
|
|
1918
|
+
|
|
1919
|
+
Examples:
|
|
1920
|
+
```python
|
|
1921
|
+
# 2차 다항 회귀 추세선
|
|
1922
|
+
from hossam import *
|
|
1923
|
+
vx, vy = hs_stats.trend(x, y, degree=2, value_count=200)
|
|
1924
|
+
print(len(vx), len(vy)) # 200, 200
|
|
1925
|
+
```
|
|
1926
|
+
"""
|
|
1927
|
+
# [ a, b, c ] ==> ax^2 + bx + c
|
|
1928
|
+
x_arr = np.asarray(x)
|
|
1929
|
+
y_arr = np.asarray(y)
|
|
1629
1930
|
|
|
1630
|
-
|
|
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
|
-
)
|
|
1931
|
+
if x_arr.ndim == 0 or y_arr.ndim == 0:
|
|
1932
|
+
raise ValueError("x, y는 1차원 이상의 배열이어야 합니다.")
|
|
1642
1933
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1934
|
+
coeff = np.polyfit(x_arr, y_arr, degree)
|
|
1935
|
+
|
|
1936
|
+
minx = np.min(x_arr)
|
|
1937
|
+
maxx = np.max(x_arr)
|
|
1938
|
+
v_trend = np.linspace(minx, maxx, value_count)
|
|
1939
|
+
|
|
1940
|
+
# np.polyval 사용으로 간결하게 추세선 계산
|
|
1941
|
+
t_trend = np.polyval(coeff, v_trend)
|
|
1942
|
+
|
|
1943
|
+
return (v_trend, t_trend)
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
# ===================================================================
|
|
1947
|
+
# 선형회귀 요약 리포트
|
|
1948
|
+
# ===================================================================
|
|
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]: ...
|
|
1956
|
+
|
|
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
|
+
"""선형회귀 적합 결과를 요약 리포트로 변환한다.
|
|
1979
|
+
|
|
1980
|
+
Args:
|
|
1981
|
+
fit: statsmodels OLS 등 선형회귀 결과 객체 (`fit.summary()`를 지원해야 함).
|
|
1982
|
+
data: 종속변수와 독립변수를 모두 포함한 DataFrame.
|
|
1983
|
+
full: True이면 6개 값 반환, False이면 회귀계수 테이블(rdf)만 반환. 기본값 True.
|
|
1984
|
+
alpha: 유의수준. 기본값 0.05.
|
|
1985
|
+
|
|
1986
|
+
Returns:
|
|
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)
|
|
1998
|
+
|
|
1999
|
+
Examples:
|
|
2000
|
+
```python
|
|
2001
|
+
from hossam import *
|
|
2002
|
+
|
|
2003
|
+
df = hs_util.load_data("some_data.csv")
|
|
2004
|
+
fit = hs_stats.ols(df, yname="target")
|
|
2005
|
+
|
|
2006
|
+
# 전체 리포트
|
|
2007
|
+
pdf, rdf, result_report, model_report, variable_reports, eq = hs_stats.ols_report(fit, data, full=True)
|
|
2008
|
+
|
|
2009
|
+
# 간단한 버전 (성능지표, 회귀계수 테이블만)
|
|
2010
|
+
pdf, rdf = hs_stats.ols_report(fit, data)
|
|
2011
|
+
```
|
|
2012
|
+
"""
|
|
2013
|
+
|
|
2014
|
+
# summary2() 결과에서 실제 회귀계수 DataFrame 추출
|
|
2015
|
+
summary_obj = fit.summary2()
|
|
2016
|
+
tbl = summary_obj.tables[1] # 회귀계수 테이블은 tables[1]에 위치
|
|
2017
|
+
|
|
2018
|
+
# 종속변수 이름
|
|
2019
|
+
yname = fit.model.endog_names
|
|
1647
2020
|
|
|
1648
2021
|
# 독립변수 이름(상수항 제외)
|
|
1649
2022
|
xnames = [n for n in fit.model.exog_names if n != "const"]
|
|
1650
2023
|
|
|
1651
|
-
# 독립변수
|
|
1652
|
-
|
|
2024
|
+
# 독립변수 부분 데이터 (VIF 계산용)
|
|
2025
|
+
indi_df = data.filter(xnames)
|
|
1653
2026
|
|
|
2027
|
+
# 독립변수 결과를 누적
|
|
1654
2028
|
variables = []
|
|
1655
2029
|
|
|
1656
2030
|
# VIF 계산 (상수항 포함 설계행렬 사용)
|
|
1657
2031
|
vif_dict = {}
|
|
1658
|
-
|
|
1659
|
-
for i, col in enumerate(
|
|
1660
|
-
|
|
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
|
|
1661
2044
|
|
|
1662
2045
|
for idx, row in tbl.iterrows():
|
|
1663
2046
|
name = idx
|
|
1664
2047
|
if name not in xnames:
|
|
1665
2048
|
continue
|
|
1666
2049
|
|
|
1667
|
-
|
|
2050
|
+
b = float(row['Coef.'])
|
|
1668
2051
|
se = float(row['Std.Err.'])
|
|
1669
|
-
|
|
1670
|
-
p = float(row['P>|
|
|
2052
|
+
t = float(row['t'])
|
|
2053
|
+
p = float(row['P>|t|'])
|
|
1671
2054
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
ci_high = np.exp(beta + 1.96 * se)
|
|
2055
|
+
# 표준화 회귀계수(β) 계산
|
|
2056
|
+
beta = b * (data[name].std(ddof=1) / data[yname].std(ddof=1))
|
|
1675
2057
|
|
|
2058
|
+
# VIF 값
|
|
2059
|
+
vif = vif_dict.get(name, np.nan)
|
|
2060
|
+
|
|
2061
|
+
# 유의확률과 별표 표시
|
|
1676
2062
|
stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
|
|
1677
2063
|
|
|
2064
|
+
# 한 변수에 대한 보고 정보 추가
|
|
1678
2065
|
variables.append(
|
|
1679
2066
|
{
|
|
1680
|
-
"종속변수": yname,
|
|
1681
|
-
"독립변수": name,
|
|
1682
|
-
"B
|
|
1683
|
-
"표준오차": se,
|
|
1684
|
-
"
|
|
1685
|
-
"
|
|
1686
|
-
"
|
|
1687
|
-
"
|
|
1688
|
-
"
|
|
1689
|
-
"
|
|
1690
|
-
"VIF": vif_dict.get(name, np.nan),
|
|
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, # 분산팽창계수
|
|
1691
2077
|
}
|
|
1692
2078
|
)
|
|
1693
2079
|
|
|
1694
2080
|
rdf = DataFrame(variables)
|
|
1695
2081
|
|
|
1696
|
-
#
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
|
1708
2094
|
|
|
1709
|
-
#
|
|
1710
|
-
|
|
1711
|
-
# -----------------------------
|
|
1712
|
-
tpl = (
|
|
1713
|
-
"%s에 대하여 %s로 예측하는 로지스틱 회귀분석을 실시한 결과, "
|
|
1714
|
-
"모형은 통계적으로 %s(χ²(%s) = %.3f, p %s 0.05)하였다."
|
|
1715
|
-
)
|
|
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']})"
|
|
1716
2097
|
|
|
2098
|
+
# 모형 보고 문장 구성
|
|
2099
|
+
tpl = "%s에 대하여 %s로 예측하는 회귀분석을 실시한 결과, 이 회귀모형은 통계적으로 %s(F(%s,%s) = %s, p %s 0.05)."
|
|
1717
2100
|
model_report = tpl % (
|
|
1718
|
-
|
|
1719
|
-
",
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
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 ">",
|
|
1724
2112
|
)
|
|
1725
2113
|
|
|
1726
|
-
#
|
|
1727
|
-
# 변수별 보고 문장
|
|
1728
|
-
# -----------------------------
|
|
2114
|
+
# 변수별 보고 문장 리스트 구성
|
|
1729
2115
|
variable_reports = []
|
|
2116
|
+
s = "%s의 회귀계수는 %s(p %s 0.05)로, %s에 대하여 %s 예측변인인 것으로 나타났다."
|
|
1730
2117
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
"%s 발생 odds에 %s 영향을 미치는 것으로 나타났다."
|
|
1734
|
-
)
|
|
1735
|
-
|
|
1736
|
-
for _, row in rdf.iterrows():
|
|
2118
|
+
for i in rdf.index:
|
|
2119
|
+
row = rdf.iloc[i]
|
|
1737
2120
|
variable_reports.append(
|
|
1738
2121
|
s
|
|
1739
2122
|
% (
|
|
1740
2123
|
row["독립변수"],
|
|
1741
|
-
row["
|
|
1742
|
-
"<=" if row["p-value"] < 0.05 else ">",
|
|
2124
|
+
row["B"],
|
|
2125
|
+
"<=" if float(row["p-value"]) < 0.05 else ">",
|
|
1743
2126
|
row["종속변수"],
|
|
1744
|
-
"유의미한" if row["p-value"] < 0.05 else "유의하지 않은",
|
|
2127
|
+
"유의미한" if float(row["p-value"]) < 0.05 else "유의하지 않은",
|
|
1745
2128
|
)
|
|
1746
2129
|
)
|
|
1747
2130
|
|
|
2131
|
+
# -----------------------------
|
|
2132
|
+
# 회귀식 자동 출력
|
|
2133
|
+
# -----------------------------
|
|
2134
|
+
intercept = fit.params["const"]
|
|
2135
|
+
terms = []
|
|
2136
|
+
|
|
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}")
|
|
2141
|
+
|
|
2142
|
+
equation_text = f"{yname} = {intercept:.3f}" + "".join(terms)
|
|
2143
|
+
|
|
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
|
+
)
|
|
2154
|
+
|
|
1748
2155
|
if full:
|
|
1749
|
-
return
|
|
2156
|
+
return pdf, rdf, result_report, model_report, variable_reports, equation_text
|
|
1750
2157
|
else:
|
|
1751
|
-
return
|
|
2158
|
+
return pdf, rdf
|
|
1752
2159
|
|
|
1753
2160
|
|
|
1754
2161
|
# ===================================================================
|
|
1755
|
-
#
|
|
2162
|
+
# 선형회귀
|
|
1756
2163
|
# ===================================================================
|
|
1757
|
-
|
|
2164
|
+
@overload
|
|
2165
|
+
def ols(
|
|
1758
2166
|
df: DataFrame,
|
|
1759
2167
|
yname: str,
|
|
1760
|
-
report:
|
|
1761
|
-
) ->
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
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],
|
|
1767
2195
|
Tuple[
|
|
1768
|
-
|
|
2196
|
+
RegressionResultsWrapper,
|
|
1769
2197
|
DataFrame,
|
|
1770
2198
|
DataFrame,
|
|
1771
2199
|
str,
|
|
1772
2200
|
str,
|
|
1773
|
-
|
|
1774
|
-
|
|
2201
|
+
list[str],
|
|
2202
|
+
str
|
|
2203
|
+
],
|
|
2204
|
+
RegressionResultsWrapper
|
|
1775
2205
|
]:
|
|
1776
|
-
"""
|
|
2206
|
+
"""선형회귀분석을 수행하고 적합 결과를 반환한다.
|
|
1777
2207
|
|
|
1778
|
-
|
|
2208
|
+
OLS(Ordinary Least Squares) 선형회귀분석을 실시한다.
|
|
1779
2209
|
필요시 상세한 통계 보고서를 함께 제공한다.
|
|
1780
2210
|
|
|
1781
2211
|
Args:
|
|
1782
2212
|
df (DataFrame): 종속변수와 독립변수를 모두 포함한 데이터프레임.
|
|
1783
|
-
yname (str): 종속변수 컬럼명.
|
|
1784
|
-
report: 리포트 모드 설정. 다음 값 중 하나:
|
|
2213
|
+
yname (str): 종속변수 컬럼명.
|
|
2214
|
+
report (bool | str): 리포트 모드 설정. 다음 값 중 하나:
|
|
1785
2215
|
- False (기본값): 리포트 미사용. fit 객체만 반환.
|
|
1786
2216
|
- 1 또는 'summary': 요약 리포트 반환 (full=False).
|
|
1787
2217
|
- 2 또는 'full': 풀 리포트 반환 (full=True).
|
|
1788
2218
|
- True: 풀 리포트 반환 (2와 동일).
|
|
1789
2219
|
|
|
1790
2220
|
Returns:
|
|
1791
|
-
statsmodels.
|
|
1792
|
-
|
|
2221
|
+
statsmodels.regression.linear_model.RegressionResultsWrapper: report=False일 때.
|
|
2222
|
+
선형회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
|
|
1793
2223
|
|
|
1794
|
-
tuple (
|
|
1795
|
-
(fit, rdf, result_report, variable_reports) 형태로 (
|
|
2224
|
+
tuple (6개): report=1 또는 'summary'일 때.
|
|
2225
|
+
(fit, rdf, result_report, model_report, variable_reports, equation_text) 형태로 (pdf 제외).
|
|
1796
2226
|
|
|
1797
|
-
tuple (
|
|
1798
|
-
(fit,
|
|
1799
|
-
- fit:
|
|
1800
|
-
-
|
|
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
|
|
1801
2231
|
- rdf: 회귀계수 표 (DataFrame)
|
|
1802
|
-
- result_report: 적합도
|
|
2232
|
+
- result_report: 적합도 요약 (str)
|
|
1803
2233
|
- model_report: 모형 보고 문장 (str)
|
|
1804
2234
|
- variable_reports: 변수별 보고 문장 리스트 (list[str])
|
|
2235
|
+
- equation_text: 회귀식 문자열 (str)
|
|
1805
2236
|
|
|
1806
2237
|
Examples:
|
|
1807
2238
|
```python
|
|
@@ -1810,51 +2241,46 @@ def logit(
|
|
|
1810
2241
|
import numpy as np
|
|
1811
2242
|
|
|
1812
2243
|
df = DataFrame({
|
|
1813
|
-
'target': np.random.
|
|
2244
|
+
'target': np.random.normal(100, 10, 100),
|
|
1814
2245
|
'x1': np.random.normal(0, 1, 100),
|
|
1815
2246
|
'x2': np.random.normal(0, 1, 100)
|
|
1816
2247
|
})
|
|
1817
2248
|
|
|
1818
2249
|
# 적합 결과만 반환
|
|
1819
|
-
fit = hs_stats.
|
|
2250
|
+
fit = hs_stats.ols(df, 'target')
|
|
1820
2251
|
|
|
1821
2252
|
# 요약 리포트 반환
|
|
1822
|
-
fit,
|
|
2253
|
+
fit, pdf, rdf = hs_stats.ols(df, 'target', report='summary')
|
|
1823
2254
|
|
|
1824
2255
|
# 풀 리포트 반환
|
|
1825
|
-
fit,
|
|
2256
|
+
fit, pdf, rdf, result_report, model_report, var_reports, eq = hs_stats.ols(df, 'target', report='full')
|
|
1826
2257
|
```
|
|
1827
2258
|
"""
|
|
1828
2259
|
x = df.drop(yname, axis=1)
|
|
1829
2260
|
y = df[yname]
|
|
1830
2261
|
|
|
1831
2262
|
X_const = sm.add_constant(x)
|
|
1832
|
-
|
|
1833
|
-
|
|
2263
|
+
linear_model = sm.OLS(y, X_const)
|
|
2264
|
+
linear_fit = linear_model.fit()
|
|
1834
2265
|
|
|
1835
2266
|
# report 파라미터에 따른 처리
|
|
1836
2267
|
if not report or report is False:
|
|
1837
2268
|
# 리포트 미사용
|
|
1838
|
-
return
|
|
1839
|
-
elif report ==
|
|
1840
|
-
# 요약 리포트 (full=False)
|
|
1841
|
-
cdf, rdf = logit_report(logit_fit, df, threshold=0.5, full=False, alpha=0.05)
|
|
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:
|
|
2269
|
+
return linear_fit
|
|
2270
|
+
elif report == 'full':
|
|
1846
2271
|
# 풀 리포트 (full=True)
|
|
1847
|
-
|
|
1848
|
-
return
|
|
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
|
|
1849
2274
|
else:
|
|
1850
|
-
#
|
|
1851
|
-
|
|
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
|
|
1852
2278
|
|
|
1853
2279
|
|
|
1854
2280
|
# ===================================================================
|
|
1855
2281
|
# 선형성 검정 (Linearity Test)
|
|
1856
2282
|
# ===================================================================
|
|
1857
|
-
def ols_linearity_test(fit, power: int = 2, alpha: float = 0.05
|
|
2283
|
+
def ols_linearity_test(fit: RegressionResultsWrapper, power: int = 2, alpha: float = 0.05) -> DataFrame:
|
|
1858
2284
|
"""회귀모형의 선형성을 Ramsey RESET 검정으로 평가한다.
|
|
1859
2285
|
|
|
1860
2286
|
적합된 회귀모형에 대해 Ramsey RESET(Regression Specification Error Test) 검정을 수행하여
|
|
@@ -1866,10 +2292,7 @@ def ols_linearity_test(fit, power: int = 2, alpha: float = 0.05, plot: bool = Fa
|
|
|
1866
2292
|
power (int, optional): RESET 검정에 사용할 거듭제곱 수. 기본값 2.
|
|
1867
2293
|
power=2일 때 예측값의 제곱항이 추가됨.
|
|
1868
2294
|
power가 클수록 더 높은 차수의 비선형성을 감지.
|
|
1869
|
-
alpha (float, optional): 유의수준. 기본값 0.05
|
|
1870
|
-
plot (bool, optional): True이면 잔차 플롯을 출력. 기본값 False.
|
|
1871
|
-
title (str, optional): 플롯 제목. 기본값 None.
|
|
1872
|
-
save_path (str, optional): 플롯을 저장할 경로. 기본값 None
|
|
2295
|
+
alpha (float, optional): 유의수준. 기본값 0.05
|
|
1873
2296
|
|
|
1874
2297
|
Returns:
|
|
1875
2298
|
DataFrame: 선형성 검정 결과를 포함한 데이터프레임.
|
|
@@ -1952,16 +2375,13 @@ def ols_linearity_test(fit, power: int = 2, alpha: float = 0.05, plot: bool = Fa
|
|
|
1952
2375
|
"해석": [interpretation]
|
|
1953
2376
|
})
|
|
1954
2377
|
|
|
1955
|
-
if plot:
|
|
1956
|
-
ols_residplot(fit, lowess=True, mse=True, title=title, save_path=save_path)
|
|
1957
|
-
|
|
1958
2378
|
return result_df
|
|
1959
2379
|
|
|
1960
2380
|
|
|
1961
2381
|
# ===================================================================
|
|
1962
2382
|
# 정규성 검정 (Normality Test)
|
|
1963
2383
|
# ===================================================================
|
|
1964
|
-
def ols_normality_test(fit, alpha: float = 0.05, plot: bool = False, title: str = None, save_path: str = None) -> DataFrame:
|
|
2384
|
+
def ols_normality_test(fit: RegressionResultsWrapper, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
|
|
1965
2385
|
"""회귀모형 잔차의 정규성을 검정한다.
|
|
1966
2386
|
|
|
1967
2387
|
회귀모형의 잔차가 정규분포를 따르는지 Shapiro-Wilk 검정과 Jarque-Bera 검정으로 평가한다.
|
|
@@ -2029,7 +2449,7 @@ def ols_normality_test(fit, alpha: float = 0.05, plot: bool = False, title: str
|
|
|
2029
2449
|
# 2. Jarque-Bera 검정 (항상 수행)
|
|
2030
2450
|
try:
|
|
2031
2451
|
stat_jb, p_jb = jarque_bera(residuals)
|
|
2032
|
-
significant_jb = p_jb <= alpha
|
|
2452
|
+
significant_jb = p_jb <= alpha # type: ignore
|
|
2033
2453
|
|
|
2034
2454
|
if significant_jb:
|
|
2035
2455
|
interpretation_jb = f"정규성 위반 (p={p_jb:.4f} <= {alpha})"
|
|
@@ -2062,7 +2482,7 @@ def ols_normality_test(fit, alpha: float = 0.05, plot: bool = False, title: str
|
|
|
2062
2482
|
# ===================================================================
|
|
2063
2483
|
# 등분산성 검정 (Homoscedasticity Test)
|
|
2064
2484
|
# ===================================================================
|
|
2065
|
-
def ols_variance_test(fit, alpha: float = 0.05) -> DataFrame:
|
|
2485
|
+
def ols_variance_test(fit: RegressionResultsWrapper, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
|
|
2066
2486
|
"""회귀모형의 등분산성 가정을 검정한다.
|
|
2067
2487
|
|
|
2068
2488
|
잔차의 분산이 예측값의 수준에 관계없이 일정한지 Breusch-Pagan 검정과 White 검정으로 평가한다.
|
|
@@ -2071,6 +2491,9 @@ def ols_variance_test(fit, alpha: float = 0.05) -> DataFrame:
|
|
|
2071
2491
|
Args:
|
|
2072
2492
|
fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
|
|
2073
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
|
|
2074
2497
|
|
|
2075
2498
|
Returns:
|
|
2076
2499
|
DataFrame: 등분산성 검정 결과를 포함한 데이터프레임.
|
|
@@ -2147,617 +2570,423 @@ def ols_variance_test(fit, alpha: float = 0.05) -> DataFrame:
|
|
|
2147
2570
|
if not results:
|
|
2148
2571
|
raise ValueError("등분산성 검정을 수행할 수 없습니다.")
|
|
2149
2572
|
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
# ===================================================================
|
|
2155
|
-
# 독립성 검정 (Independence Test - Durbin-Watson)
|
|
2156
|
-
# ===================================================================
|
|
2157
|
-
def ols_independence_test(fit, alpha: float = 0.05) -> DataFrame:
|
|
2158
|
-
"""회귀모형의 독립성 가정(자기상관 없음)을 검정한다.
|
|
2159
|
-
|
|
2160
|
-
Durbin-Watson 검정을 사용하여 잔차의 1차 자기상관 여부를 검정한다.
|
|
2161
|
-
시계열 데이터나 순서가 있는 데이터에서 주로 사용된다.
|
|
2162
|
-
|
|
2163
|
-
Args:
|
|
2164
|
-
fit: statsmodels 회귀분석 결과 객체 (RegressionResultsWrapper).
|
|
2165
|
-
alpha (float, optional): 유의수준. 기본값은 0.05.
|
|
2166
|
-
|
|
2167
|
-
Returns:
|
|
2168
|
-
DataFrame: 검정 결과 데이터프레임.
|
|
2169
|
-
- 검정: 검정 방법명
|
|
2170
|
-
- 검정통계량(DW): Durbin-Watson 통계량 (0~4 범위, 2에 가까울수록 자기상관 없음)
|
|
2171
|
-
- 독립성_위반: 자기상관 의심 여부 (True/False)
|
|
2172
|
-
- 해석: 검정 결과 해석
|
|
2173
|
-
|
|
2174
|
-
Examples:
|
|
2175
|
-
```python
|
|
2176
|
-
from hossam import *
|
|
2177
|
-
fit = hs_stats.logit(df, 'target')
|
|
2178
|
-
result = hs_stats.ols_independence_test(fit)
|
|
2179
|
-
```
|
|
2180
|
-
|
|
2181
|
-
Notes:
|
|
2182
|
-
- Durbin-Watson 통계량 해석:
|
|
2183
|
-
* 2에 가까우면: 자기상관 없음 (독립성 만족)
|
|
2184
|
-
* 0에 가까우면: 양의 자기상관 (독립성 위반)
|
|
2185
|
-
* 4에 가까우면: 음의 자기상관 (독립성 위반)
|
|
2186
|
-
- 일반적으로 1.5~2.5 범위를 자기상관 없음으로 판단
|
|
2187
|
-
- 시계열 데이터나 관측치에 순서가 있는 경우 중요한 검정
|
|
2188
|
-
"""
|
|
2189
|
-
from pandas import DataFrame
|
|
2190
|
-
|
|
2191
|
-
# Durbin-Watson 통계량 계산
|
|
2192
|
-
dw_stat = durbin_watson(fit.resid)
|
|
2193
|
-
|
|
2194
|
-
# 자기상관 판단 (1.5 < DW < 2.5 범위를 독립성 만족으로 판단)
|
|
2195
|
-
is_autocorrelated = dw_stat < 1.5 or dw_stat > 2.5
|
|
2196
|
-
|
|
2197
|
-
# 해석 메시지 생성
|
|
2198
|
-
if dw_stat < 1.5:
|
|
2199
|
-
interpretation = f"DW={dw_stat:.4f} < 1.5 (양의 자기상관)"
|
|
2200
|
-
elif dw_stat > 2.5:
|
|
2201
|
-
interpretation = f"DW={dw_stat:.4f} > 2.5 (음의 자기상관)"
|
|
2202
|
-
else:
|
|
2203
|
-
interpretation = f"DW={dw_stat:.4f} (독립성 가정 만족)"
|
|
2204
|
-
|
|
2205
|
-
# 결과 데이터프레임 생성
|
|
2206
|
-
result_df = DataFrame(
|
|
2207
|
-
{
|
|
2208
|
-
"검정": ["Durbin-Watson"],
|
|
2209
|
-
"검정통계량(DW)": [dw_stat],
|
|
2210
|
-
"독립성_위반": [is_autocorrelated],
|
|
2211
|
-
"해석": [interpretation],
|
|
2212
|
-
}
|
|
2213
|
-
)
|
|
2573
|
+
if plot:
|
|
2574
|
+
ols_residplot(fit, lowess=True, mse=True, title=title, save_path=save_path)
|
|
2214
2575
|
|
|
2576
|
+
result_df = DataFrame(results)
|
|
2215
2577
|
return result_df
|
|
2216
2578
|
|
|
2217
|
-
# ===================================================================
|
|
2218
|
-
# 쌍별 상관분석 (선형성/이상치 점검 후 Pearson/Spearman 자동 선택)
|
|
2219
|
-
# ===================================================================
|
|
2220
|
-
def corr_pairwise(
|
|
2221
|
-
data: DataFrame,
|
|
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을 자동 선택해 상관을 요약한다.
|
|
2230
|
-
|
|
2231
|
-
절차:
|
|
2232
|
-
1) z-score 기준(|z|>z_thresh)으로 각 변수의 이상치 존재 여부를 파악
|
|
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 추가
|
|
2237
|
-
|
|
2238
|
-
Args:
|
|
2239
|
-
data (DataFrame): 분석 대상 데이터프레임.
|
|
2240
|
-
fields (list[str]|None): 분석할 숫자형 컬럼 이름 리스트. None이면 모든 숫자형 컬럼 사용. 기본값 None.
|
|
2241
|
-
alpha (float, optional): 유의수준. 기본 0.05.
|
|
2242
|
-
z_thresh (float, optional): 이상치 판단 임계값(|z| 기준). 기본 3.0.
|
|
2243
|
-
min_n (int, optional): 쌍별 최소 표본 크기. 미만이면 계산 생략. 기본 8.
|
|
2244
|
-
linearity_power (tuple[int,...], optional): RESET 검정에서 포함할 차수 집합. 기본 (2,).
|
|
2245
|
-
p_adjust (str, optional): 다중비교 보정 방법. "none" 또는 statsmodels.multipletests 지원값 중 하나(e.g., "fdr_bh"). 기본 "none".
|
|
2246
|
-
|
|
2247
|
-
Returns:
|
|
2248
|
-
tuple[DataFrame, DataFrame]: 두 개의 데이터프레임을 반환.
|
|
2249
|
-
[0] result_df: 각 변수쌍별 결과 테이블. 컬럼:
|
|
2250
|
-
var_a, var_b, n, linearity(bool), outlier_flag(bool), chosen('pearson'|'spearman'),
|
|
2251
|
-
corr, pval, significant(bool), strength(str), (보정 사용 시) pval_adj, significant_adj
|
|
2252
|
-
[1] corr_matrix: 상관계수 행렬 (행과 열에 변수명, 값에 상관계수)
|
|
2253
|
-
|
|
2254
|
-
Examples:
|
|
2255
|
-
```python
|
|
2256
|
-
from hossam import *
|
|
2257
|
-
from pandas import DataFrame
|
|
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'])
|
|
2264
|
-
```
|
|
2265
|
-
"""
|
|
2266
|
-
|
|
2267
|
-
# 0) 컬럼 선정 (숫자형만)
|
|
2268
|
-
if fields is None:
|
|
2269
|
-
# None이면 모든 숫자형 컬럼 사용
|
|
2270
|
-
cols = data.select_dtypes(include=[np.number]).columns.tolist()
|
|
2271
|
-
else:
|
|
2272
|
-
# fields 리스트에서 데이터에 있는 것만 선택하되, 숫자형만 필터링
|
|
2273
|
-
cols = [c for c in fields if c in data.columns and is_numeric_dtype(data[c])]
|
|
2274
|
-
|
|
2275
|
-
# 사용 가능한 컬럼이 2개 미만이면 상관분석 불가능
|
|
2276
|
-
if len(cols) < 2:
|
|
2277
|
-
empty_df = DataFrame(columns=["var_a", "var_b", "n", "linearity", "outlier_flag", "chosen", "corr", "pval", "significant", "strength"])
|
|
2278
|
-
return empty_df, DataFrame()
|
|
2279
|
-
|
|
2280
|
-
# z-score 기반 이상치 유무 계산
|
|
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()
|
|
2289
|
-
|
|
2290
|
-
rows = []
|
|
2291
|
-
|
|
2292
|
-
for a, b in combinations(cols, 2):
|
|
2293
|
-
# 공통 관측치 사용
|
|
2294
|
-
pair_df = data[[a, b]].dropna()
|
|
2295
|
-
if len(pair_df) < max(3, min_n):
|
|
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
|
|
2312
|
-
|
|
2313
|
-
x = pair_df[a]
|
|
2314
|
-
y = pair_df[b]
|
|
2315
|
-
|
|
2316
|
-
# 상수열/분산 0 체크 → 상관계수 계산 불가
|
|
2317
|
-
if x.nunique(dropna=True) <= 1 or y.nunique(dropna=True) <= 1:
|
|
2318
|
-
rows.append(
|
|
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
|
|
2333
2579
|
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
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
|
|
2580
|
+
# ===================================================================
|
|
2581
|
+
# 독립성 검정 (Independence Test - Durbin-Watson)
|
|
2582
|
+
# ===================================================================
|
|
2583
|
+
def ols_independence_test(fit: RegressionResultsWrapper, alpha: float = 0.05) -> DataFrame:
|
|
2584
|
+
"""회귀모형의 독립성 가정(자기상관 없음)을 검정한다.
|
|
2348
2585
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2586
|
+
Durbin-Watson 검정을 사용하여 잔차의 1차 자기상관 여부를 검정한다.
|
|
2587
|
+
시계열 데이터나 순서가 있는 데이터에서 주로 사용된다.
|
|
2351
2588
|
|
|
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
|
|
2589
|
+
Args:
|
|
2590
|
+
fit: statsmodels 회귀분석 결과 객체 (RegressionResultsWrapper).
|
|
2591
|
+
alpha (float, optional): 유의수준. 기본값은 0.05.
|
|
2363
2592
|
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
strength = "medium"
|
|
2371
|
-
elif abs_r > 0:
|
|
2372
|
-
strength = "weak"
|
|
2373
|
-
else:
|
|
2374
|
-
strength = "no correlation"
|
|
2593
|
+
Returns:
|
|
2594
|
+
DataFrame: 검정 결과 데이터프레임.
|
|
2595
|
+
- 검정: 검정 방법명
|
|
2596
|
+
- 검정통계량(DW): Durbin-Watson 통계량 (0~4 범위, 2에 가까울수록 자기상관 없음)
|
|
2597
|
+
- 독립성_위반: 자기상관 의심 여부 (True/False)
|
|
2598
|
+
- 해석: 검정 결과 해석
|
|
2375
2599
|
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
"outlier_flag": outlier_flag,
|
|
2383
|
-
"chosen": chosen,
|
|
2384
|
-
"corr": corr_val,
|
|
2385
|
-
"pval": pval,
|
|
2386
|
-
"significant": significant,
|
|
2387
|
-
"strength": strength,
|
|
2388
|
-
}
|
|
2389
|
-
)
|
|
2600
|
+
Examples:
|
|
2601
|
+
```python
|
|
2602
|
+
from hossam import *
|
|
2603
|
+
fit = hs_stats.logit(df, 'target')
|
|
2604
|
+
result = hs_stats.ols_independence_test(fit)
|
|
2605
|
+
```
|
|
2390
2606
|
|
|
2391
|
-
|
|
2607
|
+
Notes:
|
|
2608
|
+
- Durbin-Watson 통계량 해석:
|
|
2609
|
+
* 2에 가까우면: 자기상관 없음 (독립성 만족)
|
|
2610
|
+
* 0에 가까우면: 양의 자기상관 (독립성 위반)
|
|
2611
|
+
* 4에 가까우면: 음의 자기상관 (독립성 위반)
|
|
2612
|
+
- 일반적으로 1.5~2.5 범위를 자기상관 없음으로 판단
|
|
2613
|
+
- 시계열 데이터나 관측치에 순서가 있는 경우 중요한 검정
|
|
2614
|
+
"""
|
|
2615
|
+
from pandas import DataFrame
|
|
2392
2616
|
|
|
2393
|
-
#
|
|
2394
|
-
|
|
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
|
|
2617
|
+
# Durbin-Watson 통계량 계산
|
|
2618
|
+
dw_stat = durbin_watson(fit.resid)
|
|
2401
2619
|
|
|
2402
|
-
#
|
|
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 # 대칭성
|
|
2620
|
+
# 자기상관 판단 (1.5 < DW < 2.5 범위를 독립성 만족으로 판단)
|
|
2621
|
+
is_autocorrelated = dw_stat < 1.5 or dw_stat > 2.5
|
|
2414
2622
|
|
|
2415
|
-
|
|
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} (독립성 가정 만족)"
|
|
2416
2630
|
|
|
2631
|
+
# 결과 데이터프레임 생성
|
|
2632
|
+
result_df = DataFrame(
|
|
2633
|
+
{
|
|
2634
|
+
"검정": ["Durbin-Watson"],
|
|
2635
|
+
"검정통계량(DW)": [dw_stat],
|
|
2636
|
+
"독립성_위반": [is_autocorrelated],
|
|
2637
|
+
"해석": [interpretation],
|
|
2638
|
+
}
|
|
2639
|
+
)
|
|
2417
2640
|
|
|
2418
|
-
|
|
2419
|
-
# 일원 분산분석 (One-way ANOVA)
|
|
2420
|
-
# ===================================================================
|
|
2421
|
-
def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) -> tuple[DataFrame, str, DataFrame | None, str]:
|
|
2422
|
-
"""일원분산분석(One-way ANOVA)을 일괄 처리한다.
|
|
2641
|
+
return result_df
|
|
2423
2642
|
|
|
2424
|
-
정규성 및 등분산성 검정을 자동으로 수행한 후,
|
|
2425
|
-
그 결과에 따라 적절한 ANOVA 방식을 선택하여 분산분석을 수행한다.
|
|
2426
|
-
ANOVA 결과가 유의하면 자동으로 사후검정을 실시한다.
|
|
2427
2643
|
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
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
|
+
선형성, 정규성, 등분산성, 독립성 검정을 순차적으로 실시하고 결과를 하나의 데이터프레임으로 반환한다.
|
|
2433
2649
|
|
|
2434
2650
|
Args:
|
|
2435
|
-
|
|
2436
|
-
dv (str): 종속변수(Dependent Variable) 컬럼명.
|
|
2437
|
-
between (str): 그룹 구분 변수 컬럼명.
|
|
2651
|
+
fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
|
|
2438
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.
|
|
2439
2656
|
|
|
2440
2657
|
Returns:
|
|
2441
|
-
|
|
2442
|
-
- anova_df (DataFrame): ANOVA 또는 Welch 결과 테이블(Source, ddof1, ddof2, F, p-unc, np2 등 포함).
|
|
2443
|
-
- anova_report (str): 정규성/등분산 여부와 F, p값, 효과크기를 요약한 보고 문장.
|
|
2444
|
-
- posthoc_df (DataFrame|None): 사후검정 결과(Tukey HSD 또는 Games-Howell). ANOVA가 유의할 때만 생성.
|
|
2445
|
-
- posthoc_report (str): 사후검정 유무와 유의한 쌍 정보를 요약한 보고 문장.
|
|
2658
|
+
None
|
|
2446
2659
|
|
|
2447
2660
|
Examples:
|
|
2448
2661
|
```python
|
|
2449
2662
|
from hossam import *
|
|
2450
|
-
|
|
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)
|
|
2451
2678
|
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
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)
|
|
2456
2684
|
|
|
2457
|
-
anova_df, anova_report, posthoc_df, posthoc_report = hs_stats.oneway_anova(df, dv='score', between='group')
|
|
2458
2685
|
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2686
|
+
# ===================================================================
|
|
2687
|
+
# 로지스틱 회귀 요약 리포트
|
|
2688
|
+
# ===================================================================
|
|
2689
|
+
def logit_report(
|
|
2690
|
+
fit: BinaryResultsWrapper,
|
|
2691
|
+
data: DataFrame,
|
|
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
|
+
"""로지스틱 회귀 적합 결과를 상세 리포트로 변환한다.
|
|
2464
2707
|
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
if between not in data.columns:
|
|
2472
|
-
raise ValueError(f"'{between}' 컬럼이 데이터프레임에 없습니다.")
|
|
2708
|
+
Args:
|
|
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.
|
|
2473
2714
|
|
|
2474
|
-
|
|
2715
|
+
Returns:
|
|
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]].
|
|
2475
2723
|
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
group_names = sorted(df_filtered[between].unique())
|
|
2480
|
-
normality_satisfied = True
|
|
2724
|
+
full=False일 때:
|
|
2725
|
+
- 성능 지표 표 (`cdf`, DataFrame)
|
|
2726
|
+
- 회귀계수 표 (`rdf`, DataFrame)
|
|
2481
2727
|
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
normality_satisfied = False
|
|
2488
|
-
break
|
|
2728
|
+
Examples:
|
|
2729
|
+
```python
|
|
2730
|
+
from hossam import *
|
|
2731
|
+
from pandas import DataFrame
|
|
2732
|
+
import numpy as np
|
|
2489
2733
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
for group in group_names:
|
|
2496
|
-
group_data_dict[group] = df_filtered[df_filtered[between] == group][dv].dropna().values
|
|
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
|
+
})
|
|
2497
2739
|
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
if normality_satisfied:
|
|
2501
|
-
# 정규성을 만족하면 Bartlett 검정
|
|
2502
|
-
s, p = bartlett(*group_data_dict.values())
|
|
2503
|
-
else:
|
|
2504
|
-
# 정규성을 만족하지 않으면 Levene 검정
|
|
2505
|
-
s, p = levene(*group_data_dict.values())
|
|
2506
|
-
equal_var_satisfied = p > alpha
|
|
2507
|
-
else:
|
|
2508
|
-
# 그룹이 1개인 경우 등분산성 검정 불가능
|
|
2509
|
-
equal_var_satisfied = True
|
|
2740
|
+
# 로지스틱 회귀 적합
|
|
2741
|
+
fit = hs_stats.logit(df, yname="target")
|
|
2510
2742
|
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
else:
|
|
2519
|
-
# 등분산을 만족하지 않을 때 Welch's ANOVA 사용
|
|
2520
|
-
anova_method = "Welch"
|
|
2521
|
-
anova_df = welch_anova(data=df_filtered, dv=dv, between=between)
|
|
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
|
+
```
|
|
2749
|
+
"""
|
|
2522
2750
|
|
|
2523
|
-
#
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
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)
|
|
2527
2758
|
|
|
2528
|
-
#
|
|
2529
|
-
|
|
2530
|
-
|
|
2759
|
+
# 혼동행렬
|
|
2760
|
+
cm = confusion_matrix(y_true, y_pred_fix)
|
|
2761
|
+
tn, fp, fn, tp = cm.ravel()
|
|
2531
2762
|
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
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
|
|
2535
2770
|
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
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
|
+
)
|
|
2542
2783
|
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2784
|
+
# -----------------------------
|
|
2785
|
+
# 회귀계수 표 구성 (OR 중심)
|
|
2786
|
+
# -----------------------------
|
|
2787
|
+
tbl = fit.summary2().tables[1]
|
|
2547
2788
|
|
|
2548
|
-
|
|
2549
|
-
|
|
2789
|
+
# 독립변수 이름(상수항 제외)
|
|
2790
|
+
xnames = [n for n in fit.model.exog_names if n != "const"]
|
|
2550
2791
|
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
f"해석: {anova_sig_text} {assumption_text}"
|
|
2554
|
-
)
|
|
2792
|
+
# 독립변수
|
|
2793
|
+
x = data[xnames]
|
|
2555
2794
|
|
|
2556
|
-
|
|
2557
|
-
anova_report += f" 효과 크기(η²p) ≈ {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
|
|
2795
|
+
variables = []
|
|
2558
2796
|
|
|
2559
|
-
#
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
|
|
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
|
|
2565
2802
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
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)
|
|
2803
|
+
for idx, row in tbl.iterrows():
|
|
2804
|
+
name = idx
|
|
2805
|
+
if name not in xnames:
|
|
2806
|
+
continue
|
|
2575
2807
|
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2808
|
+
beta = float(row['Coef.'])
|
|
2809
|
+
se = float(row['Std.Err.'])
|
|
2810
|
+
z = float(row['z'])
|
|
2811
|
+
p = float(row['P>|z|'])
|
|
2580
2812
|
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2813
|
+
or_val = np.exp(beta)
|
|
2814
|
+
ci_low = np.exp(beta - 1.96 * se)
|
|
2815
|
+
ci_high = np.exp(beta + 1.96 * se)
|
|
2584
2816
|
|
|
2585
|
-
if
|
|
2586
|
-
# 유의성 여부 컬럼 추가
|
|
2587
|
-
posthoc_df['significant'] = posthoc_df[p_col] <= alpha
|
|
2817
|
+
stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
|
|
2588
2818
|
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
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
|
+
)
|
|
2595
2834
|
|
|
2596
|
-
|
|
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 정보를 찾지 못해 유의성을 확인할 수 없습니다."
|
|
2835
|
+
rdf = DataFrame(variables)
|
|
2606
2836
|
|
|
2607
|
-
#
|
|
2608
|
-
#
|
|
2609
|
-
#
|
|
2610
|
-
|
|
2837
|
+
# ---------------------------------
|
|
2838
|
+
# 모델 적합도 + 예측 성능 지표
|
|
2839
|
+
# ---------------------------------
|
|
2840
|
+
auc = roc_auc_score(y_true, y_pred)
|
|
2611
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
|
+
)
|
|
2612
2849
|
|
|
2613
|
-
#
|
|
2614
|
-
#
|
|
2615
|
-
#
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
factor_b: str,
|
|
2621
|
-
alpha: float = 0.05,
|
|
2622
|
-
) -> tuple[DataFrame, str, DataFrame | None, str]:
|
|
2623
|
-
"""두 범주형 요인에 대한 이원분산분석을 수행하고 해석용 보고문을 반환한다.
|
|
2850
|
+
# -----------------------------
|
|
2851
|
+
# 모형 보고 문장
|
|
2852
|
+
# -----------------------------
|
|
2853
|
+
tpl = (
|
|
2854
|
+
"%s에 대하여 %s로 예측하는 로지스틱 회귀분석을 실시한 결과, "
|
|
2855
|
+
"모형은 통계적으로 %s(χ²(%s) = %.3f, p %s 0.05)하였다."
|
|
2856
|
+
)
|
|
2624
2857
|
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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
|
+
)
|
|
2630
2866
|
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
factor_b (str): 두 번째 요인 컬럼명.
|
|
2636
|
-
alpha (float, optional): 유의수준. 기본 0.05.
|
|
2867
|
+
# -----------------------------
|
|
2868
|
+
# 변수별 보고 문장
|
|
2869
|
+
# -----------------------------
|
|
2870
|
+
variable_reports = []
|
|
2637
2871
|
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
- posthoc_df (DataFrame|None): 유의한 요인에 대한 Tukey 사후검정 결과(요인명, A, B, p 포함). 없으면 None.
|
|
2643
|
-
- posthoc_report (str): 사후검정 유무 및 유의 쌍 요약 문장.
|
|
2872
|
+
s = (
|
|
2873
|
+
"%s의 오즈비는 %.3f(p %s 0.05)로, "
|
|
2874
|
+
"%s 발생 odds에 %s 영향을 미치는 것으로 나타났다."
|
|
2875
|
+
)
|
|
2644
2876
|
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
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
|
+
)
|
|
2652
2888
|
|
|
2653
|
-
|
|
2889
|
+
if full:
|
|
2890
|
+
return cdf, rdf, result_report, model_report, variable_reports, cm
|
|
2891
|
+
else:
|
|
2892
|
+
return cdf, rdf
|
|
2654
2893
|
|
|
2655
|
-
# 1) 셀별 정규성 검정
|
|
2656
|
-
normality_satisfied = True
|
|
2657
|
-
for (a, b), subset in df_filtered.groupby([factor_a, factor_b], observed=False):
|
|
2658
|
-
vals = subset[dv].dropna()
|
|
2659
|
-
if len(vals) > 0:
|
|
2660
|
-
_, p = normaltest(vals)
|
|
2661
|
-
if p <= alpha:
|
|
2662
|
-
normality_satisfied = False
|
|
2663
|
-
break
|
|
2664
2894
|
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
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
|
+
"""로지스틱 회귀분석을 수행하고 적합 결과를 반환한다.
|
|
2918
|
+
|
|
2919
|
+
종속변수가 이항(binary) 형태일 때 로지스틱 회귀분석을 실시한다.
|
|
2920
|
+
필요시 상세한 통계 보고서를 함께 제공한다.
|
|
2675
2921
|
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
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와 동일).
|
|
2682
2930
|
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
return float(row[col])
|
|
2687
|
-
except Exception:
|
|
2688
|
-
return default
|
|
2931
|
+
Returns:
|
|
2932
|
+
statsmodels.genmod.generalized_linear_model.BinomialResults: report=False일 때.
|
|
2933
|
+
로지스틱 회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
|
|
2689
2934
|
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
sig_flags = {}
|
|
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)
|
|
2935
|
+
tuple (4개): report=1 또는 'summary'일 때.
|
|
2936
|
+
(fit, rdf, result_report, variable_reports) 형태로 (cdf 제외).
|
|
2708
2937
|
|
|
2709
|
-
|
|
2710
|
-
|
|
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])
|
|
2711
2946
|
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2947
|
+
Examples:
|
|
2948
|
+
```python
|
|
2949
|
+
from hossam import *
|
|
2950
|
+
from pandas import DataFrame
|
|
2951
|
+
import numpy as np
|
|
2716
2952
|
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
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
|
+
})
|
|
2721
2958
|
|
|
2722
|
-
#
|
|
2723
|
-
|
|
2724
|
-
continue
|
|
2959
|
+
# 적합 결과만 반환
|
|
2960
|
+
fit = hs_stats.logit(df, 'target')
|
|
2725
2961
|
|
|
2726
|
-
#
|
|
2727
|
-
|
|
2728
|
-
continue
|
|
2962
|
+
# 요약 리포트 반환
|
|
2963
|
+
fit, rdf, result_report, var_reports = hs_stats.logit(df, 'target', report='summary')
|
|
2729
2964
|
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
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]
|
|
2736
2971
|
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
posthoc_df = concat(posthoc_df_list, ignore_index=True)
|
|
2741
|
-
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]
|
|
2742
|
-
p_col = p_cols[0] if p_cols else None
|
|
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 정보를 찾지 못했습니다."
|
|
2972
|
+
X_const = sm.add_constant(x)
|
|
2973
|
+
logit_model = sm.Logit(y, X_const)
|
|
2974
|
+
logit_fit = logit_model.fit(disp=False)
|
|
2759
2975
|
|
|
2760
|
-
|
|
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
|
# ===================================================================
|
|
@@ -2851,7 +3080,7 @@ def predict(fit, data: DataFrame | Series) -> DataFrame | Series | float:
|
|
|
2851
3080
|
|
|
2852
3081
|
# Series 입력인 경우 단일 값 반환
|
|
2853
3082
|
if is_series:
|
|
2854
|
-
return float(predictions.iloc[0])
|
|
3083
|
+
return float(predictions.iloc[0]) # type: ignore
|
|
2855
3084
|
|
|
2856
3085
|
# DataFrame 입력인 경우
|
|
2857
3086
|
if isinstance(data, DataFrame):
|
|
@@ -2881,123 +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
|
|
2928
|
-
if is_numeric_dtype(data[col]) and col != dv]
|
|
2929
|
-
|
|
2930
|
-
# dv가 수치형인지 확인
|
|
2931
|
-
if not is_numeric_dtype(data[dv]):
|
|
2932
|
-
raise ValueError(f"Dependent variable '{dv}' must be numeric type")
|
|
2933
|
-
|
|
2934
|
-
results = []
|
|
2935
|
-
|
|
2936
|
-
for var in fields:
|
|
2937
|
-
if not is_numeric_dtype(data[var]):
|
|
2938
|
-
continue
|
|
2939
|
-
|
|
2940
|
-
# 결측치 제거
|
|
2941
|
-
valid_idx = data[[var, dv]].notna().all(axis=1)
|
|
2942
|
-
x = data.loc[valid_idx, var].values
|
|
2943
|
-
y = data.loc[valid_idx, dv].values
|
|
2944
|
-
|
|
2945
|
-
if len(x) < 3:
|
|
2946
|
-
continue
|
|
2947
|
-
|
|
2948
|
-
# 정규성 검사 (Shapiro-Wilk: n <= 5000 권장, 그 외 D'Agostino)
|
|
2949
|
-
method_x = 's' if len(x) <= 5000 else 'n'
|
|
2950
|
-
method_y = 's' if len(y) <= 5000 else 'n'
|
|
2951
|
-
|
|
2952
|
-
normal_x_result = normal_test(data[[var]], columns=[var], method=method_x)
|
|
2953
|
-
normal_y_result = normal_test(data[[dv]], columns=[dv], method=method_y)
|
|
2954
|
-
|
|
2955
|
-
# 정규성 판정 (p > alpha면 정규분포 가정)
|
|
2956
|
-
normal_x = normal_x_result.loc[var, 'p-val'] > alpha if var in normal_x_result.index else False
|
|
2957
|
-
normal_y = normal_y_result.loc[dv, 'p-val'] > alpha if dv in normal_y_result.index else False
|
|
2958
|
-
|
|
2959
|
-
# Pearson (모두 정규) vs Spearman (하나라도 비정규)
|
|
2960
|
-
if normal_x and normal_y:
|
|
2961
|
-
r, p = pearsonr(x, y)
|
|
2962
|
-
corr_type = 'Pearson'
|
|
2963
|
-
else:
|
|
2964
|
-
r, p = spearmanr(x, y)
|
|
2965
|
-
corr_type = 'Spearman'
|
|
2966
|
-
|
|
2967
|
-
# Cohen's d 계산 (상관계수에서 효과크기로 변환)
|
|
2968
|
-
# d = 2*r / sqrt(1-r^2)
|
|
2969
|
-
if r**2 < 1:
|
|
2970
|
-
d = (2 * r) / np.sqrt(1 - r**2)
|
|
2971
|
-
else:
|
|
2972
|
-
d = 0
|
|
2973
|
-
|
|
2974
|
-
# 효과크기 분류 (Cohen's d 기준)
|
|
2975
|
-
# Small: 0.2 < |d| <= 0.5
|
|
2976
|
-
# Medium: 0.5 < |d| <= 0.8
|
|
2977
|
-
# Large: |d| > 0.8
|
|
2978
|
-
abs_d = abs(d)
|
|
2979
|
-
if abs_d > 0.8:
|
|
2980
|
-
effect_size = 'Large'
|
|
2981
|
-
elif abs_d > 0.5:
|
|
2982
|
-
effect_size = 'Medium'
|
|
2983
|
-
elif abs_d > 0.2:
|
|
2984
|
-
effect_size = 'Small'
|
|
2985
|
-
else:
|
|
2986
|
-
effect_size = 'Negligible'
|
|
2987
|
-
|
|
2988
|
-
results.append({
|
|
2989
|
-
'Variable': var,
|
|
2990
|
-
'Correlation': r,
|
|
2991
|
-
'Corr_Type': corr_type,
|
|
2992
|
-
'P-value': p,
|
|
2993
|
-
'Cohens_d': d,
|
|
2994
|
-
'Effect_Size': effect_size
|
|
2995
|
-
})
|
|
2996
|
-
|
|
2997
|
-
result_df = DataFrame(results)
|
|
2998
|
-
|
|
2999
|
-
# 상관계수로 정렬 (절댓값 기준 내림차순)
|
|
3000
|
-
if len(result_df) > 0:
|
|
3001
|
-
result_df = result_df.sort_values('Correlation', key=lambda x: x.abs(), ascending=False).reset_index(drop=True)
|
|
3002
|
-
|
|
3003
|
-
return result_df
|
|
3113
|
+
)
|