hossam 0.4.5__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, Any
4
+ from typing import overload, Tuple, Literal, LiteralString, Union, Any
5
5
 
6
6
  # -------------------------------------------------------------
7
7
  import numpy as np
@@ -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(결측치 무작위성) 검정
@@ -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()
@@ -955,12 +971,12 @@ def ttest_ind(x, y, equal_var: bool | None = None) -> DataFrame:
955
971
  result.append({
956
972
  "test": n,
957
973
  "alternative": a,
974
+ "interpretation": itp,
975
+ "equal_var_checked": var_checked,
958
976
  "statistic": round(s, 3), # type: ignore
959
977
  "p-value": round(p, 4), # type: ignore
960
978
  "H0": p > 0.05, # type: ignore
961
979
  "H1": p <= 0.05, # type: ignore
962
- "interpretation": itp,
963
- "equal_var_checked": var_checked
964
980
  })
965
981
  except Exception as e:
966
982
  result.append({
@@ -1112,1652 +1128,1865 @@ 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 = 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
- """
1144
-
1145
- df = data.copy()
1165
+ from pandas import DataFrame
1146
1166
 
1147
- # y 분리 (있다면)
1148
- y = None
1149
- if yname and yname in df.columns:
1150
- y = df[yname]
1151
- df = df.drop(columns=[yname])
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
+ })
1152
1171
 
1153
- # 제외할 목록 정리
1154
- ignore = ignore or []
1155
- ignore_cols_present = [c for c in ignore if c in df.columns]
1172
+ anova_df, anova_report, posthoc_df, posthoc_report = hs_stats.oneway_anova(df, dv='score', between='group')
1156
1173
 
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])]
1174
+ # 사후검정결과는 ANOVA가 유의할 때만 생성됨
1175
+ if posthoc_df is not None:
1176
+ print(posthoc_report)
1177
+ print(posthoc_df.head())
1178
+ ```
1160
1179
 
1161
- # VIF 대상 X 구성 (수치형에서 제외 목록 제거)
1162
- X = df[numeric_cols]
1163
- if ignore_cols_present:
1164
- X = X.drop(columns=ignore_cols_present, errors="ignore")
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}' 컬럼이 데이터프레임에 없습니다.")
1165
1188
 
1166
- # 수치형 변수가 없으면 바로 반환
1167
- if X.shape[1] == 0:
1168
- result = data.copy()
1169
- return result
1189
+ df_filtered = data[[dv, between]].dropna()
1170
1190
 
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))# type: ignore
1186
- except Exception:
1187
- # 계산 실패 시 무한대로 처리하여 우선 제거 대상으로
1188
- vifs[col] = float("inf")
1189
- return vifs
1191
+ # ============================================
1192
+ # 1. 정규성 검정 (각 그룹별로 수행)
1193
+ # ============================================
1194
+ group_names = sorted(df_filtered[between].unique())
1195
+ normality_satisfied = True
1190
1196
 
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])
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
1205
1204
 
1206
- # 출력 옵션이 False일 경우 최종 값만 출력
1207
- if not verbose:
1208
- final_vifs = _compute_vifs(X) if X.shape[1] > 0 else {}
1209
- print(final_vifs)
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
1210
1212
 
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")
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
1215
1225
 
1216
- return result
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)
1217
1237
 
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)
1218
1242
 
1243
+ # 유의성 여부 컬럼 추가
1244
+ if 'p-unc' in anova_df.columns:
1245
+ anova_df['significant'] = anova_df['p-unc'] <= alpha
1219
1246
 
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 데이터에 대한 추세선을 구한다.
1247
+ # ANOVA 결과가 유의한지 확인
1248
+ p_unc = float(anova_df.loc[0, 'p-unc']) # type: ignore
1249
+ anova_significant = p_unc <= alpha
1225
1250
 
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.
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
1231
1257
 
1232
- Returns:
1233
- tuple: (v_trend, t_trend)
1258
+ df1 = _safe_get('ddof1')
1259
+ df2 = _safe_get('ddof2')
1260
+ fval = _safe_get('F')
1261
+ eta2 = _safe_get('np2')
1234
1262
 
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)
1263
+ anova_sig_text = "그룹별 평균이 다를 가능성이 높습니다." if anova_significant else "그룹별 평균 차이에 대한 근거가 부족합니다."
1264
+ assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않았고'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않았다'}고 판단됩니다."
1246
1265
 
1247
- if x_arr.ndim == 0 or y_arr.ndim == 0:
1248
- raise ValueError("x, y는 1차원 이상의 배열이어야 합니다.")
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
+ )
1249
1270
 
1250
- coeff = np.polyfit(x_arr, y_arr, degree)
1271
+ if not np.isnan(eta2):
1272
+ anova_report += f" 효과 크기(η²p) ≈ {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
1251
1273
 
1252
- minx = np.min(x_arr)
1253
- maxx = np.max(x_arr)
1254
- v_trend = np.linspace(minx, maxx, value_count)
1274
+ # ============================================
1275
+ # 4. 사후검정 (ANOVA 유의할 때만)
1276
+ # ============================================
1277
+ posthoc_df = None
1278
+ posthoc_method = 'None'
1279
+ posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
1255
1280
 
1256
- # np.polyval 사용으로 간결하게 추세선 계산
1257
- t_trend = np.polyval(coeff, v_trend)
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)
1258
1290
 
1259
- return (v_trend, t_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)
1295
+
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
1299
+
1300
+ if p_col:
1301
+ # 유의성 여부 컬럼 추가
1302
+ posthoc_df['significant'] = posthoc_df[p_col] <= alpha
1303
+
1304
+ sig_pairs_df = posthoc_df[posthoc_df[p_col] <= alpha]
1305
+ sig_count = len(sig_pairs_df)
1306
+ total_count = len(posthoc_df)
1307
+ pair_samples = []
1308
+ if not sig_pairs_df.empty and {'A', 'B'}.issubset(sig_pairs_df.columns):
1309
+ pair_samples = [f"{row['A']} vs {row['B']}" for _, row in sig_pairs_df.head(3).iterrows()]
1310
+
1311
+ if sig_count > 0:
1312
+ posthoc_report = (
1313
+ f"{posthoc_method} 사후검정에서 {sig_count}/{total_count}쌍이 의미 있는 차이를 보였습니다 (alpha={alpha})."
1314
+ )
1315
+ if pair_samples:
1316
+ posthoc_report += " 예: " + ", ".join(pair_samples) + " 등."
1317
+ else:
1318
+ posthoc_report = f"{posthoc_method} 사후검정에서 추가로 유의한 쌍은 발견되지 않았습니다."
1319
+ else:
1320
+ posthoc_report = f"{posthoc_method} 결과는 생성했지만 p-value 정보를 찾지 못해 유의성을 확인할 수 없습니다."
1321
+
1322
+ # ============================================
1323
+ # 5. 결과 반환
1324
+ # ============================================
1325
+ return anova_df, anova_report, posthoc_df, posthoc_report
1260
1326
 
1261
1327
 
1262
1328
  # ===================================================================
1263
- # 선형회귀 요약 리포트
1329
+ # 이원 분산분석 (Two-way ANOVA: 두 범주형 독립변수)
1264
1330
  # ===================================================================
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
- """선형회귀 적합 결과를 요약 리포트로 변환한다.
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
+ """두 범주형 요인에 대한 이원분산분석을 수행하고 해석용 보고문을 반환한다.
1339
+
1340
+ 분석 흐름:
1341
+ 1) 각 셀(요인 조합)별 정규성 검정
1342
+ 2) 전체 셀을 대상으로 등분산성 검정 (정규성 충족 시 Bartlett, 불충족 시 Levene)
1343
+ 3) 두 요인 및 교호작용을 포함한 2원 ANOVA 수행
1344
+ 4) 유의한 요인에 대해 Tukey HSD 사후검정(요인별) 실행
1270
1345
 
1271
1346
  Args:
1272
- fit: statsmodels OLS 선형회귀 결과 객체 (`fit.summary()`를 지원해야 함).
1273
- data: 종속변수와 독립변수를 모두 포함한 DataFrame.
1274
- full: True이면 6개 값 반환, False이면 회귀계수 테이블(rdf) 반환. 기본값 True.
1275
- alpha: 유의수준. 기본값 0.05.
1347
+ data (DataFrame): 종속변수와 개의 범주형 요인을 포함한 데이터프레임.
1348
+ dv (str): 종속변수 컬럼명.
1349
+ factor_a (str): 번째 요인 컬럼명.
1350
+ factor_b (str): 번째 요인 컬럼명.
1351
+ alpha (float, optional): 유의수준. 기본 0.05.
1276
1352
 
1277
1353
  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): 상수항과 계수를 포함한 회귀식 표현.
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): 사후검정 유무 유의 쌍 요약 문장.
1285
1359
 
1286
- full=False일 때:
1287
- - 성능 지표 표 (`pdf`, DataFrame): R, R², Adj. R², F, p-value, Durbin-Watson.
1288
- - 회귀계수 표 (`rdf`, DataFrame)
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}' 컬럼이 데이터프레임에 없습니다.")
1289
1367
 
1290
- Examples:
1291
- ```python
1292
- from hossam import *
1368
+ df_filtered = data[[dv, factor_a, factor_b]].dropna()
1293
1369
 
1294
- df = hs_util.load_data("some_data.csv")
1295
- fit = hs_stats.ols(df, yname="target")
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
1296
1379
 
1297
- # 전체 리포트
1298
- pdf, rdf, result_report, model_report, variable_reports, eq = hs_stats.ols_report(fit, data, full=True)
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
1299
1390
 
1300
- # 간단한 버전 (성능지표, 회귀계수 테이블만)
1301
- pdf, rdf = hs_stats.ols_report(fit, data)
1302
- ```
1303
- """
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
1304
1397
 
1305
- # summary2() 결과에서 실제 회귀계수 DataFrame 추출
1306
- summary_obj = fit.summary2()
1307
- tbl = summary_obj.tables[1] # 회귀계수 테이블은 tables[1]에 위치
1398
+ # 보고문 생성
1399
+ def _safe(row, col, default=np.nan):
1400
+ try:
1401
+ return float(row[col])
1402
+ except Exception:
1403
+ return default
1308
1404
 
1309
- # 종속변수 이름
1310
- yname = fit.model.endog_names
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":
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)
1311
1423
 
1312
- # 독립변수 이름(상수항 제외)
1313
- xnames = [n for n in fit.model.exog_names if n != "const"]
1424
+ assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않음'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않음'}으로 판단했습니다."
1425
+ anova_report = " ".join(reports) + " " + assumption_text
1314
1426
 
1315
- # 독립변수 부분 데이터 (VIF 계산용)
1316
- indi_df = data.filter(xnames)
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()
1317
1431
 
