#!/usr/bin/env python
# coding: utf-8
# # Logistic Regression
# In[1]:
import pandas as pd
import numpy as np
pd.options.display.max_columns=None
pd.options.display.max_rows=None
# ## 알고 가야 할 것 들
#
# ### 1. Evidence of Weight
# ### 2. Information Value
# ### 3. VIF
# ### 4. C 통계량
# ### 5. AIC
# ### 6. ROC, AUC
# ## 데이터 셋을 로딩합니다.
# In[2]:
path = 'https://archive.ics.uci.edu/ml/machine-learning-databases/statlog/german/german.data'
dataset = pd.read_csv(path, delimiter=' ', header=None)
# In[4]:
dataset.shape
# In[5]:
dataset.head()
# In[6]:
COL = [
'Status_of_existing_checking_account',
'Duration_in_month',
'Credit_history',
'Purpose',
'Credit_amount',
'Savings_account_bonds',
'Present_employment_since',
'Installment_rate_in_percentage_of_disposable_income',
'Personal_status_and_sex',
'Other_debtors_guarantors',
'Present_residence_since',
'Property',
'Age_in_years',
'Other_installment_plans',
'Housing',
'Number_of_existing_credits_at_this_bank',
'Job',
'Number_of_people_being_liable_to_provide_maintenance_for',
'Telephone',
'foreign_worker',
'Target'
]
# In[7]:
dataset.columns = COL
# In[9]:
# 총 20개의 설명 변수와 1개의 종속 변수
dataset.head()
# Label
# - 1 : Good (갚을 수 있다 / 신용도가 좋다) $\to$ 0으로 변경
# - 2 : Bad (갚을 수 없다 / 신용도가 나쁘다) $\to$ 1로 변경
# - Ham(0) / Spam(1)
# - 정상사람(0) / 암환자(1)
# - 이벤트가 발생하지 않음(0) / 이벤트가 발생함(1)
# - 귀무가설(0) / 대립가설(1)
# In[10]:
# Label을 1/2 -> 0/1로 변경
dataset['Target'] = dataset['Target'] - 1
# ## 1. Information Value (정보 가치)
# 모델에서 변수의 사용유무를 판단하는 feature selection에서 유용한 방법입니다. 주로 모델을 학습하기전 첫 단계에서 변수들을 제거하는 데 사용합니다. 최종 모델에서는 대략 10개 내외의 변수를 사용하도록 합니다. IV와 WOE 신용채무능력이 가능한(good) 고객과 불가능한(bad) 고객을 예측하는 로지스틱 회귀 모델링과 밀접한 관계가 있습니다. 신용 정보 관련분야에서는 good customer는 부채를 갚을 수 있는 고객, bad customer는 부채를 갚을 수 없는 고객을 뜻합니다. 일반적으로 이야기할 때는 good customer는 non-events를 의미하고 bad customer는 events를 의미합니다.
#
# > 신용 관련 분야
#
# $
# \begin{align}
# &\text{WOE} = ln{\frac{\text{distribution of good}}{\text{distribution of bad}}} \\
# &\text{IV} = \sum{(\text{WOE} \times (\text{distribution of good} - \text{distribution of bad}))}\\
# \end{align}
# $
#
# >일반적
#
# $
# \begin{align}
# &\text{WOE} = ln{\frac{\text{distribution of non-events}}{\text{distribution of events}}} \\
# &\text{IV} = \sum{(\text{WOE} \times (\text{distribution of non-events} - \text{distribution of events}))}\\
# \end{align}
# $
#
#
# - 구간을 나눌 때, `pd.qcut`을 사용하였습니다. bin의 갯수만큼 값의 구간을 나누고 값을 해당 구간에 mapping 시켜줍니다.
# - Information value를 측정할 때, EOW가 inf, -inf가 나올 수 있습니다. 분포값이 0이 될 수 있기 때문입니다. 이 때는 EOW를 0으로 변경합니다.
#
#
# |
Information Value|예측력|
# |:-----------:|:------------------------------:|
# |0 to 0.02|무의미|
# |0.02 to 0.1|낮은 예측|
# |0.1 to 0.3|중간 예측|
# |0.3 to 0.5|강한 예측|
# |0.5 to 1|너무 강한 예측(의심되는 수치)|
# 예제)
#
#
# In[83]:
max_bin = 10 # 전체 데이터의 예측력에 해를 가하지 않는 한에서 구간을 테스트하였습니다.
def calc_iv(df, col, label, max_bin = max_bin):
"""IV helper function"""
bin_df = df[[col, label]].copy()
# Categorical column
if bin_df[col].dtype == 'object':
bin_df = bin_df.groupby(col)[label].agg(['count', 'sum'])
# Numerical column
else:
bin_df.loc[:, 'bins'] = pd.qcut(bin_df[col].rank(method='first'), max_bin)
# bin_df.loc[:, 'bins'] = pd.cut(bin_df[col], max_bin)
bin_df = bin_df.groupby('bins')[label].agg(['count', 'sum'])
bin_df.columns = ['total', 'abuse']
bin_df['normal'] = bin_df['total'] - bin_df['abuse']
bin_df['normal_dist'] = bin_df['normal'] / sum(bin_df['normal'])
bin_df['abuse_dist'] = bin_df['abuse'] / sum(bin_df['abuse'])
bin_df['woe'] = np.log(bin_df['normal_dist'] / bin_df['abuse_dist'])
bin_df['iv'] = bin_df['woe'] * (bin_df['normal_dist'] - bin_df['abuse_dist'])
bin_df.replace([np.inf, -np.inf], 0, inplace=True)
bin_df = bin_df[bin_df['total'] > 0]
iv_val = sum(filter(lambda x: x != float('inf'), bin_df['iv']))
return bin_df, col, iv_val
# In[84]:
ch_df, ch, ch_i_val = calc_iv(dataset,'Credit_history', 'Target')
ch_df
# In[85]:
# 중간 정도의 예측 능력
print('information value', ch_i_val)
# In[86]:
dim_df, dim, dim_i_val = calc_iv(dataset,'Duration_in_month', 'Target')
dim_df
# In[87]:
# 높은 예측 능력
dim_i_val
# In[88]:
# 함수를 만들어서 전체 iv를 살펴보자
col_iv = {}
for col in [idx for idx in dataset.columns.tolist()]:
if col == 'Target':
continue
_, col, iv = calc_iv(dataset, col, 'Target')
col_iv[col] = iv
# In[47]:
col_iv
# In[89]:
import operator
candidates = sorted(col_iv.items(), key=operator.itemgetter(1), reverse=True)
display(candidates)
# Status_of_existing_checking_account가 가장 강력한 예측 능력(의심 될 정도로)를 가졌고, 다음이 Credit_history이다. Telephone은 가장 낮은 예측력을 가지고 있다.
# 사용할 컬럼은 본인이 선택해야한다. 나은 예측을 위해, 모델을 생성하고, 테스트하고 다시 돌아와서 컬럼을 생성하고를 반복한다.
# 우선 상위 15개만을 선택하자
# In[93]:
# 분석하려는 feature의 갯수와 예측력의 trade-off를 조정하였습니다.
iv_cols = [key for key, iv in candidates if iv >= 0.044]
display(len(iv_cols))
display(sorted(iv_cols))
# In[94]:
# 상수로 설정합니다.
IV_COL = iv_cols[:]
# Information Value에 의하여 상위 15개의 feature를 선정하였습니다. 전체 feature를 사용하여 로지스틱 회귀를 구했을 때의 accuracy와 신용불량자에 대한 precision의 감소폭이 적으면서 (전체 데이터셋을 가장 잘 대변해 줄 수 있으면서) feature의 수를 줄이는 과정을 몇 번 테스트 하면서 얻은 값입니다.
#
# 먼저, IV에서 선택된 컬럼들로 이루어진 모델에서부터 시작합니다.
# In[111]:
# Category column들은 one hot vectore로 변환합니다.
cate_features = {}
num_fetures = []
for col in IV_COL:
if dataset[col].dtype == 'object':
cate_features[col] = pd.get_dummies(dataset[col], prefix=col)
else:
num_fetures.append(col)
# In[112]:
cate_features.keys()
# In[113]:
cate_features['Status_of_existing_checking_account'].head()
# 더미 변수를 생성하게 되면, 자유도는 더미 변수가 가질 수 있는 unique 값이 $n$ 이라고 했을때 $n-1$이 되므로, 각 변수에서 더미변수 축을 한개씩 제거해준다. 제거하지 않으면 `NaN`값이 발생합니다.
#
# 후진 제거법을 통해 무의미한 변수를 하나씩 제거해 나가기 위해 별도의 리스트를 하나 생성합니다. 이 리스트는 실행이 반복될 때마다 가장 무의미한 변수(p-value가 큰 변수)와 다중공선성을 갖는 변수를 제거 한다.
# In[114]:
removed_features = []
# In[115]:
for col, dummies in cate_features.items():
dropped_col = dummies.columns[-1]
removed_features.append(dropped_col)
cate_features[col] = dummies.drop(dropped_col, axis=1)
# In[116]:
removed_features
# In[117]:
cate_features['Status_of_existing_checking_account'].head()
# In[126]:
final_dataset = dataset[num_fetures]
# In[127]:
for col, df in cate_features.items():
final_dataset = pd.concat([final_dataset, df], axis=1)
# In[128]:
final_dataset.shape
# In[129]:
final_dataset.head()
# ## 2. Training set, test set을 나눕니다.
# In[110]:
from sklearn.model_selection import train_test_split
# In[132]:
train_x, test_x, train_y, test_y = \
train_test_split( \
final_dataset, dataset['Target'], test_size=0.2, random_state=42)
# In[133]:
print('train_set', train_x.shape)
print('test_set', test_x.shape)
# ## 3. Statsmodels를 이용하여 로지스틱 회귀를 돌립니다.
# In[134]:
import statsmodels.api as sm
# In[153]:
logistic_model = sm.Logit(
train_y,
sm.add_constant(train_x)
).fit()
# ### 성능 측정
# In[154]:
from sklearn.metrics import auc
from sklearn.metrics import accuracy_score
from sklearn.metrics import classification_report
# In[155]:
train_pred = pd.DataFrame({
'probs': logistic_model.predict(sm.add_constant(train_x)),
'class': train_y
})
train_pred['y_pred'] = 0
train_pred.loc[train_pred['probs'] > 0.5, 'y_pred'] = 1
# Test prediction
test_pred = pd.DataFrame({
'probs': logistic_model.predict(sm.add_constant(test_x)),
'class': test_y
})
test_pred['y_pred'] = 0
test_pred.loc[test_pred['probs'] > 0.5, 'y_pred'] = 1
# In[156]:
print('\nTraining Confusion matrix:')
display(pd.crosstab(train_pred['y_pred'], train_pred['class'], rownames=['Predict'], colnames=['Actual'], margins=True))
print('\nTraining Accuracy: ', round(accuracy_score(train_pred['class'], train_pred['y_pred']), 4))
print('\nTraining classification report:\n', classification_report(train_pred['class'], train_pred['y_pred'], digits=4))
# In[157]:
print('Test Confusion matrix:')
display(pd.crosstab(test_pred['y_pred'], test_pred['class'], rownames=['Predict'], colnames=['Actual'], margins=True))
print('\nTest Accuracy: ', round(accuracy_score(test_pred['class'], test_pred['y_pred']), 4))
print('\nTest classification report:\n', classification_report(test_pred['class'], test_pred['y_pred'], digits=4))
# ## 2. 후진제거법
# - 가장 무의미한 변수 (p-value)
# - 다중공선성을 갖는 변수 (Variance Inflation Factor)
# ### (A) P-value
# P-value가 가장 높은 변수를 제거합니다. 여기서 P-value는 각 독립변수의 유의성 검정(t-test)에서 얻어진 값입니다.
# In[179]:
unnecesarries = []
# In[180]:
logistic_model.summary2()
# 가장 무의미한 변수는 P값이 0.9580인 Personal_status_and_sex_A92이다.
# 체크하고 변수를 1개씩 제거하고를 반복해야한다.
# In[181]:
# Help 함수
def get_unrelated_cols(model, pvalue):
cols = model.pvalues[model.pvalues >= pvalue].keys().tolist()
print(len(cols))
print(cols)
return cols
# In[182]:
unrelated_cols = get_unrelated_cols(logistic_model, 0.8187)
# Personal_status_and_sex_A92을 삭제한다.
# In[174]:
unnecesarries.append('Personal_status_and_sex_A92')
# ### (B) 다중공선성
# 테스트와 상관없다고 판단되는 변수들을 제거한후에 다중공선성을 체크합니다. **다중공선성(multicollinearity)** 이란 독립변수들간의 선형관계가 존재하는 것을 나타냅니다. 독립변수 전체의 set에서 하나의 독립변수를 골라 종속변수로 보고, 그 독립변수를 제외한 set을 이용하여 선형회귀분석을 하여 구할 수 있습니다.
#
# 사실 변수간의 선형관계 알아보기 위해서는 산점도 or 상관계수를 사용해도 되지만 **VIF(분산 팽창 지수, Variance Inflation Factor)** 를 이용하여 구한다.
# In[183]:
def get_max_vif(df, removal_cols):
vifs = []
cnames = df.drop(removal_cols, axis=1).columns.tolist()
for i in range(len(cnames)):
xvar = cnames[:]
yvar = xvar.pop(i)
model = sm.OLS(
df.drop(removal_cols, axis=1)[yvar],
sm.add_constant(df.drop(removal_cols, axis=1)[xvar]))
res = model.fit()
vif = 1 / (1 - res.rsquared)
vifs.append((yvar, round(vif, 3)))
vifs = sorted(vifs, key=operator.itemgetter(1), reverse=True)
return vifs
# In[184]:
vifs = get_max_vif(train_x, unnecesarries)
# In[185]:
vifs
# In[177]:
unnecesarries.append(vifs[0][0])
# In[178]:
unnecesarries
# 무의미한 변수(가장 높은 p-value)는 Personal_status_and_sex_A92였고 pvalue가 0.9580, VIF가 3.302이다.
#
# 다중공선성이 가장 높았던 변수는 Housing_A152였고 pvalue가 0.4770, VIF가 5.846이다.
#
# **무의미한 변수를 먼저 삭제해야하며, 지우기 전에, 두 변수의 pvalue와 VIF를 각각 비교해보는것이 좋다**.
#
# 확실히 Personal_status_and_sex_A92는 삭제해야 한다.
# 모델을 삭제하기 앞서 모델 채택 기준을 살펴봐야한다.
# ## 3. 모델 채택 기준
# ### (A) C통계량 (concordance statistics)
# - 로지스틱 회귀 모델에서 이항 결과의 적합도(goodness of fit)에 관한 품질을 측정하는 척도
# - ROC curve와 동일하다.
# $$C=Pr[\pi(B|x_{i}) > \pi(B|x_{j}) | Y_{i} = 1, Y_{j} = 0]$$
# $$C 통계량(C 인덱스) = 0.5 + (\frac{일치쌍 비율 - 불일치쌍 비율}{2})$$
# In[189]:
def get_c_stat(iv_pred):
noraml_test_df = iv_pred[iv_pred['class'] == 0][['class', 'probs']]
spammer_test_df = iv_pred[iv_pred['class'] == 1][['class', 'probs']]
noraml_test_df['key'] = 0
spammer_test_df['key'] = 0
cross_join_df = noraml_test_df.merge(spammer_test_df, how='outer', on='key').drop('key', axis=1)
cross_join_df['concordance'] = cross_join_df['probs_x'] < cross_join_df['probs_y']
cross_join_df['in_concordance'] = cross_join_df['probs_x'] > cross_join_df['probs_y']
cross_join_df['tie'] = cross_join_df['probs_x'] == cross_join_df['probs_y']
results = cross_join_df.agg({'concordance': np.sum, 'in_concordance': np.sum, 'tie': np.sum}) / len(cross_join_df)
c_stat = 0.5 + (results['concordance'] - results['in_concordance']) / 2
return c_stat
# ### (B) AIC, Likelihood
#
# - $AIC=-2 \times ln(L) + 2 \times k$
#
# Akaike information criterion는 주어진 데이터 집합에 관해 통계 모델의 상대적 품질을 측정한다. 이 척도는 편향과 분산의 트레이드 오프이다. 더 작은 AIC를 선호한다.
#
# - $L = \Pi_{i=1}^{n}p(x_i)^{y_i}(1-p(x_i))^{1-y_i}$
#
# log likelihood는 큰 값을 선호한다.
# In[190]:
def get_aic_value(model):
return -2 * model.llf + 2 * (len(model.params) - 1)
# > 무의미한 변수를 지우기 전
# In[211]:
c_stat, aic = get_c_stat(train_pred), get_aic_value(logistic_model)
print('c_stat:', c_stat)
print('aic:', aic)
print('loglikehood:', logistic_model.llf)
# In[193]:
c_stat, aic = get_c_stat(test_pred), get_aic_value(logistic_model)
print('c_stat:', c_stat)
print('aic:', aic)
print('loglikehood:', logistic_model.llf)
# > 지우고 난 후
# In[202]:
unnecesarries.extend(['Personal_status_and_sex_A92', 'Housing_A152'])
# In[208]:
drop_logistic_model = sm.Logit(
train_y,
sm.add_constant(train_x.drop(unnecesarries, axis=1))
).fit()
# In[213]:
train_pred = pd.DataFrame({
'probs': drop_logistic_model.predict(sm.add_constant(train_x.drop(unnecesarries, axis=1))),
'class': train_y
})
train_pred['y_pred'] = 0
train_pred.loc[train_pred['probs'] > 0.5, 'y_pred'] = 1
# Test prediction
test_pred = pd.DataFrame({
'probs': drop_logistic_model.predict(sm.add_constant(test_x.drop(unnecesarries, axis=1))),
'class': test_y
})
test_pred['y_pred'] = 0
test_pred.loc[test_pred['probs'] > 0.5, 'y_pred'] = 1
# In[214]:
c_stat, aic = get_c_stat(train_pred), get_aic_value(drop_logistic_model)
print('c_stat:', c_stat)
print('aic:', aic)
print('loglikehood:', drop_logistic_model.llf)
# In[215]:
c_stat, aic = get_c_stat(test_pred), get_aic_value(drop_logistic_model)
print('c_stat:', c_stat)
print('aic:', aic)
print('loglikehood:', drop_logistic_model.llf)
# In[206]:
drop_logistic_model.summary2()
# ## 4. 무의미한 변수와 다중공선성이 있는 변수가 없을때까지 반복한다. (기준은 C통계량, AIC, Likelihood)
# 원래는 이 과정을 각각 변수를 확인하면서, 변수를 하나씩 제거해야 한다. 하지만 시간 관계상..
# #### Step 1. Pvalue
# In[218]:
# Pvalue를 확인하고, 상한선을 구한다.
unrelated_cols = get_unrelated_cols(drop_logistic_model, 0.5)
# In[219]:
unnecesarries.extend(unrelated_cols)
# #### Step 2. VIF
# In[221]:
vifs = get_max_vif(train_x, unnecesarries)
# In[222]:
vifs[:10]
# In[226]:
unnecesarries.extend([elem[0] for elem in vifs[:10]])
# In[228]:
drop_logistic_model = sm.Logit(
train_y,
sm.add_constant(train_x.drop(unnecesarries, axis=1))
).fit()
# In[229]:
train_pred = pd.DataFrame({
'probs': drop_logistic_model.predict(sm.add_constant(train_x.drop(unnecesarries, axis=1))),
'class': train_y
})
train_pred['y_pred'] = 0
train_pred.loc[train_pred['probs'] > 0.5, 'y_pred'] = 1
# Test prediction
test_pred = pd.DataFrame({
'probs': drop_logistic_model.predict(sm.add_constant(test_x.drop(unnecesarries, axis=1))),
'class': test_y
})
test_pred['y_pred'] = 0
test_pred.loc[test_pred['probs'] > 0.5, 'y_pred'] = 1
# In[230]:
c_stat, aic = get_c_stat(train_pred), get_aic_value(drop_logistic_model)
print('c_stat:', c_stat)
print('aic:', aic)
print('loglikehood:', drop_logistic_model.llf)
# In[231]:
c_stat, aic = get_c_stat(test_pred), get_aic_value(drop_logistic_model)
print('c_stat:', c_stat)
print('aic:', aic)
print('loglikehood:', drop_logistic_model.llf)
# In[232]:
drop_logistic_model.summary2()
# In[235]:
print('Train Confusion matrix:')
display(pd.crosstab(train_pred['y_pred'], train_pred['class'], rownames=['Predict'], colnames=['Actual'], margins=True))
# In[233]:
print('Test Confusion matrix:')
display(pd.crosstab(test_pred['y_pred'], test_pred['class'], rownames=['Predict'], colnames=['Actual'], margins=True))
# ## 5. ROC curve, AUC
# - ROC = FPR vs TPR(Recall) 의 비율
# - AUC = ROC 곡선 아래의 면적
# In[237]:
import matplotlib.pyplot as plt
from sklearn import metrics
from sklearn.metrics import auc
plt.rcParams["figure.figsize"] = (7,7)
# In[238]:
fpr, tpr, thresholds = metrics.roc_curve(train_pred['class'], train_pred['probs'], pos_label=1)
# In[240]:
roc_auc = auc(fpr, tpr)
# In[241]:
plt.figure()
lw = 2
plt.plot(fpr, tpr, color='darkorange', lw=lw, label='ROC curve (area = %0.4f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.xlabel('False Postive Ratio')
plt.ylabel('True Postive Ratio(Recall)')
plt.title('ROC curve')
plt.legend(loc='lower right')
plt.show()
# ## 6. 결정경계
# In[253]:
for i in np.arange(0, 1, 0.1):
train_pred['y_pred'] = 0
train_pred.loc[train_pred['probs'] > i, 'y_pred'] = 1
acc = round(accuracy_score(train_pred['class'], train_pred['y_pred']), 4)
print("Threshold", round(i,1), "Train accuracy:",acc)
# In[254]:
for i in np.arange(0, 1, 0.1):
test_pred['y_pred'] = 0
test_pred.loc[test_pred['probs'] > i, 'y_pred'] = 1
acc = round(accuracy_score(test_pred['class'], test_pred['y_pred']), 4)
print("Threshold", round(i,1), "Test accuracy:",acc)
# In[261]:
# Test prediction
test_pred = pd.DataFrame({
'probs': drop_logistic_model.predict(sm.add_constant(test_x.drop(unnecesarries, axis=1))),
'class': test_y
})
test_pred['y_pred'] = 0
test_pred.loc[test_pred['probs'] > 0.4, 'y_pred'] = 1
# In[262]:
print('Test Confusion matrix:')
display(pd.crosstab(test_pred['y_pred'], test_pred['class'], rownames=['Predict'], colnames=['Actual'], margins=True))