CHALLENGE : House Loan을 받을 수 있는 사람인지 먼저 분류하자!
순수하게 통계학적인 방법의 Feature Selection을 해서 예측 모형을 완성해 보자.고 접근합니다.
DATA LOCATION
https://datahack.analyticsvidhya.com/contest/practice-problem-loan-prediction-iii/download/train-file
Loan Prediction
https://datahack.analyticsvidhya.com/contest/practice-problem-loan-prediction-iii/#ProblemStatement
Statistical Tests | Feature Selection using Statistical Tests
https://www.analyticsvidhya.com/blog/2021/06/feature-selection-using-statistical-tests/
이 글을 보기 위해서 해당 사이트에 가입까지는 권하지 않겠습니다. 글 내용이 실제 제목과 별로 관련이 없습니다.
DATA DESCRIPTION
Variable Description
Loan_ID Unique Loan ID
Gender Male/ Female
Married Applicant married (Y/N)
Dependents Number of dependents
Education Applicant Education (Graduate/ Under Graduate)
Self_Employed Self employed (Y/N)
ApplicantIncome Applicant income
CoapplicantIncome Coapplicant income
LoanAmount Loan amount in thousands
Loan_Amount_Term Term of loan in months
Credit_History credit history meets guidelines
Property_Area Urban/ Semi Urban/ Rural
Loan_Status (Target) Loan approved (Y/N)
TARGET DATA DESCIPRTION
Variable Description
Loan_ID Unique Loan ID
Loan_Status (Target) Loan approved (Y/N)
데이터 파일은 가입하지 않고도, Data Location 주소를 브라우저에 입력하면 다운받아 집니다.
어쩄든 처음 데이터를 로드해서 대~충 훑어보는 것은 이제는 묵음으로 진행해도 되겠지요.
import pandas as pd
df_raw = pd.read_csv('train_ctrUa4K.csv')
df_raw.head()
Loan_ID | Gender | Married | Dependents | Education | Self_Employed | ApplicantIncome | CoapplicantIncome | LoanAmount | Loan_Amount_Term | Credit_History | Property_Area | Loan_Status | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | LP001002 | Male | No | 0 | Graduate | No | 5849 | 0.0 | NaN | 360.0 | 1.0 | Urban | Y |
1 | LP001003 | Male | Yes | 1 | Graduate | No | 4583 | 1508.0 | 128.0 | 360.0 | 1.0 | Rural | N |
2 | LP001005 | Male | Yes | 0 | Graduate | Yes | 3000 | 0.0 | 66.0 | 360.0 | 1.0 | Urban | Y |
3 | LP001006 | Male | Yes | 0 | Not Graduate | No | 2583 | 2358.0 | 120.0 | 360.0 | 1.0 | Urban | Y |
4 | LP001008 | Male | No | 0 | Graduate | No | 6000 | 0.0 | 141.0 | 360.0 | 1.0 | Urban | Y |
df_raw.info() # data type좀 보고
RangeIndex: 614 entries, 0 to 613 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Loan_ID 614 non-null object 1 Gender 601 non-null object 2 Married 611 non-null object 3 Dependents 599 non-null object 4 Education 614 non-null object 5 Self_Employed 582 non-null object 6 ApplicantIncome 614 non-null int64 7 CoapplicantIncome 614 non-null float64 8 LoanAmount 592 non-null float64 9 Loan_Amount_Term 600 non-null float64 10 Credit_History 564 non-null float64 11 Property_Area 614 non-null object 12 Loan_Status 614 non-null object dtypes: float64(4), int64(1), object(8) memory usage: 62.5+ KB
null이 좀 있군요? 카테고리 데이터도 좀 있고, 연속형 데이터도 좀 있군요. 어쨌든 마음 편하게 null은 모두 drop하고 시작해 보겠습니다.
df_raw.dropna(inplace=True) # null 날리고
df_raw.info() # 어느정도 남았나 보고
Int64Index: 480 entries, 1 to 613 Data columns (total 13 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Loan_ID 480 non-null object 1 Gender 480 non-null object 2 Married 480 non-null object 3 Dependents 480 non-null object 4 Education 480 non-null object 5 Self_Employed 480 non-null object 6 ApplicantIncome 480 non-null int64 7 CoapplicantIncome 480 non-null float64 8 LoanAmount 480 non-null float64 9 Loan_Amount_Term 480 non-null float64 10 Credit_History 480 non-null float64 11 Property_Area 480 non-null object 12 Loan_Status 480 non-null object dtypes: float64(4), int64(1), object(8) memory usage: 52.5+ KB
아니, 이거 drop해 보니, 많은 걸 버렸군요. 아이고, 어쩌지. 그래도 시작부터 골치썪는 것 보다는 지금은 이렇게 해 보시죠. 일단 전반적으로 데이터가 어떤 식으로 구성되어 있는지 함 보시죱.
import matplotlib.pyplot as plt
coi = df_raw.select_dtypes('object').columns.to_list() # object column만 가져오고
coi.remove('Loan_ID') # 필요 없는거 빼고
fig, axes = plt.subplots(nrows=len(coi)//2 + len(coi)%2, ncols=2, figsize=(6, 15))
for ax, col in zip(axes.flatten(), coi):
df_raw[col].value_counts().plot(kind='bar', ax=ax)
ax.set_title(col)
plt.tight_layout()
plt.show()
카테고리 데이터에 대해서 대~충 그 의미를 파악해 봅시다요.
⓵ 암... 남자가 훨씬 많군요? 알겠습니다.
⓶ 기혼자가 많군요! 역시나 집은 기혼자죠 ㅠㅠ
⓷ Dependent가 없는 사람이 꽤나 많군요? 이거는 일반적인 상식과는 좀 반하는 것 같습니다만. 여튼 이렇습니다.
⓸ 이것도 뭐 당연한 이야기겠지만 사회인이 집에 대한 Loan을 많이 하겠죠? 흠냐.
⓹ 암~ 자기 사업을 가지고 있는 사람 보다는 소득이 확실하게 예측되는 월급쟁이들이 많이 Apply하겠습니다. 헵
⓺ Property는 그렇게 많이 차이난다고 보기 힘들겠습니다. ㅎㅎ
⓻ 마지막으로 Loan_Status는 Y가 꽤나 많습니다. 대출이 잘 되나보군요. 이 변수는 종속변수이기 때문에 나중에 Training할 때 Balance를 잘 맞춰서 학습시켜야 할 것 같습니답!
자, 이제 연속형 대에터들을 한눈에 봐 볼까요?
import matplotlib.pyplot as plt
df_raw.hist(figsize=(7,5))
plt.tight_layout()
plt.show()
연속형 데이터는 대부분 Positive Skewed 형태를 취하고 있는데, 특이한 경우는 Loan_Amount_Term이군요? 거의 400 정도가 대부분을 차지하고 있습니다. 이거는 뭐 거의 대출 상품이 거의 이런 식으로 고정되어 있어서 그런것 아닌가 싶습니다. ㅎㅎ
일단, 여기까지 봤으면 머 대~충 느낌적인 느낌은 알겠고요, 그러면 이 Feature들이 얼마나 종속변수에 영향을 미치는지를 대~충 분석해 보도록 하시죠. 통계적인 방법을 동원하려고 합니다.
그전에 이번에는 Binary Classification을 xgboost모형을 이용해서 구현하려고 하는데, 일단 모든 Feature를 떄려 넣고, 얼마나 성능이 나오는지 한번 보는 것이 어떨까 합니다. 왜냐하면 데이터가 480개밖에 없기 때문에 신경망을 이용한다거나 하는 건 좀 부족해 보입니다. Feature수에 비해서 데이터 수가 적어서 아마도 Overfitting이 될 가능성이 클 것이라 미리 예상을! 합니다.
그냥 다 떄려 넣고 xgboost를 해 봅시다.
from sklearn.model_selection import train_test_split
from xgboost import XGBClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import MinMaxScaler
df_data = df_raw.copy()
df_data.drop('Loan_ID', axis=1, inplace=True)
# 범주형 Feature와 연속형 Feature를 구분해 보자
col_cate = data.select_dtypes('object').columns.to_list()
col_cont = data.select_dtypes('float').columns.to_list()
print(col_cate, col_cont)
# 범주형 피처를 숫자로 인코딩
for col in col_cate :
label_encoder = LabelEncoder()
df_data[col] = label_encoder.fit_transform(data[col])
# 연속형 피처를 스케일링
scaler = MinMaxScaler()
df_data[col_cont] = scaler.fit_transform(data[col_cont])
# 데이터를 Feature와 Label로 구분
y_train = df_data.pop('Loan_Status')
x_train = df_data.copy()
x_train = pd.get_dummies(x_train)
y_train = pd.get_dummies(y_train, drop_first=True)
x_train,x_test,y_train,y_test = train_test_split(x_train, y_train, stratify=y_train, test_size=0.2, random_state=4)
# 모형을 설정하고
model = XGBClassifier(random_state=11)
# 학습 가즈아!
model.fit(x_train, y_train)
y_pred = model.predict(x_test)
# 성능을 확인
print('훈련세트 정확도: {:.3f}' .format(model.score(x_train, y_train)))
print('테스트세트 정확도: {:.3f}' .format(model.score(x_test, y_test)))
[] ['CoapplicantIncome', 'LoanAmount', 'Loan_Amount_Term', 'Credit_History'] 384 96 384 96 훈련세트 정확도: 1.000 테스트세트 정확도: 0.771
후야~ 훈련센트 정확도 100%에 테스트세트 정확도가 77.1%군요???? 이건 더 이상 생각할 필요없이 과적합입니다. 후후. 이제는 이런 것도 미리 감이 좀 오는군요.
이럴 때는 확실하게 데이터를 많이 늘리거나, 과적합 방지를 위한 일반화 테크닉들을 사용해도 되겠지만, 통계적 Feature Selection을 통해서 Feature를 줄이는 것을 통해서 성능 향상을 해 보겠습니다. 자, 말만 나불거리지 말고 Feature Selection을 한번 해 봅시다.
아 그 전에 가만히 보니까, credit_history가 범주형으로 생각할 수 있는 것 같아서 이걸 타입변환후에 보도록 하겠습니다.
df_raw['Credit_History'] = df_raw['Credit_History'].astype('str')
coi.append('Credit_History')
자, 범주형 변수들과 Label인 Loan_Status 사이의 독립성 검정을 한번 해보는 것으로 시작하겠습니다. 이거 잊고 있는 건 아니겠죠?
범주형 변수들과 Label의 독립성 검정
from scipy.stats import chi2_contingency
print(coi)
lst_coi = []
for feature in coi:
if feature == 'Loan_Status' : continue # 이건 Label이니까 빼고
contingency_table = pd.crosstab(df_raw[feature], df_raw['Loan_Status'])
chi2 , p ,dof, expected = chi2_contingency(contingency_table)
if p<0.05 :
print(f"{feature} is SIGNIFICANT")
print(f"Chi-square value = {chi2}, p-value = {p}")
lst_coi.append(feature)
else :
print(f"{feature} is insignificant")
print("SIGNIFICANT FEATURES") # 유의한 Feature만 보고
lst_coi
['Gender', 'Married', 'Dependents', 'Education', 'Self_Employed', 'Property_Area', 'Loan_Status', 'Credit_History'] Gender is insignificant Married is SIGNIFICANT Chi-square value = 5.557140235492528, p-value = 0.018405456386355375 Dependents is insignificant Education is insignificant Self_Employed is insignificant Property_Area is SIGNIFICANT Chi-square value = 12.2259455519901, p-value = 0.0022139594148752133 Credit_History is SIGNIFICANT Chi-square value = 131.29328312402075, p-value = 2.135981766869101e-30 SIGNIFICANT FEATURES
['Married', 'Property_Area', 'Credit_History']
자, 결과를 보니깐~ 이거 머 종속변수와 독립이 아닌게, Married, Property_Area, Credit_History 정도군요? 아하. 다른 Feature들은 종속변수와 독립이라는 뜻인데, 그러면 범주형 변수는 요 3개 Feature를 집중적으로 파면 좋겠네요.
그러면, 연속형 변수는 어떨까요? 그냥 개인적인 의견으로는 Loan Amount를 빼고는 모두 그다지 의미가 없을 것 같습니다. 너무 뭉쳐있거든요.
일단 LoanAmount만 MinMax Scaling을 해서 Feature로 적절한지 한번 보겠습니다.
연속형 변수와 Label 그룹간의 t 검정 (차이)
from sklearn.preprocessing import MinMaxScaler
df_preprocess = df_raw.copy()
cols = ['LoanAmount']
scaler = MinMaxScaler()
df_preprocess[cols] = scaler.fit_transform(df_preprocess[cols]) # MinMax Scaling
df_preprocess.head()
Loan_ID | Gender | Married | Dependents | Education | Self_Employed | ApplicantIncome | CoapplicantIncome | LoanAmount | Loan_Amount_Term | Credit_History | Property_Area | Loan_Status | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | LP001003 | Male | Yes | 1 | Graduate | No | 4583 | 1508.0 | 0.201354 | 360.0 | 1.0 | Rural | N |
2 | LP001005 | Male | Yes | 0 | Graduate | Yes | 3000 | 0.0 | 0.096447 | 360.0 | 1.0 | Urban | Y |
3 | LP001006 | Male | Yes | 0 | Not Graduate | No | 2583 | 2358.0 | 0.187817 | 360.0 | 1.0 | Urban | Y |
4 | LP001008 | Male | No | 0 | Graduate | No | 6000 | 0.0 | 0.223350 | 360.0 | 1.0 | Urban | Y |
5 | LP001011 | Male | Yes | 2 | Graduate | Yes | 5417 | 4196.0 | 0.436548 | 360.0 | 1.0 | Urban | Y |
Loan_Status에 따라 (그룹)에 따라 LoanAmount가 차이가 나는지 z test도 한번 하자! 이거 매우 중요한 과정이에요.
df_loan_y = df_preprocess[df_preprocess['Loan_Status']=='Y']['LoanAmount']
df_loan_n = df_preprocess[df_preprocess['Loan_Status']=='N']['LoanAmount']
from scipy.stats import bartlett
bartlett(df_loan_y, df_loan_n)
BartlettResult(statistic=6.771581019957911, pvalue=0.009262074884700013)
등분산 검정에서 실패했군요. 그렇지만 걱정 없습니다. 우리에게는 Welch's 검정이 있잖아요! Welch's 가즈아!!!
from scipy.stats import ttest_ind
ttest_ind(df_loan_y, df_loan_n, equal_var=False)
Ttest_indResult(statistic=-1.4692607777044502, pvalue=0.14305832311596678)
아이고, 차이가 없네요. 그러면 어쨌든 LoanAmount는 각 종속변수에 대해 차이가 나지 않으므로, 제거할 마음의 준비를 하면 좋겠습니다. 마음 먹었으면 가차없이 제거!
자, 그럼 이제 필요한 column만 남겨볼까요?
유의한 Feature로만 선택 완료!
coi = ['Married', 'Property_Area', 'Credit_History', 'Loan_Status']
df_data = df_preprocess[coi].copy()
df_data.head()
Married | Property_Area | Credit_History | Loan_Status | |
---|---|---|---|---|
1 | Yes | Rural | 1.0 | N |
2 | Yes | Urban | 1.0 | Y |
3 | Yes | Urban | 1.0 | Y |
4 | No | Urban | 1.0 | Y |
5 | Yes | Urban | 1.0 | Y |
자, 필요한 Feature(컬럼)만 남겼으니까 이제 xgboost 한번 가 보시죠. 결과가 어떨지 기대되지요? 후후훟
xgboost 진행 시켜!
from sklearn.model_selection import train_test_split
print(df_data.head())
y_train = df_data.pop('Loan_Status')
x_train = df_data.copy()
x_train = pd.get_dummies(x_train)
y_train = pd.get_dummies(y_train, drop_first=True)
x_train,x_test,y_train,y_test = train_test_split(x_train, y_train, stratify=y_train, test_size=0.2, random_state=4)
len(x_train), len(x_test), len(y_train), len(y_test)
Married Property_Area Credit_History Loan_Status 1 Yes Rural 1.0 N 2 Yes Urban 1.0 Y 3 Yes Urban 1.0 Y 4 No Urban 1.0 Y 5 Yes Urban 1.0 Y
(384, 96, 384, 96)
자, train과 test 데이터를 나눠 봤습니다. 헤헤.
XGBoost 예측 모형 학습. 자~ 갑니다~
from xgboost import XGBClassifier
model = XGBClassifier(random_state=11)
# 학습 가즈아!
model.fit(x_train, y_train)
y_pred = model.predict(x_test)
# 성능을 확인
print('훈련세트 정확도: {:.3f}' .format(model.score(x_train, y_train)))
print('테스트세트 정확도: {:.3f}' .format(model.score(x_test, y_test)))
훈련세트 정확도: 0.810 테스트세트 정확도: 0.802
엣헴.. 이거 훈련세트 정확도 81%, 테스트세트 정확도 80.2% 입니다!!!! 어떄요? 놀랍죠?
모든 Feature들이 예측을 하는데 도움이 되지는 않는다는 것, 그리고 통계적인 방법만으로도 더 향상된 예측모형을 만들 수 있다는 그런 Insight! 통계 왜 배우나 했더니 뭐 이런데 다~ 써먹습니다. 이런 것도 해 봤으니 꽃같이 웃어봐요.
Feature Selection을 할 때에는 정말 여러가지 방법들이 엄청 제안되고 제시되는데 대부분 노가다를 하는 방법들입니다. 노가다라는게 모든 Feature 조합으로 넣고 빼고 바꿔서 결과를 보고 제일 괜찮은 것을 고른 다는 뭐 그런건데, 좋은 예측모형을 자동으로 만들어내면 좋겠지만, 이런 식으로 통계 검정 접근으로도 얼마든지 잘 할 수 있겠습니다. 물론 혹시 더 좋은 Feature 조합이 있는거 아니야? 하는 불안감이 들수도 있겠지마는~ 그런 마음은 버리고 조금 더 논리적인 접근을 해서 결론을 내는 편이 데이터를 더 잘 이해할 수 있겠습니다.
게다가 Feature 3개만으로 하니까 얼마나 깔끔하고 좋아요?
댓글