--- name: feature-engineering-cookbook description: "피처 엔지니어링 기법 카탈로그: 수치형/범주형/시계열/텍스트 변환, 피처 선택, 피처 스토어 설계. '피처 엔지니어링', '특성 공학', '변수 변환', '인코딩', '스케일링', '피처 선택', '피처 스토어', '피처 중요도' 등 데이터 전처리 및 피처 설계 시 이 스킬을 사용한다. data-engineer의 피처 엔지니어링 역량을 강화한다. 단, 모델 설계나 학습 관리는 이 스킬의 범위가 아니다." --- # Feature Engineering Cookbook — 피처 엔지니어링 기법 카탈로그 데이터 타입별 변환 기법, 피처 선택 방법, 피처 스토어 설계 가이드. ## 수치형 변환 ### 스케일링 | 방법 | 공식 | 적합 | 부적합 | |------|------|------|--------| | StandardScaler | (x - μ) / σ | 정규분포, SVM, 로지스틱 회귀 | 이상치 민감 | | MinMaxScaler | (x - min) / (max - min) | [0,1] 필요, 신경망 | 이상치 민감 | | RobustScaler | (x - Q2) / (Q3 - Q1) | 이상치 존재 | — | | PowerTransformer | Box-Cox / Yeo-Johnson | 왜도 큰 분포 | 음수값(Box-Cox) | | QuantileTransformer | 분위수 기반 | 균일/정규 분포 변환 | 순서 관계 파괴 | ### 이산화 (Binning) ```python # 등간격 (Equal Width) pd.cut(df['age'], bins=5) # 등빈도 (Equal Frequency) pd.qcut(df['income'], q=5) # 도메인 기반 bins = [0, 18, 30, 50, 65, 100] labels = ['미성년', '청년', '중년', '장년', '노년'] pd.cut(df['age'], bins=bins, labels=labels) ``` ### 수학적 변환 ```python # 로그 변환 (오른쪽 꼬리 분포) df['log_income'] = np.log1p(df['income']) # 제곱근 (카운트 데이터) df['sqrt_count'] = np.sqrt(df['count']) # 역수 (반비례 관계) df['inv_distance'] = 1 / (df['distance'] + 1) ``` ## 범주형 인코딩 | 방법 | 카디널리티 | 순서 | 트리 모델 | 선형 모델 | |------|-----------|------|----------|----------| | Label Encoding | 무관 | 있음 | ✅ | ❌ | | One-Hot Encoding | 낮음(<20) | 없음 | ✅ | ✅ | | Target Encoding | 높음 | N/A | ✅ | ✅ | | Frequency Encoding | 높음 | N/A | ✅ | ✅ | | Binary Encoding | 중간 | N/A | ✅ | ✅ | | Ordinal Encoding | 무관 | 있음 | ✅ | ✅ | ### Target Encoding (과적합 방지) ```python from sklearn.model_selection import KFold def target_encode_cv(train, col, target, n_folds=5): """K-Fold 기반 타깃 인코딩 — 데이터 누수 방지""" global_mean = train[target].mean() encoded = pd.Series(index=train.index, dtype=float) kf = KFold(n_splits=n_folds, shuffle=True, random_state=42) for train_idx, val_idx in kf.split(train): means = train.iloc[train_idx].groupby(col)[target].mean() encoded.iloc[val_idx] = train.iloc[val_idx][col].map(means) encoded.fillna(global_mean, inplace=True) return encoded ``` ## 시계열 피처 ```python # 날짜 분해 df['year'] = df['date'].dt.year df['month'] = df['date'].dt.month df['dayofweek'] = df['date'].dt.dayofweek df['is_weekend'] = df['dayofweek'].isin([5, 6]).astype(int) df['hour'] = df['date'].dt.hour df['is_business_hour'] = df['hour'].between(9, 18).astype(int) # 순환 인코딩 (월, 시간 등 주기적 변수) df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12) df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12) # Lag 피처 df['sales_lag_1'] = df['sales'].shift(1) df['sales_lag_7'] = df['sales'].shift(7) # Rolling 통계 df['sales_ma_7'] = df['sales'].rolling(7).mean() df['sales_std_7'] = df['sales'].rolling(7).std() ``` ## 피처 선택 방법 ### 필터 방법 | 방법 | 수치→수치 | 범주→수치 | 수치→범주 | |------|----------|----------|----------| | 상관계수 (Pearson) | ✅ | — | — | | 상호정보량 (MI) | ✅ | ✅ | ✅ | | 카이제곱 | — | — | ✅ | | ANOVA F-test | — | — | ✅ | | 분산 기반 | ✅ (분산=0 제거) | — | — | ### 래퍼/임베디드 방법 ```python # 트리 기반 피처 중요도 from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier().fit(X, y) importances = pd.Series(model.feature_importances_, index=X.columns) top_features = importances.nlargest(20).index # Permutation Importance (모델 무관) from sklearn.inspection import permutation_importance result = permutation_importance(model, X_test, y_test, n_repeats=10) # SHAP (해석 가능한 피처 중요도) import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test) ``` ## 결측치 처리 의사결정 ``` 결측 비율 확인 ├── < 5%: 제거 또는 단순 대체(평균/중앙값/최빈값) ├── 5~30%: 모델 기반 대체 (KNN, MICE, 트리 기반) ├── 30~50%: 결측 자체를 피처로 + 대체 │ df['col_missing'] = df['col'].isna().astype(int) └── > 50%: 컬럼 제거 고려 (비즈니스 중요도 확인) ``` ## 데이터 누수 방지 체크리스트 - [ ] 타깃 변수에서 파생된 피처가 없는가? - [ ] 미래 정보를 사용하는 피처가 없는가? - [ ] train/test 분할 전에 인코딩/스케일링을 하지 않았는가? - [ ] Target Encoding에 CV를 적용했는가? - [ ] 시계열 데이터에서 미래 데이터를 참조하지 않는가?