数据挖掘--金融信用评分卡的制作(python)

金融风控模型之如何制作评分卡

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的分享

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值