1318
- # 독립변수 결과를 누적
1319
- variables = []
1432
+ for factor, sig in sig_flags.items():
1433
+ if factor is None:
1434
+ continue
1435
+ factor_lower = str(factor).lower()
1320
1436
 
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부터 시작
1325
- try:
1326
- with np.errstate(divide='ignore', invalid='ignore'):
1327
- vif_value = variance_inflation_factor(indi_df_const.values, i) # type: ignore
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
1437
+ # 교호작용(residual 포함) 혹은 비유의 항은 건너뛴다
1438
+ if factor_lower in ["residual", interaction_name, interaction_name_spaced] or not sig:
1439
+ continue
1335
1440
 
1336
- for idx, row in tbl.iterrows():
1337
- name = idx
1338
- if name not in xnames:
1441
+ # 실제 컬럼이 아니면 건너뛴다 (ex: "A * B" 같은 교호작용 이름)
1442
+ if factor not in df_filtered.columns:
1339
1443
  continue
1340
1444
 
1341
- b = float(row['Coef.'])
1342
- se = float(row['Std.Err.'])
1343
- t = float(row['t'])
1344
- p = float(row['P>|t|'])
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)
1345
1451
 
1346
- # 표준화 회귀계수(β) 계산
1347
- beta = b * (data[name].std(ddof=1) / data[yname].std(ddof=1))
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 정보를 찾지 못했습니다."
1348
1474
 
1349
- # VIF
1350
- vif = vif_dict.get(name, np.nan)
1475
+ return anova_df, anova_report, posthoc_df, posthoc_report
1351
1476
 
1352
- # 유의확률과 별표 표시
1353
- stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
1354
1477
 
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
- )
1370
1478
 
1371
- rdf = DataFrame(variables)
1479
+ # ===================================================================
1480
+ # 종속변수에 대한 편상관계수 및 효과크기 분석 (Correlation & Effect Size)
1481
+ # ===================================================================
1482
+ def corr_effect_size(data: DataFrame, dv: str, *fields: str, alpha: float = 0.05) -> DataFrame:
1483
+ """종속변수와의 편상관계수 및 효과크기를 계산한다.
1372
1484
 
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
1485
+ 독립변수와 종속변수 간의 상관계수를 계산하되, 정규성과 선형성을 검사하여
1486
+ Pearson 또는 Spearman 상관계수를 적절히 선택한다.
1487
+ Cohen's d (효과크기)를 계산하여 상관 강도를 정량화한다.
1385
1488
 
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']})"
1489
+ Args:
1490
+ data (DataFrame): 분석 대상 데이터프레임.
1491
+ dv (str): 종속변수 컬럼 이름.
1492
+ *fields (str): 독립변수 컬럼 이름들. 지정하지 않으면 수치형 컬럼 중 dv 제외 모두 사용.
1493
+ alpha (float, optional): 유의수준. 기본 0.05.
1388
1494
 
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
- )
1495
+ Returns:
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')
1404
1503
 
1405
- # 변수별 보고 문장 리스트 구성
1406
- variable_reports = []
1407
- s = "%s의 회귀계수는 %s(p %s 0.05)로, %s에 대하여 %s 예측변인인 것으로 나타났다."
1504
+ Examples:
1505
+ ```python
1506
+ from hossam import *
1507
+ from pandas import DataFrame
1408
1508
 
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
- )
1509
+ df = DataFrame({'age': [20, 30, 40, 50],
1510
+ 'bmi': [22, 25, 28, 30],
1511
+ 'charges': [1000, 2000, 3000, 4000]})
1421
1512
 
1422
- # -----------------------------
1423
- # 회귀식 자동 출력
1424
- # -----------------------------
1425
- intercept = fit.params["const"]
1426
- terms = []
1513
+ result = hs_stats.corr_effect_size(df, 'charges', 'age', 'bmi')
1514
+ ```
1515
+ """
1427
1516
 
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}")
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
1432
1520
 
1433
- equation_text = f"{yname} = {intercept:.3f}" + "".join(terms)
1521
+ # dv가 수치형인지 확인
1522
+ if not is_numeric_dtype(data[dv]):
1523
+ raise ValueError(f"Dependent variable '{dv}' must be numeric type")
1434
1524
 
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
- )
1525
+ results = []
1445
1526
 
1446
- if full:
1447
- return pdf, rdf, result_report, model_report, variable_reports, equation_text
1448
- else:
1449
- return pdf, rdf
1527
+ for var in fields:
1528
+ if not is_numeric_dtype(data[var]):
1529
+ continue
1450
1530
 
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
1451
1535
 
1452
- # ===================================================================
1453
- # 선형회귀
1454
- # ===================================================================
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
- """선형회귀분석을 수행하고 적합 결과를 반환한다.
1536
+ if len(x) < 3:
1537
+ continue
1469
1538
 
1470
- OLS(Ordinary Least Squares) 선형회귀분석을 실시한다.
1471
- 필요시 상세한 통계 보고서를 함께 제공한다.
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'
1472
1542
 
1473
- 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와 동일).
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)
1481
1545
 
1482
- Returns:
1483
- statsmodels.regression.linear_model.RegressionResultsWrapper: report=False 때.
1484
- 선형회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
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
1485
1549
 
1486
- tuple (6개): report=1 또는 'summary'일 때.
1487
- (fit, rdf, result_report, model_report, variable_reports, equation_text) 형태로 (pdf 제외).
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'
1488
1557
 
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)
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
1498
1564
 
1499
- Examples:
1500
- ```python
1501
- from hossam import *
1502
- from pandas import DataFrame
1503
- import numpy as np
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'
1504
1578
 
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)
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
1509
1586
  })
1510
1587
 
1511
- # 적합 결과만 반환
1512
- fit = hs_stats.ols(df, 'target')
1513
-
1514
- # 요약 리포트 반환
1515
- fit, pdf, rdf = hs_stats.ols(df, 'target', report='summary')
1516
-
1517
- # 풀 리포트 반환
1518
- fit, pdf, rdf, result_report, model_report, var_reports, eq = hs_stats.ols(df, 'target', report='full')
1519
- ```
1520
- """
1521
- x = df.drop(yname, axis=1)
1522
- y = df[yname]
1588
+ result_df = DataFrame(results)
1523
1589
 
1524
- X_const = sm.add_constant(x)
1525
- linear_model = sm.OLS(y, X_const)
1526
- linear_fit = linear_model.fit()
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)
1527
1593
 
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) # type: ignore
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) # type: ignore
1539
- return linear_fit, pdf, rdf, result_report, model_report, variable_reports, equation_text
1540
- else:
1541
- # 기본값: 리포트 미사용
1542
- return linear_fit
1594
+ return result_df
1543
1595
 
1544
1596
 
1545
1597
  # ===================================================================
1546
- # 로지스틱 회귀 요약 리포트
1598
+ # 쌍별 상관분석 (선형성/이상치 점검 후 Pearson/Spearman 자동 선택)
1547
1599
  # ===================================================================
1548
- def logit_report(
1549
- fit: BinaryResultsWrapper,
1600
+ def corr_pairwise(
1550
1601
  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
- """로지스틱 회귀 적합 결과를 상세 리포트로 변환한다.
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을 자동 선택해 상관을 요약한다.
1566
1610
 
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.
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 추가
1573
1617
 
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]].
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".
1582
1626
 
1583
- full=False일 때:
1584
- - 성능 지표 (`cdf`, DataFrame)
1585
- - 회귀계수 (`rdf`, DataFrame)
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: 상관계수 행렬 (행과 열에 변수명, 값에 상관계수)
1586
1633
 
1587
1634
  Examples:
1588
1635
  ```python
1589
1636
  from hossam import *
1590
1637
  from pandas import DataFrame
1591
- import numpy as np
1592
-
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
- })
1598
-
1599
- # 로지스틱 회귀 적합
1600
- fit = hs_stats.logit(df, yname="target")
1601
-
1602
- # 전체 리포트
1603
- cdf, rdf, result_report, model_report, variable_reports, cm = hs_stats.logit_report(fit, df, full=True)
1604
1638
 
1605
- # 간단한 버전 (주요 테이블만)
1606
- cdf, rdf = hs_stats.logit_report(fit, df)
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'])
1607
1644
  ```
1608
1645
  """
1609
1646
 
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)
1617
-
1618
- # 혼동행렬
1619
- cm = confusion_matrix(y_true, y_pred_fix)
1620
- tn, fp, fn, tp = cm.ravel()
1621
-
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
1629
-
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
- )
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])]
1642
1654
 
1643
- # -----------------------------
1644
- # 회귀계수 구성 (OR 중심)
1645
- # -----------------------------
1646
- tbl = fit.summary2().tables[1]
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()
1647
1659
 
1648
- # 독립변수 이름(상수항 제외)
1649
- xnames = [n for n in fit.model.exog_names if n != "const"]
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()
1650
1669
 
1651
- # 독립변수
1652
- x = data[xnames]
1670
+ rows = []
1653
1671
 
1654
- variables = []
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
1655
1692
 
1656
- # VIF 계산 (상수항 포함 설계행렬 사용)
1657
- 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) # type: ignore
1693
+ x = pair_df[a]
1694
+ y = pair_df[b]
1661
1695
 
1662
- for idx, row in tbl.iterrows():
1663
- name = idx
1664
- if name not in xnames:
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
+ )
1665
1712
  continue
1666
1713
 
1667
- beta = float(row['Coef.'])
1668
- se = float(row['Std.Err.'])
1669
- z = float(row['z'])
1670
- p = float(row['P>|z|'])
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
1671
1728
 
1672
- or_val = np.exp(beta)
1673
- ci_low = np.exp(beta - 1.96 * se)
1674
- ci_high = np.exp(beta + 1.96 * se)
1729
+ # 2) 이상치 플래그 (두 변수 중 하나라도 z-outlier 있으면 True)
1730
+ outlier_flag = bool(z_outlier_flags.get(a, False) or z_outlier_flags.get(b, False))
1675
1731
 
1676
- stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
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
1677
1743
 
1678
- variables.append(
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(
1679
1757
  {
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),
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,
1691
1768
  }
1692
1769
  )
1693
1770
 
1694
- rdf = DataFrame(variables)
1771
+ result_df = DataFrame(rows)
1695
1772
 
1696
- # ---------------------------------
1697
- # 모델 적합도 + 예측 성능 지표
1698
- # ---------------------------------
1699
- auc = roc_auc_score(y_true, y_pred)
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
1700
1781
 
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
- )
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 # 대칭성
1708
1794
 
