hossam 0.4.18__py3-none-any.whl → 0.4.19__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hossam/hs_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 data[f].dtypes not in [
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
  # 사분위수
@@ -366,8 +363,10 @@ def describe(data: DataFrame, *fields: str, columns: list | None = None):
366
363
  - 분포 특성은 왜도 값으로 판정합니다.
367
364
  - 로그변환 필요성은 왜도의 절댓값 크기로 판정합니다.
368
365
  """
366
+ num_columns = data.select_dtypes(include=np.number).columns
367
+
369
368
  if not fields:
370
- fields = tuple(data.select_dtypes(include=['int', 'int32', 'int64', 'float', 'float32', 'float64']).columns)
369
+ fields = tuple(num_columns)
371
370
 
372
371
  # 기술통계량 구하기
373
372
  desc = data[list(fields)].describe().T
@@ -383,17 +382,7 @@ def describe(data: DataFrame, *fields: str, columns: list | None = None):
383
382
  additional_stats = []
384
383
  for f in fields:
385
384
  # 숫자 타입이 아니라면 건너뜀
386
- if data[f].dtype not in [
387
- 'int',
388
- 'int32',
389
- 'int64',
390
- 'float',
391
- 'float32',
392
- 'float64',
393
- 'int64',
394
- 'float64',
395
- 'float32'
396
- ]:
385
+ if f not in num_columns:
397
386
  continue
398
387
 
399
388
  # 사분위수
@@ -509,6 +498,8 @@ def category_describe(data: DataFrame, *fields: str):
509
498
  - 숫자형 컬럼은 자동으로 제외됩니다.
510
499
  - NaN 값도 하나의 범주로 포함됩니다.
511
500
  """
501
+ num_columns = data.select_dtypes(include=np.number).columns
502
+
512
503
  if not fields:
513
504
  # 명목형(범주형) 컬럼 선택: object, category, bool 타입
514
505
  fields = data.select_dtypes(include=['object', 'category', 'bool']).columns # type: ignore
@@ -517,14 +508,7 @@ def category_describe(data: DataFrame, *fields: str):
517
508
  summary = []
518
509
  for f in fields:
519
510
  # 숫자형 컬럼은 건너뜀
520
- if data[f].dtypes in [
521
- "int",
522
- "int32",
523
- "int64",
524
- "float",
525
- "float32",
526
- "float64",
527
- ]:
511
+ if f in num_columns:
528
512
  continue
529
513
 
530
514
  # 각 범주값의 빈도수 계산 (NaN 포함)
@@ -768,6 +752,7 @@ def equal_var_test(data: DataFrame, columns: list | str | None = None, normal_di
768
752
  normality_result = normal_test(data[numeric_cols], method="n")
769
753
  # 모든 컬럼이 정규분포를 따르는지 확인
770
754
  all_normal = normality_result["is_normal"].all()
755
+ normality_method = normality_result["method"].iloc[0]
771
756
  normal_dist = all_normal # type: ignore
772
757
 
773
758
  try:
@@ -779,13 +764,14 @@ def equal_var_test(data: DataFrame, columns: list | str | None = None, normal_di
779
764
  s, p = levene(*fields)
780
765
 
781
766
  result_df = DataFrame([{
767
+ "normality_method": normality_method,
768
+ "normality_checked": normal_dist,
782
769
  "method": method_name,
783
770
  "statistic": s,
784
771
  "p-value": p,
785
772
  "is_equal_var": p > 0.05,
786
773
  "n_columns": len(fields),
787
- "columns": ", ".join(numeric_cols[:len(fields)]),
788
- "normality_checked": normality_checked
774
+ "columns": ", ".join(numeric_cols[:len(fields)])
789
775
  }])
790
776
 
791
777
  return result_df
@@ -853,52 +839,40 @@ def ttest_1samp(data, mean_value: float = 0.0) -> DataFrame:
853
839
  alternative: list = ["two-sided", "less", "greater"]
854
840
  result: list = []
855
841
 
856
- # 데이터가 없거나 분산이 0인 경우
857
- if len(col_data) == 0 or col_data.std(ddof=1) == 0:
858
- for a in alternative:
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:
859
865
  result.append({
860
866
  "alternative": a,
861
867
  "statistic": np.nan,
862
868
  "p-value": np.nan,
863
869
  "H0": False,
864
870
  "H1": False,
865
- "interpretation": f"검정 불가 (데이터 부족 또는 분산=0)"
871
+ "interpretation": f"검정 실패: {str(e)}"
866
872
  })
867
- else:
868
- for a in alternative:
869
- try:
870
- s, p = ttest_1samp(col_data, mean_value, alternative=a) # type: ignore
871
-
872
- itp = None
873
-
874
- if a == "two-sided":
875
- itp = "μ {0} {1}".format("==" if p > 0.05 else "!=", mean_value)
876
- elif a == "less":
877
- itp = "μ {0} {1}".format(">=" if p > 0.05 else "<", mean_value)
878
- else:
879
- itp = "μ {0} {1}".format("<=" if p > 0.05 else ">", mean_value)
880
-
881
- result.append({
882
- "alternative": a,
883
- "statistic": round(s, 3),
884
- "p-value": round(p, 4),
885
- "H0": p > 0.05,
886
- "H1": p <= 0.05,
887
- "interpretation": itp,
888
- })
889
- except Exception as e:
890
- result.append({
891
- "alternative": a,
892
- "statistic": np.nan,
893
- "p-value": np.nan,
894
- "H0": False,
895
- "H1": False,
896
- "interpretation": f"검정 실패: {str(e)}"
897
- })
898
873
 
899
874
  rdf = DataFrame(result)
900
875
  rdf.set_index(["field", "alternative"], inplace=True)
901
-
902
876
  return rdf
903
877
 
904
878
 
@@ -984,6 +958,9 @@ def ttest_ind(
984
958
  # 두 데이터를 DataFrame으로 구성하여 등분산성 검정
985
959
  temp_df = DataFrame({'x': x_data, 'y': y_data})
986
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]
987
964
  equal_var = var_result["is_equal_var"].iloc[0]
988
965
 
989
966
  alternative: list = ["two-sided", "less", "greater"]
@@ -1009,8 +986,9 @@ def ttest_ind(
1009
986
  "test": n,
1010
987
  "alternative": a,
1011
988
  "interpretation": itp,
1012
- "equal_var_checked": var_checked,
1013
- "statistic": round(s, 3), # type: ignore
989
+ normality_method: normality_checked,
990
+ equal_var_method: equal_var,
991
+ n: round(s, 3), # type: ignore
1014
992
  "p-value": round(p, 4), # type: ignore
1015
993
  "H0": p > 0.05, # type: ignore
1016
994
  "H1": p <= 0.05, # type: ignore
@@ -1019,12 +997,13 @@ def ttest_ind(
1019
997
  result.append({
1020
998
  "test": "t-test_ind" if equal_var else "Welch's t-test",
1021
999
  "alternative": a,
1022
- "statistic": np.nan,
1000
+ "interpretation": f"검정 실패: {str(e)}",
1001
+ normality_method: normality_checked,
1002
+ equal_var_method: equal_var,
1003
+ n: np.nan,
1023
1004
  "p-value": np.nan,
1024
1005
  "H0": False,
1025
- "H1": False,
1026
- "interpretation": f"검정 실패: {str(e)}",
1027
- "equal_var_checked": var_checked
1006
+ "H1": False
1028
1007
  })
1029
1008
 
1030
1009
  rdf = DataFrame(result)
@@ -1035,7 +1014,7 @@ def ttest_ind(
1035
1014
  # ===================================================================
1036
1015
  # 대응표본 t-검정 또는 Wilcoxon test
1037
1016
  # ===================================================================
1038
- def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
1017
+ def ttest_rel(x, y, normality: bool | None = None) -> DataFrame:
1039
1018
  """대응표본 t-검정 또는 Wilcoxon signed-rank test를 수행한다.
1040
1019
 
1041
1020
  대응표본 t-검정은 동일 개체에서 측정된 두 시점의 평균 차이를 검정한다.
@@ -1044,7 +1023,7 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
1044
1023
  Args:
1045
1024
  x (array-like): 첫 번째 측정값의 연속형 데이터 (리스트, Series, ndarray 등).
1046
1025
  y (array-like): 두 번째 측정값의 연속형 데이터 (리스트, Series, ndarray 등).
1047
- parametric (bool | None, optional): 정규성 가정 여부.
1026
+ normality (bool | None, optional): 정규성 가정 여부.
1048
1027
  - True: 대응표본 t-검정 (차이의 정규분포 가정)
1049
1028
  - False: Wilcoxon signed-rank test (비모수 검정, 더 강건함)
1050
1029
  - None: 차이의 정규성을 자동으로 검정하여 판별
@@ -1097,36 +1076,31 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
1097
1076
  raise ValueError(f"최소 2개 이상의 대응 데이터가 필요합니다. 현재: {len(x_data)}")
1098
1077
 
1099
1078
  # parametric이 None이면 차이의 정규성을 자동으로 검정
1100
- var_checked = False
1101
- if parametric is None:
1102
- var_checked = True
1103
- # 대응표본의 차이 계산 정규성 검정
1104
- diff = x_data - y_data
1105
- try:
1106
- _, p_normal = shapiro(diff) # 표본 크기 5000 이하일 때 권장
1107
- parametric = p_normal > 0.05 # p > 0.05면 정규분포 따름
1108
- except Exception:
1109
- # shapiro 실패 시 normaltest 사용
1110
- try:
1111
- _, p_normal = normaltest(diff)
1112
- parametric = p_normal > 0.05
1113
- except Exception:
1114
- # 둘 다 실패하면 기본값으로 비모수 검정 사용
1115
- 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
1116
1086
 
1117
1087
  alternative: list = ["two-sided", "less", "greater"]
1118
1088
  result: list = []
1119
1089
  fmt: str = "μ(x) {0} μ(y)"
1120
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
+
1121
1097
  for a in alternative:
1122
1098
  try:
1123
- if parametric:
1099
+ if normality:
1124
1100
  s, p = ttest_rel(x_data, y_data, alternative=a) # type: ignore
1125
- n = "t-test_paired"
1126
1101
  else:
1127
1102
  # Wilcoxon signed-rank test (대응표본용 비모수 검정)
1128
1103
  s, p = wilcoxon(x_data, y_data, alternative=a)
1129
- n = "Wilcoxon signed-rank"
1130
1104
 
1131
1105
  itp = None
1132
1106
 
@@ -1140,28 +1114,27 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
1140
1114
  result.append({
1141
1115
  "test": n,
1142
1116
  "alternative": a,
1117
+ normality_method: normality,
1118
+ "interpretation": itp,
1143
1119
  "statistic": round(s, 3) if not np.isnan(s) else s, # type: ignore
1144
1120
  "p-value": round(p, 4) if not np.isnan(p) else p, # type: ignore
1145
1121
  "H0": p > 0.05, # type: ignore
1146
1122
  "H1": p <= 0.05, # type: ignore
1147
- "interpretation": itp,
1148
- "normality_checked": var_checked
1149
1123
  })
1150
1124
  except Exception as e:
1151
1125
  result.append({
1152
- "test": "t-test_paired" if parametric else "Wilcoxon signed-rank",
1126
+ "test": n,
1153
1127
  "alternative": a,
1128
+ normality_method: normality,
1129
+ "interpretation": f"검정 실패: {str(e)}",
1154
1130
  "statistic": np.nan,
1155
1131
  "p-value": np.nan,
1156
1132
  "H0": False,
1157
- "H1": False,
1158
- "interpretation": f"검정 실패: {str(e)}",
1159
- "normality_checked": var_checked
1133
+ "H1": False
1160
1134
  })
1161
1135
 
1162
1136
  rdf = DataFrame(result)
1163
1137
  rdf.set_index(["test", "alternative"], inplace=True)
1164
-
1165
1138
  return rdf
1166
1139
 
1167
1140
 
@@ -1170,7 +1143,8 @@ def ttest_rel(x, y, parametric: bool | None = None) -> DataFrame:
1170
1143
  # ===================================================================
1171
1144
  # 일원 분산분석 (One-way ANOVA)
1172
1145
  # ===================================================================
1173
- 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] :
1174
1148
  """일원분산분석(One-way ANOVA)을 일괄 처리한다.
1175
1149
 
1176
1150
  정규성 및 등분산성 검정을 자동으로 수행한 후,
@@ -1188,13 +1162,12 @@ def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) ->
1188
1162
  dv (str): 종속변수(Dependent Variable) 컬럼명.
1189
1163
  between (str): 그룹 구분 변수 컬럼명.
1190
1164
  alpha (float, optional): 유의수준. 기본값 0.05.
1165
+ posthoc (bool, optional): 사후검정 수행 여부. 기본값 False.
1191
1166
 
1192
1167
  Returns:
1193
1168
  tuple:
1194
1169
  - anova_df (DataFrame): ANOVA 또는 Welch 결과 테이블(Source, ddof1, ddof2, F, p-unc, np2 등 포함).
1195
- - anova_report (str): 정규성/등분산 여부와 F, p값, 효과크기를 요약한 보고 문장.
1196
1170
  - posthoc_df (DataFrame|None): 사후검정 결과(Tukey HSD 또는 Games-Howell). ANOVA가 유의할 때만 생성.
1197
- - posthoc_report (str): 사후검정 유무와 유의한 쌍 정보를 요약한 보고 문장.
1198
1171
 
1199
1172
  Examples:
1200
1173
  ```python
@@ -1206,7 +1179,7 @@ def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) ->
1206
1179
  'group': ['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B']
1207
1180
  })
1208
1181
 
1209
- anova_df, anova_report, posthoc_df, posthoc_report = hs_stats.oneway_anova(df, dv='score', between='group')
1182
+ anova_df, posthoc_df = hs_stats.oneway_anova(df, dv='score', between='group')
1210
1183
 
1211
1184
  # 사후검정결과는 ANOVA가 유의할 때만 생성됨
1212
1185
  if posthoc_df is not None:
@@ -1263,57 +1236,61 @@ def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) ->
1263
1236
  # ============================================
1264
1237
  # 3. ANOVA 수행
1265
1238
  # ============================================
1239
+ anova_df: DataFrame
1240
+ anova_method: str
1241
+
1266
1242
  if equal_var_satisfied:
1267
1243
  # 등분산을 만족할 때 일반적인 ANOVA 사용
1268
1244
  anova_method = "ANOVA"
1269
1245
  anova_df = anova(data=df_filtered, dv=dv, between=between)
1246
+ en = "Bartlett"
1270
1247
  else:
1271
1248
  # 등분산을 만족하지 않을 때 Welch's ANOVA 사용
1272
1249
  anova_method = "Welch"
1273
1250
  anova_df = welch_anova(data=df_filtered, dv=dv, between=between)
1251
+ en = "Levene"
1274
1252
 
1275
1253
  # ANOVA 결과에 메타정보 추가
1276
1254
  anova_df.insert(1, 'normality', normality_satisfied)
1277
- anova_df.insert(2, 'equal_var', equal_var_satisfied)
1278
- anova_df.insert(3, 'method', anova_method)
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
1279
1257
 
1280
- # 유의성 여부 컬럼 추가
1281
- if 'p-unc' in anova_df.columns:
1282
- anova_df['significant'] = anova_df['p-unc'] <= alpha
1258
+ if posthoc == False:
1259
+ return anova_df
1283
1260
 
1284
1261
  # ANOVA 결과가 유의한지 확인
1285
1262
  p_unc = float(anova_df.loc[0, 'p-unc']) # type: ignore
1286
1263
  anova_significant = p_unc <= alpha
1287
1264
 
1288
1265
  # ANOVA 보고 문장 생성
1289
- def _safe_get(col: str, default: float = np.nan) -> float:
1290
- try:
1291
- return float(anova_df.loc[0, col]) if col in anova_df.columns else default # type: ignore
1292
- except Exception:
1293
- return default
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
1294
1271
 
1295
- df1 = _safe_get('ddof1')
1296
- df2 = _safe_get('ddof2')
1297
- fval = _safe_get('F')
1298
- eta2 = _safe_get('np2')
1272
+ # df1 = _safe_get('ddof1')
1273
+ # df2 = _safe_get('ddof2')
1274
+ # fval = _safe_get('F')
1275
+ # eta2 = _safe_get('np2')
1299
1276
 
1300
- anova_sig_text = "그룹별 평균이 다를 가능성이 높습니다." if anova_significant else "그룹별 평균 차이에 대한 근거가 부족합니다."
1301
- assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않았고'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않았다'}고 판단됩니다."
1277
+ # anova_sig_text = "그룹별 평균이 다를 가능성이 높습니다." if anova_significant else "그룹별 평균 차이에 대한 근거가 부족합니다."
1278
+ # assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않았고'}, 등분산성은 {'충족되었다' if equal_var_satisfied else '충족되지 않았다'}고 판단됩니다."
1302
1279
 
1303
- anova_report = (
1304
- f"{between}별로 {dv} 평균을 비교한 {anova_method} 결과: F({df1:.3f}, {df2:.3f}) = {fval:.3f}, p = {p_unc:.4f}. "
1305
- f"해석: {anova_sig_text} {assumption_text}"
1306
- )
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
+ # )
1307
1284
 
1308
- if not np.isnan(eta2):
1309
- anova_report += f" 효과 크기(η²p) ≈ {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
1285
+ # if not np.isnan(eta2):
1286
+ # anova_report += f" 효과 크기(η²p) ≈ {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
1310
1287
 
1311
1288
  # ============================================
1312
1289
  # 4. 사후검정 (ANOVA 유의할 때만)
1313
1290
  # ============================================
1314
- posthoc_df = None
1315
- posthoc_method = 'None'
1316
- posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
1291
+ posthoc_df: DataFrame
1292
+ posthoc_method: str
1293
+ #posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
1317
1294
 
1318
1295
  if anova_significant:
1319
1296
  if equal_var_satisfied:
@@ -1328,38 +1305,39 @@ def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) ->
1328
1305
  # 사후검정 결과에 메타정보 추가
1329
1306
  # posthoc_df.insert(0, 'normality', normality_satisfied)
1330
1307
  # posthoc_df.insert(1, 'equal_var', equal_var_satisfied)
1331
- posthoc_df.insert(0, 'method', posthoc_method)
1308
+ posthoc_df.insert(0, 'method', posthoc_method) # type: ignore
1332
1309
 
1333
1310
  # p-value 컬럼 탐색
1334
- 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
1335
1312
  p_col = p_cols[0] if p_cols else None
1336
-
1337
- if p_col:
1338
- # 유의성 여부 컬럼 추가
1339
- posthoc_df['significant'] = posthoc_df[p_col] <= alpha
1340
-
1341
- sig_pairs_df = posthoc_df[posthoc_df[p_col] <= alpha]
1342
- sig_count = len(sig_pairs_df)
1343
- total_count = len(posthoc_df)
1344
- pair_samples = []
1345
- if not sig_pairs_df.empty and {'A', 'B'}.issubset(sig_pairs_df.columns):
1346
- pair_samples = [f"{row['A']} vs {row['B']}" for _, row in sig_pairs_df.head(3).iterrows()]
1347
-
1348
- if sig_count > 0:
1349
- posthoc_report = (
1350
- f"{posthoc_method} 사후검정에서 {sig_count}/{total_count}쌍이 의미 있는 차이를 보였습니다 (alpha={alpha})."
1351
- )
1352
- if pair_samples:
1353
- posthoc_report += " 예: " + ", ".join(pair_samples) + " 등."
1354
- else:
1355
- posthoc_report = f"{posthoc_method} 사후검정에서 추가로 유의한 쌍은 발견되지 않았습니다."
1356
- else:
1357
- posthoc_report = f"{posthoc_method} 결과는 생성했지만 p-value 정보를 찾지 못해 유의성을 확인할 수 없습니다."
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 정보를 찾지 못해 유의성을 확인할 수 없습니다."
1358
1335
 
1359
1336
  # ============================================
1360
1337
  # 5. 결과 반환
1361
1338
  # ============================================
1362
- 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
1363
1341
 
1364
1342
 
1365
1343
  # ===================================================================
@@ -1640,8 +1618,7 @@ def corr_pairwise(
1640
1618
  alpha: float = 0.05,
1641
1619
  z_thresh: float = 3.0,
1642
1620
  min_n: int = 8,
1643
- linearity_power: tuple[int, ...] = (2,),
1644
- p_adjust: str = "none",
1621
+ #linearity_power: tuple[int, ...] = (2,)
1645
1622
  ) -> tuple[DataFrame, DataFrame]:
1646
1623
  """각 변수 쌍에 대해 선형성·이상치 여부를 점검한 뒤 Pearson/Spearman을 자동 선택해 상관을 요약한다.
1647
1624
 
@@ -1650,7 +1627,6 @@ def corr_pairwise(
1650
1627
  2) 단순회귀 y~x에 대해 Ramsey RESET(linearity_power)로 선형성 검정 (모든 p>alpha → 선형성 충족)
1651
1628
  3) 선형성 충족이고 양쪽 변수에서 |z|>z_thresh 이상치가 없으면 Pearson, 그 외엔 Spearman 선택
1652
1629
  4) 상관계수/유의확률, 유의성 여부, 강도(strong/medium/weak/no correlation) 기록
1653
- 5) 선택적으로 다중비교 보정(p_adjust="fdr_bh" 등) 적용하여 pval_adj와 significant_adj 추가
1654
1630
 
1655
1631
  Args:
1656
1632
  data (DataFrame): 분석 대상 데이터프레임.
@@ -1658,14 +1634,13 @@ def corr_pairwise(
1658
1634
  alpha (float, optional): 유의수준. 기본 0.05.
1659
1635
  z_thresh (float, optional): 이상치 판단 임계값(|z| 기준). 기본 3.0.
1660
1636
  min_n (int, optional): 쌍별 최소 표본 크기. 미만이면 계산 생략. 기본 8.
1661
- linearity_power (tuple[int,...], optional): RESET 검정에서 포함할 차수 집합. 기본 (2,).
1662
- p_adjust (str, optional): 다중비교 보정 방법. "none" 또는 statsmodels.multipletests 지원값 중 하나(e.g., "fdr_bh"). 기본 "none".
1637
+ #linearity_power (tuple[int,...], optional): RESET 검정에서 포함할 차수 집합. 기본 (2,).
1663
1638
 
1664
1639
  Returns:
1665
1640
  tuple[DataFrame, DataFrame]: 두 개의 데이터프레임을 반환.
1666
1641
  [0] result_df: 각 변수쌍별 결과 테이블. 컬럼:
1667
1642
  var_a, var_b, n, linearity(bool), outlier_flag(bool), chosen('pearson'|'spearman'),
1668
- corr, pval, significant(bool), strength(str), (보정 사용 시) pval_adj, significant_adj
1643
+ corr, pval, significant(bool), strength(str)
1669
1644
  [1] corr_matrix: 상관계수 행렬 (행과 열에 변수명, 값에 상관계수)
1670
1645
 
1671
1646
  Examples:
@@ -1709,7 +1684,7 @@ def corr_pairwise(
1709
1684
  for a, b in combinations(cols, 2):
1710
1685
  # 공통 관측치 사용
1711
1686
  pair_df = data[[a, b]].dropna()
1712
- if len(pair_df) < max(3, min_n):
1687
+ if len(pair_df) < min_n:
1713
1688
  # 표본이 너무 적으면 계산하지 않음
1714
1689
  rows.append(
1715
1690
  {
@@ -1753,13 +1728,16 @@ def corr_pairwise(
1753
1728
  try:
1754
1729
  X_const = sm.add_constant(x)
1755
1730
  model = sm.OLS(y, X_const).fit()
1756
- pvals = []
1757
- for pwr in linearity_power:
1758
- reset = linear_reset(model, power=pwr, use_f=True)
1759
- pvals.append(reset.pvalue)
1760
- # 모든 차수에서 유의하지 않을 때 선형성 충족으로 간주
1761
- if len(pvals) > 0:
1762
- linearity_ok = all([pv > alpha for pv in pvals])
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
1763
1741
  except Exception:
1764
1742
  linearity_ok = False
1765
1743
 
@@ -1807,16 +1785,8 @@ def corr_pairwise(
1807
1785
 
1808
1786
  result_df = DataFrame(rows)
1809
1787
 
1810
- # 5) 다중비교 보정 (선택)
1811
- if p_adjust.lower() != "none" and not result_df.empty:
1812
- # 유효한 p만 보정
1813
- mask = result_df["pval"].notna()
1814
- if mask.any():
1815
- _, p_adj, _, _ = multipletests(result_df.loc[mask, "pval"], alpha=alpha, method=p_adjust)
1816
- result_df.loc[mask, "pval_adj"] = p_adj
1817
- result_df["significant_adj"] = result_df["pval_adj"] <= alpha
1818
1788
 
1819
- # 6) 상관행렬 생성 (result_df 기반)
1789
+ # 5) 상관행렬 생성 (result_df 기반)
1820
1790
  # 모든 변수를 행과 열로 하는 대칭 행렬 생성
1821
1791
  corr_matrix = DataFrame(np.nan, index=cols, columns=cols)
1822
1792
  # 대각선: 1 (자기상관)
@@ -1889,17 +1859,21 @@ def vif_filter(
1889
1859
  result = data.copy()
1890
1860
  return result
1891
1861
 
1892
- def _compute_vifs(X_: DataFrame) -> dict:
1862
+ def _compute_vifs(X_: DataFrame, verbose: bool = False) -> DataFrame:
1893
1863
  # NA 제거 후 상수항 추가
1894
1864
  X_clean = X_.dropna()
1865
+
1895
1866
  if X_clean.shape[0] == 0:
1896
1867
  # 데이터가 모두 NA인 경우 VIF 계산 불가: NaN 반환
1897
- return {col: np.nan for col in X_.columns}
1868
+ return DataFrame({col: [np.nan] for col in X_.columns})
1869
+
1898
1870
  if X_clean.shape[1] == 1:
1899
1871
  # 단일 예측변수의 경우 다른 설명변수가 없으므로 VIF는 1로 간주
1900
- return {col: 1.0 for col in X_clean.columns}
1872
+ return DataFrame({col: [1.0] for col in X_clean.columns})
1873
+
1901
1874
  exog = sm.add_constant(X_clean, prepend=True)
1902
1875
  vifs = {}
1876
+
1903
1877
  for i, col in enumerate(X_clean.columns, start=0):
1904
1878
  # exog의 첫 열은 상수항이므로 변수 인덱스는 +1
1905
1879
  try:
@@ -1907,27 +1881,40 @@ def vif_filter(
1907
1881
  except Exception:
1908
1882
  # 계산 실패 시 무한대로 처리하여 우선 제거 대상으로
1909
1883
  vifs[col] = float("inf")
1910
- return vifs
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
1911
1893
 
1912
1894
  # 반복 제거 루프
1895
+ i = 0
1913
1896
  while True:
1914
1897
  if X.shape[1] == 0:
1915
1898
  break
1916
- vifs = _compute_vifs(X)
1917
- if verbose:
1918
- print(vifs)
1899
+
1900
+ print(f"📇 VIF 제거 반복 {i+1}회차\n")
1901
+ vifs = _compute_vifs(X, verbose=verbose)
1902
+
1919
1903
  # 모든 변수가 임계값 이하이면 종료
1920
- max_key = max(vifs, key=lambda k: (vifs[k] if not np.isnan(vifs[k]) else -np.inf))
1921
- max_vif = vifs[max_key]
1904
+ max_vif = vifs.iloc[0]["VIF"]
1905
+ max_key = vifs.iloc[0]["Variable"]
1906
+
1922
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))
1923
1912
  break
1913
+
1924
1914
  # 가장 큰 VIF 변수 제거
1925
1915
  X = X.drop(columns=[max_key])
1926
-
1927
- # 출력 옵션이 False일 경우 최종 값만 출력
1928
- if not verbose:
1929
- final_vifs = _compute_vifs(X) if X.shape[1] > 0 else {}
1930
- print(final_vifs)
1916
+ print(f"제거된 변수: {max_key} (VIF={max_vif:.2f})")
1917
+ i += 1
1931
1918
 
1932
1919
  # 원본 컬럼 순서 유지하며 제거된 수치형 컬럼만 제외
1933
1920
  kept_numeric_cols = list(X.columns)
@@ -2104,19 +2091,20 @@ def ols_report(
2104
2091
  var_row = {
2105
2092
  "종속변수": yname, # 종속변수 이름
2106
2093
  "독립변수": name, # 독립변수 이름
2107
- "B": f"{b:.6f}", # 비표준화 회귀계수(B)
2094
+ "B(비표준화 계수)": np.round(b, 4), # 비표준화 회귀계수(B)
2108
2095
  }
2109
2096
  # logvar가 True면 exp(B) 컬럼 추가
2110
2097
  if 'logvar' in locals() and logvar:
2111
- var_row["exp(B)"] = f"{np.exp(b):.6f}"
2098
+ var_row["exp(B)"] = np.round(np.exp(b), 4)
2099
+
2112
2100
  var_row.update({
2113
- "표준오차": f"{se:.6f}", # 계수 표준오차
2114
- "Beta": beta, # 표준화 회귀계수(β)
2115
- "t": f"{t:.3f}{stars}", # t-통계량(+별표)
2116
- "p-value": p, # 계수 유의확률
2117
- "significant": p <= alpha, # 유의성 여부 (boolean)
2118
- "공차": 1 / vif, # 공차(Tolerance = 1/VIF)
2119
- "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), # 분산팽창계수
2120
2108
  })
2121
2109
  variables.append(var_row)
2122
2110
 
@@ -2135,11 +2123,15 @@ def ols_report(
2135
2123
  continue
2136
2124
  result_dict[key] = value
2137
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
+
2138
2130
  # 적합도 보고 문자열 구성
2139
- 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']})"
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})"
2140
2132
 
2141
2133
  # 모형 보고 문장 구성
2142
- tpl = "%s에 대하여 %s로 예측하는 회귀분석을 실시한 결과, 이 회귀모형은 통계적으로 %s(F(%s,%s) = %s, p %s 0.05)."
2134
+ tpl = "%s에 대하여 %s로 예측하는 회귀분석을 실시한 결과, 이 회귀모형은 통계적으로 %s(F(%s,%s) = %0.3f, p %s 0.05)."
2143
2135
  model_report = tpl % (
2144
2136
  rdf["종속변수"][0],
2145
2137
  ",".join(list(rdf["독립변수"])),
@@ -2150,27 +2142,27 @@ def ols_report(
2150
2142
  ),
2151
2143
  result_dict["Df Model"],
2152
2144
  result_dict["Df Residuals"],
2153
- result_dict["F-statistic"],
2145
+ float(result_dict["F-statistic"]),
2154
2146
  "<=" if float(result_dict["Prob (F-statistic)"]) <= 0.05 else ">",
2155
2147
  )
2156
2148
 
2157
2149
  # 변수별 보고 문장 리스트 구성
2158
2150
  variable_reports = []
2159
- s_normal = "%s가 1 증가하면 %s가 %.2f만큼 변하는 것으로 나타남. (p %s 0.05, %s)"
2160
- s_log = "%s가 1 증가하면 %s가 약 %.2f배 변하는 것으로 나타남. (p %s 0.05, %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)"
2161
2153
 
2162
2154
  for i in rdf.index:
2163
2155
  row = rdf.iloc[i]
2164
2156
  if logvar:
2165
- effect = np.exp(float(row["B"]))
2157
+ effect = np.exp(float(row["B(비표준화 계수)"]))
2166
2158
  variable_reports.append(
2167
2159
  s_log
2168
2160
  % (
2169
2161
  row["독립변수"],
2170
2162
  row["종속변수"],
2171
2163
  effect,
2172
- "<=" if float(row["p-value"]) < 0.05 else ">",
2173
- "유의함" if float(row["p-value"]) < 0.05 else "유의하지 않음",
2164
+ "<=" if float(row["유의확률"]) < 0.05 else ">",
2165
+ "유의함" if float(row["유의확률"]) < 0.05 else "유의하지 않음",
2174
2166
  )
2175
2167
  )
2176
2168
  else:
@@ -2179,9 +2171,9 @@ def ols_report(
2179
2171
  % (
2180
2172
  row["독립변수"],
2181
2173
  row["종속변수"],
2182
- float(row["B"]),
2183
- "<=" if float(row["p-value"]) < 0.05 else ">",
2184
- "유의함" if float(row["p-value"]) < 0.05 else "유의하지 않음",
2174
+ float(row["B(비표준화 계수)"]),
2175
+ "<=" if float(row["유의확률"]) < 0.05 else ">",
2176
+ "유의함" if float(row["유의확률"]) < 0.05 else "유의하지 않음",
2185
2177
  )
2186
2178
  )
2187
2179
 
@@ -2201,8 +2193,9 @@ def ols_report(
2201
2193
  # 성능 지표 표 생성 (pdf)
2202
2194
  pdf = DataFrame(
2203
2195
  {
2204
- "R": [float(result_dict.get('R-squared', np.nan))],
2205
- "R²": [float(result_dict.get('Adj. R-squared', np.nan))],
2196
+ "R": [r],
2197
+ "R²": [r2],
2198
+ "Adj. R²": [adj_r2],
2206
2199
  "F": [float(result_dict.get('F-statistic', np.nan))],
2207
2200
  "p-value": [float(result_dict.get('Prob (F-statistic)', np.nan))],
2208
2201
  "Durbin-Watson": [float(result_dict.get('Durbin-Watson', np.nan))],
@@ -2337,7 +2330,7 @@ def ols(df: DataFrame, yname: str, report: Literal[False, "summary", "full"] = "
2337
2330
  # ===================================================================
2338
2331
  # 선형성 검정 (Linearity Test)
2339
2332
  # ===================================================================
2340
- 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:
2341
2334
  """회귀모형의 선형성을 Ramsey RESET 검정으로 평가한다.
2342
2335
 
2343
2336
  적합된 회귀모형에 대해 Ramsey RESET(Regression Specification Error Test) 검정을 수행하여
@@ -2432,6 +2425,9 @@ def ols_linearity_test(fit: RegressionResultsWrapper, power: int = 2, alpha: flo
2432
2425
  "해석": [interpretation]
2433
2426
  })
2434
2427
 
2428
+ if plot:
2429
+ ols_residplot(fit, lowess=True, mse=True, title=title, save_path=save_path)
2430
+
2435
2431
  return result_df
2436
2432
 
2437
2433
 
@@ -2473,8 +2469,6 @@ def ols_normality_test(fit: RegressionResultsWrapper, alpha: float = 0.05, plot:
2473
2469
  - p-value > alpha: 정규성 가정 만족 (귀무가설 채택)
2474
2470
  - p-value <= alpha: 정규성 가정 위반 (귀무가설 기각)
2475
2471
  """
2476
- from scipy.stats import jarque_bera
2477
-
2478
2472
  # fit 객체에서 잔차 추출
2479
2473
  residuals = fit.resid
2480
2474
  n = len(residuals)
@@ -2669,8 +2663,6 @@ def ols_independence_test(fit: RegressionResultsWrapper, alpha: float = 0.05) ->
2669
2663
  - 일반적으로 1.5~2.5 범위를 자기상관 없음으로 판단
2670
2664
  - 시계열 데이터나 관측치에 순서가 있는 경우 중요한 검정
2671
2665
  """
2672
- from pandas import DataFrame
2673
-
2674
2666
  # Durbin-Watson 통계량 계산
2675
2667
  dw_stat = durbin_watson(fit.resid)
2676
2668