金融风控模型之如何制作评分卡
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
import warnings
from imblearn.over_sampling import SMOTE
from scipy import stats
from sklearn.linear_model import LogisticRegression
import scikitplot as skplt
warnings.filterwarnings('ignore')
# data_path = './data_file/Acard.csv'
#
# data = pd.read_csv(data_path,index_col=0)
# #
# # ====================================第一部分: 缺失值的处理=============================================
# # 先给数据进行去重
# data = data.drop_duplicates()
# # 重新设置索引值
# data.index = range(data.shape[0])
#
# # 发现有两列缺失值 MonthlyIncome(收入) NumberOfDependents(家属人数)
# # MonthlyIncome 用这一列的平均值来填充 fillna
# # NumberOfDependents 这个用随机森林学习的结果填充
# data['NumberOfDependents'] = data['NumberOfDependents'].fillna(int(data['NumberOfDependents'].mean()))
#
# data.index = range(data.shape[0])
#
# # 填补 MonthlyIncome 这一列的缺失值 用随机森林的方法
# def fill_missing_value(x,y,to_fill):
# df = x.copy()
# fill = x.loc[:,to_fill]
# df = pd.concat([df.loc[:,df.columns != to_fill],pd.DataFrame(y)],axis=1)
# y_train = fill[fill.notnull()]
# y_test = fill[fill.isnull()]
# x_train = df.iloc[y_train.index,:]
# x_test = df.iloc[y_test.index,:]
# # 用随机森林算法来预测缺失值
# rfr = RandomForestRegressor(n_estimators=100)
# rfr.fit(x_train,y_train)
# y_pred = rfr.predict(x_test)
# return y_pred
#
# x = data.iloc[:,1:]
# y = data['SeriousDlqin2yrs']
#
# y_predict = fill_missing_value(x,y,'MonthlyIncome')
#
# data.loc[data.loc[:,'MonthlyIncome'].isnull(),'MonthlyIncome'] = y_predict
#
# data.index = range(data.shape[0])
#
# print(data.isnull().sum()/data.shape[0])
#
# print(data.info())
#
# data.to_csv('./data_file/Acard_not_have_null_data.csv')
# ==================================第二部分: 描述性统计(根据经验 对数据进行处理[ 异常值过滤等 ])===========================================
data = pd.read_csv('./data_file/Acard_not_have_null_data.csv', index_col=0)
# 到这一步的时候 数据中已经没有了缺失值
# 描述性统计 根据经验 选出特征中可能存在异常值的特征 用箱线图看 过滤掉异常值
# x1 = data['age']
# fig,axes = plt.subplots()
# axes.boxplot(x1)
# axes.set_xticklabels(['age'])
# plt.show()
# cols = data.columns
# print(cols)
# for col in cols[1:]:
# print(data[col].value_counts())
# x = data[col]
# fig, axes = plt.subplots()
# axes.boxplot(x)
# axes.set_xticklabels(col)
# axes.set_title(col)
# plt.show()
# 通过对以上的箱线图 进行分析
# 1.RevolvingUtilizationOfUnsecuredLines(贷款以及信用卡可用额度与总额度的比值): 每个人的信用不同,自然这个特征高低属于正常
# 2.age(年龄): 年龄分布差距很大,一百多岁的 0 岁的都有 所以这是一些异常值 需要对数据过滤一下
# 3.NumberOfTime30-59DaysPastDueNotWorse(出现35-59天逾期 但是没有发展到更坏的次数): 这个次数无论是多少 都是正常的 因为是真实的数据
# 4.DebtRatio(每月花销/每月总收入): 这个也是正常的 每个人的情况不同
# 5.MonthlyIncome(月收入): 这个也是正常的 每个人的情况不同(挣得多少都有 所以很正常)
# 6.NumberOfOpenCreditLinesAndLoans(开放式贷款和信贷数量): 这个也很正常
# 7.NumberOfTimes90DaysLate(两年内出现 90 天逾期或更坏的次数): 通过观察图, 有 96 天 5次, 和 98 天 220次 (可以要也可以不要) 我选择 不要
# 8.NumberRealEstateLoansOrLines(抵押贷款和房地产贷款数量,包括房屋净值信贷额度): 这个也很正常
# 9.NumberOfTime60-89DaysPastDueNotWorse(两年内出现 60-89 天逾期但是没有发展到更坏的次数): 无需处理
# 10.NumberOfDependents(家属人数[不包括自身]): 这个也没啥异常值
# 总的来说 需要处理异常值的只有两列 (age,NumberOfTimes90DaysLate)
# 处理 age (处理后的 age 显得正常了许多)
data = data[data['age'] < 100]
data = data[data['age'] > 0]
# 处理 NumberOfTimes90DaysLate
data = data[data.loc[:, 'NumberOfTimes90DaysLate'] < 90]
# =======================================第三部分: 处理样本不均衡问题==============================================================
# 观察 标签的分布 看能不能看出啥
# (发现样本分布不均衡 大部分都是好的用户 很少有坏的用户) 接下来就是处理样本不均衡的步骤了
# data['SeriousDlqin2yrs'].value_counts().plot(kind='bar')
# plt.show()
# 调用 imblearn.over_sampling 下的 SMOTE 来处理样本不均衡问题(过采样)
sm = SMOTE(random_state=42) # 进行实例化
# 将 特征 与 标签 分离 进行 fit_sample 发现 处理过后的正负样本达到了 1:1
x = data.iloc[:, 1:]
y = data.iloc[:, 0]
x, y = sm.fit_sample(x, y)
new_data = pd.concat([y, x], axis=1)
# new_data['SeriousDlqin2yrs'].value_counts().plot(kind='bar')
# plt.show()
# ====================================第四部分: 进行训练集和测试集的分割 并做备份=====================================================
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=42)
train_data = pd.concat([y_train, x_train], axis=1)
train_data.columns = data.columns
test_data = pd.concat([y_test, x_test], axis=1)
test_data.columns = data.columns
# 将文件进行备份
# train_data.to_csv('./data_file/Acard_train.csv')
# test_data.to_csv('./data_file/Acard_test.csv')
# ===================================第五部分: 对数据进行分箱处理====================================================================
def grapdforbestbin(df, x, y, n=5, q=20, graph=True):
df = df[[x, y]].copy()
df['qcut'], bins = pd.qcut(df[x], q=q, retbins=True, duplicates='drop')
count_y0 = df.loc[df[y] == 0].groupby(by='qcut').count()[y]
count_y1 = df.loc[df[y] == 1].groupby(by='qcut').count()[y]
num_bins = [*zip(bins, bins[1:], count_y0, count_y1)]
# 计算 WOE
def get_woe(num_bins):
columns = ['min', 'max', 'count_0', 'count_1']
df = pd.DataFrame(num_bins, columns=columns)
df['total'] = df['count_0'] + df['count_1']
# df['percentage'] = df['total'] / df['total'].sum()
# df['bad_rate'] = df['count_1'] / df['total']
df['good%'] = df['count_0'] / df['count_0'].sum()
df['bad%'] = df['count_1'] / df['count_1'].sum()
df['woe'] = np.log(df['good%'] / df['bad%'])
return df
# 计算 IV 值
def get_iv(num_bins):
iv = np.sum((num_bins['good%'] - num_bins['bad%']) * num_bins['woe'])
return iv
IV = [] # 用于存放 IV 值
axisx = [] # 用于存放 WOE 值
num_bins_ = num_bins.copy()
while len(num_bins_) > n:
pvs = []
for i in range(len(num_bins_) - 1):
x1 = num_bins_[i][2:]
x2 = num_bins_[i + 1][2:]
pv = stats.chi2_contingency([x1, x2])[1]
pvs.append(pv)
i = pvs.index(max(pvs))
num_bins_[i:i + 2] = [(num_bins_[i][0], num_bins_[i + 1][1], num_bins_[i][2] + num_bins_[i + 1][2],
num_bins_[i][3] + num_bins_[i + 1][3])]
bins_df = pd.DataFrame(get_woe(num_bins_))
axisx.append(len(num_bins_))
IV.append(get_iv(bins_df))
if graph:
plt.figure()
plt.plot(axisx, IV)
plt.xticks(axisx)
plt.xlabel('number of box')
plt.ylabel('IV')
plt.show()
return bins_df
# print(new_data[1:-1].info())
# 先根据 2 分箱 然后找出最佳的分箱数 记录下
# for i in new_data.columns[1:-1]:
# print(i)
# grapdforbestbin(data,i,'SeriousDlqin2yrs',n=2,q=20)
# 自动分箱: 这些数据是根据画出的折线图 寻找最优的折线点(最优分箱个数)(把连续数据变为离散)
auto_bins = {
'RevolvingUtilizationOfUnsecuredLines': 5,
'age': 6,
'DebtRatio': 4,
'MonthlyIncome': 3,
'NumberOfOpenCreditLinesAndLoans': 7
}
# 手动分箱: 因为这些数据本来就是离散的,所以画图画不出来,就需要我们手动进行分箱
# 观察数据的分布 自己决定分箱边界
# NumberOfTime30-59DaysPastDueNotWorse 这一列范围是 0-13 根据每个特征的值 均匀分箱
# print(new_data['NumberOfTime30-59DaysPastDueNotWorse'].value_counts())
# NumberOfTimes90DaysLate 范围是 0-17 进行均匀分箱
# print(new_data['NumberOfTimes90DaysLate'].value_counts())
# NumberRealEstateLoansOrLines 范围是 0-54 进行均匀分箱
# print(new_data['NumberRealEstateLoansOrLines'].value_counts())
# NumberOfDependents 范围是 0-3 进行均匀分箱
# print(new_data['NumberOfDependents'].value_counts())
hand_bins = {
'NumberOfTime30-59DaysPastDueNotWorse': [0, 1, 2, 13],
'NumberOfTimes90DaysLate': [0, 1, 2, 17],
'NumberRealEstateLoansOrLines': [0, 1, 2, 4, 54],
'NumberOfTime60-89DaysPastDueNotWorse': [0, 1, 2, 8],
'NumberOfDependents': [0, 1, 2, 3]
}
# 这一步是因为手动分箱后 有可能再来新的数据 但是并不在你所分箱范围之内 索性将它范围变大 让新来的数据有家可归
# 这个是看别人写的 中间的 *v[:-1] 没明白 就自己写了下面那个容易懂的
# hand_bins = {k: [-np.inf, *v[:-1], np.inf] for k, v in hand_bins.items()}
# 容易懂的那个 这一步是将 hand_bins 分箱后的边界值换成 -np.inf 和 np.inf
for k, v in hand_bins.items():
v.insert(0, -np.inf)
v[-1] = np.inf
# print(hand_bins)
# 这一步是将 auto_bins 分箱后的边界值换成 -np.inf 和 np.inf , 最后和 hand_bins 进行合并 存入到一个新的字典中
bins_of_col = {}
for col in auto_bins:
bins_df = grapdforbestbin(new_data, col, 'SeriousDlqin2yrs', n=auto_bins[col], q=20, graph=False)
bins_list = sorted(set(bins_df['min']).union(bins_df['max']))
bins_list[0], bins_list[-1] = -np.inf, np.inf
bins_of_col[col] = bins_list
bins_of_col.update(hand_bins)
# print(bins_of_col)
#
# # ========================================第六部分: 计算各箱的 WOE 并映射到数据====================================================
# new_data1 = new_data.copy()
# new_data1 = new_data1[['NumberOfTime30-59DaysPastDueNotWorse', 'SeriousDlqin2yrs']].copy()
# print(new_data1['NumberOfTime30-59DaysPastDueNotWorse'].value_counts())
# # 新增 cut 一列 给每个 age 分配相应的区间
# new_data1['cut'] = pd.cut(new_data1['NumberOfTime30-59DaysPastDueNotWorse'], [-np.inf, 0, 1, 2,np.inf])
# print(new_data1['cut'].value_counts())
# bins_df2 = new_data1.groupby('cut')['SeriousDlqin2yrs'].value_counts()#.unstack()
# print(bins_df2)
# bins_df2['woe'] = np.log((bins_df2[0] / bins_df2[0].sum()) / (bins_df2[1] / bins_df2[1].sum()))
# print(bins_df2)
#
def get_woe_all(df, col, y, bins):
'''
获取所有特征的 woe
:param df: 数据表
:param col: 想要计算的特征(一列)
:param y: 标签列
:param bins: 箱子的个数 列表 在 bins_of_col字典中 通过标签取值
:return: woe 值
'''
df = df[[col, y]].copy()
df['cut'] = pd.cut(df[col], bins)
# print(df['cut'])
bins_df2 = df.groupby('cut')[y].value_counts().unstack()
bins_df2['woe'] = np.log((bins_df2[0] / bins_df2[0].sum()) / (bins_df2[1] / bins_df2[1].sum()))
return bins_df2
woe_all = {}
# for key,value in bins_of_col.items():
# woe_all[key] = get_woe_all(new_data,key,'SeriousDlqin2yrs',value)
for col in bins_of_col:
woe_all[col] = get_woe_all(new_data, col, 'SeriousDlqin2yrs', bins_of_col[col])
# print(woe_all)
# for col in woe_all:
# print(col)
# print(woe_all[col]['woe'])
# print('============================')
model_woe = pd.DataFrame(index=new_data.index)
for col in bins_of_col:
model_woe[col] = pd.cut(new_data[col], bins_of_col[col])
model_woe[col] = model_woe[col].map(woe_all[col]['woe'])
model_woe['SeriousDlqin2yrs'] = new_data['SeriousDlqin2yrs']
print(model_woe)
# 将测试集 也进行同样的处理
path = './data_file/Acard_test.csv'
vali_data = pd.read_csv(path, index_col=0)
woe_all_test = {}
# 计算测试数据的 woe 中 存到字典中
for col in bins_of_col:
woe_all_test[col] = get_woe_all(vali_data, col, 'SeriousDlqin2yrs', bins_of_col[col])
# 通过字典中的 woe 值 对测试集中的数据进行替换
vali_woe = pd.DataFrame(index=vali_data.index)
for col in bins_of_col:
vali_woe[col] = pd.cut(vali_data[col], bins_of_col[col]).map(woe_all_test[col]['woe'])
vali_woe['SeriousDlqin2yrs'] = vali_data['SeriousDlqin2yrs']
# ===================================第七部分: 将映射完成的数据进行用逻辑回归训练======================================================
x = model_woe.iloc[:, :-1]
y = model_woe.iloc[:, -1]
vali_x = vali_woe.iloc[:, :-1]
vali_y = vali_woe.iloc[:, -1]
# c1 = np.linspace(0.01,1,20)
# c2 = np.linspace(0.01,0.2,10)
# # 通过不同间隔的取值 最终 在 0.025 的时候分数最高
# score = []
# # for i in c2:
# # lr = LogisticRegression(solver='liblinear',C=i).fit(x,y)
# # score.append(lr.score(vali_x,vali_y))
#
# # 在迭代次数 为 4 的时候是最好的
# for i in [1,2,3,4,5,6]:
# lr = LogisticRegression(solver='liblinear',C=0.025,max_iter=i).fit(x,y)
# score.append(lr.score(vali_x,vali_y))
#
# plt.figure()
# plt.plot([1,2,3,4,5,6],score)
# plt.grid()
# plt.title('score')
# plt.show()
# 这里的 max_iter 和 C 的值 是根据上面的调参 观察图形得出来的
lr = LogisticRegression(solver='liblinear', max_iter=4, C=0.025)
lr.fit(x, y)
print(lr.score(vali_x, vali_y))
# 根据上面的结果 模型的分数有点低 接下来用 ROC 做模型的提升
vali_proba_df = pd.DataFrame(lr.predict_proba(vali_x))
skplt.metrics.plot_roc(vali_y, vali_proba_df, plot_micro=False, figsize=(6, 6), plot_macro=False)
# plt.show()
# ===============================================第八部分: 制作评分卡==================================================================
# 评分卡中的公式 Score = A - B * log(odds)
# log(odds) 代表了一个人违约的可能性
# A B 两个常熟可以通过假设值来求出
B = 20 / np.log(2)
A = 600 + B * np.log(1 / 60)
print(A, B)
print(lr.intercept_)
base_score = A - B * lr.intercept_
print(base_score)
file = './score/Score_data.csv'
with open(file, "w") as fdata:
fdata.write("base_score,{}\n".format(base_score))
for i, col in enumerate(x.columns):
# print(lr.coef_) # 一次项系数
# print(lr.intercept_) # 截距
score = woe_all[col]['woe'] * (-B * lr.coef_[0][i])
score.name = "Score"
score.index.name = col
score.to_csv(file, header=True, mode='a')
数据集在这里:
链接:https://pan.baidu.com/s/1jJytgbvny6Ay2AIsrJOtsw
提取码:bzoh
复制这段内容后打开百度网盘手机App,操作更方便哦–来自百度网盘超级会员V5的分享