1709
- # -----------------------------
1710
- # 모형 보고 문장
1711
- # -----------------------------
1712
- tpl = (
1713
- "%s에 대하여 %s로 예측하는 로지스틱 회귀분석을 실시한 결과, "
1714
- "모형은 통계적으로 %s(χ²(%s) = %.3f, p %s 0.05)하였다."
1715
- )
1716
-
1717
- 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 ">",
1724
- )
1725
-
1726
- # -----------------------------
1727
- # 변수별 보고 문장
1728
- # -----------------------------
1729
- variable_reports = []
1730
-
1731
- s = (
1732
- "%s의 오즈비는 %.3f(p %s 0.05)로, "
1733
- "%s 발생 odds에 %s 영향을 미치는 것으로 나타났다."
1734
- )
1735
-
1736
- for _, row in rdf.iterrows():
1737
- variable_reports.append(
1738
- s
1739
- % (
1740
- row["독립변수"],
1741
- row["OR"],
1742
- "<=" if row["p-value"] < 0.05 else ">",
1743
- row["종속변수"],
1744
- "유의미한" if row["p-value"] < 0.05 else "유의하지 않은",
1745
- )
1746
- )
1795
+ return result_df, corr_matrix
1747
1796
 
1748
- if full:
1749
- return cdf, rdf, result_report, model_report, variable_reports, cm
1750
- else:
1751
- return cdf, rdf
1752
1797
 
1753
1798
 
1754
1799
  # ===================================================================
1755
- # 로지스틱 회귀
1800
+ # 독립변수간 다중공선성 제거
1756
1801
  # ===================================================================
1757
- def logit(
1758
- df: DataFrame,
1759
- yname: str,
1760
- report: Union[bool, str, int] = False
1761
- ) -> Union[
1762
- BinaryResultsWrapper,
1763
- Tuple[
1764
- BinaryResultsWrapper,
1765
- DataFrame
1766
- ],
1767
- Tuple[
1768
- BinaryResultsWrapper,
1769
- DataFrame,
1770
- DataFrame,
1771
- str,
1772
- str,
1773
- list[str]
1774
- ]
1775
- ]:
1776
- """로지스틱 회귀분석을 수행하고 적합 결과를 반환한다.
1777
-
1778
- 종속변수가 이항(binary) 형태일 때 로지스틱 회귀분석을 실시한다.
1779
- 필요시 상세한 통계 보고서를 함께 제공한다.
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 이상인 변수를 반복적으로 제거한다.
1780
1810
 
1781
1811
  Args:
1782
- df (DataFrame): 종속변수와 독립변수를 모두 포함한 데이터프레임.
1783
- yname (str): 종속변수 컬럼명. 이항 변수여야 한다.
1784
- report: 리포트 모드 설정. 다음 중 하나:
1785
- - False (기본값): 리포트 미사용. fit 객체만 반환.
1786
- - 1 또는 'summary': 요약 리포트 반환 (full=False).
1787
- - 2 또는 'full': 풀 리포트 반환 (full=True).
1788
- - True: 풀 리포트 반환 (2와 동일).
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.
1789
1817
 
1790
1818
  Returns:
1791
- statsmodels.genmod.generalized_linear_model.BinomialResults: report=False일 때.
1792
- 로지스틱 회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
1793
-
1794
- tuple (4개): report=1 또는 'summary'일 때.
1795
- (fit, rdf, result_report, variable_reports) 형태로 (cdf 제외).
1796
-
1797
- tuple (6개): report=2, 'full' 또는 True일 때.
1798
- (fit, cdf, rdf, result_report, model_report, variable_reports) 형태로:
1799
- - fit: 로지스틱 회귀 적합 결과 객체
1800
- - cdf: 성능 지표 표 (DataFrame)
1801
- - rdf: 회귀계수 표 (DataFrame)
1802
- - result_report: 적합도 및 예측 성능 요약 (str)
1803
- - model_report: 모형 보고 문장 (str)
1804
- - variable_reports: 변수별 보고 문장 리스트 (list[str])
1819
+ DataFrame: VIF가 threshold 이하인 변수만 남은 데이터프레임 (원본 컬럼 순서 유지)
1805
1820
 
1806
1821
  Examples:
1807
1822
  ```python
1823
+ # 기본 사용 예
1808
1824
  from hossam import *
1809
- from pandas import DataFrame
1810
- import numpy as np
1825
+ filtered = hs_stats.vif_filter(df, yname="target", ignore=["id"], threshold=10.0)
1826
+ ```
1827
+ """
1811
1828
 
1812
- df = DataFrame({
1813
- 'target': np.random.binomial(1, 0.5, 100),
1814
- 'x1': np.random.normal(0, 1, 100),
1815
- 'x2': np.random.normal(0, 1, 100)
1816
- })
1829
+ df = data.copy()
1817
1830
 
1818
- # 적합 결과만 반환
1819
- fit = hs_stats.logit(df, 'target')
1831
+ # y 분리 (있다면)
1832
+ y = None
1833
+ if yname and yname in df.columns:
1834
+ y = df[yname]
1835
+ df = df.drop(columns=[yname])
1820
1836
 
1821
- # 요약 리포트 반환
1822
- fit, rdf, result_report, var_reports = hs_stats.logit(df, 'target', report='summary')
1837
+ # 제외할 목록 정리
1838
+ ignore = ignore or []
1839
+ ignore_cols_present = [c for c in ignore if c in df.columns]
1823
1840
 
1824
- # 리포트 반환
1825
- fit, cdf, rdf, result_report, model_report, var_reports = hs_stats.logit(df, 'target', report='full')
1826
- ```
1827
- """
1828
- x = df.drop(yname, axis=1)
1829
- y = df[yname]
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])]
1830
1844
 
1831
- X_const = sm.add_constant(x)
1832
- logit_model = sm.Logit(y, X_const)
1833
- logit_fit = logit_model.fit(disp=False)
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
1834
1901
 
1835
- # report 파라미터에 따른 처리
1836
- if not report or report is False:
1837
- # 리포트 미사용
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) # type: ignore
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:
1846
- # 풀 리포트 (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) # type: ignore
1848
- return logit_fit, cdf, rdf, result_report, model_report, variable_reports
1849
- else:
1850
- # 기본값: 리포트 미사용
1851
- return logit_fit
1852
1902
 
1853
1903
 
1854
1904
  # ===================================================================
1855
- # 선형성 검정 (Linearity Test)
1905
+ # x, y 데이터에 대한 추세선을 구한다.
1856
1906
  # ===================================================================
1857
- def ols_linearity_test(fit, power: int = 2, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
1858
- """회귀모형의 선형성을 Ramsey RESET 검정으로 평가한다.
1859
-
1860
- 적합된 회귀모형에 대해 Ramsey RESET(Regression Specification Error Test) 검정을 수행하여
1861
- 모형의 선형성 가정이 타당한지를 검정한다. 귀무가설은 '모형이 선형이다'이다.
1907
+ def trend(x: Any, y: Any, degree: int = 1, value_count: int = 100) -> Tuple[np.ndarray, np.ndarray]:
1908
+ """x, y 데이터에 대한 추세선을 구한다.
1862
1909
 
1863
1910
  Args:
1864
- fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
1865
- OLS 또는 WLS 모형이어야 한다.
1866
- power (int, optional): RESET 검정에 사용할 거듭제곱 수. 기본값 2.
1867
- power=2일 예측값의 제곱항이 추가됨.
1868
- power가 클수록 더 높은 차수의 비선형성을 감지.
1869
- alpha (float, optional): 유의수준. 기본값 0.05.
1870
- plot (bool, optional): True이면 잔차 플롯을 출력. 기본값 False.
1871
- title (str, optional): 플롯 제목. 기본값 None.
1872
- save_path (str, optional): 플롯을 저장할 경로. 기본값 None
1911
+ x (_type_): 산점도 그래프에 대한 x 데이터
1912
+ y (_type_): 산점도 그래프에 대한 y 데이터
1913
+ degree (int, optional): 추세선 방정식의 차수. Defaults to 1.
1914
+ value_count (int, optional): x 데이터의 범위 안에서 간격 수. Defaults to 100.
1873
1915
 
1874
1916
  Returns:
1875
- DataFrame: 선형성 검정 결과를 포함한 데이터프레임.
1876
- - 검정통계량: F-statistic
1877
- - p-value: 검정의 p값
1878
- - 유의성: alpha 기준 결과 (bool)
1879
- - 해석: 선형성 판정 (문자열)
1917
+ tuple: (v_trend, t_trend)
1880
1918
 
1881
1919
  Examples:
1882
1920
  ```python
1921
+ # 2차 다항 회귀 추세선
1883
1922
  from hossam import *
