hossam 0.3.15__py3-none-any.whl → 0.3.17__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 +15 -11
- hossam/hs_plot.py +141 -14
- hossam/hs_prep.py +316 -34
- hossam/hs_stats.py +300 -12
- hossam/mcp/__init__.py +12 -0
- hossam/mcp/hs_classroom.py +22 -0
- hossam/mcp/hs_gis.py +30 -0
- hossam/mcp/hs_plot.py +53 -0
- hossam/mcp/hs_prep.py +61 -0
- hossam/mcp/hs_stats.py +25 -0
- hossam/mcp/hs_timeserise.py +22 -0
- hossam/mcp/hs_util.py +30 -0
- hossam/mcp/loader.py +29 -0
- hossam/mcp/server.py +675 -0
- hossam-0.3.17.dist-info/METADATA +205 -0
- hossam-0.3.17.dist-info/RECORD +27 -0
- hossam-0.3.17.dist-info/entry_points.txt +2 -0
- hossam-0.3.15.dist-info/METADATA +0 -636
- hossam-0.3.15.dist-info/RECORD +0 -16
- {hossam-0.3.15.dist-info → hossam-0.3.17.dist-info}/WHEEL +0 -0
- {hossam-0.3.15.dist-info → hossam-0.3.17.dist-info}/licenses/LICENSE +0 -0
- {hossam-0.3.15.dist-info → hossam-0.3.17.dist-info}/top_level.txt +0 -0
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
|
-
#
|
|
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
|
-
>>>
|
|
220
|
-
|
|
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
|
-
|
|
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
|
# ===================================================================
|