hossam 0.4.17__py3-none-any.whl → 0.4.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hossam/__init__.py +78 -18
- hossam/hs_classroom.py +27 -4
- hossam/hs_cluster.py +99 -21
- hossam/hs_plot.py +36 -40
- hossam/hs_reg.py +313 -0
- hossam/hs_stats.py +250 -221
- {hossam-0.4.17.dist-info → hossam-0.4.19.dist-info}/METADATA +2 -1
- hossam-0.4.19.dist-info/RECORD +18 -0
- hossam-0.4.17.dist-info/RECORD +0 -17
- {hossam-0.4.17.dist-info → hossam-0.4.19.dist-info}/WHEEL +0 -0
- {hossam-0.4.17.dist-info → hossam-0.4.19.dist-info}/licenses/LICENSE +0 -0
- {hossam-0.4.17.dist-info → hossam-0.4.19.dist-info}/top_level.txt +0 -0
hossam/hs_stats.py
CHANGED
|
@@ -30,7 +30,8 @@ from scipy.stats import (
|
|
|
30
30
|
wilcoxon,
|
|
31
31
|
pearsonr,
|
|
32
32
|
spearmanr,
|
|
33
|
-
chi2
|
|
33
|
+
chi2,
|
|
34
|
+
jarque_bera
|
|
34
35
|
)
|
|
35
36
|
|
|
36
37
|
import statsmodels.api as sm
|
|
@@ -46,6 +47,7 @@ from pingouin import anova, pairwise_tukey, welch_anova, pairwise_gameshowell
|
|
|
46
47
|
|
|
47
48
|
from .hs_plot import ols_residplot, ols_qqplot, get_default_ax, finalize_plot
|
|
48
49
|
from .hs_prep import unmelt
|
|
50
|
+
from .hs_util import pretty_table
|
|
49
51
|
|
|
50
52
|
# ===================================================================
|
|
51
53
|
# MCAR(결측치 무작위성) 검정
|
|
@@ -212,17 +214,12 @@ def outlier_table(data: DataFrame, *fields: str):
|
|
|
212
214
|
if not fields:
|
|
213
215
|
fields = tuple(data.columns)
|
|
214
216
|
|
|
217
|
+
num_columns = data.select_dtypes(include=np.number).columns
|
|
218
|
+
|
|
215
219
|
result = []
|
|
216
220
|
for f in fields:
|
|
217
221
|
# 숫자 타입이 아니라면 건너뜀
|
|
218
|
-
if
|
|
219
|
-
"int",
|
|
220
|
-
"int32",
|
|
221
|
-
"int64",
|
|
222
|
-
"float",
|
|
223
|
-
"float32",
|
|
224
|
-
"float64",
|
|
225
|
-
]:
|
|
222
|
+
if f not in num_columns:
|
|
226
223
|
continue
|
|
227
224
|
|
|
228
225
|
# 사분위수
|
|
@@ -244,6 +241,41 @@ def outlier_table(data: DataFrame, *fields: str):
|
|
|
244
241
|
outlier_count = ((data[f] < down) | (data[f] > up)).sum()
|
|
245
242
|
outlier_rate = (outlier_count / len(data)) * 100
|
|
246
243
|
|
|
244
|
+
# 왜도
|
|
245
|
+
skew = data[f].skew()
|
|
246
|
+
|
|
247
|
+
# 이상치 개수 및 비율
|
|
248
|
+
outlier_count = ((data[f] < down) | (data[f] > up)).sum()
|
|
249
|
+
outlier_rate = (outlier_count / len(data)) * 100
|
|
250
|
+
|
|
251
|
+
# 분포 특성 판정 (왜도 기준)
|
|
252
|
+
abs_skew = abs(skew) # type: ignore
|
|
253
|
+
if abs_skew < 0.5: # type: ignore
|
|
254
|
+
dist = "거의 대칭"
|
|
255
|
+
elif abs_skew < 1.0: # type: ignore
|
|
256
|
+
if skew > 0: # type: ignore
|
|
257
|
+
dist = "약한 우측 꼬리"
|
|
258
|
+
else:
|
|
259
|
+
dist = "약한 좌측 꼬리"
|
|
260
|
+
elif abs_skew < 2.0: # type: ignore
|
|
261
|
+
if skew > 0: # type: ignore
|
|
262
|
+
dist = "중간 우측 꼬리"
|
|
263
|
+
else:
|
|
264
|
+
dist = "중간 좌측 꼬리"
|
|
265
|
+
else:
|
|
266
|
+
if skew > 0: # type: ignore
|
|
267
|
+
dist = "극단 우측 꼬리"
|
|
268
|
+
else:
|
|
269
|
+
dist = "극단 좌측 꼬리"
|
|
270
|
+
|
|
271
|
+
# 로그변환 필요성 판정
|
|
272
|
+
if abs_skew < 0.5: # type: ignore
|
|
273
|
+
log_need = "낮음"
|
|
274
|
+
elif abs_skew < 1.0: # type: ignore
|
|
275
|
+
log_need = "중간"
|
|
276
|
+
else:
|
|
277
|
+
log_need = "높음"
|
|
278
|
+
|
|
247
279
|
iq = {
|
|
248
280
|
"field": f,
|
|
249
281
|
"q1": q1,
|
|
@@ -254,9 +286,11 @@ def outlier_table(data: DataFrame, *fields: str):
|
|
|
254
286
|
"down": down,
|
|
255
287
|
"min": min_value,
|
|
256
288
|
"max": max_value,
|
|
257
|
-
"skew": skew,
|
|
258
289
|
"outlier_count": outlier_count,
|
|
259
|
-
"outlier_rate": outlier_rate
|
|
290
|
+
"outlier_rate": outlier_rate,
|
|
291
|
+
"skew": skew,
|
|
292
|
+
"dist": dist,
|
|
293
|
+
"log_need": log_need
|
|
260
294
|
}
|
|
261
295
|
|
|
262
296
|
result.append(iq)
|
|
@@ -329,8 +363,10 @@ def describe(data: DataFrame, *fields: str, columns: list | None = None):
|
|
|
329
363
|
- 분포 특성은 왜도 값으로 판정합니다.
|
|
330
364
|
- 로그변환 필요성은 왜도의 절댓값 크기로 판정합니다.
|
|
331
365
|
"""
|
|
366
|
+
num_columns = data.select_dtypes(include=np.number).columns
|
|
367
|
+
|
|
332
368
|
if not fields:
|
|
333
|
-
fields = tuple(
|
|
369
|
+
fields = tuple(num_columns)
|
|
334
370
|
|
|
335
371
|
# 기술통계량 구하기
|
|
336
372
|
desc = data[list(fields)].describe().T
|
|
@@ -346,17 +382,7 @@ def describe(data: DataFrame, *fields: str, columns: list | None = None):
|
|
|
346
382
|
additional_stats = []
|
|
347
383
|
for f in fields:
|
|
348
384
|
# 숫자 타입이 아니라면 건너뜀
|
|
349
|
-
if
|
|
350
|
-
'int',
|
|
351
|
-
'int32',
|
|
352
|
-
'int64',
|
|
353
|
-
'float',
|
|
354
|
-
'float32',
|
|
355
|
-
'float64',
|
|
356
|
-
'int64',
|
|
357
|
-
'float64',
|
|
358
|
-
'float32'
|
|
359
|
-
]:
|
|
385
|
+
if f not in num_columns:
|
|
360
386
|
continue
|
|
361
387
|
|
|
362
388
|
# 사분위수
|
|
@@ -472,6 +498,8 @@ def category_describe(data: DataFrame, *fields: str):
|
|
|
472
498
|
- 숫자형 컬럼은 자동으로 제외됩니다.
|
|
473
499
|
- NaN 값도 하나의 범주로 포함됩니다.
|
|
474
500
|
"""
|
|
501
|
+
num_columns = data.select_dtypes(include=np.number).columns
|
|
502
|
+
|
|
475
503
|
if not fields:
|
|
476
504
|
# 명목형(범주형) 컬럼 선택: object, category, bool 타입
|
|
477
505
|
fields = data.select_dtypes(include=['object', 'category', 'bool']).columns # type: ignore
|
|
@@ -480,14 +508,7 @@ def category_describe(data: DataFrame, *fields: str):
|
|
|
480
508
|
summary = []
|
|
481
509
|
for f in fields:
|
|
482
510
|
# 숫자형 컬럼은 건너뜀
|
|
483
|
-
if
|
|
484
|
-
"int",
|
|
485
|
-
"int32",
|
|
486
|
-
"int64",
|
|
487
|
-
"float",
|
|
488
|
-
"float32",
|
|
489
|
-
"float64",
|
|
490
|
-
]:
|
|
511
|
+
if f in num_columns:
|
|
491
512
|
continue
|
|
492
513
|
|
|
493
514
|
# 각 범주값의 빈도수 계산 (NaN 포함)
|
|
@@ -731,6 +752,7 @@ def equal_var_test(data: DataFrame, columns: list | str | None = None, normal_di
|
|
|
731
752
|
normality_result = normal_test(data[numeric_cols], method="n")
|
|
732
753
|
# 모든 컬럼이 정규분포를 따르는지 확인
|
|
733
754
|
all_normal = normality_result["is_normal"].all()
|
|
755
|
+
normality_method = normality_result["method"].iloc[0]
|
|
734
756
|
normal_dist = all_normal # type: ignore
|
|
735
757
|
|
|
736
758
|
try:
|
|
@@ -742,13 +764,14 @@ def equal_var_test(data: DataFrame, columns: list | str | None = None, normal_di
|
|
|
742
764
|
s, p = levene(*fields)
|
|
743
765
|
|
|
744
766
|
result_df = DataFrame([{
|
|
767
|
+
"normality_method": normality_method,
|
|
768
|
+
"normality_checked": normal_dist,
|
|
745
769
|
"method": method_name,
|
|
746
770
|
"statistic": s,
|
|
747
771
|
"p-value": p,
|
|
748
772
|
"is_equal_var": p > 0.05,
|
|
749
773
|
"n_columns": len(fields),
|
|
750
|
-
"columns": ", ".join(numeric_cols[:len(fields)])
|
|
751
|
-
"normality_checked": normality_checked
|
|
774
|
+
"columns": ", ".join(numeric_cols[:len(fields)])
|
|
752
775
|
}])
|
|
753
776
|
|
|
754
777
|
return result_df
|
|
@@ -816,52 +839,40 @@ def ttest_1samp(data, mean_value: float = 0.0) -> DataFrame:
|
|
|
816
839
|
alternative: list = ["two-sided", "less", "greater"]
|
|
817
840
|
result: list = []
|
|
818
841
|
|
|
819
|
-
#
|
|
820
|
-
|
|
821
|
-
|
|
842
|
+
# 각 대립가설 방향에 대해 t-검정 수행
|
|
843
|
+
for a in alternative:
|
|
844
|
+
try:
|
|
845
|
+
s, p = ttest_1samp(col_data, mean_value, alternative=a) # type: ignore
|
|
846
|
+
|
|
847
|
+
itp = None
|
|
848
|
+
|
|
849
|
+
if a == "two-sided":
|
|
850
|
+
itp = "μ {0} {1}".format("==" if p > 0.05 else "!=", mean_value)
|
|
851
|
+
elif a == "less":
|
|
852
|
+
itp = "μ {0} {1}".format(">=" if p > 0.05 else "<", mean_value)
|
|
853
|
+
else:
|
|
854
|
+
itp = "μ {0} {1}".format("<=" if p > 0.05 else ">", mean_value)
|
|
855
|
+
|
|
856
|
+
result.append({
|
|
857
|
+
"alternative": a,
|
|
858
|
+
"statistic": round(s, 3),
|
|
859
|
+
"p-value": round(p, 4),
|
|
860
|
+
"H0": p > 0.05,
|
|
861
|
+
"H1": p <= 0.05,
|
|
862
|
+
"interpretation": itp,
|
|
863
|
+
})
|
|
864
|
+
except Exception as e:
|
|
822
865
|
result.append({
|
|
823
866
|
"alternative": a,
|
|
824
867
|
"statistic": np.nan,
|
|
825
868
|
"p-value": np.nan,
|
|
826
869
|
"H0": False,
|
|
827
870
|
"H1": False,
|
|
828
|
-
"interpretation": f"검정
|
|
871
|
+
"interpretation": f"검정 실패: {str(e)}"
|
|
829
872
|
})
|
|
830
|
-
else:
|
|
831
|
-
for a in alternative:
|
|
832
|
-
try:
|
|
833
|
-
s, p = ttest_1samp(col_data, mean_value, alternative=a) # type: ignore
|
|
834
|
-
|
|
835
|
-
itp = None
|
|
836
|
-
|
|
837
|
-
if a == "two-sided":
|
|
838
|
-
itp = "μ {0} {1}".format("==" if p > 0.05 else "!=", mean_value)
|
|
839
|
-
elif a == "less":
|
|
840
|
-
itp = "μ {0} {1}".format(">=" if p > 0.05 else "<", mean_value)
|
|
841
|
-
else:
|
|
842
|
-
itp = "μ {0} {1}".format("<=" if p > 0.05 else ">", mean_value)
|
|
843
|
-
|
|
844
|
-
result.append({
|
|
845
|
-
"alternative": a,
|
|
846
|
-
"statistic": round(s, 3),
|
|
847
|
-
"p-value": round(p, 4),
|
|
848
|
-
"H0": p > 0.05,
|
|
849
|
-
"H1": p <= 0.05,
|
|
850
|
-
"interpretation": itp,
|
|
851
|
-
})
|
|
852
|
-
except Exception as e:
|
|
853
|
-
result.append({
|
|
854
|
-
"alternative": a,
|
|
855
|
-
"statistic": np.nan,
|
|
856
|
-
"p-value": np.nan,
|
|
857
|
-
"H0": False,
|
|
858
|
-
"H1": False,
|
|
859
|
-
"interpretation": f"검정 실패: {str(e)}"
|
|
860
|
-
})
|
|
861
873
|
|
|
862
874
|
rdf = DataFrame(result)
|
|
863
875
|
rdf.set_index(["field", "alternative"], inplace=True)
|
|
864
|
-
|
|
865
876
|
return rdf
|
|
866
877
|
|
|
867
878
|
|
|
@@ -947,6 +958,9 @@ def ttest_ind(
|
|
|
947
958
|
# 두 데이터를 DataFrame으로 구성하여 등분산성 검정
|
|
948
959
|
temp_df = DataFrame({'x': x_data, 'y': y_data})
|
|
949
960
|
var_result = equal_var_test(temp_df)
|
|
961
|
+
normality_method = var_result["normality_method"].iloc[0]
|
|
962
|
+
normality_checked = var_result["normality_checked"].iloc[0]
|
|
963
|
+
equal_var_method = var_result["method"].iloc[0]
|
|
950
964
|
equal_var = var_result["is_equal_var"].iloc[0]
|
|
951
965
|
|
|
952
966
|
alternative: list = ["two-sided", "less", "greater"]
|
|
@@ -972,8 +986,9 @@ def ttest_ind(
|
|
|
972
986
|
"test": n,
|
|
973
987
|
"alternative": a,
|
|
974
988
|
"interpretation": itp,
|
|
975
|
-
|
|
976
|
-
|
|
989
|
+
normality_method: normality_checked,
|
|
990
|
+
equal_var_method: equal_var,
|
|
991
|
+
n: round(s, 3), # type: ignore
|
|
977
992
|
"p-value": round(p, 4), # type: ignore
|
|
978
993
|
"H0": p > 0.05, # type: ignore
|
|
979
994
|
"H1": p <= 0.05, # type: ignore
|
|
@@ -982,12 +997,13 @@ def ttest_ind(
|
|
|
982
997
|
result.append({
|
|
983
998
|
"test": "t-test_ind" if equal_var else "Welch's t-test",
|
|
984
999
|
"alternative": a,
|
|
985
|
-
"
|
|
1000
|
+
"interpretation": f"검정 실패: {str(e)}",
|
|
1001
|
+
normality_method: normality_checked,
|
|
1002
|
+
equal_var_method: equal_var,
|
|
1003
|
+
n: np.nan,
|
|
986
1004
|
"p-value": np.nan,
|
|
987
1005
|
"H0": False,
|
|
988
|
-
"H1": False
|
|
989
|
-
"interpretation": f"검정 실패: {str(e)}",
|
|
990
|
-
"equal_var_checked": var_checked
|
|
1006
|
+
"H1": False
|
|
991
1007
|
})
|
|
992
1008
|
|
|
993
1009
|
rdf = DataFrame(result)
|
|
@@ -998,7 +1014,7 @@ def ttest_ind(
|
|
|
998
1014
|
# ===================================================================
|
|
999
1015
|
# 대응표본 t-검정 또는 Wilcoxon test
|
|
1000
1016
|
# ===================================================================
|
|
1001
|
-
def ttest_rel(x, y,
|
|
1017
|
+
def ttest_rel(x, y, normality: bool | None = None) -> DataFrame:
|
|
1002
1018
|
"""대응표본 t-검정 또는 Wilcoxon signed-rank test를 수행한다.
|
|
1003
1019
|
|
|
1004
1020
|
대응표본 t-검정은 동일 개체에서 측정된 두 시점의 평균 차이를 검정한다.
|
|
@@ -1007,7 +1023,7 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
|
|
|
1007
1023
|
Args:
|
|
1008
1024
|
x (array-like): 첫 번째 측정값의 연속형 데이터 (리스트, Series, ndarray 등).
|
|
1009
1025
|
y (array-like): 두 번째 측정값의 연속형 데이터 (리스트, Series, ndarray 등).
|
|
1010
|
-
|
|
1026
|
+
normality (bool | None, optional): 정규성 가정 여부.
|
|
1011
1027
|
- True: 대응표본 t-검정 (차이의 정규분포 가정)
|
|
1012
1028
|
- False: Wilcoxon signed-rank test (비모수 검정, 더 강건함)
|
|
1013
1029
|
- None: 차이의 정규성을 자동으로 검정하여 판별
|
|
@@ -1060,36 +1076,31 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
|
|
|
1060
1076
|
raise ValueError(f"최소 2개 이상의 대응 데이터가 필요합니다. 현재: {len(x_data)}")
|
|
1061
1077
|
|
|
1062
1078
|
# parametric이 None이면 차이의 정규성을 자동으로 검정
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
#
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
parametric = p_normal > 0.05 # p > 0.05면 정규분포 따름
|
|
1071
|
-
except Exception:
|
|
1072
|
-
# shapiro 실패 시 normaltest 사용
|
|
1073
|
-
try:
|
|
1074
|
-
_, p_normal = normaltest(diff)
|
|
1075
|
-
parametric = p_normal > 0.05
|
|
1076
|
-
except Exception:
|
|
1077
|
-
# 둘 다 실패하면 기본값으로 비모수 검정 사용
|
|
1078
|
-
parametric = False
|
|
1079
|
+
if normality is None:
|
|
1080
|
+
tmp_df = DataFrame({'x': x_data, 'y': y_data})
|
|
1081
|
+
normality_result = normal_test(tmp_df, method="n")
|
|
1082
|
+
# 모든 컬럼이 정규분포를 따르는지 확인
|
|
1083
|
+
all_normal = normality_result["is_normal"].all()
|
|
1084
|
+
normality_method = normality_result["method"].iloc[0]
|
|
1085
|
+
normality = all_normal # type: ignore
|
|
1079
1086
|
|
|
1080
1087
|
alternative: list = ["two-sided", "less", "greater"]
|
|
1081
1088
|
result: list = []
|
|
1082
1089
|
fmt: str = "μ(x) {0} μ(y)"
|
|
1083
1090
|
|
|
1091
|
+
if normality:
|
|
1092
|
+
s, p = ttest_rel(x_data, y_data, alternative=a) # type: ignore
|
|
1093
|
+
else:
|
|
1094
|
+
# Wilcoxon signed-rank test (대응표본용 비모수 검정)
|
|
1095
|
+
n = "Wilcoxon signed-rank"
|
|
1096
|
+
|
|
1084
1097
|
for a in alternative:
|
|
1085
1098
|
try:
|
|
1086
|
-
if
|
|
1099
|
+
if normality:
|
|
1087
1100
|
s, p = ttest_rel(x_data, y_data, alternative=a) # type: ignore
|
|
1088
|
-
n = "t-test_paired"
|
|
1089
1101
|
else:
|
|
1090
1102
|
# Wilcoxon signed-rank test (대응표본용 비모수 검정)
|
|
1091
1103
|
s, p = wilcoxon(x_data, y_data, alternative=a)
|
|
1092
|
-
n = "Wilcoxon signed-rank"
|
|
1093
1104
|
|
|
1094
1105
|
itp = None
|
|
1095
1106
|
|
|
@@ -1103,28 +1114,27 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
|
|
|
1103
1114
|
result.append({
|
|
1104
1115
|
"test": n,
|
|
1105
1116
|
"alternative": a,
|
|
1117
|
+
normality_method: normality,
|
|
1118
|
+
"interpretation": itp,
|
|
1106
1119
|
"statistic": round(s, 3) if not np.isnan(s) else s, # type: ignore
|
|
1107
1120
|
"p-value": round(p, 4) if not np.isnan(p) else p, # type: ignore
|
|
1108
1121
|
"H0": p > 0.05, # type: ignore
|
|
1109
1122
|
"H1": p <= 0.05, # type: ignore
|
|
1110
|
-
"interpretation": itp,
|
|
1111
|
-
"normality_checked": var_checked
|
|
1112
1123
|
})
|
|
1113
1124
|
except Exception as e:
|
|
1114
1125
|
result.append({
|
|
1115
|
-
"test":
|
|
1126
|
+
"test": n,
|
|
1116
1127
|
"alternative": a,
|
|
1128
|
+
normality_method: normality,
|
|
1129
|
+
"interpretation": f"검정 실패: {str(e)}",
|
|
1117
1130
|
"statistic": np.nan,
|
|
1118
1131
|
"p-value": np.nan,
|
|
1119
1132
|
"H0": False,
|
|
1120
|
-
"H1": False
|
|
1121
|
-
"interpretation": f"검정 실패: {str(e)}",
|
|
1122
|
-
"normality_checked": var_checked
|
|
1133
|
+
"H1": False
|
|
1123
1134
|
})
|
|
1124
1135
|
|
|
1125
1136
|
rdf = DataFrame(result)
|
|
1126
1137
|
rdf.set_index(["test", "alternative"], inplace=True)
|
|
1127
|
-
|
|
1128
1138
|
return rdf
|
|
1129
1139
|
|
|
1130
1140
|
|
|
@@ -1133,7 +1143,8 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
|
|
|
1133
1143
|
# ===================================================================
|
|
1134
1144
|
# 일원 분산분석 (One-way ANOVA)
|
|
1135
1145
|
# ===================================================================
|
|
1136
|
-
def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) -> tuple[DataFrame, str, DataFrame | None, str]:
|
|
1146
|
+
#def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) -> tuple[DataFrame, str, DataFrame | None, str]:
|
|
1147
|
+
def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05, posthoc: bool = False) -> DataFrame | tuple[DataFrame, DataFrame] :
|
|
1137
1148
|
"""일원분산분석(One-way ANOVA)을 일괄 처리한다.
|
|
1138
1149
|
|
|
1139
1150
|
정규성 및 등분산성 검정을 자동으로 수행한 후,
|
|
@@ -1151,13 +1162,12 @@ def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) ->
|
|
|
1151
1162
|
dv (str): 종속변수(Dependent Variable) 컬럼명.
|
|
1152
1163
|
between (str): 그룹 구분 변수 컬럼명.
|
|
1153
1164
|
alpha (float, optional): 유의수준. 기본값 0.05.
|
|
1165
|
+
posthoc (bool, optional): 사후검정 수행 여부. 기본값 False.
|
|
1154
1166
|
|
|
1155
1167
|
Returns:
|
|
1156
1168
|
tuple:
|
|
1157
1169
|
- anova_df (DataFrame): ANOVA 또는 Welch 결과 테이블(Source, ddof1, ddof2, F, p-unc, np2 등 포함).
|
|
1158
|
-
- anova_report (str): 정규성/등분산 여부와 F, p값, 효과크기를 요약한 보고 문장.
|
|
1159
1170
|
- posthoc_df (DataFrame|None): 사후검정 결과(Tukey HSD 또는 Games-Howell). ANOVA가 유의할 때만 생성.
|
|
1160
|
-
- posthoc_report (str): 사후검정 유무와 유의한 쌍 정보를 요약한 보고 문장.
|
|
1161
1171
|
|
|
1162
1172
|
Examples:
|
|
1163
1173
|
```python
|
|
@@ -1169,7 +1179,7 @@ def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) ->
|
|
|
1169
1179
|
'group': ['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B']
|
|
1170
1180
|
})
|
|
1171
1181
|
|
|
1172
|
-
anova_df,
|
|
1182
|
+
anova_df, posthoc_df = hs_stats.oneway_anova(df, dv='score', between='group')
|
|
1173
1183
|
|
|
1174
1184
|
# 사후검정결과는 ANOVA가 유의할 때만 생성됨
|
|
1175
1185
|
if posthoc_df is not None:
|
|
@@ -1226,57 +1236,61 @@ def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) ->
|
|
|
1226
1236
|
# ============================================
|
|
1227
1237
|
# 3. ANOVA 수행
|
|
1228
1238
|
# ============================================
|
|
1239
|
+
anova_df: DataFrame
|
|
1240
|
+
anova_method: str
|
|
1241
|
+
|
|
1229
1242
|
if equal_var_satisfied:
|
|
1230
1243
|
# 등분산을 만족할 때 일반적인 ANOVA 사용
|
|
1231
1244
|
anova_method = "ANOVA"
|
|
1232
1245
|
anova_df = anova(data=df_filtered, dv=dv, between=between)
|
|
1246
|
+
en = "Bartlett"
|
|
1233
1247
|
else:
|
|
1234
1248
|
# 등분산을 만족하지 않을 때 Welch's ANOVA 사용
|
|
1235
1249
|
anova_method = "Welch"
|
|
1236
1250
|
anova_df = welch_anova(data=df_filtered, dv=dv, between=between)
|
|
1251
|
+
en = "Levene"
|
|
1237
1252
|
|
|
1238
1253
|
# ANOVA 결과에 메타정보 추가
|
|
1239
1254
|
anova_df.insert(1, 'normality', normality_satisfied)
|
|
1240
|
-
anova_df.insert(2,
|
|
1241
|
-
anova_df
|
|
1255
|
+
anova_df.insert(2, en, equal_var_satisfied)
|
|
1256
|
+
anova_df[anova_method] = anova_df['p-unc'] <= alpha if 'p-unc' in anova_df.columns else False # type: ignore
|
|
1242
1257
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
anova_df['significant'] = anova_df['p-unc'] <= alpha
|
|
1258
|
+
if posthoc == False:
|
|
1259
|
+
return anova_df
|
|
1246
1260
|
|
|
1247
1261
|
# ANOVA 결과가 유의한지 확인
|
|
1248
1262
|
p_unc = float(anova_df.loc[0, 'p-unc']) # type: ignore
|
|
1249
1263
|
anova_significant = p_unc <= alpha
|
|
1250
1264
|
|
|
1251
1265
|
# ANOVA 보고 문장 생성
|
|
1252
|
-
def _safe_get(col: str, default: float = np.nan) -> float:
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1266
|
+
# def _safe_get(col: str, default: float = np.nan) -> float:
|
|
1267
|
+
# try:
|
|
1268
|
+
# return float(anova_df.loc[0, col]) if col in anova_df.columns else default # type: ignore
|
|
1269
|
+
# except Exception:
|
|
1270
|
+
# return default
|
|
1257
1271
|
|
|
1258
|
-
df1 = _safe_get('ddof1')
|
|
1259
|
-
df2 = _safe_get('ddof2')
|
|
1260
|
-
fval = _safe_get('F')
|
|
1261
|
-
eta2 = _safe_get('np2')
|
|
1272
|
+
# df1 = _safe_get('ddof1')
|
|
1273
|
+
# df2 = _safe_get('ddof2')
|
|
1274
|
+
# fval = _safe_get('F')
|
|
1275
|
+
# eta2 = _safe_get('np2')
|
|
1262
1276
|
|
|
1263
|
-
anova_sig_text = "그룹별 평균이 다를 가능성이 높습니다." if anova_significant else "그룹별 평균 차이에 대한 근거가 부족합니다."
|
|
1264
|
-
assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않았고'}, 등분산성은 {'
|
|
1277
|
+
# anova_sig_text = "그룹별 평균이 다를 가능성이 높습니다." if anova_significant else "그룹별 평균 차이에 대한 근거가 부족합니다."
|
|
1278
|
+
# assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않았고'}, 등분산성은 {'충족되었다' if equal_var_satisfied else '충족되지 않았다'}고 판단됩니다."
|
|
1265
1279
|
|
|
1266
|
-
anova_report = (
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
)
|
|
1280
|
+
# anova_report = (
|
|
1281
|
+
# f"{between}별로 {dv} 평균을 비교한 {anova_method} 결과: F({df1:.3f}, {df2:.3f}) = {fval:.3f}, p = {p_unc:.4f}. "
|
|
1282
|
+
# f"해석: {anova_sig_text} {assumption_text}"
|
|
1283
|
+
# )
|
|
1270
1284
|
|
|
1271
|
-
if not np.isnan(eta2):
|
|
1272
|
-
|
|
1285
|
+
# if not np.isnan(eta2):
|
|
1286
|
+
# anova_report += f" 효과 크기(η²p) ≈ {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
|
|
1273
1287
|
|
|
1274
1288
|
# ============================================
|
|
1275
1289
|
# 4. 사후검정 (ANOVA 유의할 때만)
|
|
1276
1290
|
# ============================================
|
|
1277
|
-
posthoc_df
|
|
1278
|
-
posthoc_method
|
|
1279
|
-
posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
|
|
1291
|
+
posthoc_df: DataFrame
|
|
1292
|
+
posthoc_method: str
|
|
1293
|
+
#posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
|
|
1280
1294
|
|
|
1281
1295
|
if anova_significant:
|
|
1282
1296
|
if equal_var_satisfied:
|
|
@@ -1291,38 +1305,39 @@ def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) ->
|
|
|
1291
1305
|
# 사후검정 결과에 메타정보 추가
|
|
1292
1306
|
# posthoc_df.insert(0, 'normality', normality_satisfied)
|
|
1293
1307
|
# posthoc_df.insert(1, 'equal_var', equal_var_satisfied)
|
|
1294
|
-
posthoc_df.insert(0, 'method', posthoc_method)
|
|
1308
|
+
posthoc_df.insert(0, 'method', posthoc_method) # type: ignore
|
|
1295
1309
|
|
|
1296
1310
|
# 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]
|
|
1311
|
+
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] # type: ignore
|
|
1298
1312
|
p_col = p_cols[0] if p_cols else None
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
else:
|
|
1320
|
-
|
|
1313
|
+
|
|
1314
|
+
# 유의성 여부 컬럼 추가
|
|
1315
|
+
posthoc_df['significant'] = posthoc_df[p_col] <= alpha if p_col else False # type: ignore
|
|
1316
|
+
|
|
1317
|
+
# if p_col:
|
|
1318
|
+
# sig_pairs_df = posthoc_df[posthoc_df[p_col] <= alpha]
|
|
1319
|
+
# sig_count = len(sig_pairs_df)
|
|
1320
|
+
# total_count = len(posthoc_df)
|
|
1321
|
+
# pair_samples = []
|
|
1322
|
+
# if not sig_pairs_df.empty and {'A', 'B'}.issubset(sig_pairs_df.columns):
|
|
1323
|
+
# pair_samples = [f"{row['A']} vs {row['B']}" for _, row in sig_pairs_df.head(3).iterrows()]
|
|
1324
|
+
|
|
1325
|
+
# if sig_count > 0:
|
|
1326
|
+
# posthoc_report = (
|
|
1327
|
+
# f"{posthoc_method} 사후검정에서 {sig_count}/{total_count}쌍이 의미 있는 차이를 보였습니다 (alpha={alpha})."
|
|
1328
|
+
# )
|
|
1329
|
+
# if pair_samples:
|
|
1330
|
+
# posthoc_report += " 예: " + ", ".join(pair_samples) + " 등."
|
|
1331
|
+
# else:
|
|
1332
|
+
# posthoc_report = f"{posthoc_method} 사후검정에서 추가로 유의한 쌍은 발견되지 않았습니다."
|
|
1333
|
+
# else:
|
|
1334
|
+
# posthoc_report = f"{posthoc_method} 결과는 생성했지만 p-value 정보를 찾지 못해 유의성을 확인할 수 없습니다."
|
|
1321
1335
|
|
|
1322
1336
|
# ============================================
|
|
1323
1337
|
# 5. 결과 반환
|
|
1324
1338
|
# ============================================
|
|
1325
|
-
return anova_df, anova_report, posthoc_df, posthoc_report
|
|
1339
|
+
#return anova_df, anova_report, posthoc_df, posthoc_report
|
|
1340
|
+
return anova_df, posthoc_df
|
|
1326
1341
|
|
|
1327
1342
|
|
|
1328
1343
|
# ===================================================================
|
|
@@ -1603,8 +1618,7 @@ def corr_pairwise(
|
|
|
1603
1618
|
alpha: float = 0.05,
|
|
1604
1619
|
z_thresh: float = 3.0,
|
|
1605
1620
|
min_n: int = 8,
|
|
1606
|
-
linearity_power: tuple[int, ...] = (2,)
|
|
1607
|
-
p_adjust: str = "none",
|
|
1621
|
+
#linearity_power: tuple[int, ...] = (2,)
|
|
1608
1622
|
) -> tuple[DataFrame, DataFrame]:
|
|
1609
1623
|
"""각 변수 쌍에 대해 선형성·이상치 여부를 점검한 뒤 Pearson/Spearman을 자동 선택해 상관을 요약한다.
|
|
1610
1624
|
|
|
@@ -1613,7 +1627,6 @@ def corr_pairwise(
|
|
|
1613
1627
|
2) 단순회귀 y~x에 대해 Ramsey RESET(linearity_power)로 선형성 검정 (모든 p>alpha → 선형성 충족)
|
|
1614
1628
|
3) 선형성 충족이고 양쪽 변수에서 |z|>z_thresh 이상치가 없으면 Pearson, 그 외엔 Spearman 선택
|
|
1615
1629
|
4) 상관계수/유의확률, 유의성 여부, 강도(strong/medium/weak/no correlation) 기록
|
|
1616
|
-
5) 선택적으로 다중비교 보정(p_adjust="fdr_bh" 등) 적용하여 pval_adj와 significant_adj 추가
|
|
1617
1630
|
|
|
1618
1631
|
Args:
|
|
1619
1632
|
data (DataFrame): 분석 대상 데이터프레임.
|
|
@@ -1621,14 +1634,13 @@ def corr_pairwise(
|
|
|
1621
1634
|
alpha (float, optional): 유의수준. 기본 0.05.
|
|
1622
1635
|
z_thresh (float, optional): 이상치 판단 임계값(|z| 기준). 기본 3.0.
|
|
1623
1636
|
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".
|
|
1637
|
+
#linearity_power (tuple[int,...], optional): RESET 검정에서 포함할 차수 집합. 기본 (2,).
|
|
1626
1638
|
|
|
1627
1639
|
Returns:
|
|
1628
1640
|
tuple[DataFrame, DataFrame]: 두 개의 데이터프레임을 반환.
|
|
1629
1641
|
[0] result_df: 각 변수쌍별 결과 테이블. 컬럼:
|
|
1630
1642
|
var_a, var_b, n, linearity(bool), outlier_flag(bool), chosen('pearson'|'spearman'),
|
|
1631
|
-
corr, pval, significant(bool), strength(str)
|
|
1643
|
+
corr, pval, significant(bool), strength(str)
|
|
1632
1644
|
[1] corr_matrix: 상관계수 행렬 (행과 열에 변수명, 값에 상관계수)
|
|
1633
1645
|
|
|
1634
1646
|
Examples:
|
|
@@ -1672,7 +1684,7 @@ def corr_pairwise(
|
|
|
1672
1684
|
for a, b in combinations(cols, 2):
|
|
1673
1685
|
# 공통 관측치 사용
|
|
1674
1686
|
pair_df = data[[a, b]].dropna()
|
|
1675
|
-
if len(pair_df) <
|
|
1687
|
+
if len(pair_df) < min_n:
|
|
1676
1688
|
# 표본이 너무 적으면 계산하지 않음
|
|
1677
1689
|
rows.append(
|
|
1678
1690
|
{
|
|
@@ -1716,13 +1728,16 @@ def corr_pairwise(
|
|
|
1716
1728
|
try:
|
|
1717
1729
|
X_const = sm.add_constant(x)
|
|
1718
1730
|
model = sm.OLS(y, X_const).fit()
|
|
1719
|
-
pvals = []
|
|
1720
|
-
for pwr in linearity_power:
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
# 모든 차수에서 유의하지 않을 때 선형성 충족으로 간주
|
|
1724
|
-
if len(pvals) > 0:
|
|
1725
|
-
|
|
1731
|
+
# pvals = []
|
|
1732
|
+
# for pwr in linearity_power:
|
|
1733
|
+
# reset = linear_reset(model, power=pwr, use_f=True)
|
|
1734
|
+
# pvals.append(reset.pvalue)
|
|
1735
|
+
# # 모든 차수에서 유의하지 않을 때 선형성 충족으로 간주
|
|
1736
|
+
# if len(pvals) > 0:
|
|
1737
|
+
# linearity_ok = all([pv > alpha for pv in pvals])
|
|
1738
|
+
|
|
1739
|
+
reset = linear_reset(model)
|
|
1740
|
+
linearity_ok = reset.pvalue > alpha
|
|
1726
1741
|
except Exception:
|
|
1727
1742
|
linearity_ok = False
|
|
1728
1743
|
|
|
@@ -1770,16 +1785,8 @@ def corr_pairwise(
|
|
|
1770
1785
|
|
|
1771
1786
|
result_df = DataFrame(rows)
|
|
1772
1787
|
|
|
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
1788
|
|
|
1782
|
-
#
|
|
1789
|
+
# 5) 상관행렬 생성 (result_df 기반)
|
|
1783
1790
|
# 모든 변수를 행과 열로 하는 대칭 행렬 생성
|
|
1784
1791
|
corr_matrix = DataFrame(np.nan, index=cols, columns=cols)
|
|
1785
1792
|
# 대각선: 1 (자기상관)
|
|
@@ -1852,17 +1859,21 @@ def vif_filter(
|
|
|
1852
1859
|
result = data.copy()
|
|
1853
1860
|
return result
|
|
1854
1861
|
|
|
1855
|
-
def _compute_vifs(X_: DataFrame) ->
|
|
1862
|
+
def _compute_vifs(X_: DataFrame, verbose: bool = False) -> DataFrame:
|
|
1856
1863
|
# NA 제거 후 상수항 추가
|
|
1857
1864
|
X_clean = X_.dropna()
|
|
1865
|
+
|
|
1858
1866
|
if X_clean.shape[0] == 0:
|
|
1859
1867
|
# 데이터가 모두 NA인 경우 VIF 계산 불가: NaN 반환
|
|
1860
|
-
return {col: np.nan for col in X_.columns}
|
|
1868
|
+
return DataFrame({col: [np.nan] for col in X_.columns})
|
|
1869
|
+
|
|
1861
1870
|
if X_clean.shape[1] == 1:
|
|
1862
1871
|
# 단일 예측변수의 경우 다른 설명변수가 없으므로 VIF는 1로 간주
|
|
1863
|
-
return {col: 1.0 for col in X_clean.columns}
|
|
1872
|
+
return DataFrame({col: [1.0] for col in X_clean.columns})
|
|
1873
|
+
|
|
1864
1874
|
exog = sm.add_constant(X_clean, prepend=True)
|
|
1865
1875
|
vifs = {}
|
|
1876
|
+
|
|
1866
1877
|
for i, col in enumerate(X_clean.columns, start=0):
|
|
1867
1878
|
# exog의 첫 열은 상수항이므로 변수 인덱스는 +1
|
|
1868
1879
|
try:
|
|
@@ -1870,27 +1881,40 @@ def vif_filter(
|
|
|
1870
1881
|
except Exception:
|
|
1871
1882
|
# 계산 실패 시 무한대로 처리하여 우선 제거 대상으로
|
|
1872
1883
|
vifs[col] = float("inf")
|
|
1873
|
-
|
|
1884
|
+
|
|
1885
|
+
vdf = DataFrame(list(vifs.items()), columns=["Variable", "VIF"])
|
|
1886
|
+
vdf.sort_values("VIF", ascending=False, inplace=True)
|
|
1887
|
+
|
|
1888
|
+
if verbose:
|
|
1889
|
+
pretty_table(vdf) # type: ignore
|
|
1890
|
+
print()
|
|
1891
|
+
|
|
1892
|
+
return vdf
|
|
1874
1893
|
|
|
1875
1894
|
# 반복 제거 루프
|
|
1895
|
+
i = 0
|
|
1876
1896
|
while True:
|
|
1877
1897
|
if X.shape[1] == 0:
|
|
1878
1898
|
break
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1899
|
+
|
|
1900
|
+
print(f"📇 VIF 제거 반복 {i+1}회차\n")
|
|
1901
|
+
vifs = _compute_vifs(X, verbose=verbose)
|
|
1902
|
+
|
|
1882
1903
|
# 모든 변수가 임계값 이하이면 종료
|
|
1883
|
-
|
|
1884
|
-
|
|
1904
|
+
max_vif = vifs.iloc[0]["VIF"]
|
|
1905
|
+
max_key = vifs.iloc[0]["Variable"]
|
|
1906
|
+
|
|
1885
1907
|
if np.isnan(max_vif) or max_vif <= threshold:
|
|
1908
|
+
if i == 0:
|
|
1909
|
+
print("▶ 모든 변수의 VIF가 임계값 이하입니다. 제거할 변수가 없습니다.\n")
|
|
1910
|
+
else:
|
|
1911
|
+
print("▶ 모든 변수의 VIF가 임계값 이하가 되어 종료합니다. 제거된 변수 {0}개\n".format(i))
|
|
1886
1912
|
break
|
|
1913
|
+
|
|
1887
1914
|
# 가장 큰 VIF 변수 제거
|
|
1888
1915
|
X = X.drop(columns=[max_key])
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
if not verbose:
|
|
1892
|
-
final_vifs = _compute_vifs(X) if X.shape[1] > 0 else {}
|
|
1893
|
-
print(final_vifs)
|
|
1916
|
+
print(f"제거된 변수: {max_key} (VIF={max_vif:.2f})")
|
|
1917
|
+
i += 1
|
|
1894
1918
|
|
|
1895
1919
|
# 원본 컬럼 순서 유지하며 제거된 수치형 컬럼만 제외
|
|
1896
1920
|
kept_numeric_cols = list(X.columns)
|
|
@@ -2067,19 +2091,20 @@ def ols_report(
|
|
|
2067
2091
|
var_row = {
|
|
2068
2092
|
"종속변수": yname, # 종속변수 이름
|
|
2069
2093
|
"독립변수": name, # 독립변수 이름
|
|
2070
|
-
"B":
|
|
2094
|
+
"B(비표준화 계수)": np.round(b, 4), # 비표준화 회귀계수(B)
|
|
2071
2095
|
}
|
|
2072
2096
|
# logvar가 True면 exp(B) 컬럼 추가
|
|
2073
2097
|
if 'logvar' in locals() and logvar:
|
|
2074
|
-
var_row["exp(B)"] =
|
|
2098
|
+
var_row["exp(B)"] = np.round(np.exp(b), 4)
|
|
2099
|
+
|
|
2075
2100
|
var_row.update({
|
|
2076
|
-
"표준오차":
|
|
2077
|
-
"
|
|
2078
|
-
"t": f"{t
|
|
2079
|
-
"
|
|
2080
|
-
"significant": p <= alpha, # 유의성 여부 (boolean)
|
|
2081
|
-
"공차": 1 / vif, # 공차(Tolerance = 1/VIF)
|
|
2082
|
-
"vif": vif, # 분산팽창계수
|
|
2101
|
+
"표준오차": np.round(se, 4), # 계수 표준오차
|
|
2102
|
+
"β(표준화 계수)": np.round(beta, 4), # 표준화 회귀계수(β)
|
|
2103
|
+
"t": f"{np.round(t, 4)}{stars}", # t-통계량(+별표)
|
|
2104
|
+
"유의확률": np.round(p, 4), # 계수 유의확률
|
|
2105
|
+
#"significant": p <= alpha, # 유의성 여부 (boolean)
|
|
2106
|
+
#"공차": 1 / vif, # 공차(Tolerance = 1/VIF)
|
|
2107
|
+
"vif": np.round(vif, 4), # 분산팽창계수
|
|
2083
2108
|
})
|
|
2084
2109
|
variables.append(var_row)
|
|
2085
2110
|
|
|
@@ -2098,11 +2123,15 @@ def ols_report(
|
|
|
2098
2123
|
continue
|
|
2099
2124
|
result_dict[key] = value
|
|
2100
2125
|
|
|
2126
|
+
r2 = float(result_dict.get('R-squared', np.nan))
|
|
2127
|
+
adj_r2 = float(result_dict.get('Adj. R-squared', np.nan))
|
|
2128
|
+
r = np.sqrt(r2) if r2 >= 0 else np.nan
|
|
2129
|
+
|
|
2101
2130
|
# 적합도 보고 문자열 구성
|
|
2102
|
-
result_report = f"𝑅({
|
|
2131
|
+
result_report = f"𝑅({r:.3f}), 𝑅^2({r2:.3f}), Adj 𝑅^2({adj_r2:.3f}), 𝐹({float(result_dict['F-statistic']):.3f}), 유의확률({float(result_dict['Prob (F-statistic)']):.3f}), Durbin-Watson({float(result_dict['Durbin-Watson']):.3f})"
|
|
2103
2132
|
|
|
2104
2133
|
# 모형 보고 문장 구성
|
|
2105
|
-
tpl = "%s에 대하여 %s로 예측하는 회귀분석을 실시한 결과, 이 회귀모형은 통계적으로 %s(F(%s,%s) = %
|
|
2134
|
+
tpl = "%s에 대하여 %s로 예측하는 회귀분석을 실시한 결과, 이 회귀모형은 통계적으로 %s(F(%s,%s) = %0.3f, p %s 0.05)."
|
|
2106
2135
|
model_report = tpl % (
|
|
2107
2136
|
rdf["종속변수"][0],
|
|
2108
2137
|
",".join(list(rdf["독립변수"])),
|
|
@@ -2113,27 +2142,27 @@ def ols_report(
|
|
|
2113
2142
|
),
|
|
2114
2143
|
result_dict["Df Model"],
|
|
2115
2144
|
result_dict["Df Residuals"],
|
|
2116
|
-
result_dict["F-statistic"],
|
|
2145
|
+
float(result_dict["F-statistic"]),
|
|
2117
2146
|
"<=" if float(result_dict["Prob (F-statistic)"]) <= 0.05 else ">",
|
|
2118
2147
|
)
|
|
2119
2148
|
|
|
2120
2149
|
# 변수별 보고 문장 리스트 구성
|
|
2121
2150
|
variable_reports = []
|
|
2122
|
-
s_normal = "%s가 1 증가하면 %s가 %.
|
|
2123
|
-
s_log = "%s가 1 증가하면 %s가 약 %.
|
|
2151
|
+
s_normal = "%s가 1 증가하면 %s(이)가 %.3f만큼 변하는 것으로 나타남. (p %s 0.05, %s)"
|
|
2152
|
+
s_log = "%s가 1 증가하면 %s(이)가 약 %.3f배 변하는 것으로 나타남. (p %s 0.05, %s)"
|
|
2124
2153
|
|
|
2125
2154
|
for i in rdf.index:
|
|
2126
2155
|
row = rdf.iloc[i]
|
|
2127
2156
|
if logvar:
|
|
2128
|
-
effect = np.exp(float(row["B"]))
|
|
2157
|
+
effect = np.exp(float(row["B(비표준화 계수)"]))
|
|
2129
2158
|
variable_reports.append(
|
|
2130
2159
|
s_log
|
|
2131
2160
|
% (
|
|
2132
2161
|
row["독립변수"],
|
|
2133
2162
|
row["종속변수"],
|
|
2134
2163
|
effect,
|
|
2135
|
-
"<=" if float(row["
|
|
2136
|
-
"유의함" if float(row["
|
|
2164
|
+
"<=" if float(row["유의확률"]) < 0.05 else ">",
|
|
2165
|
+
"유의함" if float(row["유의확률"]) < 0.05 else "유의하지 않음",
|
|
2137
2166
|
)
|
|
2138
2167
|
)
|
|
2139
2168
|
else:
|
|
@@ -2142,9 +2171,9 @@ def ols_report(
|
|
|
2142
2171
|
% (
|
|
2143
2172
|
row["독립변수"],
|
|
2144
2173
|
row["종속변수"],
|
|
2145
|
-
float(row["B"]),
|
|
2146
|
-
"<=" if float(row["
|
|
2147
|
-
"유의함" if float(row["
|
|
2174
|
+
float(row["B(비표준화 계수)"]),
|
|
2175
|
+
"<=" if float(row["유의확률"]) < 0.05 else ">",
|
|
2176
|
+
"유의함" if float(row["유의확률"]) < 0.05 else "유의하지 않음",
|
|
2148
2177
|
)
|
|
2149
2178
|
)
|
|
2150
2179
|
|
|
@@ -2164,8 +2193,9 @@ def ols_report(
|
|
|
2164
2193
|
# 성능 지표 표 생성 (pdf)
|
|
2165
2194
|
pdf = DataFrame(
|
|
2166
2195
|
{
|
|
2167
|
-
"R": [
|
|
2168
|
-
"R²": [
|
|
2196
|
+
"R": [r],
|
|
2197
|
+
"R²": [r2],
|
|
2198
|
+
"Adj. R²": [adj_r2],
|
|
2169
2199
|
"F": [float(result_dict.get('F-statistic', np.nan))],
|
|
2170
2200
|
"p-value": [float(result_dict.get('Prob (F-statistic)', np.nan))],
|
|
2171
2201
|
"Durbin-Watson": [float(result_dict.get('Durbin-Watson', np.nan))],
|
|
@@ -2300,7 +2330,7 @@ def ols(df: DataFrame, yname: str, report: Literal[False, "summary", "full"] = "
|
|
|
2300
2330
|
# ===================================================================
|
|
2301
2331
|
# 선형성 검정 (Linearity Test)
|
|
2302
2332
|
# ===================================================================
|
|
2303
|
-
def ols_linearity_test(fit: RegressionResultsWrapper, power: int = 2, alpha: float = 0.05) -> DataFrame:
|
|
2333
|
+
def ols_linearity_test(fit: RegressionResultsWrapper, power: int = 2, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
|
|
2304
2334
|
"""회귀모형의 선형성을 Ramsey RESET 검정으로 평가한다.
|
|
2305
2335
|
|
|
2306
2336
|
적합된 회귀모형에 대해 Ramsey RESET(Regression Specification Error Test) 검정을 수행하여
|
|
@@ -2395,6 +2425,9 @@ def ols_linearity_test(fit: RegressionResultsWrapper, power: int = 2, alpha: flo
|
|
|
2395
2425
|
"해석": [interpretation]
|
|
2396
2426
|
})
|
|
2397
2427
|
|
|
2428
|
+
if plot:
|
|
2429
|
+
ols_residplot(fit, lowess=True, mse=True, title=title, save_path=save_path)
|
|
2430
|
+
|
|
2398
2431
|
return result_df
|
|
2399
2432
|
|
|
2400
2433
|
|
|
@@ -2436,8 +2469,6 @@ def ols_normality_test(fit: RegressionResultsWrapper, alpha: float = 0.05, plot:
|
|
|
2436
2469
|
- p-value > alpha: 정규성 가정 만족 (귀무가설 채택)
|
|
2437
2470
|
- p-value <= alpha: 정규성 가정 위반 (귀무가설 기각)
|
|
2438
2471
|
"""
|
|
2439
|
-
from scipy.stats import jarque_bera
|
|
2440
|
-
|
|
2441
2472
|
# fit 객체에서 잔차 추출
|
|
2442
2473
|
residuals = fit.resid
|
|
2443
2474
|
n = len(residuals)
|
|
@@ -2632,8 +2663,6 @@ def ols_independence_test(fit: RegressionResultsWrapper, alpha: float = 0.05) ->
|
|
|
2632
2663
|
- 일반적으로 1.5~2.5 범위를 자기상관 없음으로 판단
|
|
2633
2664
|
- 시계열 데이터나 관측치에 순서가 있는 경우 중요한 검정
|
|
2634
2665
|
"""
|
|
2635
|
-
from pandas import DataFrame
|
|
2636
|
-
|
|
2637
2666
|
# Durbin-Watson 통계량 계산
|
|
2638
2667
|
dw_stat = durbin_watson(fit.resid)
|
|
2639
2668
|
|