1884
- fit = hs_stats.logit(df, 'target')
1885
- result = hs_stats.ols_linearity_test(fit)
1923
+ vx, vy = hs_stats.trend(x, y, degree=2, value_count=200)
1924
+ print(len(vx), len(vy)) # 200, 200
1886
1925
  ```
1887
-
1888
- Notes:
1889
- - p-value > alpha: 선형성 가정을 만족 (귀무가설 채택)
1890
- - p-value <= alpha: 선형성 가정 위반 가능 (귀무가설 기각)
1891
1926
  """
1892
- import re
1927
+ # [ a, b, c ] ==> ax^2 + bx + c
1928
+ x_arr = np.asarray(x)
1929
+ y_arr = np.asarray(y)
1893
1930
 
1894
- # Ramsey RESET 검정 수행
1895
- reset_result = linear_reset(fit, power=power)
1931
+ if x_arr.ndim == 0 or y_arr.ndim == 0:
1932
+ raise ValueError("x, y는 1차원 이상의 배열이어야 합니다.")
1896
1933
 
1897
- # ContrastResults 객체에서 결과 추출
1898
- test_stat = None
1899
- p_value = None
1934
+ coeff = np.polyfit(x_arr, y_arr, degree)
1900
1935
 
1901
- try:
1902
- # 문자열 표현에서 숫자 추출 시도
1903
- result_str = str(reset_result)
1904
-
1905
- # 정규식으로 숫자값들 추출 (F-statistic과 p-value)
1906
- numbers = re.findall(r'\d+\.?\d*[eE]?-?\d*', result_str)
1907
-
1908
- if len(numbers) >= 2:
1909
- # 일반적으로 첫 번째는 F-statistic, 마지막은 p-value
1910
- test_stat = float(numbers[0])
1911
- p_value = float(numbers[-1])
1912
- except (ValueError, IndexError, AttributeError):
1913
- pass
1914
-
1915
- # 정규식 실패 시 직접 속성 접근
1916
- if test_stat is None or p_value is None:
1917
- attr_pairs = [
1918
- ('statistic', 'pvalue'),
1919
- ('test_stat', 'pvalue'),
1920
- ('fvalue', 'pvalue'),
1921
- ]
1922
-
1923
- for attr_stat, attr_pval in attr_pairs:
1924
- if hasattr(reset_result, attr_stat) and hasattr(reset_result, attr_pval):
1925
- try:
1926
- test_stat = float(getattr(reset_result, attr_stat))
1927
- p_value = float(getattr(reset_result, attr_pval))
1928
- break
1929
- except (ValueError, TypeError):
1930
- continue
1931
-
1932
- # 여전히 값을 못 찾으면 에러
1933
- if test_stat is None or p_value is None:
1934
- raise ValueError(f"linear_reset 결과를 해석할 수 없습니다. 반환값: {reset_result}")
1935
-
1936
- # 유의성 판정
1937
- significant = p_value <= alpha
1938
-
1939
- # 해석 문구
1940
- if significant:
1941
- interpretation = f"선형성 가정 위반 (p={p_value:.4f} <= {alpha})"
1942
- else:
1943
- interpretation = f"선형성 가정 만족 (p={p_value:.4f} > {alpha})"
1944
-
1945
- # 결과를 DataFrame으로 반환
1946
- result_df = DataFrame({
1947
- "검정": ["Ramsey RESET"],
1948
- "검정통계량 (F)": [f"{test_stat:.4f}"],
1949
- "p-value": [f"{p_value:.4f}"],
1950
- "유의수준": [alpha],
1951
- "선형성_위반": [significant], # True: 선형성 위반, False: 선형성 만족
1952
- "해석": [interpretation]
1953
- })
1936
+ minx = np.min(x_arr)
1937
+ maxx = np.max(x_arr)
1938
+ v_trend = np.linspace(minx, maxx, value_count)
1954
1939
 
1955
- if plot:
1956
- ols_residplot(fit, lowess=True, mse=True, title=title, save_path=save_path)
1940
+ # np.polyval 사용으로 간결하게 추세선 계산
1941
+ t_trend = np.polyval(coeff, v_trend)
1957
1942
 
1958
- return result_df
1943
+ return (v_trend, t_trend)
1959
1944
 
1960
1945
 
1961
1946
  # ===================================================================
1962
- # 정규성 검정 (Normality Test)
1947
+ # 선형회귀 요약 리포트
1963
1948
  # ===================================================================
1964
- def ols_normality_test(fit, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
1965
- """회귀모형 잔차의 정규성을 검정한다.
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]: ...
1966
1956
 
1967
- 회귀모형의 잔차가 정규분포를 따르는지 Shapiro-Wilk 검정과 Jarque-Bera 검정으로 평가한다.
1968
- 정규성 가정은 회귀분석의 추론(신뢰구간, 가설검정)이 타당하기 위한 중요한 가정이다.
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
+ """선형회귀 적합 결과를 요약 리포트로 변환한다.
1969
1979
 
1970
1980
  Args:
1971
- fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
1972
- alpha (float, optional): 유의수준. 기본값 0.05.
1973
- plot (bool, optional): True이면 Q-Q 플롯을 출력. 기본값 False.
1974
- title (str, optional): 플롯 제목. 기본값 None.
1975
- save_path (str, optional): 플롯을 저장할 경로. 기본값 None
1981
+ fit: statsmodels OLS 등 선형회귀 결과 객체 (`fit.summary()`를 지원해야 함).
1982
+ data: 종속변수와 독립변수를 모두 포함한 DataFrame.
1983
+ full: True이면 6개 반환, False이면 회귀계수 테이블(rdf)만 반환. 기본값 True.
1984
+ alpha: 유의수준. 기본값 0.05.
1976
1985
 
1977
1986
  Returns:
1978
- DataFrame: 정규성 검정 결과를 포함한 데이터프레임.
1979
- - 검정명: 사용된 검정 방법
1980
- - 검정통계량: 검정 통계량
1981
- - p-value: 검정의 p값
1982
- - 유의수준: 설정된 유의수준
1983
- - 정규성_위반: alpha 기준 결과 (bool)
1984
- - 해석: 정규성 판정 (문자열)
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)
1985
1998
 
1986
1999
  Examples:
1987
2000
  ```python
1988
2001
  from hossam import *
1989
- fit = hs_stats.logit(df, 'target')
1990
- result = hs_stats.ols_normality_test(fit)
1991
- ```
1992
2002
 
1993
- Notes:
1994
- - Shapiro-Wilk: 샘플 크기가 작을 때 (< 5000) 강력한 검정
1995
- - Jarque-Bera: 왜도(skewness)와 첨도(kurtosis) 기반 검정
1996
- - p-value > alpha: 정규성 가정 만족 (귀무가설 채택)
1997
- - p-value <= alpha: 정규성 가정 위반 (귀무가설 기각)
1998
- """
1999
- from scipy.stats import jarque_bera
2003
+ df = hs_util.load_data("some_data.csv")
2004
+ fit = hs_stats.ols(df, yname="target")
2000
2005
 
2001
- # fit 객체에서 잔차 추출
2002
- residuals = fit.resid
2003
- n = len(residuals)
2006
+ # 전체 리포트
2007
+ pdf, rdf, result_report, model_report, variable_reports, eq = hs_stats.ols_report(fit, data, full=True)
2004
2008
 
2005
- results = []
2009
+ # 간단한 버전 (성능지표, 회귀계수 테이블만)
2010
+ pdf, rdf = hs_stats.ols_report(fit, data)
2011
+ ```
2012
+ """
2006
2013
 
2007
- # 1. Shapiro-Wilk 검정 (샘플 크기 < 5000일 권장)
2008
- if n < 5000:
2009
- try:
2010
- stat_sw, p_sw = shapiro(residuals)
2011
- significant_sw = p_sw <= alpha
2014
+ # summary2() 결과에서 실제 회귀계수 DataFrame 추출
2015
+ summary_obj = fit.summary2()
2016
+ tbl = summary_obj.tables[1] # 회귀계수 테이블은 tables[1]에 위치
2012
2017
 
2013
- if significant_sw:
2014
- interpretation_sw = f"정규성 위반 (p={p_sw:.4f} <= {alpha})"
2015
- else:
2016
- interpretation_sw = f"정규성 만족 (p={p_sw:.4f} > {alpha})"
2018
+ # 종속변수 이름
2019
+ yname = fit.model.endog_names
2017
2020
 
2018
- results.append({
2019
- "검정": "Shapiro-Wilk",
2020
- "검정통계량": f"{stat_sw:.4f}",
2021
- "p-value": f"{p_sw:.4f}",
2022
- "유의수준": alpha,
2023
- "정규성_위반": significant_sw,
2024
- "해석": interpretation_sw
2025
- })
2026
- except Exception as e:
2027
- pass
2021
+ # 독립변수 이름(상수항 제외)
2022
+ xnames = [n for n in fit.model.exog_names if n != "const"]
2028
2023
 
2029
- # 2. Jarque-Bera 검정 (항상 수행)
2030
- try:
2031
- stat_jb, p_jb = jarque_bera(residuals)
2032
- significant_jb = p_jb <= alpha # type: ignore
2024
+ # 독립변수 부분 데이터 (VIF 계산용)
2025
+ indi_df = data.filter(xnames)
2033
2026
 
2034
- if significant_jb:
2035
- interpretation_jb = f"정규성 위반 (p={p_jb:.4f} <= {alpha})"
2036
- else:
2037
- interpretation_jb = f"정규성 만족 (p={p_jb:.4f} > {alpha})"
2027
+ # 독립변수 결과를 누적
2028
+ variables = []
2038
2029
 
2039
- results.append({
2040
- "검정": "Jarque-Bera",
2041
- "검정통계량": f"{stat_jb:.4f}",
2042
- "p-value": f"{p_jb:.4f}",
2043
- "유의수준": alpha,
2044
- "정규성_위반": significant_jb,
2045
- "해석": interpretation_jb
2046
- })
2047
- except Exception as e:
2048
- pass
2030
+ # VIF 계산 (상수항 포함 설계행렬 사용)
2031
+ vif_dict = {}
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
2049
2044
 
2050
- # 결과를 DataFrame으로 반환
2051
- if not results:
2052
- raise ValueError("정규성 검정을 수행할 수 없습니다.")
2045
+ for idx, row in tbl.iterrows():
2046
+ name = idx
2047
+ if name not in xnames:
2048
+ continue
2053
2049
 
2050
+ b = float(row['Coef.'])
2051
+ se = float(row['Std.Err.'])
2052
+ t = float(row['t'])
2053
+ p = float(row['P>|t|'])
2054
2054
 
2055
- if plot:
2056
- ols_qqplot(fit, title=title, save_path=save_path)
2055
+ # 표준화 회귀계수(β) 계산
2056
+ beta = b * (data[name].std(ddof=1) / data[yname].std(ddof=1))
2057
2057
 
2058
- result_df = DataFrame(results)
2059
- return result_df
2058
+ # VIF
2059
+ vif = vif_dict.get(name, np.nan)
2060
2060
 
2061
+ # 유의확률과 별표 표시
2062
+ stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
2061
2063
 
2062
- # ===================================================================
2063
- # 등분산성 검정 (Homoscedasticity Test)
2064
- # ===================================================================
2065
- def ols_variance_test(fit, alpha: float = 0.05) -> DataFrame:
2066
- """회귀모형의 등분산성 가정을 검정한다.
2064
+ # 한 변수에 대한 보고 정보 추가
2065
+ variables.append(
2066
+ {
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, # 분산팽창계수
2077
+ }
2078
+ )
2067
2079
 
2068
- 잔차의 분산이 예측값의 수준에 관계없이 일정한지 Breusch-Pagan 검정과 White 검정으로 평가한다.
2069
- 등분산성 가정은 회귀분석의 추론(표준오차, 검정통계량)이 정확하기 위한 중요한 가정이다.
2080
+ rdf = DataFrame(variables)
2070
2081
 
2071
- Args:
2072
- fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
2073
- alpha (float, optional): 유의수준. 기본값 0.05.
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
2074
2094
 
2075
- Returns:
2076
- DataFrame: 등분산성 검정 결과를 포함한 데이터프레임.
2077
- - 검정명: 사용된 검정 방법
2078
- - 검정통계량: 검정 통계량 값 (LM 또는 F)
2079
- - p-value: 검정의 p값
2080
- - 유의수준: 설정된 유의수준
2081
- - 등분산성_위반: alpha 기준 결과 (bool)
2082
- - 해석: 등분산성 판정 (문자열)
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']})"
2083
2097
 
2084
- Examples:
2085
- ```python
2086
- from hossam import *
2087
- fit = hs_stats.logit(df, 'target')
2088
- result = hs_stats.ols_variance_test(fit)
2089
- ```
2098
+ # 모형 보고 문장 구성
2099
+ tpl = "%s에 대하여 %s로 예측하는 회귀분석을 실시한 결과, 이 회귀모형은 통계적으로 %s(F(%s,%s) = %s, p %s 0.05)."
2100
+ model_report = tpl % (
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 ">",
2112
+ )
2090
2113
 
