주제는 ‘Home Credit Default Risk’
배경
간단하게 말해서 대출을 받은 고객이 상환능력이 있는지 없는지를 분류하는 예측 모델을 만드는 것이 최종 목표이다.
자세한 내용은 하기 링크를 확인.
우선, 이런 데이터를 처음 봤다. 도메인 지식도 지식이지만 missing value가 굉장히 많고 변수도 이렇게 많은 데이터는 처음 보는 상황이라 많이 당황스러웠다.
테이블별 메타 정보를 하나하나 설명하기에는 포스팅이 끝나지 않을 것 같아서, 내가 참고한 커널과 진행 방식을 소개해보려고 한다.
우선 사이킷런의 lightgbm으로 많이 진행하길래, 비슷하게 학습하며 연습해보려 했지만 설치 이슈가 계속 발생해서 간단하게 tapping하면서 학습할 수 있는 logistic regression으로 학습시키기로 결정했다.
진행 방법
어느 유저가 해당 커널을 공유해주셨고, 감사하게도 잘 참고하며 학습할 수 있었다. 아! 그리고 EDA 부분은 양이 너무 많아서 생략하고 진행하려고 한다.
- 데이터 불러오기
import numpy as np
import pandas as pdimport matplotlib.pyplot as plt
import seaborn as snsimport warnings
warnings.filterwarnings('ignore')import osapp_train = pd.read_csv('./home-credit-default-risk/application_train.csv')
app_test = pd.read_csv('./home-credit-default-risk/application_test.csv')
bureau = pd.read_csv('./home-credit-default-risk/bureau.csv')
bureau_balance = pd.read_csv('./home-credit-default-risk/bureau_balance.csv')
pos_cash_balance = pd.read_csv('./home-credit-default-risk/pos_cash_balance.csv')previous_app = pd.read_csv('./home-credit-default-risk/previous_application.csv')
installments_payments = pd.read_csv('./home-credit-default-risk/installments_payments.csv')
credit_card_balance = pd.read_csv('./home-credit-default-risk/credit_card_balance.csv')
2. 1차 피처 엔지니어링
buruau 테이블의 ‘SK_ID_CURR’를 기준으로, 과거 모든 대출 기록 count하는 변수 생성
previous_loan_counts = bureau.groupby('SK_ID_CURR', as_index=False)['SK_ID_BUREAU'].count().rename(columns = {'SK_ID_BUREAU': 'previous_loan_counts'})
previous_loan_counts.head()
그리고 bureau의 특정 칼럼에 따라 집계값을 생성하는 함수를 정의했다.
def normalize_categorical(df, group_var, col_name):
# 카테고리 컬럼을 선택.
categorical = pd.get_dummies(df.select_dtypes('object'))
categorical[group_var] = df[group_var]
# 그룹핑하는 칼럼에 따른 sum,mean값을 aggregation할 수 있도록 설정
categorical = categorical.groupby(group_var).agg(['sum', 'mean'])
column_names = []for var in categorical.columns.levels[0]:
for stat in ['count', 'count_norm']:
column_names.append('%s_%s_%s' % (col_name, var, stat))
categorical.columns = column_names
return categoricalbureau_counts = normalize_categorical(bureau, group_var = 'SK_ID_CURR', col_name = 'bureau')
bureau_counts.head()
3. 데이터 그룹핑
하나의 데이터셋에 merge할 수 있도록 ‘SK_ID_CURR’ 기준으로 그룹핑했다.
data_bureau_agg=bureau.groupby(by='SK_ID_CURR').mean()
data_credit_card_balance_agg=credit_card_balance.groupby(by='SK_ID_CURR').mean()
data_previous_application_agg=previous_app.groupby(by='SK_ID_CURR').mean()
data_installments_payments_agg=installments_payments.groupby(by='SK_ID_CURR').mean()
data_POS_CASH_balance_agg=pos_cash_balance.groupby(by='SK_ID_CURR').mean()data_bureau_agg.head()
4. 데이터 병합
def merge(df):
df = df.join(data_bureau_agg, how='left', on='SK_ID_CURR', lsuffix='1', rsuffix='2')
df = df.join(bureau_counts, on = 'SK_ID_CURR', how = 'left')
df = df.merge(previous_loan_counts, on = 'SK_ID_CURR', how = 'left')
df = df.join(data_credit_card_balance_agg, how='left', on='SK_ID_CURR', lsuffix='1', rsuffix='2')
df = df.join(data_previous_application_agg, how='left', on='SK_ID_CURR', lsuffix='1', rsuffix='2')
df = df.join(data_installments_payments_agg, how='left', on='SK_ID_CURR', lsuffix='1', rsuffix='2')
return dftrain = merge(app_train)
test = merge(app_test)
display(train.head())
그런 다음 train,test 데이터를 병합했다.
ntrain = train.shape[0]
ntest = test.shape[0]y_train = train.TARGET.valuesall_data = pd.concat([train, test]).reset_index(drop=True)
all_data.drop(['TARGET'], axis=1, inplace=True)
5. 2차 피처 엔지니어링
음수로 되어 있는 값은 절대값 처리 하고, ‘DAYS_BIRTH’는 365를 나누어줌
def correct_birth(df):
df['DAYS_BIRTH'] = round((df['DAYS_BIRTH'] * (-1))/365)
return dfdef convert_abs(df):
df['DAYS_EMPLOYED'] = abs(df['DAYS_EMPLOYED'])
df['DAYS_REGISTRATION'] = abs(df['DAYS_REGISTRATION'])
df['DAYS_ID_PUBLISH'] = abs(df['DAYS_ID_PUBLISH'])
df['DAYS_LAST_PHONE_CHANGE'] = abs(df['DAYS_LAST_PHONE_CHANGE'])
return dfdef missing(df):
features = ['previous_loan_counts','NONLIVINGAPARTMENTS_MEDI', 'NONLIVINGAPARTMENTS_AVG','NONLIVINGAREA_MEDI','OWN_CAR_AGE']
for f in features:
df[f] = df[f].fillna(0 )
return dfdef transform_app(df):
df = correct_birth(df)
df = convert_abs(df)
df = missing(df)
return dfall_data = transform_app(all_data)
그리고 모바일 폰과, 거주지와 관련된 변수를 생성후 useless한 칼럼은 다시 제거했다.
all_data['NO_OF_CLIENT_PHONES'] = all_data['FLAG_MOBIL'] + all_data['FLAG_EMP_PHONE'] + all_data['FLAG_WORK_PHONE']all_data['FLAG_CLIENT_OUTSIDE_CITY'] = np.where((all_data['REG_CITY_NOT_WORK_CITY']==1) & (all_data['REG_CITY_NOT_LIVE_CITY']==1),1,0)all_data['FLAG_CLIENT_OUTSIDE_REGION'] = np.where((all_data['REG_REGION_NOT_LIVE_REGION']==1) & (all_data['REG_REGION_NOT_WORK_REGION']==1),1,0)
all_data.head()def delete(df):
return df.drop(['FLAG_MOBIL', 'FLAG_EMP_PHONE' ,'FLAG_WORK_PHONE','REG_CITY_NOT_WORK_CITY','REG_CITY_NOT_LIVE_CITY','REG_REGION_NOT_LIVE_REGION','REG_REGION_NOT_WORK_REGION'], axis=1)
def transform(df):
df = delete(df)
return dfall_data = transform(all_data)
all_data.head()
또한 모델링하는데 필요없는 ID와 같은 고유 식별 값들도 제거했다.
def delete_id(df):
return df.drop(['SK_ID_CURR', 'SK_ID_PREV','SK_ID_BUREAU'], axis = 1)all_data = delete_id(all_data)
6. Missing Value 처리
기본적으로 column별 몇 개의 missing값이 있는지 확인해보고자 함수를 활용했다.
def missing_values_table(df):
mis_val = df.isnull().sum()
mis_val_percent = 100 * df.isnull().sum() / len(df)
mis_val_table = pd.concat([mis_val, mis_val_percent], axis=1)
mis_val_table_ren_columns = mis_val_table.rename(
columns = {0 : 'Missing Values', 1 : '% of Total Values'})
mis_val_table_ren_columns = mis_val_table_ren_columns[
mis_val_table_ren_columns.iloc[:,1] != 0].sort_values(
'% of Total Values', ascending=False).round(1)
print ("The dataset has " + str(df.shape[1]) + " columns.\n"
"There are " + str(mis_val_table_ren_columns.shape[0]) +
" columns that have missing values.")
return mis_val_table_ren_columnsmissing_values_table(all_data)
확인 후 데이터 타입이 object와 number형에 따라 다르게 미싱 처리를 진행했다.
# 수치형 데이터는 중앙값def miss_numerical(df):
features = ['previous_loan_counts','NONLIVINGAPARTMENTS_MEDI', 'NONLIVINGAPARTMENTS_AVG','NONLIVINGAREA_MEDI','OWN_CAR_AGE']
numerical_features = all_data.select_dtypes(exclude = ["object"] ).columns
for f in numerical_features:
if f not in features:
df[f] = df[f].fillna(df[f].median())
return df# 카테고리 데이터는 최빈값
def miss_categorical(df):
categorical_features = all_data.select_dtypes(include = ["object"]).columns
for f in categorical_features:
df[f] = df[f].fillna(df[f].mode()[0])
return dfdef transform_feature(df):
df = miss_numerical(df)
df = miss_categorical(df)
return dfall_data = transform_feature(all_data)all_data.head()
7. 피처 스케일링
수치형 데이터는 MinMaxScaler를 활용하여 스케일링을 진행했다.
모든 수치형 데이터의 값은 [0,1]사이에 있도록 설정했다.
from sklearn.preprocessing import MinMaxScalerdef encoder(df):
scaler = MinMaxScaler()
numerical = all_data.select_dtypes(exclude = ["object"]).columns
features_transform = pd.DataFrame(data= df)
features_transform[numerical] = scaler.fit_transform(df[numerical])
display(features_transform.head(n = 5))
return dfall_data = encoder(all_data)
8. 인코딩
로지스틱은 문자형을 입력변수로 못 받기 때문에,
1번. 카테고리형 col 중 속성값이 2개 이하인 데이터는 label 인코딩을 진행
2번. 2개 이상인 데이터는 one hot encoding 진행
1번.
from sklearn.preprocessing import LabelEncoderle = LabelEncoder()
le_count = 0for col in all_data:
if all_data[col].dtype == 'object':if len(list(all_data[col].unique())) <= 2:
le.fit(all_data[col])
all_data[col] = le.transform(all_data[col])
le_count += 1
print('%d columns were label encoded.' % le_count)
2번.
all_data = pd.get_dummies(all_data)display(all_data.shape)
9. 다중공선성 검정
독립변수 끼리의 상관 계수가 지나치게 높으면 다중공선성의 문제가 있는데 해결 방법으로 특정 임계값을 정하여 그 값을 초과했을 때 해당 변수의 일부를 제거하는 방법이 있다.
문제는 그 임계값을 어떻게 설정하느냐에 대한 rule이 없었기에, 구글링을 해본 결과 Simon Fraser University(SFU)에서 제공한 PDF에서 참고했다.
번역을 하면,
엄지손가락의 규칙 : 상관 관계가 0.8을 초과한 경우 심각한 다중공선성 존재 가능으로 해석이 된다. 이 문서를 보고 임계값은 0.8로 정했고 그 임계값을 초과하는 경우는 변수에서 제외하는 방향으로 설정했다.
corrs = all_data.corr()threshold = 0.8above_threshold_vars = {}for col in corrs:
above_threshold_vars[col] = list(corrs.index[corrs[col]> threshold])len(above_threshold_vars)
cols_to_remove = []
cols_seen = []
cols_to_remove_pair = []for key, value in above_threshold_vars.items():
cols_seen.append(key)
for x in value :
if x == key:
next
else:
if x not in cols_seen:
cols_to_remove.append(x)
cols_to_remove_pair.append(key)
cols_to_remove = list(set(cols_to_remove))
이 과정을 통해 58개의 col이 제거 되었다.
all_data_removed = all_data.drop(columns=cols_to_remove)
all_data_removed.shape
10. 모델링
train = all_data_removed[:ntrain]
test = all_data_removed[ntrain:]from sklearn.model_selection import train_test_splitX_train, X_test, Y_train, Y_test = train_test_split(train, y_train, test_size = 0.3, random_state = 200)
print("X Training shape", X_train.shape)
print("X Testing shape", X_test.shape)
print("Y Training shape", Y_train.shape)
print("Y Testing shape", Y_test.shape)
우선 데이터를 split 해준 다음,
from sklearn.metrics import make_scorer
from sklearn.metrics import confusion_matrix, precision_recall_curve, roc_curve, auc, log_loss
from sklearn.metrics import roc_auc_score
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCVlogreg = LogisticRegression(random_state=0, class_weight='balanced', C=100)
logreg.fit(X_train, Y_train)
Y_pred = logreg.predict_proba(X_test)[:,1]print('Train/Test split results:')
print("ROC", roc_auc_score(Y_test, Y_pred))
ROC검정을 실시했고..!
약 75%정도의 결과가 나왔다.
또한 변수의 중요도를 확인해본 결과 다음과 같이 나왔다.
from sklearn.linear_model import LogisticRegressionCVclf = LogisticRegressionCV(max_iter=3000)
clf.fit(X_train, Y_train)
coefs = np.abs(clf.coef_[0])
indices = np.argsort(coefs)[::-1]plt.figure()
plt.title("Feature importances (Logistic Regression)")
plt.bar(range(10), coefs[indices[:10]],
color="r", align="center")
plt.xticks(range(10), X_train.columns[indices[:10]], rotation=45, ha='right')
plt.subplots_adjust(bottom=0.3)
11. 보완점
(1) 파생 변수를 충분히 고려하지 못했다.
→ 도메인 지식의 중요성.. 어떤 변수가 중요하고 어떤 변수 기준으로 파생 변수를 생성하면 좋을 지 고민해야 하는데 이 부분이 시간 소모가 참 컸고 결국 레퍼런스 커널에서 제공해준 아이디어를 활용할 뿐 자체적으로 진행 하지 못했음.
→ 재미있는 라이브러리를 발견. 사이킷런의 PolynamialFeatures 인데. 타겟과 상관 관계가 있는 변수들을 몇 개 뽑은 다음, fit 시켜주면 여러가지 변수를 조합한 새로운 다항 변수가 생성됨. 추후에 사용해보면 좋을듯
poly_features = train[['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3', 'AGE', 'TARGET']] from sklearn.preprocessing import PolynomialFeatures poly_transformer = PolynomialFeatures(degree = 3) poly_transformer.fit(poly_features) poly_features = poly_transformer.transform(poly_features) poly_features_test = poly_transformer.transform(poly_features_test) print('Polynomial Features shape: ', poly_features.shape)
2. imbalanced data의 문제를 해결하지 못했다.
→ 정확도 기준으로 평가를 진행하긴 했지만, ROC를 기준으로 평가를 하더라도 imbalanced data에 민감하다고 함. 때문에 피처 엔지니어링과 모델링 사이에 imbalanced data를 upsampling 할 수 있는 방안을 모색할 필요가 있음
3. categorical data encoding 문제
→ 일반적인 방법으로 카테고리형 변수의 속성값이 2개 이하인경우 label encoding , 3개 이상인 경우 one hot encoding을 진행했는데, lightGBM에서는 built-in 함수를 제공한다고 하는데 사용하지 못했음.
4. Missing value 처리
→ 카테고리형 데이터의 경우 최빈값, 수치형 데이터의 경우 median으로 진행했음. 크게 뾰족한 수가 없었기 때문에 그렇게 진행했는데, lightGBM을 활용해보면 어땠을까 생각이 들었음
2월까지 해당 주제로 학습을 계속 할 것이기에 아직 끝이 난 과제는 아니지만 타이타닉 데이터보다 150배는 어려운 것 같았다..
해당 4개의 문제를 2월 안에 최대한 해결하고 최대한 많은 것을 배우고 다시 공유해보려 한다.
참고 자료
https://lightgbm.readthedocs.io/en/latest/Advanced-Topics.html