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/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
  # 사분위수
@@ -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(data.select_dtypes(include=['int', 'int32', 'int64', 'float', 'float32', 'float64']).columns)
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 data[f].dtype not in [
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 data[f].dtypes in [
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
- # 데이터가 없거나 분산이 0인 경우
820
- if len(col_data) == 0 or col_data.std(ddof=1) == 0:
821
- 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:
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"검정 불가 (데이터 부족 또는 분산=0)"
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
- "equal_var_checked": var_checked,
976
- "statistic": round(s, 3), # type: ignore
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
- "statistic": np.nan,
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, parametric: bool | None = None) -> DataFrame:
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
- parametric (bool | None, optional): 정규성 가정 여부.
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
- var_checked = False
1064
- if parametric is None:
1065
- var_checked = True
1066
- # 대응표본의 차이 계산 정규성 검정
1067
- diff = x_data - y_data
1068
- try:
1069
- _, p_normal = shapiro(diff) # 표본 크기 5000 이하일 때 권장
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 parametric:
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": "t-test_paired" if parametric else "Wilcoxon signed-rank",
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, 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')
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, 'equal_var', equal_var_satisfied)
1241
- 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
1242
1257
 
1243
- # 유의성 여부 컬럼 추가
1244
- if 'p-unc' in anova_df.columns:
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
- 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
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 '충족되지 않았고'}, 등분산성은 {'충족' 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 '충족되지 않았다'}고 판단됩니다."
1265
1279
 
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
- )
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
- anova_report += f" 효과 크기(η²p) ≈ {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
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 = None
1278
- posthoc_method = 'None'
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
- if p_col:
1301
- # 유의성 여부 컬럼 추가
1302
- posthoc_df['significant'] = posthoc_df[p_col] <= alpha
1303
-
1304
- sig_pairs_df = posthoc_df[posthoc_df[p_col] <= alpha]
1305
- sig_count = len(sig_pairs_df)
1306
- total_count = len(posthoc_df)
1307
- pair_samples = []
1308
- if not sig_pairs_df.empty and {'A', 'B'}.issubset(sig_pairs_df.columns):
1309
- pair_samples = [f"{row['A']} vs {row['B']}" for _, row in sig_pairs_df.head(3).iterrows()]
1310
-
1311
- if sig_count > 0:
1312
- posthoc_report = (
1313
- f"{posthoc_method} 사후검정에서 {sig_count}/{total_count}쌍이 의미 있는 차이를 보였습니다 (alpha={alpha})."
1314
- )
1315
- if pair_samples:
1316
- posthoc_report += " 예: " + ", ".join(pair_samples) + " 등."
1317
- else:
1318
- posthoc_report = f"{posthoc_method} 사후검정에서 추가로 유의한 쌍은 발견되지 않았습니다."
1319
- else:
1320
- posthoc_report = f"{posthoc_method} 결과는 생성했지만 p-value 정보를 찾지 못해 유의성을 확인할 수 없습니다."
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), (보정 사용 시) pval_adj, significant_adj
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) < max(3, min_n):
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
- 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])
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
- # 6) 상관행렬 생성 (result_df 기반)
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) -> dict:
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
- 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
1874
1893
 
1875
1894
  # 반복 제거 루프
1895
+ i = 0
1876
1896
  while True:
1877
1897
  if X.shape[1] == 0:
1878
1898
  break
1879
- vifs = _compute_vifs(X)
1880
- if verbose:
1881
- print(vifs)
1899
+
1900
+ print(f"📇 VIF 제거 반복 {i+1}회차\n")
1901
+ vifs = _compute_vifs(X, verbose=verbose)
1902
+
1882
1903
  # 모든 변수가 임계값 이하이면 종료
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]
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
- # 출력 옵션이 False일 경우 최종 값만 출력
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": f"{b:.6f}", # 비표준화 회귀계수(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)"] = f"{np.exp(b):.6f}"
2098
+ var_row["exp(B)"] = np.round(np.exp(b), 4)
2099
+
2075
2100
  var_row.update({
2076
- "표준오차": f"{se:.6f}", # 계수 표준오차
2077
- "Beta": beta, # 표준화 회귀계수(β)
2078
- "t": f"{t:.3f}{stars}", # t-통계량(+별표)
2079
- "p-value": p, # 계수 유의확률
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"𝑅({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})"
2103
2132
 
2104
2133
  # 모형 보고 문장 구성
2105
- 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)."
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가 %.2f만큼 변하는 것으로 나타남. (p %s 0.05, %s)"
2123
- 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)"
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["p-value"]) < 0.05 else ">",
2136
- "유의함" if float(row["p-value"]) < 0.05 else "유의하지 않음",
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["p-value"]) < 0.05 else ">",
2147
- "유의함" 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 "유의하지 않음",
2148
2177
  )
2149
2178
  )
2150
2179
 
@@ -2164,8 +2193,9 @@ def ols_report(
2164
2193
  # 성능 지표 표 생성 (pdf)
2165
2194
  pdf = DataFrame(
2166
2195
  {
2167
- "R": [float(result_dict.get('R-squared', np.nan))],
2168
- "R²": [float(result_dict.get('Adj. R-squared', np.nan))],
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