2091
- Notes:
2092
- - Breusch-Pagan: 잔차 제곱과 독립변수의 선형관계 검정
2093
- - White: 잔차 제곱과 독립변수 제곱, 교차항의 관계 검정
2094
- - p-value > alpha: 등분산성 가정 만족 (귀무가설 채택)
2095
- - p-value <= alpha: 이분산성 존재 (귀무가설 기각)
2096
- """
2114
+ # 변수별 보고 문장 리스트 구성
2115
+ variable_reports = []
2116
+ s = "%s의 회귀계수는 %s(p %s 0.05)로, %s에 대하여 %s 예측변인인 것으로 나타났다."
2097
2117
 
2098
- # fit 객체에서 필요한 정보 추출
2099
- exog = fit.model.exog # 설명변수 (상수항 포함)
2100
- resid = fit.resid # 잔차
2118
+ for i in rdf.index:
2119
+ row = rdf.iloc[i]
2120
+ variable_reports.append(
2121
+ s
2122
+ % (
2123
+ row["독립변수"],
2124
+ row["B"],
2125
+ "<=" if float(row["p-value"]) < 0.05 else ">",
2126
+ row["종속변수"],
2127
+ "유의미한" if float(row["p-value"]) < 0.05 else "유의하지 않은",
2128
+ )
2129
+ )
2101
2130
 
2102
- results = []
2131
+ # -----------------------------
2132
+ # 회귀식 자동 출력
2133
+ # -----------------------------
2134
+ intercept = fit.params["const"]
2135
+ terms = []
2103
2136
 
2104
- # 1. Breusch-Pagan 검정
2105
- try:
2106
- lm, lm_pvalue, fvalue, f_pvalue = het_breuschpagan(resid, exog)
2107
- significant_bp = lm_pvalue <= alpha
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}")
2108
2141
 
2109
- if significant_bp:
2110
- interpretation_bp = f"등분산성 위반 (p={lm_pvalue:.4f} <= {alpha})"
2111
- else:
2112
- interpretation_bp = f"등분산성 만족 (p={lm_pvalue:.4f} > {alpha})"
2142
+ equation_text = f"{yname} = {intercept:.3f}" + "".join(terms)
2113
2143
 
2114
- results.append({
2115
- "검정": "Breusch-Pagan",
2116
- "검정통계량 (LM)": f"{lm:.4f}",
2117
- "p-value": f"{lm_pvalue:.4f}",
2118
- "유의수준": alpha,
2119
- "등분산성_위반": significant_bp,
2120
- "해석": interpretation_bp
2121
- })
2122
- except Exception as e:
2123
- pass
2144
+ # 성능 지표 표 생성 (pdf)
2145
+ pdf = DataFrame(
2146
+ {
2147
+ "R": [float(result_dict.get('R-squared', np.nan))],
2148
+ "": [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
+ )
2124
2154
 
2125
- # 2. White 검정
2126
- try:
2127
- lm, lm_pvalue, fvalue, f_pvalue = het_white(resid, exog)
2128
- significant_white = lm_pvalue <= alpha
2155
+ if full:
2156
+ return pdf, rdf, result_report, model_report, variable_reports, equation_text
2157
+ else:
2158
+ return pdf, rdf
2129
2159
 
2130
- if significant_white:
2131
- interpretation_white = f"등분산성 위반 (p={lm_pvalue:.4f} <= {alpha})"
2132
- else:
2133
- interpretation_white = f"등분산성 만족 (p={lm_pvalue:.4f} > {alpha})"
2134
2160
 
2135
- results.append({
2136
- "검정": "White",
2137
- "검정통계량 (LM)": f"{lm:.4f}",
2138
- "p-value": f"{lm_pvalue:.4f}",
2139
- "유의수준": alpha,
2140
- "등분산성_위반": significant_white,
2141
- "해석": interpretation_white
2161
+ # ===================================================================
2162
+ # 선형회귀
2163
+ # ===================================================================
2164
+ @overload
2165
+ def ols(
2166
+ df: DataFrame,
2167
+ yname: str,
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],
2195
+ Tuple[
2196
+ RegressionResultsWrapper,
2197
+ DataFrame,
2198
+ DataFrame,
2199
+ str,
2200
+ str,
2201
+ list[str],
2202
+ str
2203
+ ],
2204
+ RegressionResultsWrapper
2205
+ ]:
2206
+ """선형회귀분석을 수행하고 적합 결과를 반환한다.
2207
+
2208
+ OLS(Ordinary Least Squares) 선형회귀분석을 실시한다.
2209
+ 필요시 상세한 통계 보고서를 함께 제공한다.
2210
+
2211
+ Args:
2212
+ df (DataFrame): 종속변수와 독립변수를 모두 포함한 데이터프레임.
2213
+ yname (str): 종속변수 컬럼명.
2214
+ report (bool | str): 리포트 모드 설정. 다음 값 중 하나:
2215
+ - False (기본값): 리포트 미사용. fit 객체만 반환.
2216
+ - 1 또는 'summary': 요약 리포트 반환 (full=False).
2217
+ - 2 또는 'full': 풀 리포트 반환 (full=True).
2218
+ - True: 풀 리포트 반환 (2와 동일).
2219
+
2220
+ Returns:
2221
+ statsmodels.regression.linear_model.RegressionResultsWrapper: report=False일 때.
2222
+ 선형회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
2223
+
2224
+ tuple (6개): report=1 또는 'summary'일 때.
2225
+ (fit, rdf, result_report, model_report, variable_reports, equation_text) 형태로 (pdf 제외).
2226
+
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
2231
+ - rdf: 회귀계수 표 (DataFrame)
2232
+ - result_report: 적합도 요약 (str)
2233
+ - model_report: 모형 보고 문장 (str)
2234
+ - variable_reports: 변수별 보고 문장 리스트 (list[str])
2235
+ - equation_text: 회귀식 문자열 (str)
2236
+
2237
+ Examples:
2238
+ ```python
2239
+ from hossam import *
2240
+ from pandas import DataFrame
2241
+ import numpy as np
2242
+
2243
+ df = DataFrame({
2244
+ 'target': np.random.normal(100, 10, 100),
2245
+ 'x1': np.random.normal(0, 1, 100),
2246
+ 'x2': np.random.normal(0, 1, 100)
2142
2247
  })
2143
- except Exception as e:
2144
- pass
2145
2248
 
2146
- # 결과를 DataFrame으로 반환
2147
- if not results:
2148
- raise ValueError("등분산성 검정을 수행할 수 없습니다.")
2249
+ # 적합 결과만 반환
2250
+ fit = hs_stats.ols(df, 'target')
2149
2251
 
2150
- result_df = DataFrame(results)
2151
- return result_df
2252
+ # 요약 리포트 반환
2253
+ fit, pdf, rdf = hs_stats.ols(df, 'target', report='summary')
2254
+
2255
+ # 풀 리포트 반환
2256
+ fit, pdf, rdf, result_report, model_report, var_reports, eq = hs_stats.ols(df, 'target', report='full')
2257
+ ```
2258
+ """
2259
+ x = df.drop(yname, axis=1)
2260
+ y = df[yname]
2261
+
2262
+ X_const = sm.add_constant(x)
2263
+ linear_model = sm.OLS(y, X_const)
2264
+ linear_fit = linear_model.fit()
2265
+
2266
+ # report 파라미터에 따른 처리
2267
+ if not report or report is False:
2268
+ # 리포트 미사용
2269
+ return linear_fit
2270
+ elif report == 'full':
2271
+ # 풀 리포트 (full=True)
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
2274
+ else:
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
2152
2278
 
2153
2279
 
2154
2280
  # ===================================================================
2155
- # 독립성 검정 (Independence Test - Durbin-Watson)
2281
+ # 선형성 검정 (Linearity Test)
2156
2282
  # ===================================================================
2157
- def ols_independence_test(fit, alpha: float = 0.05) -> DataFrame:
2158
- """회귀모형의 독립성 가정(자기상관 없음)을 검정한다.
2283
+ def ols_linearity_test(fit: RegressionResultsWrapper, power: int = 2, alpha: float = 0.05) -> DataFrame:
2284
+ """회귀모형의 선형성을 Ramsey RESET 검정으로 평가한다.
2159
2285
 
2160
- Durbin-Watson 검정을 사용하여 잔차의 1차 자기상관 여부를 검정한다.
2161
- 시계열 데이터나 순서가 있는 데이터에서 주로 사용된다.
2286
+ 적합된 회귀모형에 대해 Ramsey RESET(Regression Specification Error Test) 검정을 수행하여
2287
+ 모형의 선형성 가정이 타당한지를 검정한다. 귀무가설은 '모형이 선형이다'이다.
2162
2288
 
2163
2289
  Args:
2164
- fit: statsmodels 회귀분석 결과 객체 (RegressionResultsWrapper).
2165
- alpha (float, optional): 유의수준. 기본값은 0.05.
2290
+ fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
2291
+ OLS 또는 WLS 모형이어야 한다.
2292
+ power (int, optional): RESET 검정에 사용할 거듭제곱 수. 기본값 2.
2293
+ power=2일 때 예측값의 제곱항이 추가됨.
2294
+ power가 클수록 더 높은 차수의 비선형성을 감지.
2295
+ alpha (float, optional): 유의수준. 기본값 0.05
2166
2296
 
2167
2297
  Returns:
2168
- DataFrame: 검정 결과 데이터프레임.
2169
- - 검정: 검정 방법명
2170
- - 검정통계량(DW): Durbin-Watson 통계량 (0~4 범위, 2에 가까울수록 자기상관 없음)
2171
- - 독립성_위반: 자기상관 의심 여부 (True/False)
2172
- - 해석: 검정 결과 해석
2298
+ DataFrame: 선형성 검정 결과를 포함한 데이터프레임.
2299
+ - 검정통계량: F-statistic
2300
+ - p-value: 검정의 p값
2301
+ - 유의성: alpha 기준 결과 (bool)
2302
+ - 해석: 선형성 판정 (문자열)
2173
2303
 
2174
2304
  Examples:
2175
2305
  ```python
2176
2306
  from hossam import *
2177
2307
  fit = hs_stats.logit(df, 'target')
2178
- result = hs_stats.ols_independence_test(fit)
2308
+ result = hs_stats.ols_linearity_test(fit)
2179
2309
  ```
2180
2310
 
2181
2311
  Notes:
2182
- - Durbin-Watson 통계량 해석:
2183
- * 2에 가까우면: 자기상관 없음 (독립성 만족)
2184
- * 0에 가까우면: 양의 자기상관 (독립성 위반)
2185
- * 4에 가까우면: 음의 자기상관 (독립성 위반)
2186
- - 일반적으로 1.5~2.5 범위를 자기상관 없음으로 판단
2187
- - 시계열 데이터나 관측치에 순서가 있는 경우 중요한 검정
2312
+ - p-value > alpha: 선형성 가정을 만족 (귀무가설 채택)
2313
+ - p-value <= alpha: 선형성 가정 위반 가능 (귀무가설 기각)
2188
2314
  """
2189
- from pandas import DataFrame
2315
+ import re
2190
2316
 
