hossam 0.3.14__py3-none-any.whl → 0.3.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hossam/__init__.py CHANGED
@@ -15,14 +15,14 @@ except Exception:
15
15
 
16
16
  hs_fig = SimpleNamespace(
17
17
  dpi=200,
18
- width=600,
19
- height=320,
20
- font_size=6,
18
+ width=800,
19
+ height=480,
20
+ font_size=9.5,
21
21
  font_weight="light",
22
- frame_width=0.4,
23
- line_width=1,
22
+ frame_width=0.5,
23
+ line_width=1.5,
24
24
  grid_alpha=0.3,
25
- grid_width=0.4,
25
+ grid_width=0.5,
26
26
  fill_alpha=0.3
27
27
  )
28
28
 
hossam/hs_plot.py CHANGED
@@ -56,6 +56,9 @@ def get_default_ax(width: int = hs_fig.width, height: int = hs_fig.height, rows:
56
56
  rows (int): 서브플롯 행 개수.
57
57
  cols (int): 서브플롯 열 개수.
58
58
  dpi (int): 해상도(DPI).
59
+ flatten (bool): Axes 배열을 1차원 리스트로 평탄화할지 여부.
60
+ ws (int|None): 서브플롯 가로 간격(`wspace`). rows/cols가 1보다 클 때만 적용.
61
+ hs (int|None): 서브플롯 세로 간격(`hspace`). rows/cols가 1보다 클 때만 적용.
59
62
 
60
63
  Returns:
61
64
  tuple[Figure, Axes]: 생성된 matplotlib Figure와 Axes 객체.
@@ -100,7 +103,7 @@ def finalize_plot(ax: Axes, callback: any = None, outparams: bool = False, save_
100
103
  callback (Callable|None): 추가 설정을 위한 사용자 콜백.
101
104
  outparams (bool): 내부에서 생성한 Figure인 경우 True.
102
105
  save_path (str|None): 이미지 저장 경로. None이 아니면 해당 경로로 저장.
103
- grid (bool): 그리드 표시 여부.
106
+ grid (bool): 그리드 표시 여부. 기본값은 True입니다.
104
107
 
105
108
  Returns:
106
109
  None
@@ -166,6 +169,7 @@ def lineplot(
166
169
  height (int): 캔버스 세로 픽셀.
167
170
  linewidth (float): 선 굵기.
168
171
  dpi (int): 해상도.
172
+ save_path (str|None): 이미지 저장 경로. None이면 화면에 표시.
169
173
  callback (Callable|None): Axes 후처리 콜백.
170
174
  ax (Axes|None): 외부에서 전달한 Axes.
171
175
  **params: seaborn lineplot 추가 인자.
@@ -230,6 +234,7 @@ def boxplot(
230
234
  height (int): 캔버스 세로 픽셀.
231
235
  linewidth (float): 선 굵기.
232
236
  dpi (int): 그림 크기 및 해상도.
237
+ save_path (str|None): 이미지 저장 경로. None이면 화면에 표시.
233
238
  callback (Callable|None): Axes 후처리 콜백.
234
239
  ax (Axes|None): 외부에서 전달한 Axes.
235
240
  **params: seaborn boxplot 추가 인자.
@@ -1847,6 +1852,7 @@ def distribution_by_class(
1847
1852
  linewidth=linewidth,
1848
1853
  dpi=dpi,
1849
1854
  callback=callback,
1855
+ save_path=save_path
1850
1856
  )
1851
1857
  elif type == "hist":
1852
1858
  histplot(
@@ -1861,6 +1867,7 @@ def distribution_by_class(
1861
1867
  linewidth=linewidth,
1862
1868
  dpi=dpi,
1863
1869
  callback=callback,
1870
+ save_path=save_path
1864
1871
  )
1865
1872
  elif type == "histkde":
1866
1873
  histplot(
@@ -1875,6 +1882,7 @@ def distribution_by_class(
1875
1882
  linewidth=linewidth,
1876
1883
  dpi=dpi,
1877
1884
  callback=callback,
1885
+ save_path=save_path
1878
1886
  )
1879
1887
 
1880
1888
 
@@ -1892,6 +1900,7 @@ def scatter_by_class(
1892
1900
  height: int = hs_fig.height,
1893
1901
  linewidth: float = hs_fig.line_width,
1894
1902
  dpi: int = hs_fig.dpi,
1903
+ save_path: str = None,
1895
1904
  callback: any = None,
1896
1905
  ) -> None:
1897
1906
  """종속변수(y)와 각 연속형 독립변수(x) 간 산점도/볼록껍질을 그린다.
@@ -1939,10 +1948,14 @@ def scatter_by_class(
1939
1948
 
1940
1949
  if outline:
1941
1950
  for v in group:
1942
- convex_hull(data, v[0], v[1], hue, palette, width, height, linewidth, dpi, callback)
1951
+ convex_hull(data=data, xname=v[0], yname=v[1], hue=hue, palette=palette,
1952
+ width=width, height=height, linewidth=linewidth, dpi=dpi, callback=callback,
1953
+ save_path=save_path)
1943
1954
  else:
1944
1955
  for v in group:
1945
- scatterplot(data, v[0], v[1], hue, palette, width, height, linewidth, dpi, callback)
1956
+ scatterplot(data=data, xname=v[0], yname=v[1], hue=hue, palette=palette,
1957
+ width=width, height=height, linewidth=linewidth, dpi=dpi, callback=callback,
1958
+ save_path=save_path)
1946
1959
 
1947
1960
 
1948
1961
  # ===================================================================
@@ -1951,7 +1964,7 @@ def scatter_by_class(
1951
1964
  def categorical_target_distribution(
1952
1965
  data: DataFrame,
1953
1966
  yname: str,
1954
- xnames: list | str | None = None,
1967
+ hue: list | str | None = None,
1955
1968
  kind: str = "box",
1956
1969
  kde_fill: bool = True,
1957
1970
  palette: str | None = None,
@@ -1960,6 +1973,7 @@ def categorical_target_distribution(
1960
1973
  linewidth: float = hs_fig.line_width,
1961
1974
  dpi: int = hs_fig.dpi,
1962
1975
  cols: int = 2,
1976
+ save_path: str = None,
1963
1977
  callback: any = None,
1964
1978
  ) -> None:
1965
1979
  """명목형 변수별로 종속변수 분포 차이를 시각화한다.
@@ -1967,7 +1981,7 @@ def categorical_target_distribution(
1967
1981
  Args:
1968
1982
  data (DataFrame): 시각화할 데이터.
1969
1983
  yname (str): 종속변수 컬럼명(연속형 추천).
1970
- xnames (list|str|None): 명목형 독립변수 목록. None이면 자동 탐지.
1984
+ hue (list|str|None): 명목형 독립변수 목록. None이면 자동 탐지.
1971
1985
  kind (str): 'box', 'violin', 'kde'.
1972
1986
  kde_fill (bool): kind='kde'일 때 영역 채우기 여부.
1973
1987
  palette (str|None): 팔레트 이름.
@@ -1983,13 +1997,13 @@ def categorical_target_distribution(
1983
1997
  """
1984
1998
 
1985
1999
  # 명목형 컬럼 후보: object, category, bool
1986
- if xnames is None:
2000
+ if hue is None:
1987
2001
  cat_cols = data.select_dtypes(include=["object", "category", "bool", "boolean"]).columns
1988
2002
  target_cols = [c for c in cat_cols if c != yname]
1989
- elif isinstance(xnames, str):
1990
- target_cols = [xnames]
2003
+ elif isinstance(hue, str):
2004
+ target_cols = [hue]
1991
2005
  else:
1992
- target_cols = list(xnames)
2006
+ target_cols = list(hue)
1993
2007
 
1994
2008
  if len(target_cols) == 0:
1995
2009
  return
@@ -2030,7 +2044,7 @@ def categorical_target_distribution(
2030
2044
 
2031
2045
 
2032
2046
  # ===================================================================
2033
- #
2047
+ # ROC 커브를 시각화 한다.
2034
2048
  # ===================================================================
2035
2049
  def roc_curve_plot(
2036
2050
  fit,
@@ -2100,14 +2114,13 @@ def roc_curve_plot(
2100
2114
 
2101
2115
 
2102
2116
  # ===================================================================
2103
- #
2117
+ # 혼동행렬 시각화
2104
2118
  # ===================================================================
2105
2119
  def confusion_matrix_plot(
2106
2120
  fit,
2107
2121
  threshold: float = 0.5,
2108
2122
  width: int = hs_fig.width,
2109
2123
  height: int = hs_fig.height,
2110
- linewidth: float = hs_fig.line_width,
2111
2124
  dpi: int = hs_fig.dpi,
2112
2125
  save_path: str = None,
2113
2126
  callback: any = None,
@@ -2120,7 +2133,6 @@ def confusion_matrix_plot(
2120
2133
  threshold (float): 예측 확률을 이진 분류로 변환할 임계값. 기본값 0.5.
2121
2134
  width (int): 캔버스 가로 픽셀.
2122
2135
  height (int): 캔버스 세로 픽셀.
2123
- linewidth (float): 선 굵기 (현재 사용되지 않음).
2124
2136
  dpi (int): 해상도.
2125
2137
  callback (Callable|None): Axes 후처리 콜백.
2126
2138
  ax (Axes|None): 외부에서 전달한 Axes. None이면 새로 생성.
@@ -2152,7 +2164,7 @@ def confusion_matrix_plot(
2152
2164
 
2153
2165
 
2154
2166
  # ===================================================================
2155
- #
2167
+ # 레이더 차트(방사형 차트)
2156
2168
  # ===================================================================
2157
2169
  def radarplot(
2158
2170
  df: DataFrame,
@@ -2279,3 +2291,118 @@ def radarplot(
2279
2291
  ax.set_title('Radar Chart', pad=20)
2280
2292
 
2281
2293
  finalize_plot(ax, callback, outparams, save_path)
2294
+
2295
+
2296
+ # ===================================================================
2297
+ # 연속형 데이터 분포 시각화 (KDE + Boxplot)
2298
+ # ===================================================================
2299
+ def distribution_plot(
2300
+ data: DataFrame,
2301
+ column: str,
2302
+ clevel: float = 0.95,
2303
+ orient: str = "h",
2304
+ hue: str | None = None,
2305
+ kind: str = "boxplot",
2306
+ width: int = hs_fig.width,
2307
+ height: int = hs_fig.height,
2308
+ linewidth: float = hs_fig.line_width,
2309
+ dpi: int = hs_fig.dpi,
2310
+ save_path: str = None,
2311
+ callback: any = None,
2312
+ ) -> None:
2313
+ """연속형 데이터의 분포를 KDE와 Boxplot으로 시각화한다.
2314
+
2315
+ 1행 2열의 서브플롯을 생성하여:
2316
+ - 왼쪽: KDE with 신뢰구간
2317
+ - 오른쪽: Boxplot
2318
+
2319
+ Args:
2320
+ data (DataFrame): 시각화할 데이터.
2321
+ column (str): 분석할 컬럼명.
2322
+ clevel (float): KDE 신뢰수준 (0~1). 기본값 0.95.
2323
+ orient (str): Boxplot 방향 ('v' 또는 'h'). 기본값 'h'.
2324
+ hue (str|None): 명목형 컬럼명. 지정하면 각 범주별로 행을 늘려 KDE와 boxplot을 그림.
2325
+ kind (str): 두 번째 그래프의 유형 (boxplot, hist). 기본값 "boxplot".
2326
+ width (int): 캔버스 가로 픽셀.
2327
+ height (int): 캔버스 세로 픽셀.
2328
+ linewidth (float): 선 굵기.
2329
+ dpi (int): 그림 크기 및 해상도.
2330
+ save_path (str|None): 저장 경로.
2331
+ callback (Callable|None): Axes 후처리 콜백.
2332
+
2333
+ Returns:
2334
+ None
2335
+ """
2336
+ if hue is None:
2337
+ # 1행 2열 서브플롯 생성
2338
+ fig, axes = get_default_ax(width, height, rows=1, cols=2, dpi=dpi)
2339
+
2340
+ kde_confidence_interval(
2341
+ data=data,
2342
+ xnames=column,
2343
+ clevel=clevel,
2344
+ linewidth=linewidth,
2345
+ ax=axes[0],
2346
+ )
2347
+
2348
+ if kind == "hist":
2349
+ histplot(
2350
+ df=data,
2351
+ xname=column,
2352
+ linewidth=linewidth,
2353
+ ax=axes[1]
2354
+ )
2355
+ else:
2356
+ boxplot(
2357
+ df=data[column],
2358
+ linewidth=linewidth,
2359
+ ax=axes[1]
2360
+ )
2361
+
2362
+ fig.suptitle(f"Distribution of {column}", fontsize=14, y=1.02)
2363
+ else:
2364
+ if hue not in data.columns:
2365
+ raise ValueError(f"hue column '{hue}' not found in DataFrame")
2366
+
2367
+ categories = list(pd.Series(data[hue].dropna().unique()).sort_values())
2368
+ n_cat = len(categories) if categories else 1
2369
+
2370
+ fig, axes = get_default_ax(width, height, rows=n_cat, cols=2, dpi=dpi)
2371
+ axes_2d = np.atleast_2d(axes)
2372
+
2373
+ for idx, cat in enumerate(categories):
2374
+ subset = data[data[hue] == cat]
2375
+ left_ax, right_ax = axes_2d[idx, 0], axes_2d[idx, 1]
2376
+
2377
+ kde_confidence_interval(
2378
+ data=subset,
2379
+ xnames=column,
2380
+ clevel=clevel,
2381
+ linewidth=linewidth,
2382
+ ax=left_ax,
2383
+ )
2384
+ left_ax.set_title(f"{hue} = {cat}")
2385
+
2386
+ if kind == "hist":
2387
+ histplot(
2388
+ df=subset,
2389
+ xname=column,
2390
+ linewidth=linewidth,
2391
+ ax=right_ax,
2392
+ )
2393
+ else:
2394
+ boxplot(
2395
+ df=subset[column],
2396
+ linewidth=linewidth,
2397
+ ax=right_ax
2398
+ )
2399
+
2400
+ fig.suptitle(f"Distribution of {column} by {hue}", fontsize=14, y=1.02)
2401
+
2402
+ plt.tight_layout()
2403
+
2404
+ if save_path:
2405
+ plt.savefig(save_path, bbox_inches='tight', dpi=dpi)
2406
+ plt.close()
2407
+ else:
2408
+ plt.show()
hossam/hs_prep.py CHANGED
@@ -10,6 +10,7 @@ from itertools import combinations
10
10
  #
11
11
  # ===================================================================
12
12
  import pandas as pd
13
+ import jenkspy
13
14
  from pandas import DataFrame
14
15
  from sklearn.preprocessing import StandardScaler, MinMaxScaler
15
16
  from sklearn.impute import SimpleImputer
@@ -177,7 +178,7 @@ def set_category(data: DataFrame, *args: str) -> DataFrame:
177
178
 
178
179
 
179
180
  # ===================================================================
180
- # Melted 형태를 원래 모양으로 복구하여 변수를 펼친다
181
+ # 명목형 변수의 종류에 따른 데이터 분리
181
182
  # ===================================================================
182
183
  def unmelt(
183
184
  data: DataFrame, id_vars: str = "class", value_vars: str = "values"
@@ -185,49 +186,32 @@ def unmelt(
185
186
  """두 개의 컬럼으로 구성된 데이터프레임에서 하나는 명목형, 나머지는 연속형일 경우
186
187
  명목형 변수의 값에 따라 고유한 변수를 갖는 데이터프레임으로 변환한다.
187
188
 
189
+ 각 그룹의 데이터 길이가 다를 경우 짧은 쪽에 NaN을 채워 동일한 길이로 맞춥니다.
190
+ 이는 독립표본 t-검정(ttest_ind) 등의 분석을 위한 데이터 준비에 유용합니다.
191
+
188
192
  Args:
189
193
  data (DataFrame): 데이터프레임
190
194
  id_vars (str, optional): 명목형 변수의 컬럼명. Defaults to 'class'.
191
195
  value_vars (str, optional): 연속형 변수의 컬럼명. Defaults to 'values'.
192
196
 
193
197
  Returns:
194
- DataFrame: 변환된 데이터프레임
195
- """
196
- result = data.groupby(id_vars)[value_vars].apply(list)
197
- mydict = {}
198
-
199
- for i in result.index:
200
- mydict[i] = result[i]
201
-
202
- return DataFrame(mydict)
203
-
204
-
205
- # ===================================================================
206
- # 결측치를 평균, 중앙값 등의 전략으로 대체한다
207
- # ===================================================================
208
- def replace_missing_value(data: DataFrame, strategy: str = "mean") -> DataFrame:
209
- """SimpleImputer로 결측치를 대체한다.
210
-
211
- Args:
212
- data (DataFrame): 결측치가 포함된 데이터프레임
213
- strategy (str, optional): 결측치 대체 방식(mean, median, most_frequent, constant). Defaults to "mean".
214
-
215
- Returns:
216
- DataFrame: 결측치가 대체된 데이터프레임
198
+ DataFrame: 변환된 데이터프레임 (각 그룹이 개별 컬럼으로 구성)
217
199
 
218
200
  Examples:
219
- >>> from hossam.prep import replace_missing_value
220
- >>> out = hs_replace_missing_value(df.select_dtypes(include="number"), strategy="median")
201
+ >>> df = pd.DataFrame({
202
+ ... 'group': ['A', 'A', 'B', 'B', 'B'],
203
+ ... 'value': [1, 2, 3, 4, 5]
204
+ ... })
205
+ >>> result = unmelt(df, id_vars='group', value_vars='value')
206
+ >>> # 결과: A 컬럼에는 [1, 2, NaN], B 컬럼에는 [3, 4, 5]
221
207
  """
208
+ # 그룹별로 값들을 리스트로 모음
209
+ grouped = data.groupby(id_vars, observed=True)[value_vars].apply(lambda x: x.tolist())
210
+ series_dict = {}
211
+ for idx, values in grouped.items():
212
+ series_dict[str(idx)] = pd.Series(values)
222
213
 
223
- allowed = {"mean", "median", "most_frequent", "constant"}
224
- if strategy not in allowed:
225
- raise ValueError(f"strategy는 {allowed} 중 하나여야 합니다.")
226
-
227
- imr = SimpleImputer(missing_values=np.nan, strategy=strategy)
228
- df_imr = imr.fit_transform(data.values)
229
- return DataFrame(df_imr, index=data.index, columns=data.columns)
230
-
214
+ return DataFrame(series_dict)
231
215
 
232
216
  # ===================================================================
233
217
  # 지정된 변수의 이상치 테이블로 반환한다
@@ -439,6 +423,304 @@ def labelling(data: DataFrame, *fields: str) -> DataFrame:
439
423
  return df
440
424
 
441
425
 
426
+ # ===================================================================
427
+ # 연속형 변수를 다양한 기준으로 구간화하여 명목형 변수로 추가한다
428
+ # ===================================================================
429
+ def bin_continuous(
430
+ data: DataFrame,
431
+ field: str,
432
+ method: str = "natural_breaks",
433
+ bins: int | list[float] | None = None,
434
+ labels: list[str] | None = None,
435
+ new_col: str | None = None,
436
+ is_log_transformed: bool = False,
437
+ apply_labels: bool = True,
438
+ ) -> DataFrame:
439
+ """연속형 변수를 다양한 알고리즘으로 구간화해 명목형 파생변수를 추가한다.
440
+
441
+ 지원 방법:
442
+ - "natural_breaks"(기본): Jenks 자연 구간화. jenkspy 미사용 시 quantile로 대체
443
+ 기본 라벨: "X~Y" 형식 (예: "18~30", "30~40")
444
+ - "quantile"/"qcut"/"equal_freq": 분위수 기반 동빈도
445
+ 기본 라벨: "X~Y" 형식
446
+ - "equal_width"/"uniform": 동일 간격
447
+ 기본 라벨: "X~Y" 형식
448
+ - "std": 평균±표준편차를 경계로 4구간 생성
449
+ 라벨: "low", "mid_low", "mid_high", "high"
450
+ - "lifecourse"/"life_stage": 생애주기 5단계
451
+ 라벨: "아동", "청소년", "청년", "중년", "노년" (경계: 0, 13, 19, 40, 65)
452
+ - "age_decade": 10대 단위 연령대
453
+ 라벨: "아동", "10대", "20대", "30대", "40대", "50대", "60대 이상"
454
+ - "health_band"/"policy_band": 의료비 위험도 기반 연령대
455
+ 라벨: "18-29", "30-39", "40-49", "50-64", "65+"
456
+ - 커스텀 구간: bins에 경계 리스트 전달 (예: [0, 30, 50, 100])
457
+
458
+ Args:
459
+ data (DataFrame): 입력 데이터프레임
460
+ field (str): 구간화할 연속형 변수명
461
+ method (str): 구간화 알고리즘 키워드 (기본값: "natural_breaks")
462
+ bins (int|list[float]|None):
463
+ - int: 생성할 구간 개수 (quantile, equal_width, natural_breaks에서 사용)
464
+ - list: 경계값 리스트 (커스텀 구간화)
465
+ - None: 기본값 사용 (quantile/equal_width는 4~5, natural_breaks는 5)
466
+ labels (list[str]|None): 구간 레이블 목록
467
+ - None: method별 기본 라벨 자동 생성
468
+ - list: 사용자 정의 라벨 (구간 개수와 일치해야 함)
469
+ new_col (str|None): 생성할 컬럼명
470
+ - None: f"{field}_bin" 사용 (예: "age_bin")
471
+ is_log_transformed (bool): 대상 컬럼이 로그 변환되어 있는지 여부
472
+ - True: 지정된 컬럼을 역변환(exp)한 후 구간화
473
+ - False: 원래 값 그대로 구간화 (기본값)
474
+ apply_labels (bool): 구간에 숫자 인덱스를 적용할지 여부
475
+ - True: 숫자 인덱스 사용 (0, 1, 2, 3, ...) (기본값)
476
+ - False: 문자 라벨 적용 (예: "18~30", "아동")
477
+
478
+ Returns:
479
+ DataFrame: 원본에 구간화된 명목형 컬럼이 추가된 데이터프레임
480
+
481
+ Examples:
482
+ 동일 간격으로 5개 구간 생성 (숫자 인덱스):
483
+ >>> df = pd.DataFrame({'age': [20, 35, 50, 65]})
484
+ >>> result = bin_continuous(df, 'age', method='equal_width', bins=5)
485
+ >>> print(result['age_bin']) # 0, 1, 2, ... (숫자 인덱스)
486
+
487
+ 문자 레이블 사용:
488
+ >>> result = bin_continuous(df, 'age', method='equal_width', bins=5, apply_labels=False)
489
+ >>> print(result['age_bin']) # 20~30, 30~40, ... (문자 레이블)
490
+
491
+ 생애주기 기반 구간화:
492
+ >>> result = bin_continuous(df, 'age', method='lifecourse')
493
+ >>> print(result['age_bin']) # 0, 1, 2, 3, 4 (숫자 인덱스)
494
+
495
+ 생애주기 문자 레이블:
496
+ >>> result = bin_continuous(df, 'age', method='lifecourse', apply_labels=False)
497
+ >>> print(result['age_bin']) # 아동, 청소년, 청년, 중년, 노년
498
+
499
+ 의료비 위험도 기반 연령대 (health_band):
500
+ >>> result = bin_continuous(df, 'age', method='health_band', apply_labels=False)
501
+ >>> print(result['age_bin']) # 18-29, 30-39, 40-49, 50-64, 65+
502
+
503
+ 로그 변환된 컬럼 역변환 후 구간화:
504
+ >>> df_log = pd.DataFrame({'charges_log': [np.log(1000), np.log(5000), np.log(50000)]})
505
+ >>> result = bin_continuous(df_log, 'charges_log', method='equal_width', is_log_transformed=True)
506
+ >>> print(result['charges_log_bin']) # 0, 1, 2 (숫자 인덱스)
507
+ """
508
+
509
+ if field not in data.columns:
510
+ return data
511
+
512
+ df = data.copy()
513
+ series = df[field].copy()
514
+
515
+ # 로그 변환 역변환
516
+ if is_log_transformed:
517
+ series = np.exp(series)
518
+
519
+ new_col = new_col or f"{field}_bin"
520
+ method_key = (method or "").lower()
521
+
522
+ def _cut(edges: list[float], default_labels: list[str] | None = None, right: bool = False, ordered: bool = True):
523
+ nonlocal labels
524
+ use_labels = None
525
+
526
+ # apply_labels=True일 때 숫자 인덱스, False일 때 문자 레이블
527
+ if apply_labels:
528
+ # 숫자 인덱스 생성
529
+ numeric_labels = list(range(len(edges) - 1))
530
+ use_labels = numeric_labels
531
+ else:
532
+ # 문자 레이블 적용
533
+ use_labels = labels if labels is not None else default_labels
534
+
535
+ df[new_col] = pd.cut(
536
+ series,
537
+ bins=edges,
538
+ labels=use_labels,
539
+ right=right,
540
+ include_lowest=True,
541
+ ordered=False, # 레이블이 있으므로 ordered=False 사용
542
+ )
543
+ df[new_col] = df[new_col].astype("category")
544
+
545
+ # 생애주기 구분
546
+ if method_key in {"lifecourse", "life_stage", "lifecycle", "life"}:
547
+ edges = [0, 13, 19, 40, 65, np.inf]
548
+ # 나이 구간을 함께 표기한 라벨 (apply_labels=False에서 사용)
549
+ default_labels = [
550
+ "아동(0~12)",
551
+ "청소년(13~18)",
552
+ "청년(19~39)",
553
+ "중년(40~64)",
554
+ "노년(65+)",
555
+ ]
556
+ _cut(edges, default_labels, right=False)
557
+ return df
558
+
559
+ # 연령대(10단위)
560
+ if method_key in {"age_decade", "age10", "decade"}:
561
+ edges = [0, 13, 20, 30, 40, 50, 60, np.inf]
562
+ default_labels = ["아동", "10대", "20대", "30대", "40대", "50대", "60대 이상"]
563
+ _cut(edges, default_labels, right=False)
564
+ return df
565
+
566
+ # 건강/제도 기준 (의료비 위험군 분류 기준)
567
+ if method_key in {"health_band", "policy_band", "health"}:
568
+ # 연령 데이터 최소값(예: 18세)과 레이블을 일치시킴
569
+ edges = [0, 19, 30, 40, 50, 65, np.inf]
570
+ default_labels = ["0~18", "19-29", "30-39", "40-49", "50-64", "65+"]
571
+ _cut(edges, default_labels, right=False)
572
+ return df
573
+
574
+ # 표준편차 기반
575
+ if method_key == "std":
576
+ mu = series.mean()
577
+ sd = series.std(ddof=0)
578
+ edges = [-np.inf, mu - sd, mu, mu + sd, np.inf]
579
+ default_labels = ["low", "mid_low", "mid_high", "high"]
580
+ _cut(edges, default_labels, right=True)
581
+ return df
582
+
583
+ # 동일 간격
584
+ if method_key in {"equal_width", "uniform"}:
585
+ k = bins if isinstance(bins, int) and bins > 0 else 5
586
+ _, edges = pd.cut(series, bins=k, include_lowest=True, retbins=True)
587
+
588
+ # apply_labels=True: 숫자 인덱스 / False: 문자 레이블
589
+ if apply_labels:
590
+ # 숫자 인덱스 사용 (0, 1, 2, ...)
591
+ numeric_labels = list(range(len(edges) - 1))
592
+ df[new_col] = pd.cut(series, bins=edges, labels=numeric_labels, include_lowest=True, ordered=False)
593
+ else:
594
+ # 문자 레이블 적용
595
+ if labels is None:
596
+ auto_labels = []
597
+ for i in range(len(edges) - 1):
598
+ left = f"{edges[i]:.2f}" if edges[i] != -np.inf else "-∞"
599
+ right = f"{edges[i+1]:.2f}" if edges[i+1] != np.inf else "∞"
600
+ # 정수값인 경우 소수점 제거
601
+ try:
602
+ left = str(int(float(left))) if float(left) == int(float(left)) else left
603
+ right = str(int(float(right))) if float(right) == int(float(right)) else right
604
+ except:
605
+ pass
606
+ auto_labels.append(f"{left}~{right}")
607
+ df[new_col] = pd.cut(series, bins=edges, labels=auto_labels, include_lowest=True, ordered=False)
608
+ else:
609
+ df[new_col] = pd.cut(series, bins=edges, labels=labels, include_lowest=True, ordered=False)
610
+
611
+ df[new_col] = df[new_col].astype("category")
612
+ return df
613
+
614
+ # 분위수 기반 동빈도
615
+ if method_key in {"quantile", "qcut", "equal_freq"}:
616
+ k = bins if isinstance(bins, int) and bins > 0 else 4
617
+ # apply_labels=False일 때 기본 레이블을 사분위수 위치(Q1~)로 설정
618
+ default_q_labels = labels if labels is not None else [f"Q{i+1}" for i in range(k)]
619
+ try:
620
+ if apply_labels:
621
+ # 숫자 인덱스 사용
622
+ numeric_labels = list(range(k))
623
+ df[new_col] = pd.qcut(series, q=k, labels=numeric_labels, duplicates="drop")
624
+ else:
625
+ # 사분위수 위치 기반 문자 레이블(Q1, Q2, ...)
626
+ df[new_col] = pd.qcut(series, q=k, labels=default_q_labels, duplicates="drop")
627
+ except ValueError:
628
+ _, edges = pd.cut(series, bins=k, include_lowest=True, retbins=True)
629
+ # apply_labels=True: 숫자 인덱스 / False: 문자 레이블
630
+ n_bins = len(edges) - 1
631
+ if apply_labels:
632
+ numeric_labels = list(range(n_bins))
633
+ df[new_col] = pd.cut(series, bins=edges, labels=numeric_labels, include_lowest=True, ordered=False)
634
+ else:
635
+ if labels is None:
636
+ position_labels = [f"Q{i+1}" for i in range(n_bins)]
637
+ df[new_col] = pd.cut(
638
+ series, bins=edges, labels=position_labels, include_lowest=True, ordered=False
639
+ )
640
+ else:
641
+ df[new_col] = pd.cut(series, bins=edges, labels=labels, include_lowest=True, ordered=False)
642
+ df[new_col] = df[new_col].astype("category")
643
+ return df
644
+
645
+ # 자연 구간화 (Jenks) - 의존성 없으면 분위수로 폴백
646
+ if method_key in {"natural_breaks", "natural", "jenks"}:
647
+ k = bins if isinstance(bins, int) and bins > 1 else 5
648
+ series_nonnull = series.dropna()
649
+ k = min(k, max(2, series_nonnull.nunique()))
650
+ edges = None
651
+ try:
652
+ edges = jenkspy.jenks_breaks(series_nonnull.to_list(), nb_class=k)
653
+ edges[0] = -np.inf
654
+ edges[-1] = np.inf
655
+ except Exception:
656
+ try:
657
+ use_labels = labels if apply_labels else None
658
+ df[new_col] = pd.qcut(series, q=k, labels=use_labels, duplicates="drop")
659
+ df[new_col] = df[new_col].astype("category")
660
+ return df
661
+ except Exception:
662
+ edges = None
663
+
664
+ if edges:
665
+ # apply_labels=True: 숫자 인덱스 / False: 문자 레이블
666
+ if apply_labels:
667
+ # 숫자 인덱스 사용
668
+ numeric_labels = list(range(len(edges) - 1))
669
+ df[new_col] = pd.cut(series, bins=edges, labels=numeric_labels, include_lowest=True, ordered=False)
670
+ df[new_col] = df[new_col].astype("category")
671
+ else:
672
+ if labels is None:
673
+ auto_labels = []
674
+ for i in range(len(edges) - 1):
675
+ left = f"{edges[i]:.2f}" if edges[i] != -np.inf else "-∞"
676
+ right = f"{edges[i+1]:.2f}" if edges[i+1] != np.inf else "∞"
677
+ # 정수값인 경우 소수점 제거
678
+ try:
679
+ left = str(int(float(left))) if float(left) == int(float(left)) else left
680
+ right = str(int(float(right))) if float(right) == int(float(right)) else right
681
+ except:
682
+ pass
683
+ auto_labels.append(f"{left}~{right}")
684
+ _cut(edges, auto_labels, right=True, ordered=False)
685
+ else:
686
+ _cut(edges, labels, right=True, ordered=False)
687
+ else:
688
+ _, cut_edges = pd.cut(series, bins=k, include_lowest=True, retbins=True)
689
+ if apply_labels:
690
+ # 숫자 인덱스 사용
691
+ numeric_labels = list(range(len(cut_edges) - 1))
692
+ df[new_col] = pd.cut(series, bins=cut_edges, labels=numeric_labels, include_lowest=True, ordered=False)
693
+ else:
694
+ if labels is None:
695
+ auto_labels = []
696
+ for i in range(len(cut_edges) - 1):
697
+ left = f"{cut_edges[i]:.2f}" if cut_edges[i] != -np.inf else "-∞"
698
+ right = f"{cut_edges[i+1]:.2f}" if cut_edges[i+1] != np.inf else "∞"
699
+ # 정수값인 경우 소수점 제거
700
+ try:
701
+ left = str(int(float(left))) if float(left) == int(float(left)) else left
702
+ right = str(int(float(right))) if float(right) == int(float(right)) else right
703
+ except:
704
+ pass
705
+ auto_labels.append(f"{left}~{right}")
706
+ df[new_col] = pd.cut(series, bins=cut_edges, labels=auto_labels, include_lowest=True, ordered=False)
707
+ else:
708
+ df[new_col] = pd.cut(series, bins=cut_edges, labels=labels, include_lowest=True, ordered=False)
709
+ df[new_col] = df[new_col].astype("category")
710
+ return df
711
+
712
+ # 커스텀 경계
713
+ if isinstance(bins, list) and len(bins) >= 2:
714
+ edges = sorted(bins)
715
+ _cut(edges, labels, right=False)
716
+ return df
717
+
718
+ # 기본 폴백: 분위수 4구간
719
+ df[new_col] = pd.qcut(series, q=4, labels=labels, duplicates="drop")
720
+ df[new_col] = df[new_col].astype("category")
721
+ return df
722
+
723
+
442
724
  # ===================================================================
443
725
  # 지정된 변수에 로그 먼저 변환을 적용한다
444
726
  # ===================================================================
hossam/hs_stats.py CHANGED
@@ -2702,3 +2702,155 @@ def predict(fit, data: DataFrame | Series) -> DataFrame | Series | float:
2702
2702
  f"모형 학습 시 사용한 특성과 입력 데이터의 특성이 일치하는지 확인하세요.\n"
2703
2703
  f"원본 오류: {str(e)}"
2704
2704
  )
2705
+
2706
+
2707
+ # ===================================================================
2708
+ # 확장된 기술통계량 (Extended Descriptive Statistics)
2709
+ # ===================================================================
2710
+ def summary(data: DataFrame, *fields: str, columns: list = None):
2711
+ """데이터프레임의 연속형 변수에 대한 확장된 기술통계량을 반환한다.
2712
+
2713
+ 각 연속형(숫자형) 컬럼의 기술통계량(describe)을 구하고, 이에 사분위수 범위(IQR),
2714
+ 이상치 경계값(UP, DOWN), 왜도(skew), 이상치 개수 및 비율, 분포 특성, 로그변환 필요성을
2715
+ 추가하여 반환한다.
2716
+
2717
+ Args:
2718
+ data (DataFrame): 분석 대상 데이터프레임.
2719
+ *fields (str): 분석할 컬럼명 목록. 지정하지 않으면 모든 숫자형 컬럼을 처리.
2720
+ columns (list, optional): 반환할 통계량 컬럼 목록. None이면 모든 통계량 반환.
2721
+
2722
+ Returns:
2723
+ DataFrame: 각 필드별 확장된 기술통계량을 포함한 데이터프레임.
2724
+ 행은 다음과 같은 통계량을 포함:
2725
+
2726
+ - count (float): 비결측치의 수
2727
+ - mean (float): 평균값
2728
+ - std (float): 표준편차
2729
+ - min (float): 최소값
2730
+ - 25% (float): 제1사분위수 (Q1)
2731
+ - 50% (float): 제2사분위수 (중앙값)
2732
+ - 75% (float): 제3사분위수 (Q3)
2733
+ - max (float): 최대값
2734
+ - iqr (float): 사분위 범위 (Q3 - Q1)
2735
+ - up (float): 이상치 상한 경계값 (Q3 + 1.5 * IQR)
2736
+ - down (float): 이상치 하한 경계값 (Q1 - 1.5 * IQR)
2737
+ - skew (float): 왜도
2738
+ - outlier_count (int): 이상치 개수
2739
+ - outlier_rate (float): 이상치 비율(%)
2740
+ - dist (str): 분포 특성 ("극단 우측 꼬리", "거의 대칭" 등)
2741
+ - log_need (str): 로그변환 필요성 ("높음", "중간", "낮음")
2742
+
2743
+ Examples:
2744
+ 전체 숫자형 컬럼에 대한 확장된 기술통계:
2745
+
2746
+ >>> from hossam import summary
2747
+ >>> import pandas as pd
2748
+ >>> df = pd.DataFrame({
2749
+ ... 'x': [1, 2, 3, 4, 5, 100],
2750
+ ... 'y': [10, 20, 30, 40, 50, 60],
2751
+ ... 'z': ['a', 'b', 'c', 'd', 'e', 'f']
2752
+ ... })
2753
+ >>> result = summary(df)
2754
+ >>> print(result)
2755
+
2756
+ 특정 컬럼만 분석:
2757
+
2758
+ >>> result = summary(df, 'x', 'y')
2759
+ >>> print(result)
2760
+
2761
+ Notes:
2762
+ - 숫자형이 아닌 컬럼은 자동으로 제외됩니다.
2763
+ - 결과는 필드(컬럼)가 행으로, 통계량이 열로 구성됩니다.
2764
+ - Tukey의 1.5 * IQR 규칙을 사용하여 이상치를 판정합니다.
2765
+ - 분포 특성은 왜도 값으로 판정합니다.
2766
+ - 로그변환 필요성은 왜도의 절댓값 크기로 판정합니다.
2767
+ """
2768
+ if not fields:
2769
+ fields = data.select_dtypes(include=['int', 'int32', 'int64', 'float', 'float32', 'float64']).columns
2770
+
2771
+ # 기술통계량 구하기
2772
+ desc = data[list(fields)].describe().T
2773
+
2774
+ # 추가 통계량 계산
2775
+ additional_stats = []
2776
+ for f in fields:
2777
+ # 숫자 타입이 아니라면 건너뜀
2778
+ if data[f].dtype not in [
2779
+ 'int',
2780
+ 'int32',
2781
+ 'int64',
2782
+ 'float',
2783
+ 'float32',
2784
+ 'float64',
2785
+ 'int64',
2786
+ 'float64',
2787
+ 'float32'
2788
+ ]:
2789
+ continue
2790
+
2791
+ # 사분위수
2792
+ q1 = data[f].quantile(q=0.25)
2793
+ q3 = data[f].quantile(q=0.75)
2794
+
2795
+ # 이상치 경계 (Tukey's fences)
2796
+ iqr = q3 - q1
2797
+ down = q1 - 1.5 * iqr
2798
+ up = q3 + 1.5 * iqr
2799
+
2800
+ # 왜도
2801
+ skew = data[f].skew()
2802
+
2803
+ # 이상치 개수 및 비율
2804
+ outlier_count = ((data[f] < down) | (data[f] > up)).sum()
2805
+ outlier_rate = (outlier_count / len(data)) * 100
2806
+
2807
+ # 분포 특성 판정 (왜도 기준)
2808
+ abs_skew = abs(skew)
2809
+ if abs_skew < 0.5:
2810
+ dist = "거의 대칭"
2811
+ elif abs_skew < 1.0:
2812
+ if skew > 0:
2813
+ dist = "약한 우측 꼬리"
2814
+ else:
2815
+ dist = "약한 좌측 꼬리"
2816
+ elif abs_skew < 2.0:
2817
+ if skew > 0:
2818
+ dist = "중간 우측 꼬리"
2819
+ else:
2820
+ dist = "중간 좌측 꼬리"
2821
+ else:
2822
+ if skew > 0:
2823
+ dist = "극단 우측 꼬리"
2824
+ else:
2825
+ dist = "극단 좌측 꼬리"
2826
+
2827
+ # 로그변환 필요성 판정
2828
+ if abs_skew < 0.5:
2829
+ log_need = "낮음"
2830
+ elif abs_skew < 1.0:
2831
+ log_need = "중간"
2832
+ else:
2833
+ log_need = "높음"
2834
+
2835
+ additional_stats.append({
2836
+ 'field': f,
2837
+ 'iqr': iqr,
2838
+ 'up': up,
2839
+ 'down': down,
2840
+ 'outlier_count': outlier_count,
2841
+ 'outlier_rate': outlier_rate,
2842
+ 'skew': skew,
2843
+ 'dist': dist,
2844
+ 'log_need': log_need
2845
+ })
2846
+
2847
+ additional_df = DataFrame(additional_stats).set_index('field')
2848
+
2849
+ # 결과 병합
2850
+ result = concat([desc, additional_df], axis=1)
2851
+
2852
+ # columns 파라미터가 지정된 경우 해당 컬럼만 필터링
2853
+ if columns is not None:
2854
+ result = result[columns]
2855
+
2856
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hossam
3
- Version: 0.3.14
3
+ Version: 0.3.16
4
4
  Summary: Hossam Data Helper
5
5
  Author-email: Lee Kwang-Ho <leekh4232@gmail.com>
6
6
  License-Expression: MIT
@@ -1,16 +1,16 @@
1
1
  hossam/NotoSansKR-Regular.ttf,sha256=0SCufUQwcVWrWTu75j4Lt_V2bgBJIBXl1p8iAJJYkVY,6185516
2
- hossam/__init__.py,sha256=_JKBoe04oOsil2xB2Ab4FD9Mat1hm7zngpx2ClJE51g,2609
2
+ hossam/__init__.py,sha256=hJTS1Yl85JqG0iBaVkQcYtguP5f-zO0_wT-ZAouQyzI,2613
3
3
  hossam/data_loader.py,sha256=UpC_gn-xUWij0s-MO51qrzJNz3b5-RCz1N6esQMZUJM,6320
4
4
  hossam/hs_classroom.py,sha256=b2vzxHapxibnJwcRwWvOfLfczjF-G3ZdT9hIUt4z4oU,27407
5
5
  hossam/hs_gis.py,sha256=9ER8gXG2Or0DZ1fpbJR84WsNVPcxu788FsNtR6LsEgo,11379
6
- hossam/hs_plot.py,sha256=U_rBU6PueGZboe0DzhSWUu3FGWrTdghI2XU9lVRuNg8,73966
7
- hossam/hs_prep.py,sha256=sHoirbIXfv594SayqReuaVj_KbfjQD3upQ-VLaX27_w,22580
8
- hossam/hs_stats.py,sha256=a50n8fWOVjsOgKQIzmlkOrAdtDOEzAbNoHvSiIGXc6c,107062
6
+ hossam/hs_plot.py,sha256=7cngzrEeVeBvANyReI9kd3yHZGMOFZjvgBbBGA7rT2E,78467
7
+ hossam/hs_prep.py,sha256=hTHNJvHMVBkV7xthV6igSz0QIKy-bTyVXHzQKblVfQw,36220
8
+ hossam/hs_stats.py,sha256=nhfdZjo4NeyQ3_Pk-xTAN8OLNBDHH8nH9UDtvBNs5AU,112373
9
9
  hossam/hs_timeserise.py,sha256=loRofR-m2NMxHaDEWDhZjo6DwayEf4c7qkSoCErfBWY,42165
10
10
  hossam/hs_util.py,sha256=E4LnzPlRdWeqICv7TtTL9DT5PogqBhOuTgYiaav565U,7461
11
11
  hossam/leekh.png,sha256=1PB5NQ24SDoHA5KMiBBsWpSa3iniFcwFTuGwuOsTHfI,6395
12
- hossam-0.3.14.dist-info/licenses/LICENSE,sha256=nIqzhlcFY_2D6QtFsYjwU7BWkafo-rUJOQpDZ-DsauI,941
13
- hossam-0.3.14.dist-info/METADATA,sha256=eoM_2q6o7rV1gNfuDe93bJnDXoA5_CUfVvOgyvJmVqc,13116
14
- hossam-0.3.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- hossam-0.3.14.dist-info/top_level.txt,sha256=_-7bwjhthHplWhywEaHIJX2yL11CQCaLjCNSBlk6wiQ,7
16
- hossam-0.3.14.dist-info/RECORD,,
12
+ hossam-0.3.16.dist-info/licenses/LICENSE,sha256=nIqzhlcFY_2D6QtFsYjwU7BWkafo-rUJOQpDZ-DsauI,941
13
+ hossam-0.3.16.dist-info/METADATA,sha256=5Yhqjz7S5BTs_OTGaC_l4YzbhQXL8mssrS73hjjCy5s,13116
14
+ hossam-0.3.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ hossam-0.3.16.dist-info/top_level.txt,sha256=_-7bwjhthHplWhywEaHIJX2yL11CQCaLjCNSBlk6wiQ,7
16
+ hossam-0.3.16.dist-info/RECORD,,