06. Feature Engineering 실전 가이드
인코딩, 스케일링, 결측치 처리, 파생변수
학습 목표
이 튜토리얼을 완료하면 다음을 할 수 있습니다:
- 범주형 인코딩: One-Hot, Label, Target, Frequency Encoding의 차이를 이해하고 상황에 맞게 선택
- 수치형 변환: Log 변환, Binning, 다양한 스케일러를 적용하여 데이터 분포 개선
- 결측치 처리: Simple, KNN, Iterative Imputer와 Missing Indicator 활용
- 파생변수 생성: 도메인 지식을 활용한 새로운 feature 생성
- Feature Selection: 상관관계, F-Score, 모델 기반 방법으로 중요 변수 선택
- Pipeline 구축: Data Leakage를 방지하는 안전한 전처리 파이프라인 설계
핵심 개념
왜 Feature Engineering이 중요한가?
머신러닝 성능에 영향을 미치는 요소들:
| 요소 | 영향 |
|---|---|
| 데이터 품질 | 가장 중요 - "Garbage in, garbage out" |
| Feature Engineering | 핵심 요소 - 같은 데이터로 성능 차이를 만듦 |
| 알고리즘 선택 | 중요하지만 feature보다 영향력 낮음 |
| 하이퍼파라미터 | 마지막 미세 조정 단계 |
Kaggle 대회에서 상위권 솔루션들의 공통점: 대부분 비슷한 알고리즘(XGBoost, LightGBM)을 사용하지만, feature engineering에서 차이가 납니다. Andrew Ng: "Applied ML is basically feature engineering."
1. 범주형 인코딩
| 방식 | 설명 | 적합한 상황 |
|---|---|---|
| One-Hot | 이진 벡터 | 범주 수 적을 때, 선형 모델 |
| Label | 정수 매핑 | 트리 모델 |
| Target | 타겟 평균 | 고카디널리티 |
| Frequency | 빈도수 | Leakage 방지 |
One-Hot Encoding
범주형 변수를 이진 벡터로 변환합니다. 범주 간 순서가 없을 때 사용합니다.
# pandas 방식
df_encoded = pd.get_dummies(df, columns=['sex'], drop_first=True)
# sklearn 방식
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse_output=False, drop='first')
encoded = encoder.fit_transform(df[['sex']])
print(f'Feature names: {encoder.get_feature_names_out()}')범주가 많으면 차원 폭발(Curse of Dimensionality)이 발생합니다! 8개 이상의 고유값이 있다면 Target이나 Frequency Encoding을 고려하세요.
Label Encoding
범주를 정수로 매핑합니다. 트리 기반 모델에서 효과적입니다.
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['sex_le'] = le.fit_transform(df['sex'])Label Encoding은 순서 관계를 암시할 수 있습니다. 선형 모델에서는 주의가 필요하지만, 트리 모델에서는 문제없습니다.
Target Encoding (with Smoothing)
범주별 타겟의 평균값으로 인코딩합니다. 고카디널리티 범주에 효과적입니다.
def target_encode_smoothed(df, col, target, m=10):
"""Smoothing으로 과적합 방지"""
global_mean = df[target].mean()
agg = df.groupby(col)[target].agg(['mean', 'count'])
smoothed = (agg['count'] * agg['mean'] + m * global_mean) / (agg['count'] + m)
return df[col].map(smoothed)
# 사용 예시
df['deck_smoothed'] = target_encode_smoothed(df, 'deck', 'survived', m=10)Smoothing 파라미터 m은 샘플이 적은 범주를 전체 평균으로 당깁니다. m=10이 일반적인 시작점입니다.
Frequency Encoding
범주의 출현 빈도로 인코딩합니다. Target 정보를 사용하지 않아 Leakage가 없습니다.
freq_map = df['deck'].value_counts() / len(df)
df['deck_freq'] = df['deck'].map(freq_map)2. 수치형 변환
스케일러 비교
| 스케일러 | 특징 | 사용 시점 |
|---|---|---|
| StandardScaler | 평균=0, 표준편차=1 | 일반적인 경우 |
| MinMaxScaler | [0,1] 범위 | 신경망, 거리 기반 모델 |
| RobustScaler | 중앙값, IQR 사용 | 이상치 존재 시 |
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# 이상치가 있다면 RobustScaler
scaler = RobustScaler()
X_scaled = scaler.fit_transform(X)Log 변환
왜곡된 분포를 정규분포에 가깝게 변환합니다.
# Skewness 확인
print(f'원본 Skewness: {df["fare"].skew():.2f}')
# Log 변환 (0이 포함된 경우 log1p 사용)
df['fare_log'] = np.log1p(df['fare'])
print(f'변환 후 Skewness: {df["fare_log"].skew():.2f}')Skewness가 -1 ~ 1 범위를 벗어나면 Log 변환을 고려하세요. np.log1p()는 log(1+x)로 0값도 안전하게 처리합니다.
Binning (구간화)
연속형 변수를 범주형으로 변환합니다.
# 등간격 (Equal Width)
pd.cut(df['age'], bins=5)
# 등빈도 (Equal Frequency)
pd.qcut(df['age'], q=5)
# 도메인 기반 커스텀 구간
df['age_group'] = pd.cut(df['age'],
bins=[0, 12, 18, 35, 60, 100],
labels=['Child', 'Teen', 'Young', 'Adult', 'Senior'])3. 결측치 처리
| 방법 | 장점 | 단점 |
|---|---|---|
| 평균/중앙값 | 빠름, 간단함 | 분산 감소 |
| KNN Imputer | 변수 간 관계 고려 | 계산 비용 높음 |
| Iterative Imputer | 가장 정교함 | 복잡, 느림 |
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
# 단순 대체
simple_imp = SimpleImputer(strategy='median')
# KNN 기반 (변수 간 관계 고려)
knn_imp = KNNImputer(n_neighbors=5)
# Iterative (가장 정교함)
iter_imp = IterativeImputer(max_iter=10, random_state=42)Missing Indicator
결측 자체가 정보일 수 있습니다. 결측 여부를 별도 feature로 추가합니다.
# 결측 여부 표시
df['age_missing'] = df['age'].isna().astype(int)
# 결측치 대체
df['age'].fillna(df['age'].median(), inplace=True)
# 결측 여부별 타겟 분석
print(df.groupby('age_missing')['survived'].mean())결측이 무작위가 아니라면(예: 고령 승객의 나이 누락) Missing Indicator가 예측에 도움이 될 수 있습니다.
4. 파생변수 생성
도메인 지식을 활용하여 새로운 feature를 만듭니다.
Titanic 예시
# 가족 크기
df['family_size'] = df['sibsp'] + df['parch'] + 1
# 혼자 여행 여부
df['is_alone'] = (df['family_size'] == 1).astype(int)
# 가족 규모 그룹
df['family_group'] = pd.cut(df['family_size'],
bins=[0, 1, 4, 11],
labels=['Alone', 'Small', 'Large'])
# 1인당 요금
df['fare_per_person'] = df['fare'] / df['family_size']
# 연령대
df['age_group'] = pd.cut(df['age'],
bins=[0, 12, 18, 35, 60, 100],
labels=['Child', 'Teen', 'Young', 'Adult', 'Senior'])파생변수의 효과는 모델에 따라 다릅니다. 선형 모델은 비선형 관계를 표현하는 파생변수에서 큰 이득을 얻고, 트리 모델은 이미 비선형성을 처리하므로 효과가 적을 수 있습니다.
5. Feature Selection
다양한 방법 비교
from sklearn.feature_selection import SelectKBest, f_classif, RFE
from sklearn.ensemble import RandomForestClassifier
# 1. 상관계수 기반
corr = X.corrwith(y).abs().sort_values(ascending=False)
print('상관계수 순위:', corr.head())
# 2. F-Score (통계적 유의성)
selector = SelectKBest(score_func=f_classif, k=10)
X_selected = selector.fit_transform(X, y)
scores = pd.Series(selector.scores_, index=X.columns).sort_values(ascending=False)
# 3. RFE (Recursive Feature Elimination)
rfe = RFE(estimator=RandomForestClassifier(), n_features_to_select=10)
X_rfe = rfe.fit_transform(X, y)
# 4. Model-based (Feature Importance)
rf = RandomForestClassifier(n_estimators=100).fit(X, y)
importances = pd.Series(rf.feature_importances_, index=X.columns).sort_values(ascending=False)각 방법은 다른 관점에서 중요도를 측정합니다. 상관계수는 선형 관계만, F-Score는 통계적 유의성을, RF Importance는 예측 기여도를 봅니다. 여러 방법을 함께 활용하세요!
6. Pipeline 구축 (Leakage 방지)
Data Leakage 주의!
잘못된 방식: 전체 데이터 스케일링 후 train/test 분리
올바른 방식: 분리 후 train으로만 fit하고 각각 transform
Pipeline을 사용하면 자동으로 방지됩니다!
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
num_features = ['age', 'sibsp', 'parch', 'fare']
cat_features = ['pclass', 'sex', 'embarked']
# 수치형 전처리
num_transformer = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# 범주형 전처리
cat_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# 결합
preprocessor = ColumnTransformer([
('num', num_transformer, num_features),
('cat', cat_transformer, cat_features)
])
# 전체 파이프라인
pipeline = Pipeline([
('preprocessor', preprocessor),
('classifier', RandomForestClassifier())
])
# 사용
pipeline.fit(X_train, y_train)
score = pipeline.score(X_test, y_test)권장 사항 요약
| 상황 | 권장 방법 |
|---|---|
| 범주 적음 + 선형모델 | One-Hot |
| 트리 모델 | Label/Ordinal |
| 고카디널리티 | Target/Frequency |
| 왜곡 분포 | Log 변환 |
| 이상치 | RobustScaler |
| 결측치 관계 중요 | KNN Imputer |
| Leakage 방지 | Pipeline 필수! |
면접 질문 맛보기
- Target Encoding의 장단점과 주의사항은?
- Feature Scaling은 언제 필요하고 언제 불필요한가요?
- Pipeline을 사용하는 이유는?
더 많은 면접 질문은 Premium Interviews (opens in a new tab)에서 확인하세요.
실습 노트북
노트북에서는 추가로 다음 내용을 실습할 수 있습니다:
- Titanic과 California Housing 데이터셋을 활용한 실전 예제
- 인코딩 방법별 성능 비교 실험 (One-Hot vs Label vs Target)
- Imputation 방법별 정확도 비교
- 파생변수 추가에 따른 모델 성능 변화 측정
- 회귀 문제에서의 Log 변환 효과 비교
- 여러 모델(LogReg, RF, GBM, SVM)을 Pipeline으로 비교
이전: 05. Ensemble Methods | 다음: 07. Clustering