2191
- # Durbin-Watson 통계량 계산
2192
- dw_stat = durbin_watson(fit.resid)
2317
+ # Ramsey RESET 검정 수행
2318
+ reset_result = linear_reset(fit, power=power)
2193
2319
 
2194
- # 자기상관 판단 (1.5 < DW < 2.5 범위를 독립성 만족으로 판단)
2195
- is_autocorrelated = dw_stat < 1.5 or dw_stat > 2.5
2320
+ # ContrastResults 객체에서 결과 추출
2321
+ test_stat = None
2322
+ p_value = None
2196
2323
 
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} (독립성 가정 만족)"
2324
+ try:
2325
+ # 문자열 표현에서 숫자 추출 시도
2326
+ result_str = str(reset_result)
2204
2327
 
2205
- # 결과 데이터프레임 생성
2206
- result_df = DataFrame(
2207
- {
2208
- "검정": ["Durbin-Watson"],
2209
- "검정통계량(DW)": [dw_stat],
2210
- "독립성_위반": [is_autocorrelated],
2211
- "해석": [interpretation],
2212
- }
2213
- )
2328
+ # 정규식으로 숫자값들 추출 (F-statistic과 p-value)
2329
+ numbers = re.findall(r'\d+\.?\d*[eE]?-?\d*', result_str)
2330
+
2331
+ if len(numbers) >= 2:
2332
+ # 일반적으로 첫 번째는 F-statistic, 마지막은 p-value
2333
+ test_stat = float(numbers[0])
2334
+ p_value = float(numbers[-1])
2335
+ except (ValueError, IndexError, AttributeError):
2336
+ pass
2337
+
2338
+ # 정규식 실패 시 직접 속성 접근
2339
+ if test_stat is None or p_value is None:
2340
+ attr_pairs = [
2341
+ ('statistic', 'pvalue'),
2342
+ ('test_stat', 'pvalue'),
2343
+ ('fvalue', 'pvalue'),
2344
+ ]
2345
+
2346
+ for attr_stat, attr_pval in attr_pairs:
2347
+ if hasattr(reset_result, attr_stat) and hasattr(reset_result, attr_pval):
2348
+ try:
2349
+ test_stat = float(getattr(reset_result, attr_stat))
2350
+ p_value = float(getattr(reset_result, attr_pval))
2351
+ break
2352
+ except (ValueError, TypeError):
2353
+ continue
2354
+
2355
+ # 여전히 값을 못 찾으면 에러
2356
+ if test_stat is None or p_value is None:
2357
+ raise ValueError(f"linear_reset 결과를 해석할 수 없습니다. 반환값: {reset_result}")
2358
+
2359
+ # 유의성 판정
2360
+ significant = p_value <= alpha
2361
+
2362
+ # 해석 문구
2363
+ if significant:
2364
+ interpretation = f"선형성 가정 위반 (p={p_value:.4f} <= {alpha})"
2365
+ else:
2366
+ interpretation = f"선형성 가정 만족 (p={p_value:.4f} > {alpha})"
2367
+
2368
+ # 결과를 DataFrame으로 반환
2369
+ result_df = DataFrame({
2370
+ "검정": ["Ramsey RESET"],
2371
+ "검정통계량 (F)": [f"{test_stat:.4f}"],
2372
+ "p-value": [f"{p_value:.4f}"],
2373
+ "유의수준": [alpha],
2374
+ "선형성_위반": [significant], # True: 선형성 위반, False: 선형성 만족
2375
+ "해석": [interpretation]
2376
+ })
2214
2377
 
2215
2378
  return result_df
2216
2379
 
2380
+
2217
2381
  # ===================================================================
2218
- # 쌍별 상관분석 (선형성/이상치 점검 후 Pearson/Spearman 자동 선택)
2382
+ # 정규성 검정 (Normality Test)
2219
2383
  # ===================================================================
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을 자동 선택해 상관을 요약한다.
2384
+ def ols_normality_test(fit: RegressionResultsWrapper, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
2385
+ """회귀모형 잔차의 정규성을 검정한다.
2230
2386
 
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 추가
2387
+ 회귀모형의 잔차가 정규분포를 따르는지 Shapiro-Wilk 검정과 Jarque-Bera 검정으로 평가한다.
2388
+ 정규성 가정은 회귀분석의 추론(신뢰구간, 가설검정) 타당하기 위한 중요한 가정이다.
2237
2389
 
2238
2390
  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".
2391
+ fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
2392
+ alpha (float, optional): 유의수준. 기본값 0.05.
2393
+ plot (bool, optional): True이면 Q-Q 플롯을 출력. 기본값 False.
2394
+ title (str, optional): 플롯 제목. 기본값 None.
2395
+ save_path (str, optional): 플롯을 저장할 경로. 기본값 None
2246
2396
 
2247
2397
  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: 상관계수 행렬 (행과 열에 변수명, 값에 상관계수)
2398
+ DataFrame: 정규성 검정 결과를 포함한 데이터프레임.
2399
+ - 검정명: 사용된 검정 방법
2400
+ - 검정통계량: 검정 통계량
2401
+ - p-value: 검정의 p값
2402
+ - 유의수준: 설정된 유의수준
2403
+ - 정규성_위반: alpha 기준 결과 (bool)
2404
+ - 해석: 정규성 판정 (문자열)
2253
2405
 
2254
2406
  Examples:
2255
2407
  ```python
2256
2408
  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'])
2409
+ fit = hs_stats.logit(df, 'target')
2410
+ result = hs_stats.ols_normality_test(fit)
2264
2411
  ```
2265
- """
2266
2412
 
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])]
2413
+ Notes:
2414
+ - Shapiro-Wilk: 샘플 크기가 작을 때 (< 5000) 강력한 검정
2415
+ - Jarque-Bera: 왜도(skewness)와 첨도(kurtosis) 기반 검정
2416
+ - p-value > alpha: 정규성 가정 만족 (귀무가설 채택)
2417
+ - p-value <= alpha: 정규성 가정 위반 (귀무가설 기각)
2418
+ """
2419
+ from scipy.stats import jarque_bera
2274
2420
 
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()
2421
+ # fit 객체에서 잔차 추출
2422
+ residuals = fit.resid
2423
+ n = len(residuals)
2279
2424
 
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()
2425
+ results = []
2289
2426
 
2290
- rows = []
2427
+ # 1. Shapiro-Wilk 검정 (샘플 크기 < 5000일 때 권장)
2428
+ if n < 5000:
2429
+ try:
2430
+ stat_sw, p_sw = shapiro(residuals)
2431
+ significant_sw = p_sw <= alpha
2291
2432
 
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
2433
+ if significant_sw:
2434
+ interpretation_sw = f"정규성 위반 (p={p_sw:.4f} <= {alpha})"
2435
+ else:
2436
+ interpretation_sw = f"정규성 만족 (p={p_sw:.4f} > {alpha})"
2312
2437
 
2313
- x = pair_df[a]
2314
- y = pair_df[b]
2438
+ results.append({
2439
+ "검정": "Shapiro-Wilk",
2440
+ "검정통계량": f"{stat_sw:.4f}",
2441
+ "p-value": f"{p_sw:.4f}",
2442
+ "유의수준": alpha,
2443
+ "정규성_위반": significant_sw,
2444
+ "해석": interpretation_sw
2445
+ })
2446
+ except Exception as e:
2447
+ pass
2315
2448
 
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
2449
+ # 2. Jarque-Bera 검정 (항상 수행)
2450
+ try:
2451
+ stat_jb, p_jb = jarque_bera(residuals)
2452
+ significant_jb = p_jb <= alpha # type: ignore
2333
2453
 
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
2454
+ if significant_jb:
2455
+ interpretation_jb = f"정규성 위반 (p={p_jb:.4f} <= {alpha})"
2456
+ else:
2457
+ interpretation_jb = f"정규성 만족 (p={p_jb:.4f} > {alpha})"
2348
2458
 
2349
- # 2) 이상치 플래그 (두 변수 중 하나라도 z-outlier 있으면 True)
2350
- outlier_flag = bool(z_outlier_flags.get(a, False) or z_outlier_flags.get(b, False))
2459
+ results.append({
2460
+ "검정": "Jarque-Bera",
2461
+ "검정통계량": f"{stat_jb:.4f}",
2462
+ "p-value": f"{p_jb:.4f}",
2463
+ "유의수준": alpha,
2464
+ "정규성_위반": significant_jb,
2465
+ "해석": interpretation_jb
2466
+ })
2467
+ except Exception as e:
2468
+ pass
2351
2469
 
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
2470
+ # 결과를 DataFrame으로 반환
2471
+ if not results:
2472
+ raise ValueError("정규성 검정을 수행할 수 없습니다.")
2363
2473
 
2364
- # 4) 유의성, 강도
2365
- significant = False if np.isnan(pval) else pval <= alpha # type: ignore
2366
- abs_r = abs(corr_val) if not np.isnan(corr_val) else 0 # type: ignore
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"
2375
2474
 
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
- )
2390
-
2391
- result_df = DataFrame(rows)
2392
-
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
2401
-
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 # 대칭성
2475
+ if plot:
2476
+ ols_qqplot(fit, title=title, save_path=save_path)
2414
2477
 
2415
- return result_df, corr_matrix
2478
+ result_df = DataFrame(results)
2479
+ return result_df
2416
2480
 
2417
2481
 
2418
2482
  # ===================================================================
2419
- # 일원 분산분석 (One-way ANOVA)
2483
+ # 등분산성 검정 (Homoscedasticity Test)
2420
2484
  # ===================================================================
2421
- def oneway_anova(data: DataFrame, dv: str, between: str, alpha: float = 0.05) -> tuple[DataFrame, str, DataFrame | None, str]:
2422
- """일원분산분석(One-way ANOVA)을 일괄 처리한다.
2423
-
2424
- 정규성 및 등분산성 검정을 자동으로 수행한 후,
2425
- 그 결과에 따라 적절한 ANOVA 방식을 선택하여 분산분석을 수행한다.
2426
- ANOVA 결과가 유의하면 자동으로 사후검정을 실시한다.
2485
+ def ols_variance_test(fit: RegressionResultsWrapper, alpha: float = 0.05, plot: bool = False, title: str | None = None, save_path: str | None = None) -> DataFrame:
2486
+ """회귀모형의 등분산성 가정을 검정한다.
2427
2487
 
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)
2488
+ 잔차의 분산이 예측값의 수준에 관계없이 일정한지 Breusch-Pagan 검정과 White 검정으로 평가한다.
2489
+ 등분산성 가정은 회귀분석의 추론(표준오차, 검정통계량)이 정확하기 위한 중요한 가정이다.
2433
2490
 
2434
2491
  Args:
2435
- data (DataFrame): 분석 대상 데이터프레임. 종속변수와 그룹 변수를 포함해야 함.
2436
- dv (str): 종속변수(Dependent Variable) 컬럼명.
2437
- between (str): 그룹 구분 변수 컬럼명.
2492
+ fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
2438
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
2439
2497
 
2440
2498
  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): 사후검정 유무와 유의한 쌍 정보를 요약한 보고 문장.
2499
+ DataFrame: 등분산성 검정 결과를 포함한 데이터프레임.
2500
+ - 검정명: 사용된 검정 방법
2501
+ - 검정통계량: 검정 통계량 (LM 또는 F)
2502
+ - p-value: 검정의 p값
2503
+ - 유의수준: 설정된 유의수준
2504
+ - 등분산성_위반: alpha 기준 결과 (bool)
2505
+ - 해석: 등분산성 판정 (문자열)
2446
2506
 
2447
2507
  Examples:
2448
2508
  ```python
2449
2509
  from hossam import *
2450
- from pandas import DataFrame
2451
-
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
- })
2456
-
2457
- anova_df, anova_report, posthoc_df, posthoc_report = hs_stats.oneway_anova(df, dv='score', between='group')
2458
-
2459
- # 사후검정결과는 ANOVA가 유의할 때만 생성됨
2460
- if posthoc_df is not None:
2461
- print(posthoc_report)
2462
- print(posthoc_df.head())
2510
+ fit = hs_stats.logit(df, 'target')
2511
+ result = hs_stats.ols_variance_test(fit)
2463
2512
  ```
2464
2513
 
2465
- Raises:
2466
- ValueError: dv 또는 between 컬럼이 데이터프레임에 없을 경우.
2514
+ Notes:
2515
+ - Breusch-Pagan: 잔차 제곱과 독립변수의 선형관계 검정
2516
+ - White: 잔차 제곱과 독립변수 및 그 제곱, 교차항의 관계 검정
2517
+ - p-value > alpha: 등분산성 가정 만족 (귀무가설 채택)
2518
+ - p-value <= alpha: 이분산성 존재 (귀무가설 기각)
2467
2519
  """
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}' 컬럼이 데이터프레임에 없습니다.")
2473
-
2474
- df_filtered = data[[dv, between]].dropna()
2475
2520
 
