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/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
  # ===================================================================