1. 극단치 대체 및 인코딩
# 극단치 대체
outlier_employed = train['days_employed'].unique().min()
train.loc[(train['days_employed'] == outlier_employed), 'days_employed'] = 0
test.loc[(test['days_employed'] == outlier_employed), 'days_employed'] = 0
# 이진 범주형 값 인코딩
binary_col = ['gender', 'car', 'reality']
for column in binary_col:
unique_values = train[column].unique()
value_mapping = {value: idx for idx, value in enumerate(unique_values)}
train[column] = train[column].replace(value_mapping)
test[column] = test[column].replace(value_mapping)
#순서가 있는 범주형 피처 인코딩
edu_order = ['Lower secondary', 'Secondary / secondary special', 'Incomplete higher', 'Higher education', 'Academic degree']
# 학력 순서에 따른 인코딩 딕셔너리 생성 및 인코딩
edu_mapping = {edu: idx for idx, edu in enumerate(edu_order)}
train['edu_type'] = train['edu_type'].replace(edu_mapping)
test['edu_type'] = test['edu_type'].replace(edu_mapping)
# 바이너리 인코딩 수행
import category_encoders as ce
# 선택할 범주형 칼럼들
categorical_cols = train.select_dtypes(include='object').columns.tolist()
encoder = ce.BinaryEncoder(cols=categorical_cols)
binary_train = encoder.fit_transform(train[categorical_cols])
binary_test = encoder.transform(test[categorical_cols])
train = pd.concat([train, binary_train], axis=1)
test = pd.concat([test, binary_test], axis=1)
categorical_cols.extend(['index', 'flag_mobil'])
train.drop(categorical_cols, axis=1, inplace=True)
test.drop(categorical_cols, axis=1, inplace=True)
# 데이터 확인
train.head()
2. 기본 RandomForest 모델을 사용한 Stratified K-Fold 교차 검증
이 베이스라인은 이후 진행되는 모든 개선 사항의 효과를 평가하는 기준점으로 사용됩니다.
피처 엔지니어링, 모델 변경, 하이퍼파라미터 최적화 등의 다양한 단계를 거치면서 베이스라인보다 더 나은 성능을 내는 모델을 목표로 합니다.
각 단계에서의 변경 사항은 검증 점수를 통해 평가되며, 해당 방법의 적용 여부를 결정합니다.
검증을 위해 RandomForest모델과 k-fold 검증 방법을 사용합니다.
이 방법은 데이터를 k개의 부분집합으로 나누고, 각 부분집합을 검증 데이터로 사용하는 동안 나머지 부분집합을 학습 데이터로 사용하여 모델을 k번 학습 및 검증하는 방법입니다.
이 방법은 모델의 일반화 성능을 평가하는 데 주로 사용됩니다.
검증에 사용될 기본 모델을 구축할 때, 스케일링을 하지 않는 것이 좋습니다.
왜냐하면 스케일링은 데이터의 원래 분포와 범위를 변경시키는 과정이기 때문에, 이를 통해 얻은 모델의 성능이 직관적으로 이해하기 어려울 수 있습니다.
기본 모델은 데이터의 원래 상태에서 얼마나 잘 작동하는지를 확인하는 것이 주 목적이므로, 스케일링 없이 모델의 성능을 확인하는 것이 바람직합니다.
랜덤 포레스트는 결정 트리(decision tree)를 기반으로 하는 알고리즘입니다.
결정 트리는 스케일링을 통해 값의 범위가 변경되더라도, 결정 트리 가 만드는 구조 자체에는 영향을 주지 않습니다.
따라서 랜덤 포레스트와 같은 트리 기반 모델을 사용할 때 스케일링을 사용하지 않고 모델을 학습합니다.
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import log_loss
# 데이터 나누기
copy_train = train.copy()
train_data, valid_data = train_test_split(copy_train, stratify=copy_train['credit'], test_size=0.2, random_state=42)
x_train = train_data.drop('credit', axis=1)
y_train = train_data['credit']
x_valid = valid_data.drop('credit', axis=1)
y_valid = valid_data['credit']
# K-Fold 설정
skf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
log_losses = []
for train_idx, valid_idx in skf.split(x_train, y_train):
x_train_fold, x_valid_fold = x_train.iloc[train_idx], x_train.iloc[valid_idx]
y_train_fold, y_valid_fold = y_train.iloc[train_idx], y_train.iloc[valid_idx]
model = RandomForestClassifier(n_estimators=50, random_state=42)
model.fit(x_train_fold, y_train_fold)
pred_probs = model.predict_proba(x_valid_fold)
log_losses.append(log_loss(y_valid_fold, pred_probs))
average_log_loss = sum(log_losses) / len(log_losses)
print(f"평균 Log Loss의 값 : {average_log_loss}")
평균 Log Loss의 값 : 1.193436676150756
다중 클래스 로그 손실(multi-class log loss)은 예측의 정확성을 평가하는 지표 중 하나로, 예측한 확률의 불확실성을 측정합니다.
쉽게 말해, 우리가 결과를 예측할 때 그 결과가 얼마나 확실한지, 혹은 얼마나 불확실한지를 나타내주는 것이 로그 손실입니다.
로그 손실은 예측 확률과 실제 결과 간의 차이를 기반으로 합니다.
예측이 정확할수록 로그 손실 값은 작아지며, 부정확할수록 값은 커집니다.
이렇게 로그 손실은 단순한 정확도보다 더 깊은 차원에서 예측의 정확성과 확신도를 함께 평가하는 중요한 지표라고 할 수 있습니다.
요약하자면, log loss는 '내가 얼마나 확신하고 예측했는지'와 '그 확신이 얼마나 정확했는지'를 동시에 측정하는 지표입니다.
※ 결과 해석
검증을 통한 로그 손실 점수는 약 1.193로 나타났습니다.
이 점수를 베이스라인으로 설정하고, 향후 피처 추가나 모델 개선을 진행하면서 이 점수보다 낮아질 경우 해당 피처나 전략을 포함하려 합니다.
3. family_size 값 조정 검증 결과 확인 (6 이상인 경우 5로 변경)
# 데이터 나누기
copy_train = train.copy()
# family_size 조정
copy_train['family_size'] = copy_train['family_size'].apply(lambda x: 5 if x >= 6 else x)
train_data, valid_data = train_test_split(copy_train, stratify=copy_train['credit'], test_size=0.2, random_state=42)
x_train = train_data.drop('credit', axis=1)
y_train = train_data['credit']
x_valid = valid_data.drop('credit', axis=1)
y_valid = valid_data['credit']
# K-Fold 설정
skf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
log_losses = []
for train_idx, valid_idx in skf.split(x_train, y_train):
x_train_fold, x_valid_fold = x_train.iloc[train_idx], x_train.iloc[valid_idx]
y_train_fold, y_valid_fold = y_train.iloc[train_idx], y_train.iloc[valid_idx]
model = RandomForestClassifier(n_estimators=50, random_state=42)
model.fit(x_train_fold, y_train_fold)
pred_probs = model.predict_proba(x_valid_fold)
log_losses.append(log_loss(y_valid_fold, pred_probs))
average_log_loss = sum(log_losses) / len(log_losses)
print(f"평균 Log Loss의 값 : {average_log_loss}")
평균 Log Loss의 값 : 1.1830073558533702
※ 결과 해석
로그손실 점수가 약 1.183으로 확인되었습니다.
이는 이전의 베이스라인 모델에서의 로그손실 점수 1.1934보다 개선된 결과를 보여줍니다.
이를 통해 family_size 특성을 조정한 것이 모델의 성능 향상에 기여했다고 볼 수 있습니다.
이 피처의 수정이 긍정적인 결과를 가져왔지만 이 피처를 실제 데이터에 적용하여 모델링할 때도 동일한 효과를 기대할 수 있을지는 확인을 하여야 합니다.
피처를 여러 가지 추가하려 할 경우, 모든 피처를 한 번에 입력으로 사용하는 것이 아니라 각 피처가 모델의 성능에 어떠한 영향을 미치는지 개별적으로 평가하는 것이 중요합니다.
단순히 피처의 수를 늘리는 것이 성능 향상의 확실한 방법은 아니기 때문에, 각 피처의 중요도와 그에 따른 성능 변화를 주의 깊게 관찰하여 최종 모델에 포함할 피처를 결정해야 합니다.
4. 나이 구간화를 적용한 검증 결과 확인
# 데이터 나누기
copy_train = train.copy()
train_data, valid_data = train_test_split(copy_train, stratify=copy_train['credit'], test_size=0.2, random_state=42)
x_train = train_data.drop('credit', axis=1)
y_train = train_data['credit']
x_valid = valid_data.drop('credit', axis=1)
y_valid = valid_data['credit']
# train 데이터를 기반으로 bins를 생성합니다.
min_age = x_train['age'].min()
max_age = x_train['age'].max()
bins = list(range(min_age - 10, max_age + 10, 10))
labels = range(len(bins)-1)
# 생성된 bins를 train과 test 데이터에 적용합니다.
x_train['age_group'] = pd.cut(x_train['age'], bins=bins, labels=labels, right=False)
x_valid['age_group'] = pd.cut(x_valid['age'], bins=bins, labels=labels, right=False)
# K-Fold 설정
skf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
log_losses = []
for train_idx, valid_idx in skf.split(x_train, y_train):
x_train_fold, x_valid_fold = x_train.iloc[train_idx], x_train.iloc[valid_idx]
y_train_fold, y_valid_fold = y_train.iloc[train_idx], y_train.iloc[valid_idx]
model = RandomForestClassifier(n_estimators=50, random_state=42)
model.fit(x_train_fold, y_train_fold)
pred_probs = model.predict_proba(x_valid_fold)
log_losses.append(log_loss(y_valid_fold, pred_probs))
average_log_loss = sum(log_losses) / len(log_losses)
print(f"평균 Log Loss의 값 : {average_log_loss}")
평균 Log Loss의 값 : 1.2492426042067344
※ 결과 해석
본 검증에서 베이스라인 모델의 로그손실 점수는 약 1.1934로 나왔습니다.
반면, 나이를 구간화하여 사용한 모델에서의 로그손실 점수는 약 1.2492로, 성능이 약간 저하되었습니다.
이를 통해 현재의 나이 구간화 방식이 모델의 성능에 긍정적인 영향을 주지 않았음을 알 수 있습니다.
이전에 family_size 칼럼의 값을 조정하였을 때 로그손실 점수에 긍정적인 변화가 있었습니다.
하지만, 모든 피처의 수정이나 특성 엔지니어링이 성능 향상에 긍정적인 결과를 가져오는 것은 아닙니다.
각각의 피처 조정 방법과 그에 따른 결과를 확인하며, 최적의 피처 조합을 찾는 과정이 필요합니다.
이번 검증 결과를 바탕으로 실제 모델링 시에는 나이를 구간화한 피처는 추가하지 않는 것이 좋겠습니다.
5. 개별 피처 추가 후 로그손실 점수 검증
before_employed는 고용되기 전까지의 총 일수를 나타냅니다.
이 값을 통해 개인의 경력이 얼마나 긴지, 혹은 얼마나 짧은지를 파악할 수 있습니다.
employed_year는 근속 연수를 나타냅니다.
일반적으로 근속 연수가 길면 소득의 안정성이 높다고 판단되므로 신용도 예측에 도움이 될 수 있습니다.
income_by_employed_days는 근속일수분할로 1일당 얼마나 수입을 얻는지를 나타냅니다.
이는 개인의 수입 효율성을 나타내며, 근속일수 대비 높은 수입을 얻는 사람이 신용도가 높을 가능성이 있습니다.
근속일수가 0인 경우에는 수입 효율성을 나타내는 피처가 의미가 없으므로 0으로 설정합니다.
income_by_family는 가족 1인당 얼마나 소득을 가지는지를 나타냅니다.
가족 당 소득이 높을수록 가계의 경제적 안정성이 높아진다고 판단될 수 있습니다.
- days_birth 에서 days_employed 의 차이를 구하고 그 값을 before_employed 피처에 할당합니다.
- days_employed에서 365로 나눈 몫을 구하고 그 값을 employed_year 피처에 할당합니다.
- income_total 에서 days_employed로 나눈 값을 income_by_employed_days 에 할당합니다. 단, days_employed의 값이 0이면 0을 income_by_employed_days 피처에 할당합니다.
- income_total 에서 family_size 로 나누고 그 값을 income_by_family 피처에 할당합니다.
from tqdm import tqdm
def get_log_loss(df):
copy_train = df.copy()
# 피처엔지니어링을 한 피처 외 다른 피처가 입력되었는지 확인합니다.
print(copy_train.columns[-2:])
train_data, valid_data = train_test_split(copy_train, stratify=copy_train['credit'], test_size=0.2, random_state=42)
x_train = train_data.drop('credit', axis=1)
y_train = train_data['credit']
skf = StratifiedKFold(n_splits=4, shuffle=True, random_state=42)
log_losses = []
for train_idx, valid_idx in skf.split(x_train, y_train):
x_train_fold, x_valid_fold = x_train.iloc[train_idx], x_train.iloc[valid_idx]
y_train_fold, y_valid_fold = y_train.iloc[train_idx], y_train.iloc[valid_idx]
model = RandomForestClassifier(n_estimators=50, random_state=42)
model.fit(x_train_fold, y_train_fold)
pred_probs = model.predict_proba(x_valid_fold)
log_losses.append(log_loss(y_valid_fold, pred_probs))
average_log_loss = sum(log_losses) / len(log_losses)
return average_log_loss
# 각 피처 추가 및 검증 점수 확인
# 순서대로 고용되기 전까지의 일수, 근속연수, 근속일별 소득, 가족 수 대비 소득
fe_features = [
('before_employed', lambda df: df['days_birth'] - df['days_employed']),
('employed_year', lambda df: df['days_employed'] // 365),
('income_by_employed_days', lambda df: df.apply(lambda row: 0 if row['days_employed'] == 0 \
else row['income_total'] / row['days_employed'], axis=1)),
('income_by_family', lambda df: df['income_total'] / df['family_size'])
]
for feature_name, feature_func in tqdm(fe_features):
train[feature_name] = feature_func(train)
log_loss_score = get_log_loss(train)
print(f"{feature_name} 추가 후의 로그손실 점수: {log_loss_score}")
# 하나의 칼럼을 추가했을 때 그 결과를 확인하기 위해 drop() 메서드 사용
# drop() 메서드를 사용하지 않을 경우 네 가지의 피처를 하나씩 순차적으로 데이터프레임에 추가
train = train.drop(feature_name, axis = 1)
※ 결과 해석
베이스라인 모델로 검증한 로그손실 점수는 약 1.1934로 측정되었습니다.
특성 공학을 통해 추가한 income_by_employed_days 피처는 로그손실 점수를 약 1.1841로 개선하는 효과를 보였습니다.
이 결과를 통해, income_by_employed_days 피처가 모델의 성능 향상에 긍정적으로 작용한다는 것을 알 수 있습니다.
반면, 나머지 세 피처의 추가는 베이스라인 모델에 비해 로그손실 점수가 떨어짐을 확인할 수 있었습니다.
이러한 관찰을 통해, 추가한 세 가지 피처는 최종 모델에서 제외하는 것이 좋겠다는 결론을 내릴 수 있습니다.
6. 성별 기반 소득 보정 및 KMeans 클러스터링을 이용한 특성 공학 및 모델 평가
※ 성별 기반 소득 보정
원래 데이터에서 여자와 남성 간의 평균 소득에는 차이가 있을 수 있습니다.
이 코드에서는 남성과 여성의 평균 소득을 동일하게 맞추기 위해 보정을 수행합니다.
이를 통해 모델이 성별에 기반한 편향을 줄이고 다른 피처들에 좀 더 집중하게 만드는 효과가 있을 수 있습니다.
※ KMeans 클러스터링
KMeans 클러스터링은 비지도 학습 방법 중 하나로, 데이터 포인트들을 유사한 특성을 가진 그룹으로 나누는 알고리즘입니다.
이 코드에서는 KMeans를 사용하여 데이터를 100개의 클러스터로 나눈 후, 각 데이터 포인트가 어떤 클러스터에 속하는지를 나타내는 새로운 피처를 생성합니다.
이렇게 생성된 피처는 모델이 데이터에 숨겨진 패턴이나 구조를 파악하는 데 도움을 줄 수 있습니다.
클러스터링 기본적으로 데이터 간의 거리를 기반으로 그룹화하는 목적을 가집니다.
이러한 특성 때문에 , 일반적으로 데이터를 스케일링한 후 클러스터링을 수행하는 것이 권장됩니다.
스케일링을 통해 각 피처의 값의 범위를 동일하게 만들어주면, 한 피처가 결과에 과도하게 영향을 주는 것을 방지할 수 있습니다.
하지만 이번에는 일부 피처에 대해 스케일링을 수행하지 않았습니다.
income_total과 days_employed 같은 피처들은 스케일링하지 않은 상태에서 더 큰 의미를 가질것이라고 판단했기 때문입니다.
이런 결정을 통해 해당 피처들의 영향력을 높이고 이에따른 검증결과 점수의 변화를 확인해볼것입니다.
from sklearn.cluster import KMeans
gender_income_mean = train.groupby('gender')['income_total'].mean()
weight = gender_income_mean[1] / gender_income_mean[0] # 1: 남성, 0: 여성
cluster_gender_features = [
('adjusted_income',
lambda df: df.apply(
lambda row: row['income_total'] * weight
if row['gender'] == 0 else row['income_total'], axis=1
)),
('cluster',
lambda df: KMeans(n_clusters=100, n_init=10, random_state=42)
.fit(df.drop(['credit'], axis=1))
.predict(df.drop(['credit'], axis=1)))
]
for feature_name, feature_func in tqdm(cluster_gender_features):
train[feature_name] = feature_func(train)
log_loss_score = get_log_loss(train)
print(f"{feature_name} 추가 후의 로그손실 점수: {log_loss_score}")
train = train.drop(feature_name, axis = 1)
※ 결과 해석
cluster 피처를 추가했을 때 로그손실 점수는 약 1.1833으로 나왔습니다.
이 값은 기존의 베이스라인보다 낮은 값으로, cluster 피처가 모델의 성능을 향상시키는 데 기여했다고 볼 수 있습니다.
이는 KMeans 클러스터링을 통해 데이터에 내재된 어떤 패턴이나 구조를 잡아내어 모델의 예측 성능을 향상시킬 가능성이 있습니다.
adjusted_income 피처를 추가했을 때의 로그손실 점수는 약 1.2208로 나왔습니다.
이 값은 기존의 베이스라인보다 높은 log_loss 점수를 가집니다.
따라서 성별 기반으로 소득을 보정하는 adjusted_income 피처는 모델의 성능 향상에 기여할 수 없을 것입니다.
따라서 cluster 피처만을 사용하여 학습에 사용하도록 하겠습니다.
'머신러닝 > 머신러닝: 실전 프로젝트 학습' 카테고리의 다른 글
아파트 경매 가격 예측 프로젝트 1 : 데이터 분석 및 기본 예측 (0) | 2025.01.07 |
---|---|
신용카드 사용자 연체 예측 프로젝트 5 : 피처 선택 및 모델 고도화 (0) | 2025.01.06 |
신용카드 사용자 연체 예측 프로젝트 3 : 데이터 인코딩 (1) | 2025.01.04 |
신용카드 사용자 연체 예측 프로젝트 2 : 범주형 변수 EDA 및 결측값 대체 (0) | 2025.01.04 |
신용카드 사용자 연체 예측 프로젝트 1 : 데이터 분석 (0) | 2025.01.03 |