2476
- # ============================================
2477
- # 1. 정규성 검정 ( 그룹별로 수행)
2478
- # ============================================
2479
- group_names = sorted(df_filtered[between].unique())
2480
- normality_satisfied = True
2521
+ # fit 객체에서 필요한 정보 추출
2522
+ exog = fit.model.exog # 설명변수 (상수항 포함)
2523
+ resid = fit.resid # 잔차
2481
2524
 
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
2525
+ results = []
2489
2526
 
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
2527
+ # 1. Breusch-Pagan 검정
2528
+ try:
2529
+ lm, lm_pvalue, fvalue, f_pvalue = het_breuschpagan(resid, exog)
2530
+ significant_bp = lm_pvalue <= alpha
2497
2531
 
2498
- # 등분산 검정 수행
2499
- if len(group_names) > 1:
2500
- if normality_satisfied:
2501
- # 정규성을 만족하면 Bartlett 검정
2502
- s, p = bartlett(*group_data_dict.values())
2532
+ if significant_bp:
2533
+ interpretation_bp = f"등분산성 위반 (p={lm_pvalue:.4f} <= {alpha})"
2503
2534
  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
2535
+ interpretation_bp = f"등분산성 만족 (p={lm_pvalue:.4f} > {alpha})"
2510
2536
 
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)
2537
+ results.append({
2538
+ "검정": "Breusch-Pagan",
2539
+ "검정통계량 (LM)": f"{lm:.4f}",
2540
+ "p-value": f"{lm_pvalue:.4f}",
2541
+ "유의수준": alpha,
2542
+ "등분산성_위반": significant_bp,
2543
+ "해석": interpretation_bp
2544
+ })
2545
+ except Exception as e:
2546
+ pass
2522
2547
 
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)
2548
+ # 2. White 검정
2549
+ try:
2550
+ lm, lm_pvalue, fvalue, f_pvalue = het_white(resid, exog)
2551
+ significant_white = lm_pvalue <= alpha
2527
2552
 
2528
- # 유의성 여부 컬럼 추가
2529
- if 'p-unc' in anova_df.columns:
2530
- anova_df['significant'] = anova_df['p-unc'] <= alpha
2553
+ if significant_white:
2554
+ interpretation_white = f"등분산성 위반 (p={lm_pvalue:.4f} <= {alpha})"
2555
+ else:
2556
+ interpretation_white = f"등분산성 만족 (p={lm_pvalue:.4f} > {alpha})"
2531
2557
 
2532
- # ANOVA 결과가 유의한지 확인
2533
- p_unc = float(anova_df.loc[0, 'p-unc']) # type: ignore
2534
- anova_significant = p_unc <= alpha
2558
+ results.append({
2559
+ "검정": "White",
2560
+ "검정통계량 (LM)": f"{lm:.4f}",
2561
+ "p-value": f"{lm_pvalue:.4f}",
2562
+ "유의수준": alpha,
2563
+ "등분산성_위반": significant_white,
2564
+ "해석": interpretation_white
2565
+ })
2566
+ except Exception as e:
2567
+ pass
2535
2568
 
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 # type: ignore
2540
- except Exception:
2541
- return default
2569
+ # 결과를 DataFrame으로 반환
2570
+ if not results:
2571
+ raise ValueError("등분산성 검정을 수행할 수 없습니다.")
2542
2572
 
2543
- df1 = _safe_get('ddof1')
2544
- df2 = _safe_get('ddof2')
2545
- fval = _safe_get('F')
2546
- eta2 = _safe_get('np2')
2573
+ if plot:
2574
+ ols_residplot(fit, lowess=True, mse=True, title=title, save_path=save_path)
2547
2575
 
2548
- anova_sig_text = "그룹별 평균이 다를 가능성이 높습니다." if anova_significant else "그룹별 평균 차이에 대한 근거가 부족합니다."
2549
- assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않았고'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않았다'}고 판단됩니다."
2576
+ result_df = DataFrame(results)
2577
+ return result_df
2550
2578
 
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
- )
2555
2579
 
2556
- if not np.isnan(eta2):
2557
- anova_report += f" 효과 크기(η²p) {eta2:.3f}, 값이 클수록 그룹 차이가 뚜렷함을 의미합니다."
2580
+ # ===================================================================
2581
+ # 독립성 검정 (Independence Test - Durbin-Watson)
2582
+ # ===================================================================
2583
+ def ols_independence_test(fit: RegressionResultsWrapper, alpha: float = 0.05) -> DataFrame:
2584
+ """회귀모형의 독립성 가정(자기상관 없음)을 검정한다.
2558
2585
 
2559
- # ============================================
2560
- # 4. 사후검정 (ANOVA 유의할 때만)
2561
- # ============================================
2562
- posthoc_df = None
2563
- posthoc_method = 'None'
2564
- posthoc_report = "ANOVA 결과가 유의하지 않아 사후검정을 진행하지 않았습니다."
2586
+ Durbin-Watson 검정을 사용하여 잔차의 1차 자기상관 여부를 검정한다.
2587
+ 시계열 데이터나 순서가 있는 데이터에서 주로 사용된다.
2565
2588
 
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)
2589
+ Args:
2590
+ fit: statsmodels 회귀분석 결과 객체 (RegressionResultsWrapper).
2591
+ alpha (float, optional): 유의수준. 기본값은 0.05.
2575
2592
 
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)
2593
+ Returns:
2594
+ DataFrame: 검정 결과 데이터프레임.
2595
+ - 검정: 검정 방법명
2596
+ - 검정통계량(DW): Durbin-Watson 통계량 (0~4 범위, 2에 가까울수록 자기상관 없음)
2597
+ - 독립성_위반: 자기상관 의심 여부 (True/False)
2598
+ - 해석: 검정 결과 해석
2580
2599
 
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
2600
+ Examples:
2601
+ ```python
2602
+ from hossam import *
2603
+ fit = hs_stats.logit(df, 'target')
2604
+ result = hs_stats.ols_independence_test(fit)
2605
+ ```
2584
2606
 
2585
- if p_col:
2586
- # 유의성 여부 컬럼 추가
2587
- posthoc_df['significant'] = posthoc_df[p_col] <= alpha
2607
+ Notes:
2608
+ - Durbin-Watson 통계량 해석:
2609
+ * 2에 가까우면: 자기상관 없음 (독립성 만족)
2610
+ * 0에 가까우면: 양의 자기상관 (독립성 위반)
2611
+ * 4에 가까우면: 음의 자기상관 (독립성 위반)
2612
+ - 일반적으로 1.5~2.5 범위를 자기상관 없음으로 판단
2613
+ - 시계열 데이터나 관측치에 순서가 있는 경우 중요한 검정
2614
+ """
2615
+ from pandas import DataFrame
2588
2616
 
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()]
2617
+ # Durbin-Watson 통계량 계산
2618
+ dw_stat = durbin_watson(fit.resid)
2619
+
2620
+ # 자기상관 판단 (1.5 < DW < 2.5 범위를 독립성 만족으로 판단)
2621
+ is_autocorrelated = dw_stat < 1.5 or dw_stat > 2.5
2622
+
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} (독립성 가정 만족)"
2630
+
2631
+ # 결과 데이터프레임 생성
2632
+ result_df = DataFrame(
2633
+ {
2634
+ "검정": ["Durbin-Watson"],
2635
+ "검정통계량(DW)": [dw_stat],
2636
+ "독립성_위반": [is_autocorrelated],
2637
+ "해석": [interpretation],
2638
+ }
2639
+ )
2640
+
2641
+ return result_df
2595
2642
 
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 정보를 찾지 못해 유의성을 확인할 수 없습니다."
2606
2643
 
