hossam 0.4.4__py3-none-any.whl → 0.4.6__py3-none-any.whl

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