1. 데이터 로드
import pandas as pd
import numpy as np
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
submission = pd.read_csv('sample_submission.csv')
2. 검증을 통해 선택한 특성 공학 적용
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.cluster import KMeans
# train 데이터에 대한 특성 공학 수행
train['family_size'] = train['family_size'].apply(lambda x: 5 if x >= 6 else x)
train['income_by_employed_days'] = train.apply(lambda row: 0 if row['days_employed'] == 0 else row['income_total'] / row['days_employed'], axis=1)
kmeans_train = train.drop(['credit'], axis=1)
kmeans = KMeans(n_clusters=36, n_init=10, random_state=42).fit(kmeans_train)
train['cluster'] = kmeans.predict(kmeans_train)
# test 데이터에 대한 특성 공학 수행
test['family_size'] = test['family_size'].apply(lambda x: 5 if x >= 6 else x)
test['income_by_employed_days'] = test.apply(lambda row: 0 if row['days_employed'] == 0 else row['income_total'] / row['days_employed'], axis=1)
test['cluster'] = kmeans.predict(test)
특성 공학을 통해 추가된 피처들은 데이터셋의 다양성과 복잡성을 증가시킵니다.
이렇게 확장된 데이터를 바탕으로 오버샘플링을 수행하면, 모델은 다양한 데이터 패턴을 더욱 효과적으로 학습할 수 있습니다.
또한, 데이터의 차원이 증가함에 따라 모델은 각 데이터 포인트에 과도하게 적응하려는 과적합의 위험이 커집니다.
오버샘플링을 통해 데이터의 양을 늘리면, 이러한 과적합 위험을 상대적으로 완화시킬 수 있습니다.
마지막으로, 특성 공학 후에 오버샘플링을 수행하면, 알고리즘이 기존 데이터의 패턴과 신규 피처의 패턴을 모두 고려하여 보다 정교한 학습 샘플을 생성할 수 있습니다.
이는 오버샘플링의 효과를 극대화하며, 전체 모델 성능을 향상시키는 데 도움을 줄 수 있습니다.
3. 데이터 스케일링
income_total 칼럼의 분포를 조사한 결과, 몇몇 큰 값의 이상치가 발견되었습니다.
이상치의 영향을 최소화하는 동시에 일괄된 값 범위를 유지하기 위한 스케일링 방법으로 RobustScaler를 고려할 수 있습니다.
그러나 RobustScaler를 사용한 경우, 변환된 값의 범위가 일관성을 유지하지 않았습니다.
이 문제를 극복하기 위해, income_total의 분포를 보다 정규 분포에 가깝게 만들기 위해 Box-Cox 변환을 적용할 계획입니다.
Box-Cox 변환을 적용한 후, StandardScaler를 사용하여 전체 데이터의 스케일을 조정하려 합니다.
이러한 방식은 이상치의 영향을 축소하면서도 데이터의 스케일을 균일하게 유지하는 데 도움을 줄 것입니다.
Box-Cox 변환은 데이터를 보다 정규 분포에 가깝게 변환하는 데 사용되는 기법입니다.
이 변환은 주로 비정규분포 데이터를 정규 분포로 변환하는 데 사용되며, 데이터 내 이상치의 영향을 완화하는 효과적입니다.
Box-Cox 변환의 수식은 다음과 같습니다:
y(λ) = { (y^λ - 1) / λ, if λ ≠ 0
log(y), if λ = 0 }
y는 원래의 데이터 값을 의미하고, λ는 변환에 사용되는 파라미터입니다.
Box-Cox 변환의 주요 특징은 다음과 같습니다.
- y는 양수 값을 가져야 합니다. 만약 데이터에 음수 값이나 0이 포함되어 있다면, 양의 상수 값을 더하여 모든 값이 양수가 되도록 조정해야 합니다.
- λ 값은 보통 -5에서 5 사이의 값을 가집니다. 최적의 λ 값은 로그 가능도를 최대화하는 값으로 선택됩니다.
- λ=0인 경우, Box-Cox 변환은 로그 변환과 동일합니다.
- λ 값에 따라 다양한 변환이 가능합니다. 예를 들어, λ=0.5인 경우 제곱근 변환과 동일하고, λ=1인 경우는 원래의 데이터 그대로입니다.
- 변환 후 데이터는 보다 정규 분포에 가까운 형태를 띄게 됩니다.
이로 인해 여러 통계 분석이나 머신 러닝 모델에서 더 좋은 성능을 낼 수 있습니다.
여기서 중요한 점은, 훈련 데이터만 변환할 때 사용하고 테스트 데이터에도 동일하게 적용해야 한다는 점입니다.
훈련 데이터의 λ 값을 계산하여 적용하게 된다면, 데이터 누수(Data Leakage)의 문제가 발생합니다.
따라서, boxcox의 함수를 사용하여 훈련 데이터로 변환할 때 반환된 optimal_lambda 값을 저장하고,
이 값을 사용하여 테스트 데이터에도 동일하게 변환함으로써 데이터 누수 문제를 방지할 수 있습니다.
StandardScaler는 각 피처의 평균을 0, 표준편차를 1로 조정합니다.
이 스케일링은 모든 피처의 크기를 균일하게 만들어주는 데 필수적이며 모델의 결과가 왜곡될 수 있는 것을 방지합니다.
하지만 이미 이상치를 처리한 데이터이기 때문에, StandardScaler를 안정하게 사용할 수 있습니다.
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from scipy.stats import boxcox
# 정규분포 변환
# optimal_lambda는 최적의 람다값을 의미함 -> 모델이 자동으로 최적의 람다를 계산하여 반환
# optimal_lambda를 사용하여 test 데이터에 대해 box-cox 변환을 할 경우 데이터 누수 없이 변환할 수 있음
train['income_total'], optimal_lambda = boxcox(train['income_total'])
test['income_total'] = boxcox(test['income_total'], lmbda = optimal_lambda)
# box-cox 변환 후 분포 확인
plt.hist(train['income_total'])
plt.show()
# 훈련 데이터에서 target 칼럼인 'credit' 제외
x_train = train.drop('credit', axis=1)
y_train = train['credit']
column_list = x_train.columns
# 스케일링
scaler = StandardScaler()
x_train_scaled = pd.DataFrame(scaler.fit_transform(x_train), columns = column_list)
x_test_scaled = pd.DataFrame(scaler.transform(test), columns = column_list)
x_train_scaled.head()
4. 오버샘플링 방법 선택하기
SMOTE (Synthetic Minority Over-sampling Technique)는 소수 클래스의 데이터 포인트를 선택하고 해당 데이터 포인트의 k-최근접 이웃 중 하나를 무작위로 선택합니다.
선택된 두 데이터 포인트 사이의 선분상에 위치하는 가상의 데이터 포인트를 생성하고 소수 클래스의 데이터를 합성하여 균형을 맞춥니다.
SMOTE는 소수 클래스 주변의 공간을 확장시키기 때문에, 과적합 위험이 줄어듭니다.
그러나, 잘못된 합성 데이터를 생성할 수도 있기 때문에 주의하여야 합니다.
ADASYN (Adaptive Synthetic Sampling)은 SMOTE와 유사하게 작동하지만, 소수 클래스 데이터 주변의 데이터 밀도에 따라 합성 데이터를 생성합니다.
즉, 소수 클래스 데이터 포인트 주변의 다수 클래스 데이터 포인트가 많을수록 더 많은 합성 데이터를 생성합니다.
이 방식은 데이터 불균형이 심한 영역에서 더 많은 주의를 기울여, 모델의 예측력을 더 향상하도록 돕습니다.
ADASYN은 합성 데이터 생성 시 노이즈를 추가할 수 있기 때문에, SMOTE보다 더 다양한 합성 데이터를 생성할 수 있습니다.
from imblearn.over_sampling import SMOTE, ADASYN
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import log_loss
# 데이터 분리: 학습 데이터와 검증 데이터
X_train_split, X_val_split, y_train_split, y_val_split = train_test_split(x_train_scaled, y_train, test_size=0.2, random_state=42)
# SMOTE를 사용한 오버샘플링
smote = SMOTE(random_state=42)
X_train_smote, y_train_smote = smote.fit_resample(X_train_split, y_train_split)
# ADASYN을 사용한 오버샘플링
adasyn = ADASYN(random_state=42)
X_train_adasyn, y_train_adasyn = adasyn.fit_resample(X_train_split, y_train_split)
# RandomForest 분류기 학습 및 검증
smote_rf = RandomForestClassifier(random_state=42)
adasyn_rf = RandomForestClassifier(random_state=42)
# SMOTE
smote_rf.fit(X_train_smote, y_train_smote)
smote_proba = smote_rf.predict_proba(X_val_split)
smote_logloss = log_loss(y_val_split, smote_proba)
# ADASYN
adasyn_rf.fit(X_train_adasyn, y_train_adasyn)
adasyn_proba = adasyn_rf.predict_proba(X_val_split)
adasyn_logloss = log_loss(y_val_split, adasyn_proba)
print("SMOTE Log Loss 점수는 : ", smote_logloss)
print("ADASYN Log Loss 점수는 : ", adasyn_logloss)
SMOTE Log Loss 점수는 : 1.282533004610935
ADASYN Log Loss 점수는 : 1.2825606978460544
※ 결과 해석
SMOTE를 사용한 오버샘플링 후 학습한 모델의 로그 손실 값은 1.2724입니다.
ADASYN을 사용한 오버샘플링 후 학습한 모델의 로그 손실 값은 1.2832입니다.
이 두 값의 차이를 비교하면, SMOTE 방법을 사용한 경우가 ADASYN 방법에 비해 더 낮은 로그 손실 값을 가지므로,
이 데이터셋과 모델에 있어서 SMOTE 방법이 더 좋은 성능을 보이는 것으로 판단됩니다.
즉, 주어진 데이터와 RandomForest 분류기를 사용할 때, SMOTE 방법을 사용하는 것이 ADASYN 방법보다 더 정확한 확률 예측을 제공하며, 전반적인 모델 성능도 더 우수하다고 해석할 수 있습니다.
따라서 SMOTE 방법을 사용하여 데이터 증강을 진행하였습니다.
5. SMOTE를 활용한 데이터 오버샘플링
# SMOTE를 사용한 오버샘플링
smote = SMOTE(random_state=42)
x_train_over, y_train_over = smote.fit_resample(x_train_scaled, y_train)
이제 데이터의 불균형 문제를 해결했으므로, 다음 단계로는 피처 선택을 진행하려고 합니다.
피처 선택과 오버샘플링의 순서는 상황에 따라 선택됩니다.
- 피처 선택 후 오버샘플링을 할 경우
불필요한 피처를 제거한 상태에서 오버샘플링을 수행하면 노이즈의 영향을 줄여 오버샘플링의 효과를 극대화할 수 있습니다. - 오버샘플링 후 피처 선택을 할 경우
데이터의 불균형이 조정된 상태에서 피처 중요도를 평가하게 되며, 오버샘플링으로 생성된 데이터 포인트를 고려하여 더욱 견고한 피처 선택 기준을 설정할 수 있습니다.
현재 데이터셋은 다양한 피처를 포함하고 있어 모든 피처를 모델 학습에 사용하는 것은 과적합의 위험이 있습니다.
따라서 피처 선택 과정을 통해 모델의 일반화 성능을 향상시키고 학습 속도를 높이는 것이 중요합니다.
오버샘플링 후 피처 선택을 진행하는 이유는, 데이터의 불균형을 먼저 조정함으로써 새로 생성된 데이터 포인트의 특성을 반영한 상태에서 피처의 중요도를 평가하고자 하기 때문입니다.
오버샘플링으로 인해 데이터의 패턴이나 분포가 약간 변경될 수 있으므로, 이를 고려한 상태에서 피처를 평가하는 것이 더 신뢰성 있는 선택이 가능합니다.
이제 후진소거법(Backward Elimination)을 활용하여 피처를 선택하겠습니다.
후진소거법은 피처를 하나씩 제거하며 최적의 피처 조합을 찾아가는 방법입니다.
이 방법을 통해, 모델의 성능을 최대로 향상하는 핵심 피처들을 선별할 수 있습니다.
6. 랜덤 포레스트를 사용한 RFECV 피처 선택 및 중요도 시각화
후진 소거법은 피처 선택 방법 중 하나로, 모든 피처를 포함한 상태에서 시작하여, 반복적으로 가장 중요도가 낮은 피처를 하나씩 제거해 나가는 방법입니다.
불필요한 피처를 제거함으로써 모델을 간단하게 만드는 데, 이는 모델의 해석을 용이하게 하고, 과적합의 위험을 줄여주기도 합니다.
또한 불필요한 피처나 잡음을 포함하는 피처를 제거함으로써 모델의 예측 성능을 향상시킬 수 있습니다.
RFECV는 재귀적 피처 제거 방법과 교차 검증을 결합한 피처 선택 방법입니다.
RFECV는 모든 피처를 사용하여 모델을 학습시킨 후, 피처 중요도나 계수 등을 기반으로 가장 중요도가 낮은 피처를 하나씩 제거하면서 모델 성능 변화를 관찰합니다.
각 피처 조합에 대한 모델 성능은 교차 검증을 통해 평가되며, 이를 통해 피처를 제거한 후의 모델이 일반화 성능을 얼마나 유지하는지 확인할 수 있습니다.
교차 검증을 통한 성능 평가 결과를 바탕으로, 최적의 피처 조합을 자동으로 선택합니다.
이 과정에서 사용자가 설정한 최소 피처 수를 고려하여 선택됩니다.
RFECV를 사용하는 주요 이유는, 수동으로 피처를 제거하는 것이 아니라 자동화된 프로세스를 통해 최적의 피처 조합을 찾기 위해서입니다.
또한, 교차 검증을 통해 피처 제거 영향을 좀 더 정확하게 평가하며, 과적합의 위험을 줄여줍니다.
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_selection import RFECV
from sklearn.metrics import make_scorer, log_loss
import time
start = time.time()
# 랜덤 포레스트 분류기 설정
# rf = RandomForestClassifier(n_estimators=20, random_state=42)
rf = RandomForestClassifier(n_estimators=5, random_state=42)
# 사용자 정의 스코어 함수 생성
neg_log_loss = make_scorer(log_loss, greater_is_better=False, needs_proba=True)
# RFECV 객체 생성 (10개 이상의 피처를 선택하도록 설정)
# selector = RFECV(estimator=rf, step=1, cv=5, scoring=neg_log_loss, min_features_to_select=10)
selector = RFECV(estimator=rf, step=4, cv=3, scoring=neg_log_loss, min_features_to_select=10)
# fit 메서드를 사용하여 피처 선택 수행
selector = selector.fit(x_train_over, y_train_over)
# 선택된 피처들의 실제 이름
selected_feature_names = x_train_over.columns[selector.support_]
print("피처의 수:", selector.n_features_)
print("선택된 피처의 이름:", selected_feature_names)
# 피처 중요도 가져오기
feature_importances = selector.estimator_.feature_importances_
# 피처 중요도와 선택된 피처 이름을 함께 정렬
sorted_idx = np.argsort(feature_importances)[::-1]
sorted_names = selected_feature_names[sorted_idx]
plt.figure(figsize=(12,8))
plt.barh(range(selector.n_features_), feature_importances[sorted_idx], align='center')
plt.yticks(range(selector.n_features_), sorted_names)
plt.xlabel('Importance')
plt.ylabel('Feature')
plt.title('Feature Importances of Selected Features')
plt.gca().invert_yaxis() # 높은 중요도가 위에 오도록 y축을 뒤집음
plt.show()
print(f'소요된 시간(초): , {time.time() - start}')
※ 결과 해석
begin_month가 중요도가 가장 높다는 것은 해당 피처가 타겟 변수를 예측하는 데 중요한 역할을 한다는 것을 의미합니다.
이는 고객이 현재까지 얼마나 오랜 기간 동안 카드를 유지하고 있는지에 따라 그들의 신용 등급이 영향을 받을 수 있다는 것을 시사합니다.
중요도가 극히 떨어지는 피처가 있다면, 이는 해당 피처가 타겟 변수를 예측하는 데 상대적으로 덜 중요하다는 것을 의미합니다.
모든 피처가 동일한 중요도를 갖지 않기 때문에 이런 현상은 자주 발생합니다.
피처 중요도를 바탕으로 한 선택으로 적은 수의 피처가 제거되었으므로, 여전히 과적합의 우려가 있습니다.
모델이 훈련 데이터에 너무 잘 맞아 실제 데이터에서는 일반화 성능이 떨어질 수 있기 때문에, 과적합을 피하기 위한 추가적인 조치가 필요합니다.
따라서 피처 중요도와 교차 검증 점수를 함께 고려하여 최적의 피처 조합을 선택하고자 합니다.
7. 피처 중요도를 활용한 최적 피처 선택 및 교차 검증 점수 시각화
- feature_importances 배열의 값을 오름차순으로 정렬하여 thresholds 변수에 저장합니다.
- 각 임계값에 대해 교차 검증 점수를 저장하기 위한 빈 리스트 scores를 생성합니다.
- 첫 번째 검증 점수를 저장하기 위한 best_score 변수를 음의 무한대로 초기화합니다.
초기 최적의 임계값을 저장하기 위한 best_threshold 변수와 선택된 피처 개수를 저장할 best_feature_count 변수를 None과 0으로 초기화합니다. - thresholds에 포함된 각 임계값에 대하여 다음을 수행합니다.
- 해당 임계값보다 중요도가 큰 피처를 선택하고, 이를 select_feature()에 저장합니다.
- select_feature의 길이가 0이 아닌 경우, 선택된 피처와 데이터에 대해 임계값에서의 분류를 실행합니다.
- x_train_over에서 선택된 피처만을 추출하여 x_train_selected에 저장합니다.
모델 테스트를 분류기(RF)로 사용하여 3-fold 교차 검증을 수행하고, neg_log_loss를 사용하여 점수를 계산합니다.
계산된 점수를 scores 리스트에 추가합니다. - 계산된 점수가 best_score보다 좋으면, best_score, best_threshold, best_feature_count를 갱신합니다.
위의 순서대로 코드를 실행시키며 임계값에 따라 피처를 제거했을 때의 각 상황에 따른 검증 점수와 log_loss를 비교합니다.
neg_log_loss를 사용하여 부호가 바뀌었지만, 0에 가까울수록 성능이 좋다는 것은 일반적인 log_loss 평가와 동일합니다.
from sklearn.model_selection import cross_val_score
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
import time
from sklearn.ensemble import RandomForestClassifier # RandomForestClassifier 임포트
start = time.time()
# 중요도 임계값 설정 - 임계값이 오름차순으로 정렬됩니다.
thresholds = np.sort(feature_importances)
# 각 임계값에 따른 검증 점수 저장
scores = []
# 최적의 검증 점수와 해당 시점의 임계값 저장
best_score = float('-inf')
best_threshold = None
best_feature_count = 0
for threshold in tqdm(thresholds):
# 임계값보다 중요도가 큰 피처 선택
select_feature = selected_feature_names[feature_importances > threshold]
# 선택된 피처가 없는 경우 검증 점수 계산 스킵
if len(select_feature) == 0:
continue
# RandomForestClassifier 모델을 매 반복마다 새로 생성
# rf = RandomForestClassifier(n_estimators=20) # 전체 성능을 확인하고 싶을 경우 이 주석을 해제하고, 아래 모델 정의 부분에 주석을 설정해주세요.
rf = RandomForestClassifier(n_estimators=5) # 모델 정의
# 데이터에서 해당 피처만 추출
x_train_selected = x_train_over[select_feature]
# 교차 검증을 통한 점수 계산
score = cross_val_score(rf, x_train_selected, y_train_over, cv=3, scoring='neg_log_loss').mean()
scores.append(score)
# 최적의 검증 점수 업데이트
if score > best_score:
best_score = score
best_threshold = threshold
best_feature_count = len(select_feature)
# 검증 점수 시각화
plt.figure(figsize=(12,6))
plt.plot(thresholds[:len(scores)], scores, marker='o')
plt.xlabel("Feature Importance Threshold")
plt.ylabel("Mean CV Score")
plt.title("Mean CV Score vs. Feature Importance Threshold")
plt.show()
print(f"최적의 임계값: {best_threshold}")
print(f"해당 임계값에서의 검증 점수: {best_score}")
print(f"선택된 피처의 개수: {best_feature_count}")
print(f"선택된 피처: {selected_feature_names[feature_importances > best_threshold]}")
print(f'소요된 시간(초): , {time.time() - start}')
※ 결과 해석
로그 손실(log loss)은 예측 확률과 실제 값 사이의 차이를 측정하는 지표로, 0에 가까울수록 모델의 예측 성능이 우수함을 나타냅니다.
코드 실행 결과로 얻은 검증 점수는 neg_log_loss를 사용하였기 때문에 음수 값으로 표현되며,
neg_log_loss도 로그 손실과 마찬가지로 0에 가까울수록 모델의 예측 성능이 우수함을 나타냅니다.
코드를 실행한 결과, RFECV를 사용했을 때보다 더 적은 피처가 선택되었습니다.
선택된 피처를 활용하여 최종적으로 credit를 분류하는 모델을 개발할 것입니다.
8. 선택된 피처를 활용한 RandomForest와 XGBoost 기반의 소프트 보팅 앙상블
from sklearn.ensemble import VotingClassifier
from xgboost import XGBClassifier
start = time.time()
# RFECV를 통해 얻은 피처 중요도에 따라 최적의 임계값을 초과하는 피처들만 선택
final_use_cols = selected_feature_names[feature_importances > best_threshold]
x_train_final = x_train_over[final_use_cols]
y_final = y_train_over
# 1번 모델
rf = RandomForestClassifier(n_estimators=5, random_state=42)
# 2번 모델
xgb = XGBClassifier(n_estimators = 5, random_state=42)
# Soft Voting 기반의 앙상블 모델 생성
soft_voting_clf = VotingClassifier(
estimators=[('randomforest', rf), ('xgboost', xgb)],
voting='soft'
)
# 교차 검증을 사용하여 Soft Voting 앙상블 모델의 검증 점수 계산
soft_voting_scores = cross_val_score(soft_voting_clf, x_train_final, y_final, cv=5, scoring='neg_log_loss')
print("Soft Voting Cross Val Score:", soft_voting_scores.mean())
print(f'소요된 시간(초): , {time.time() - start}')
Soft Voting Cross Val Score: -0.725579440173478
소요된 시간(초): , 2.3650267124176025
9. 테스트 데이터셋에 대한 Soft Voting 앙상블 예측 및 제출 파일 생성
# x_train_final에서 사용된 피처와 동일한 피처를 x_test_scaled에서 선택
x_test_final = x_test_scaled[final_use_cols]
# Soft Voting 모델 학습
soft_voting_clf.fit(x_train_final, y_final)
# x_test_final 데이터셋에 대해 soft_voting_clf 모델을 사용하여 credit 칼럼의 각 클래스에 대한 예측 확률 값을 계산합니다.
soft_voting_predictions = soft_voting_clf.predict_proba(x_test_final)
# 예측된 결과(soft_voting_predictions)를 submission 데이터프레임의 2번째 칼럼부터 끝까지에 할당합니다.
# 이는 각 클래스(0, 1, 2)에 대한 확률 값을 저장하는 부분입니다.
submission.iloc[:,1:] = soft_voting_predictions
# 수정된 submission 데이터프레임의 상위 5개 행을 출력하여 확인합니다.
submission.head()
'머신러닝 > 머신러닝: 실전 프로젝트 학습' 카테고리의 다른 글
아파트 경매 가격 예측 프로젝트 2 : EDA (0) | 2025.01.07 |
---|---|
아파트 경매 가격 예측 프로젝트 1 : 데이터 분석 및 기본 예측 (0) | 2025.01.07 |
신용카드 사용자 연체 예측 프로젝트 4 : 특성 공학 및 검증 점수 확인 (3) | 2025.01.06 |
신용카드 사용자 연체 예측 프로젝트 3 : 데이터 인코딩 (1) | 2025.01.04 |
신용카드 사용자 연체 예측 프로젝트 2 : 범주형 변수 EDA 및 결측값 대체 (0) | 2025.01.04 |