2607
- # ============================================
2608
- # 5. 결과 반환
2609
- # ============================================
2610
- return anova_df, anova_report, posthoc_df, posthoc_report
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
+ 선형성, 정규성, 등분산성, 독립성 검정을 순차적으로 실시하고 결과를 하나의 데이터프레임으로 반환한다.
2649
+
2650
+ Args:
2651
+ fit: 회귀 모형 객체 (statsmodels의 RegressionResultsWrapper).
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.
2656
+
2657
+ Returns:
2658
+ None
2659
+
2660
+ Examples:
2661
+ ```python
2662
+ from hossam import *
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)
2678
+
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)
2611
2684
 
2612
2685
 
2613
2686
  # ===================================================================
2614
- # 이원 분산분석 (Two-way ANOVA: 두 범주형 독립변수)
2687
+ # 로지스틱 회귀 요약 리포트
2615
2688
  # ===================================================================
2616
- def twoway_anova(
2689
+ def logit_report(
2690
+ fit: BinaryResultsWrapper,
2617
2691
  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
- """두 범주형 요인에 대한 이원분산분석을 수행하고 해석용 보고문을 반환한다.
2624
-
2625
- 분석 흐름:
2626
- 1) 각 셀(요인 조합)별 정규성 검정
2627
- 2) 전체 셀을 대상으로 등분산성 검정 (정규성 충족 시 Bartlett, 불충족 시 Levene)
2628
- 3) 두 요인 및 교호작용을 포함한 2원 ANOVA 수행
2629
- 4) 유의한 요인에 대해 Tukey HSD 사후검정(요인별) 실행
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
+ """로지스틱 회귀 적합 결과를 상세 리포트로 변환한다.
2630
2707
 
2631
2708
  Args:
2632
- data (DataFrame): 종속변수와 개의 범주형 요인을 포함한 데이터프레임.
2633
- dv (str): 종속변수 컬럼명.
2634
- factor_a (str): 번째 요인 컬럼명.
2635
- factor_b (str): 번째 요인 컬럼명.
2636
- alpha (float, optional): 유의수준. 기본 0.05.
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.
2637
2714
 
2638
2715
  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): 사후검정 유무 유의 쌍 요약 문장.
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]].
2644
2723
 
2645
- Raises:
2646
- ValueError: 입력 컬럼이 데이터프레임에 없을 때.
2724
+ full=False일 때:
2725
+ - 성능 지표 (`cdf`, DataFrame)
2726
+ - 회귀계수 표 (`rdf`, DataFrame)
2727
+
2728
+ Examples:
2729
+ ```python
2730
+ from hossam import *
2731
+ from pandas import DataFrame
2732
+ import numpy as np
2733
+
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
+ })
2739
+
2740
+ # 로지스틱 회귀 적합
2741
+ fit = hs_stats.logit(df, yname="target")
2742
+
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
+ ```
2647
2749
  """
2648
- # 컬럼 유효성 검사
2649
- for col in [dv, factor_a, factor_b]:
2650
- if col not in data.columns:
2651
- raise ValueError(f"'{col}' 컬럼이 데이터프레임에 없습니다.")
2652
2750
 
2653
- df_filtered = data[[dv, factor_a, factor_b]].dropna()
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)
2654
2758
 
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
2759
+ # 혼동행렬
2760
+ cm = confusion_matrix(y_true, y_pred_fix)
2761
+ tn, fp, fn, tp = cm.ravel()
2664
2762
 
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
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
2770
+
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
+ )
2783
+
2784
+ # -----------------------------
2785
+ # 회귀계수 표 구성 (OR 중심)
2786
+ # -----------------------------
2787
+ tbl = fit.summary2().tables[1]
2788
+
2789
+ # 독립변수 이름(상수항 제외)
2790
+ xnames = [n for n in fit.model.exog_names if n != "const"]
2791
+
2792
+ # 독립변수
2793
+ x = data[xnames]
2794
+
2795
+ variables = []
2796
+
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
2802
+
2803
+ for idx, row in tbl.iterrows():
2804
+ name = idx
2805
+ if name not in xnames:
2806
+ continue
2807
+
2808
+ beta = float(row['Coef.'])
2809
+ se = float(row['Std.Err.'])
2810
+ z = float(row['z'])
2811
+ p = float(row['P>|z|'])
2812
+
2813
+ or_val = np.exp(beta)
2814
+ ci_low = np.exp(beta - 1.96 * se)
2815
+ ci_high = np.exp(beta + 1.96 * se)
2816
+
2817
+ stars = "***" if p < 0.001 else "**" if p < 0.01 else "*" if p < 0.05 else ""
2818
+
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
+ )
2834
+
2835
+ rdf = DataFrame(variables)
2836
+
2837
+ # ---------------------------------
2838
+ # 모델 적합도 + 예측 성능 지표
2839
+ # ---------------------------------
2840
+ auc = roc_auc_score(y_true, y_pred)
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
+ )
2849
+
2850
+ # -----------------------------
2851
+ # 모형 보고 문장
2852
+ # -----------------------------
2853
+ tpl = (
2854
+ "%s에 대하여 %s로 예측하는 로지스틱 회귀분석을 실시한 결과, "
2855
+ "모형은 통계적으로 %s(χ²(%s) = %.3f, p %s 0.05)하였다."
2856
+ )
2857
+
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
+ )
2866
+
2867
+ # -----------------------------
2868
+ # 변수별 보고 문장
2869
+ # -----------------------------
2870
+ variable_reports = []
2871
+
2872
+ s = (
2873
+ "%s의 오즈비는 %.3f(p %s 0.05)로, "
2874
+ "%s 발생 odds에 %s 영향을 미치는 것으로 나타났다."
2875
+ )
2876
+
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
+ )
2888
+
2889
+ if full:
2890
+ return cdf, rdf, result_report, model_report, variable_reports, cm
2673
2891
  else:
2674
- equal_var_satisfied = True
2892
+ return cdf, rdf
2893
+
2894
+
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
+ """로지스틱 회귀분석을 수행하고 적합 결과를 반환한다.
2675
2918
 
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
2919
+ 종속변수가 이항(binary) 형태일 로지스틱 회귀분석을 실시한다.
2920
+ 필요시 상세한 통계 보고서를 함께 제공한다.
2682
2921
 
2683
- # 보고문 생성
2684
- def _safe(row, col, default=np.nan):
2685
- try:
2686
- return float(row[col])
2687
- except Exception:
2688
- return default
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와 동일).
2689
2930
 
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)
2931
+ Returns:
2932
+ statsmodels.genmod.generalized_linear_model.BinomialResults: report=False일 때.
2933
+ 로지스틱 회귀 적합 결과 객체. fit.summary()로 상세 결과 확인 가능.
2708
2934
 
2709
- assumption_text = f"정규성은 {'대체로 만족' if normality_satisfied else '충족되지 않음'}, 등분산성은 {'충족' if equal_var_satisfied else '충족되지 않음'}으로 판단했습니다."
2710
- anova_report = " ".join(reports) + " " + assumption_text
2935
+ tuple (4개): report=1 또는 'summary' 때.
2936
+ (fit, rdf, result_report, variable_reports) 형태로 (cdf 제외).
2711
2937
 
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()
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])
2716
2946
 
2717
- for factor, sig in sig_flags.items():
2718
- if factor is None:
2719
- continue
2720
- factor_lower = str(factor).lower()
2947
+ Examples:
2948
+ ```python
2949
+ from hossam import *
2950
+ from pandas import DataFrame
2951
+ import numpy as np
2721
2952
 
2722
- # 교호작용(residual 포함) 혹은 비유의 항은 건너뛴다
2723
- if factor_lower in ["residual", interaction_name, interaction_name_spaced] or not sig:
2724
- continue
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
+ })
2725
2958
 
2726
- # 실제 컬럼이 아니면 건너뛴다 (ex: "A * B" 같은 교호작용 이름)
2727
- if factor not in df_filtered.columns:
2728
- continue
2959
+ # 적합 결과만 반환
2960
+ fit = hs_stats.logit(df, 'target')
2729
2961
 
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)
2962
+ # 요약 리포트 반환
2963
+ fit, rdf, result_report, var_reports = hs_stats.logit(df, 'target', report='summary')
2736
2964
 
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 정보를 찾지 못했습니다."
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]
2759
2971
 
2760
- return anova_df, anova_report, posthoc_df, posthoc_report
2972
+ X_const = sm.add_constant(x)
2973
+ logit_model = sm.Logit(y, X_const)
2974
+ logit_fit = logit_model.fit(disp=False)
2975
+
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
  # ===================================================================
@@ -2881,122 +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 if is_numeric_dtype(data[col]) and col != dv] # type: ignore
2928
-
2929
- # dv가 수치형인지 확인
2930
- if not is_numeric_dtype(data[dv]):
2931
- raise ValueError(f"Dependent variable '{dv}' must be numeric type")
2932
-
2933
- results = []
2934
-
2935
- for var in fields:
2936
- if not is_numeric_dtype(data[var]):
2937
- continue
2938
-
2939
- # 결측치 제거
2940
- valid_idx = data[[var, dv]].notna().all(axis=1)
2941
- x = data.loc[valid_idx, var].values
2942
- y = data.loc[valid_idx, dv].values
2943
-
2944
- if len(x) < 3:
2945
- continue
2946
-
2947
- # 정규성 검사 (Shapiro-Wilk: n <= 5000 권장, 그 외 D'Agostino)
2948
- method_x = 's' if len(x) <= 5000 else 'n'
2949
- method_y = 's' if len(y) <= 5000 else 'n'
2950
-
2951
- normal_x_result = normal_test(data[[var]], columns=[var], method=method_x)
2952
- normal_y_result = normal_test(data[[dv]], columns=[dv], method=method_y)
2953
-
2954
- # 정규성 판정 (p > alpha면 정규분포 가정)
2955
- normal_x = normal_x_result.loc[var, 'p-val'] > alpha if var in normal_x_result.index else False # type: ignore
2956
- normal_y = normal_y_result.loc[dv, 'p-val'] > alpha if dv in normal_y_result.index else False # type: ignore
2957
-
2958
- # Pearson (모두 정규) vs Spearman (하나라도 비정규)
2959
- if normal_x and normal_y:
2960
- r, p = pearsonr(x, y)
2961
- corr_type = 'Pearson'
2962
- else:
2963
- r, p = spearmanr(x, y)
2964
- corr_type = 'Spearman'
2965
-
2966
- # Cohen's d 계산 (상관계수에서 효과크기로 변환)
2967
- # d = 2*r / sqrt(1-r^2)
2968
- if r ** 2 < 1: # type: ignore
2969
- d = (2 * r) / np.sqrt(1 - r ** 2) # type: ignore
2970
- else:
2971
- d = 0
2972
-
2973
- # 효과크기 분류 (Cohen's d 기준)
2974
- # Small: 0.2 < |d| <= 0.5
2975
- # Medium: 0.5 < |d| <= 0.8
2976
- # Large: |d| > 0.8
2977
- abs_d = abs(d)
2978
- if abs_d > 0.8:
2979
- effect_size = 'Large'
2980
- elif abs_d > 0.5:
2981
- effect_size = 'Medium'
2982
- elif abs_d > 0.2:
2983
- effect_size = 'Small'
2984
- else:
2985
- effect_size = 'Negligible'
2986
-
2987
- results.append({
2988
- 'Variable': var,
2989
- 'Correlation': r,
2990
- 'Corr_Type': corr_type,
2991
- 'P-value': p,
2992
- 'Cohens_d': d,
2993
- 'Effect_Size': effect_size
2994
- })
2995
-
2996
- result_df = DataFrame(results)
2997
-
2998
- # 상관계수로 정렬 (절댓값 기준 내림차순)
2999
- if len(result_df) > 0:
3000
- result_df = result_df.sort_values('Correlation', key=lambda x: x.abs(), ascending=False).reset_index(drop=True)
3001
-
3002
- return result_df
3113
+ )