首页 > 其他分享 >【金融风控】模型评分卡构建

【金融风控】模型评分卡构建

时间:2024-11-13 09:48:26浏览次数:3  
标签:info val 评分 dev 风控 ks train 构建 data

内容介绍

  • 掌握KS值的计算方法

  • 知道评分映射方法

  • 知道LightGBM基本原理

  • 掌握使用lightGBM进行特征筛选的方法

  • 应用toad构建评分卡模型

【理解】模型构建流程

实验设计

  • 新的模型能上线一定要比原有方案有提升,需要通过实验证明

    冷启动业务初期成长期波动期策略调整新增数据源
    人工审核人工审核新旧模型对比新旧模型对比避免迭代模型新旧模型对比
    规则模型标准模型长短表现期对比稳定和波动人群线上模型、陪跑和标准模型对比
    数据驱动模型上一版模型
    • 业务逐渐稳定后,人工审核是否会去掉

      • 一般算法模型上线后,在高分段和低分段模型表现较好,中间的用户可能需要人工参与审核

      • 模型表现越来越好之后,人工审核的需求会逐步降低,但不会去掉

    • 标准模型:逻辑回归,集成学习

    • 策略和模型不会同时调整

样本设计

  • ABC卡观察期,表现期

观察期:代表的是决策时已知的信息,位于时间轴左侧,主要是用来生成用户特征的时间区间,即用来确定X变量。

观察点:不是一个具体的时间点,而是一个时间段,表示的是客户申请贷款的时间,用来搜集那些用来建模的客户样本 ,在该时间段申请的客户会是我们用来建模的样本。

表现期:用来定义用户是否好坏的时间区段,即用来确定Y标签,其代表的是决策时未知的但对决策效果非常重要而需要预测的信息。

观察期表现期
实时A卡申请时点往前6~12个月FPD7,FPD30
白名单A卡邀请时点/激活时点往前6~12个月FPD30
B卡当前未逾期用户任意用信时点前6~12个月当期/后续2-6期DPD30/DPD60
C卡当前逾期未还用户还款日后1天/30天/60天往前6~12个月当期DPD30/60/90
  • 还款状态和DPD一起刻画了用户的逾期情况

还款日前(Before Due)还款日后(After Due)
完全还款(Fully Repay)FB(好用户)FA(催回来了)
部分还款(Patially Repay)PBPA(有意愿,但无力完全还款)
展期(Extend)EB(提前展期)EA (违约后展期,可能是高危用户)
未还款(Not Repay)NBNA
  • A卡 申请新客 B卡未逾期老客 C卡 逾期老客

  • 当前逾期:出现逾期且到观测点为止未还清 NA,PA

  • 历史逾期:曾经出现过逾期已还清或当前逾期 FA,NA,PA

  • 举例

一月二月三月四月五月
还款状态还清还清还清还清还清
DPD400000

上面情况属于B卡客户

一月二月三月四月五月
还款状态还清还清还清还清未还
DPD000040

上面情况属于C卡客户

一月二月三月四月五月
还款状态还清还清还清未还未还
DPD40004010

上面情况属于C卡客户

  • 样本设计表格

训练集测试集
1月2月3月4月5月6月7月8月
总#100200300400500600700800
坏#366815121424
坏%3%3%2%2%3%2%2%3%
  • 观察坏样本的比例,不要波动过大

  • 客群描述:首单用户、内部数据丰富、剔除高危职业、收入范围在XXXX

  • 客群标签:好: FPD<=30 坏: FPD>30

模型训练与评估

  • 目前还是使用机器学习模型,少数公司在尝试深度学习

  • 模型的可解释性>稳定性>区分度

    • 区分度:AUC,KS

    • 稳定性: PSI

  • 业务指标:通过率,逾期率

    • 逾期率控制在比较合理的范围的前提下,要提高通过率

    • A卡,要保证一定过得通过率,对逾期率可以有些容忍

    • B卡,想办法把逾期率降下来,好用户提高额度

  • AUC和KS

    • AUC:ROC曲线下的面积,反映了模型输出的概率对好坏用户的排序能力

    • KS反映了好坏用户的分布的最大的差别

    • ROC曲线是对TPR和FPR的数值对的记录

    • KS = max(TPR-FPR)

  • AUC和KS的区别可以简化为

    • AUC反映模型区分度的平均状况

    • KS反映了模型区分度的最佳状况

  • PSI和特征里的PSI完全一样

模型上线整体流程

【实现】逻辑回归评分卡

评分映射方法

  • 使用逻辑回归模型可以得到一个[0,1]区间的结果, 在风控场景下可以理解为用户违约的概率, 评分卡建模时我们需要把违约的概率映射为评分

  • 举例:

    • 用户的基础分为650分

      • 当这个用户非逾期的概率是逾期的概率的2倍时,加50分

      • 非逾期的概率是逾期的概率的4倍时,加100分

      • 非逾期的概率是逾期的概率的8倍时,加150分

      • 以此类推,就得到了业内标准的评分卡换算公式

    • 正样本负样本
      • score是评分卡映射之后的输出,$P{正样本}$是样本非逾期的概率,$P{负样本}$是样本逾期的概率

    • 逻辑回归评分卡如何与评分卡公式对应

      • 逻辑回归方程为

      p = \frac{1}{1+e^{-t}} \\ 1-p = \frac{e^{-t}}{1+e^{-t}} \\ ln(p/(1-p)) = ln(e^{t})=t
  • 在信用评分模型建模时,逻辑回归的线性回归成分输出结果为

  • 由对数换底公式可知:

    只需要解出逻辑回归中每个特征的系数,然后将样本的每个特征值加权求和即可得到客户当前的标准化信用评分

    • 基础分(Base Score)为650分,步长(Point of Double Odds,PDO)为50分,这两个值需要根据业务需求进行调整

逻辑回归评分卡

import pandas as pd
from sklearn.metrics import roc_auc_score,roc_curve,auc
from sklearn.model_selection import train_test_split
from sklearn import metrics
from sklearn.linear_model import LogisticRegression
import numpy as np
import random
import math
  • 读取数据

data = pd.read_csv('../data/Bcard.txt')
data.head()

输出结果

obs_mthbad_induidtd_scorejxl_scoremj_scorerh_scorezzc_scorezcx_scoreperson_infofinance_infocredit_infoact_info
02018-10-310.0A100000050.6753490.1440720.1868990.4836400.9283280.369644-0.3225810.0238100.000.217949
12018-07-310.0A10000020.8252690.3986880.1393960.8437250.6051940.406122-0.1286770.0238100.000.423077
22018-09-300.0A10000110.3154060.6297450.5358540.1973920.6144160.3207310.0626600.0238100.100.448718
32018-07-310.0A100004810.0023860.6093600.3660810.3422430.8700060.2886920.0788530.0714290.050.179487
42018-07-310.0A10000690.4063100.4053520.7830150.5639530.7154540.512554-0.2610140.0238100.000.423077
  • 数据字段说明

    • bad_ind 为标签

    • 外部评分数据:td_score,jxl_score,mj_score,rh_score,zzc_score,zcx_score

    • 内部数据:person_info, finance_info, credit_info, act_info

    • obs_month:申请日期所在月份的最后一天(数据经过处理,将日期都处理成当月最后一天)

  • 看一下月份分布,用最后一个月做为时间外样本

data.obs_mth.unique()

输出结果

array(['2018-10-31', '2018-07-31', '2018-09-30', '2018-06-30',
       '2018-11-30'], dtype=object)
  • 划分测试数据和验证数据(时间外样本)

train = data[data.obs_mth != '2018-11-30']
val = data[data.obs_mth == '2018-11-30']
  • 取出建模用到的特征

#info结尾的是自己做的无监督系统输出的个人表现,score结尾的是收费的外部征信数据
feature_lst = ['td_score', 'jxl_score', 'mj_score','rh_score', 'zzc_score', 'zcx_score', 'person_info', 'finance_info','credit_info', 'act_info']
  • 训练模型

x = train[feature_lst]
y = train['bad_ind']

val_x =  val[feature_lst]
val_y = val['bad_ind']
#C:正则化系数
lr_model = LogisticRegression(C=0.1)
lr_model.fit(x,y)

输出结果

LogisticRegression(C=0.1, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)
  • 模型评价

    • ROC曲线:描绘的是不同的截断点时,并以FPR和TPR为横纵坐标轴,描述随着截断点的变小,TPR随着FPR的变化

      • 纵轴:TPR=正例分对的概率 = TP/(TP+FN),其实就是查全率/召回率

      • 横轴:FPR=负例分错的概率 = FP/(FP+TN) 原本是0被预测为1的样本在所有0的样本中的概率

    • KS值

      • 作图步骤

      • 根据学习器的预测结果(注意,是正例的概率值,非0/1变量)对样本进行排序(从大到小)-----这就是截断点依次选取的顺序 按顺序选取截断点,并计算TPR和FPR ---也可以只选取n个截断点,分别在1/n,2/n,3/n等位置 横轴为样本的占比百分比(最大100%),纵轴分别为TPR和FPR,可以得到KS曲线max(TPR-FPR)

      • ks = max(TPR-FPR)TPR和FPR曲线分隔最开的位置就是最好的”截断点“,最大间隔距离就是KS值,通常>0.2即可认为模型有比较好偶的预测准确性

  • 绘制ROC计算KS

#0:非违约概率,1:违约概率
y_pred = lr_model.predict_proba(x)[:,1] #取出训练集预测值
fpr_lr_train,tpr_lr_train,_ = roc_curve(y,y_pred) #计算TPR和FPR
train_ks = abs(fpr_lr_train - tpr_lr_train).max() #计算训练集KS
print('train_ks : ',train_ks)

y_pred = lr_model.predict_proba(val_x)[:,1] #计算验证集预测值
fpr_lr,tpr_lr,_ = roc_curve(val_y,y_pred)   #计算验证集预测值
val_ks = abs(fpr_lr - tpr_lr).max()         #计算验证集KS值
print('val_ks : ',val_ks)

from matplotlib import pyplot as plt
plt.plot(fpr_lr_train,tpr_lr_train,label = 'train LR') #绘制训练集ROC
plt.plot(fpr_lr,tpr_lr,label = 'evl LR')               #绘制验证集ROC
plt.plot([0,1],[0,1],'k--')
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC Curve')
plt.legend(loc = 'best')
plt.show()

显示结果:

train_ks :  0.4151676259891534
val_ks :  0.3856283523530577
  • 使用lightgbm进行特征筛选

# lightgbm版本 3.0.0
import lightgbm as lgb
from sklearn.model_selection import train_test_split
train_x,test_x,train_y,test_y = train_test_split(x,y,random_state=0,test_size=0.2)

#定义模型训练的方法
def  lgb_test(train_x,train_y,test_x,test_y):
    #objective:binary表示二分类,metric:指标采用auc
    clf =lgb.LGBMClassifier(boosting_type = 'gbdt',
                           objective = 'binary',
                           metric = 'auc',
                           learning_rate = 0.1,
                           n_estimators = 24,
                           max_depth = 5,
                           num_leaves = 20,
                           max_bin = 45,
                           min_data_in_leaf = 6,
                           bagging_fraction = 0.6,
                           bagging_freq = 0,
                           feature_fraction = 0.8,
                           )
    #eval_set:用于作为验证集的(X, y)元组对的列表。
    clf.fit(train_x,train_y,eval_set = [(train_x,train_y),(test_x,test_y)],eval_metric = 'auc')
    return clf,clf.best_score_['valid_1']['auc']

#调用方法,训练模型
lgb_model , lgb_auc  = lgb_test(train_x,train_y,test_x,test_y)
feature_importance = pd.DataFrame({'name':lgb_model.booster_.feature_name(),
                                   'importance':lgb_model.feature_importances_}).sort_values(by=['importance'],ascending=False)


#查看特征重要性
feature_importance

显示结果:

feature_importance

nameimportance
6person_info65
8credit_info57
9act_info55
7finance_info50
4zzc_score46
5zcx_score44
2mj_score39
0td_score34
3rh_score34
1jxl_score32
  • 关于特征重要性, 需要注意的是, 这是一个相对的结果, 重要性得分的大小只在当前次训练有效果。不能在不同的模型对比得分, 当模型参数发生了变化、训练的数据发生变化, 重要性得分也会有变化

    • 可以通过相对的结果判断哪个特征相对更加重要

    • 可以结合着具体模型表现,决定要去掉/保留哪些特征, 也可以换几组数据, 多跑几次这个结果, 计算重要性的平均分, 获取一个更加可靠的重要性排序

  • 模型调优,去掉几个特征,重新建模

#确定新的特征
feature_lst = ['person_info','finance_info','credit_info','act_info']
x = train[feature_lst]
y = train['bad_ind']

val_x =  val[feature_lst]
val_y = val['bad_ind']

#训练逻辑回归模型
lr_model = LogisticRegression(C=0.1)
lr_model.fit(x,y)

#预测结果,计算训练集的ks
y_pred = lr_model.predict_proba(x)[:,1]
fpr_lr_train,tpr_lr_train,_ = roc_curve(y,y_pred)
train_ks = abs(fpr_lr_train - tpr_lr_train).max()
print('train_ks : ',train_ks)

#计算测试集的ks
y_pred = lr_model.predict_proba(val_x)[:,1]
fpr_lr,tpr_lr,_ = roc_curve(val_y,y_pred)
val_ks = abs(fpr_lr - tpr_lr).max()
print('val_ks : ',val_ks)

#画图
from matplotlib import pyplot as plt
plt.plot(fpr_lr_train,tpr_lr_train,label = 'train LR')
plt.plot(fpr_lr,tpr_lr,label = 'evl LR')
plt.plot([0,1],[0,1],'k--')
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC Curve')
plt.legend(loc = 'best')
plt.show()

显示结果:

train_ks :  0.41573985983413414
val_ks :  0.3928959732014397

  • 打印回归系数

# 系数
print('变量名单:',feature_lst)
print('系数:',lr_model.coef_)
print('截距:',lr_model.intercept_)

输出结果

变量名单: ['person_info', 'finance_info', 'credit_info', 'act_info']
系数: [[ 2.48386162  4.44901224  1.88254182 -1.43356854]]
截距: [-3.90631899]
  • 生成报告

模型报告的作用,就是在模型迭代的时候,可以通过这个报告来比较新旧模型的效果。

计算出报告中所需要的字段:KS值、负样本个数、正样本个数、负样本累计个数、正样本累计个数、捕获率、负样本占比。

# 把验证集的数据代入到训练好的模型, 输出违约率, 把所有的用户的违约率按从大到小排序, 然后均匀划分成20箱
# 计算每一箱的跟违约相关的指标, 违约率和 KS
bins = 20
temp_df = pd.DataFrame() # 准备空白的df
# 用训练好的模型, 输出测试集的违约率
temp_df['bad_rate_predict'] = lr_model.predict_proba(val_x)[:,1] # 模型预测的违约率
temp_df['real_bad']= val_y.values # 真实的标签

temp_df = temp_df.sort_values('bad_rate_predict',ascending=False)

temp_df['num'] = [i for i in range(temp_df.shape[0])]
temp_df['num'] = pd.cut(temp_df['num'],bins = bins,labels=[i for i in range(bins)])



# 创建报告
report = pd.DataFrame()
# 每一组有多少1 bad标签 , 每一组有多少0 good标签
report['BAD'] =temp_df.groupby('num')['real_bad'].sum().astype(int)
report['GOOD'] =temp_df.groupby('num')['real_bad'].count().astype(int)-report['BAD']
# 累计求和 累计到这一组, 有多少1, 多少0
report['BAD_CNT'] = report['BAD'].cumsum()
report['GOOD_CNT'] = report['GOOD'].cumsum()
# 计算累计到当前组, 出现的1标签的比例 
good_total = report['GOOD_CNT'].max()
bad_total = report['BAD_CNT'].max()
report['BAD_PCTG'] = round(report['BAD_CNT']/bad_total,3)
# 当前组 1标签比例
report['BADRATE'] = report.apply(lambda x:round(x['BAD']/(x['BAD']+x['GOOD']),3),axis = 1)
# 当前组ks
def cal_ks(x):
	# tpr = tp(预测的1标签)/tp+fn(所有的1标签)  fpr = fp(预测的0标签)/fp+tn (所有的0) GOOD_CNT
	ks = (x['BAD_CNT']/bad_total)-(x['GOOD_CNT']/good_total)
	return round(abs(ks),3)
report['KS'] = report.apply(cal_ks,axis = 1)
print(report)

输出结果:

numBADGOODBAD_CNTGOOD_CNTBAD_PCTGBADRATEKS
069730697300.2100.0860.164
15074911914790.3630.0630.268
23576415422430.4700.0440.326
33176718530100.5640.0390.372
41978020437900.6220.0240.380
51878122245710.6770.0230.385
61578323753540.7230.0190.380
71478525161390.7650.0180.373
81778226869210.8170.0210.375
9679327477140.8350.0080.342
10879028285040.8600.0100.316
11679328892970.8780.0080.284
127792295100890.8990.0090.255
1310788305108770.9300.0130.235
147792312116690.9510.0090.205
155794317124630.9660.0060.170
164794321132570.9790.0050.131
174795325140520.9910.0050.093
181798326148500.9940.0010.045
192797328156471.0000.0030.000

从报告中可以看出

① 模型的KS最大值出现在第6箱(编号5),如将箱分的更细,KS值会继续增大,上限为前面通过公式计算出的KS值

② 前4箱的样本占总人数的20%,捕捉负样本占所有负样本的56.4%,如拒绝分数最低的20%的人,可以捕捉到56.4%的负样本。

  • Pyecharts绘图,绘制负样本占比和KS值,观察模型表现

from pyecharts.charts import *
from pyecharts import options as opts
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
np.set_printoptions(suppress=True)
pd.set_option('display.unicode.ambiguous_as_wide', True)
pd.set_option('display.unicode.east_asian_width', True)
line = (

    Line()
    .add_xaxis(report.index.values.tolist())
    .add_yaxis(
        "分组坏人占比",
        list(report.BADRATE),
        yaxis_index=0,
        color="red",
    )
    .set_global_opts(
        title_opts=opts.TitleOpts(title="评分卡模型表现"),
    )
    .extend_axis(
        yaxis=opts.AxisOpts(
            name="KS值",
            type_="value",
            min_=0,
            max_=0.5,
            position="right",
            axisline_opts=opts.AxisLineOpts(
                linestyle_opts=opts.LineStyleOpts(color="red")
            ),
            axislabel_opts=opts.LabelOpts(formatter="{value}"),
        )

    )
    .add_yaxis(
        "KS",
        list(report['KS']),
        yaxis_index=1,
        color="blue",
        label_opts=opts.LabelOpts(is_show=False),
    )
)
line.render()

输出结果:

  • 图中蓝色曲线为负样本占比曲线,红色曲线为KS曲线

    • 模型在第8箱的位置出现了波动,即第8箱的负样本占比高于第7箱

    • 虽然曲线图中有多处波动,但幅度不大,总体趋势较为平稳。因此模型的排序能力仍可被业务所接受。

  • 评分映射

# 定义分数转换的公式
coef_ = lr_model.coef_.tolist()[0]
# ['person_info', 'finance_info','credit_info', 'act_info']
def score(x):
	lr_result = x.person_info*coef[0]+x.finance_info*coef[1]+x.credit_info*coef[2]+x.act_info*coef[3]
	score = 600+50*lr_result/math.log(2)
	return score

#转换评分
val['score'] = val.apply(score,axis=1)
fpr, tpr, thresholds = roc_curve(val_y,val['score'])
val_ks = abs(fpr-tpr).max()
val_ks

输出结果:

val_ks : 0.3928959732014397

  • 划分评级:可以通过分数段对客群进行划分,得到每一个级别用户的逾期率。

#对应评级区间
def level(score):
    level = 0
    if score <= 550:
        level = "D"
    elif score <= 600 and score > 550 :
        level = "C"
    elif score <= 620 and score > 600:
        level = "B"
    elif  score > 620 :
        level = "A"
    return level
val['level'] = val.score.map(lambda x : level(x) )

val.level.groupby(val.level).count()/len(val)

输出结果

level
A    0.121064
B    0.256463
C    0.376463
D    0.246009
Name: level, dtype: float64
  • 如果希望某个区间的逾期率更大或者更小,可以调整评分映射函数中的基础分和系数。

【实现】集成学习评分卡

LightGBM

什么是lightGBM

lightGBM是2017年1月,微软在GitHub上开源的一个新的梯度提升框架。

github介绍链接

在开源之后,就被别人冠以“速度惊人”、“支持分布式”、“代码清晰易懂”、“占用内存小”等属性。

LightGBM主打的高效并行训练让其性能超越现有其他boosting工具。在Higgs数据集上的试验表明,LightGBM比XGBoost快将近10倍,内存占用率大约为XGBoost的1/6。

higgs数据集介绍:这是一个分类问题,用于区分产生希格斯玻色子的信号过程和不产生希格斯玻色子的信号过程。

数据链接

lightGBM原理

lightGBM 主要基于以下方面优化,提升整体特特性

  1. 基于Histogram(直方图)的决策树算法

  2. Lightgbm 的Histogram(直方图)做差加速

  3. 带深度限制的Leaf-wise的叶子生长策略

  4. 直接支持类别特征

  5. 直接支持高效并行

LightGBM特征筛选

import pandas as pd
from sklearn.metrics import roc_auc_score,roc_curve,auc
from sklearn.model_selection import train_test_split
from sklearn import metrics
from sklearn.linear_model import LogisticRegression
import numpy as np
import random
import math
import time
import lightgbm as lgb
data = pd.read_csv('../data/Bcard.txt')
data.head()

显示结果

obs_mthbad_induidtd_scorejxl_scoremj_scorerh_scorezzc_scorezcx_scoreperson_infofinance_infocredit_infoact_info
02018-10-310.0A100000050.6753490.1440720.1868990.4836400.9283280.369644-0.3225810.0238100.000.217949
12018-07-310.0A10000020.8252690.3986880.1393960.8437250.6051940.406122-0.1286770.0238100.000.423077
22018-09-300.0A10000110.3154060.6297450.5358540.1973920.6144160.3207310.0626600.0238100.100.448718
32018-07-310.0A100004810.0023860.6093600.3660810.3422430.8700060.2886920.0788530.0714290.050.179487
42018-07-310.0A10000690.4063100.4053520.7830150.5639530.7154540.512554-0.2610140.0238100.000.423077
  • 采用相同的方式划分测试集验证集

df_train = data[data['obs_mth']!='2018-11-30']
val = data[data['obs_mth']=='2018-11-30']
  • 使用LightGBM的特征重要性以及夸时间交叉验证方式进行特征筛选

    • 将数据按时间排序

df_train = df_train.sort_values(by = 'obs_mth',ascending = False)
df_train.head()

显示结果

indexobs_mthbad_induidtd_scorejxl_scoremj_scorerh_scorezzc_scorezcx_scoreperson_infofinance_infocredit_infoact_info
002018-10-310.0A100000050.6753490.1440720.1868990.4836400.9283280.369644-0.3225810.0238100.000.217949
33407334072018-10-310.0A28101760.1460550.0799220.2505680.0452400.7669060.4137130.0138630.0238100.000.269231
33383333832018-10-310.0A28076870.5513660.3007810.2250070.0454470.7357330.684182-0.2610140.0714290.030.269231
33379333792018-10-310.0A28072320.7085470.7695130.9284570.7397160.9474530.361551-0.1286770.0476190.000.269231
33376333762018-10-310.0A28069320.4822480.1166580.2862730.0566180.0470240.8904330.0788530.0476190.000.269231
  • 将数据按照时间先后顺序分成5组

df_train['rank'] = [i for i in range(df_train.shape[0])]
df_train['rank'] = pd.cut(df_train['rank'],bins = 5,labels = [i for i in range(5)])
df_train.head()

显示结果

indexobs_mthbad_induidtd_scorejxl_scoremj_scorerh_scorezzc_scorezcx_scoreperson_infofinance_infocredit_infoact_inforank
002018-10-310.0A100000050.6753490.1440720.1868990.4836400.9283280.369644-0.3225810.023810.000.2179491
56822568222018-10-310.0A54920210.6455110.0588390.5431220.2352810.6334560.186917-0.0537180.023810.100.1666671
56991569912018-10-310.0A5609740.2996290.3443160.5006350.2451910.0562030.0843140.0788530.023810.030.5384621
56970569702018-10-310.0A559120.9291990.3472490.4383090.1889310.6118420.485462-0.3225810.023810.050.7435901
57520575202018-10-310.0A6017970.1490590.8034440.1670150.2648570.2080720.704634-0.2610140.023810.000.5256411
  • 查看分组后,每组的数据量

df_train['rank'].value_counts()

显示结果

0    15967
4    15966
3    15966
2    15966
1    15966
Name: num, dtype: int64
  • 查看数据总量,与每组相加结果吻合

len(df_train)

显示结果

79831

  • 使用LightGBM进行分组交叉特征筛选

import lightgbm as lgb
# 把训练集的数据划分成 训练集和测试集
def lgb_test(train_x,train_y,test_x,test_y):
    # 创建lgb对象
	clf =lgb.LGBMClassifier(boosting_type = 'gbdt',objective = 'binary',metric = 'auc',
		learning_rate = 0.3,n_estimators = 100,max_depth = 3,num_leaves = 20,
		max_bin = 45,min_data_in_leaf = 6,bagging_fraction = 0.6,bagging_freq = 0,
		feature_fraction = 0.8)
    # 使用这个对象训练lgb模型
	clf.fit(train_x,train_y,eval_set = [(train_x,train_y),(test_x,test_y)],eval_metric = 'auc')
    # 返回训练好的lgb模型, 返回最佳的分数
	return clf,clf.best_score_['valid_1']['auc']


#准备几个空白的列表, 用来保存每次训练的关键结果
feature_list = ['td_score', 'jxl_score', 'mj_score','rh_score', 'zzc_score', 'zcx_score', 'person_info', 'finance_info','credit_info', 'act_info']
feature_importance_lst = []
ks_train_lst = []
ks_test_lst = []
auc_list = []


for rk in range(5):
    ttest = df_train[df_train['rank'] == rk] # 挑出一组作为测试数据
    ttrain = df_train[df_train['rank'] != rk] # 剩下4组作为训练数据
    
    train_x = ttrain[feature_list]
    train_y = ttrain['bad_ind']
    
    test_x = ttest[feature_list]
    test_y = ttest['bad_ind']
    
    model, auc = lgb_test(train_x,train_y,test_x,test_y)
    # 计算特征重要性
    feature_importance_df = pd.DataFrame({'name':model.booster_.feature_name(),'importance':model.feature_importances_}).set_index('name') # 为了方便后面结果的拼接, 这里把name 特征的名字作为行索引
    feature_importance_lst.append(feature_importance_df)
    auc_list.append(auc) # 把每次训练得到的验证集的AUC 保存起来
    
    # 使用测试和训练集数据做预测
    y_pred_train = model.predict_proba(train_x)[:,1]
    y_pred_test = model.predict_proba(test_x)[:,1]
    
    # 得到fpr tpr
    fpr_train,tpr_train,threshold_train = roc_curve(train_y,y_pred_train)
    fpr_test,tpr_test,threshold_test = roc_curve(test_y,y_pred_test)
    
    # 计算训练集, 测试集KS
    train_ks = abs(fpr_train-tpr_train).max()
    test_ks = abs(fpr_test-tpr_test).max()
    # 把结果保存到列表
    ks_train_lst.append(train_ks)
    ks_test_lst.append(test_ks)

显示结果:

train_ks:  0.49076511891289665
test_ks:  0.4728837205200532
feature_importance = pd.concat(feature_importance_lst,axis = 1).mean(1).sort_values(ascending = False)
feature_importance

显示结果:

['finance_info', 'person_info', 'credit_info', 'act_info']

LightGBM评分卡

  • 最终筛选出4个特征

lst = ['person_info','finance_info','credit_info','act_info']

train = data[data.obs_mth != '2018-11-30'].reset_index().copy()
evl = data[data.obs_mth == '2018-11-30'].reset_index().copy()

x = train[lst]
y = train['bad_ind']

evl_x =  evl[lst]
evl_y = evl['bad_ind']

model,auc = lgb_test(x,y,evl_x,evl_y)

y_pred = model.predict_proba(x)[:,1]
fpr_lgb_train,tpr_lgb_train,_ = roc_curve(y,y_pred)
train_ks = abs(fpr_lgb_train - tpr_lgb_train).max()
print('train_ks : ',train_ks)

y_pred = model.predict_proba(evl_x)[:,1]
fpr_lgb,tpr_lgb,_ = roc_curve(evl_y,y_pred)
evl_ks = abs(fpr_lgb - tpr_lgb).max()
print('evl_ks : ',evl_ks)

from matplotlib import pyplot as plt
plt.plot(fpr_lgb_train,tpr_lgb_train,label = 'train LR')
plt.plot(fpr_lgb,tpr_lgb,label = 'evl LR')
plt.plot([0,1],[0,1],'k--')
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC Curve')
plt.legend(loc = 'best')
plt.show()

显示结果:

train_ks :  0.49356612822896156
evl_ks :  0.435912868827033

  • 从结果中看出,LightGBM效比LR要好,但LR通过一些处理,模型表现也会有提升

  • 将集成学习评分卡结果转换成分数

def score(xbeta):
    score = 600+50*(math.log2((1-xbeta)/xbeta))  #好人的概率/坏人的概率
    return score
evl['xbeta'] = model.predict_proba(evl_x)[:,1]   
evl['score'] = evl.apply(lambda x : score(x.xbeta) ,axis=1)
evl['score']

显示结果

0        799.044524
1        981.994370
2        911.925133
3        907.718692
4        981.994370
            ...    
15970    761.518532
15971    901.987537
15972    901.987537
15973    883.922367
15974    785.625330
Name: score, Length: 15975, dtype: float64
  • 用转换的分数验证KS值

fpr,tpr,_ = roc_curve(evl_y,evl['score'])
val_ks = abs(fpr - tpr).max()
val_ks

显示结果

0.43591286882703295
  • 生成模型报告

# 把验证集的数据代入到训练好的模型, 输出违约率, 把所有的用户的违约率按从大到小排序, 然后均匀划分成20箱
# 计算每一箱的跟违约相关的指标, 违约率和 KS
bins = 20
temp_df = pd.DataFrame() # 准备空白的df
# 用训练好的模型, 输出测试集的违约率
temp_df['bad_rate_predict'] = lr_model.predict_proba(val_x)[:,1] # 模型预测的违约率
temp_df['real_bad']= val_y.values # 真实的标签

temp_df = temp_df.sort_values('bad_rate_predict',ascending=False)

temp_df['num'] = [i for i in range(temp_df.shape[0])]
temp_df['num'] = pd.cut(temp_df['num'],bins = bins,labels=[i for i in range(bins)])



# 创建报告
report = pd.DataFrame()
# 每一组有多少1 bad标签 , 每一组有多少0 good标签
report['BAD'] =temp_df.groupby('num')['real_bad'].sum().astype(int)
report['GOOD'] =temp_df.groupby('num')['real_bad'].count().astype(int)-report['BAD']
# 累计求和 累计到这一组, 有多少1, 多少0
report['BAD_CNT'] = report['BAD'].cumsum()
report['GOOD_CNT'] = report['GOOD'].cumsum()
# 计算累计到当前组, 出现的1标签的比例 
good_total = report['GOOD_CNT'].max()
bad_total = report['BAD_CNT'].max()
report['BAD_PCTG'] = round(report['BAD_CNT']/bad_total,3)
# 当前组 1标签比例
report['BADRATE'] = report.apply(lambda x:round(x['BAD']/(x['BAD']+x['GOOD']),3),axis = 1)
# 当前组ks
def cal_ks(x):
	# tpr = tp/tp+fn(所有的1标签)  fpr = fp/fp+tn (所有的0) GOOD_CNT
	ks = (x['BAD_CNT']/bad_total)-(x['GOOD_CNT']/good_total)
	return round(abs(ks),3)
report['KS'] = report.apply(cal_ks,axis = 1)
print(report)

显示结果

numBADGOODBAD_CNTGOOD_CNTBAD_PCTGBADRATEKS
095704957040.2900.1190.245
13276712714710.3870.0400.293
23176815822390.4820.0390.339
33576319330020.5880.0440.397
41878121137830.6430.0230.402
52477523545580.7160.0300.425
61678225153400.7650.0200.424
71178826261280.7990.0140.407
81178827369160.8320.0140.390
91478528777010.8750.0180.383
10978929684900.9020.0110.360
11779230392820.9240.0090.331
124795307100770.9360.0050.292
136792313108690.9540.0080.260
143796316116650.9630.0040.218
153796319124610.9730.0040.176
164794323132550.9850.0050.138
171798324140530.9880.0010.090
182797326148500.9940.0030.045
192797328156471.0000.0030.000
  • pyecharts绘图展示模型表现

from pyecharts.charts import *
from pyecharts import options as opts
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
np.set_printoptions(suppress=True)
pd.set_option('display.unicode.ambiguous_as_wide', True)
pd.set_option('display.unicode.east_asian_width', True)
line = (

    Line()
    .add_xaxis(list(report.index))
    .add_yaxis(
        "分组坏人占比",
        list(report.BADRATE),
        yaxis_index=0,
        color="red",
    )
    .set_global_opts(
        title_opts=opts.TitleOpts(title="评分卡模型表现"),
    )
    .extend_axis(
        yaxis=opts.AxisOpts(
            name="累计坏人占比",
            type_="value",
            min_=0,
            max_=0.5,
            position="right",
            axisline_opts=opts.AxisLineOpts(
                linestyle_opts=opts.LineStyleOpts(color="red")
            ),
            axislabel_opts=opts.LabelOpts(formatter="{value}"),
        )

    )
    .add_yaxis(
        "KS",
        list(report['KS']),
        yaxis_index=1,
        color="blue",
        label_opts=opts.LabelOpts(is_show=False),
    )
)
line.render()

显示结果:

【实现】整体流程梳理

加载数据

import pandas as pd  
from sklearn.metrics import roc_auc_score,roc_curve,auc  
from sklearn.model_selection import train_test_split  
from sklearn.linear_model import LogisticRegression   
import numpy as np  
import math  
import xgboost as xgb  
import toad
# 加载数据
data_all = pd.read_csv("../data/scorecard.txt")

# 指定不参与训练列名  
ex_lis = ['uid', 'samp_type', 'bad_ind']  
# 参与训练列名  
ft_lis = list(data_all.columns)  
for i in ex_lis:      
    ft_lis.remove(i) 

# 开发样本、验证样本与时间外样本  
dev = data_all[(data_all['samp_type'] == 'dev')]
val = data_all[(data_all['samp_type'] == 'val') ]  
off = data_all[(data_all['samp_type'] == 'off') ]  
  • 探索性数据分析,同时处理数值型和字符型

toad.detector.detect(data_all)

显示结果:

typesizemissinguniquemean_or_top1std_or_top2min_or_top31%_or_top410%_or_top550%_or_bottom575%_or_bottom490%_or_bottom399%_or_bottom2max_or_bottom1
bad_indfloat64958060.00%20.01876710.13570200000011
uidobject958060.00%95806Ab99_96002866062686144:0.00%A7511004:0.00%A10729014:0.00%A8502810:0.00%A594541:0.00%A8899777:0.00%A10150838:0.00%A3044048:0.00%A1888452:0.00%A7659794:0.00%
td_scorefloat64958060.00%958060.4997390.2883495.46966e-060.009613410.09970560.5007190.7479840.9000240.9900410.999999
jxl_scorefloat64958060.00%958060.4993380.288851.28155e-050.009946780.09910250.4997950.7486460.8997030.9893480.999985
mj_scorefloat64958060.00%958060.501640.2886796.92442e-060.01050760.1008820.5030480.7520320.8993080.9900470.999993
rh_scorefloat64958060.00%958060.4984070.2877975.00212e-060.009916320.09994830.4974660.7471880.8992860.9894730.999986
zzc_scorefloat64958060.00%958060.5006270.2890671.15778e-050.01018560.09901140.5016880.7509860.8999240.9900430.999998
zcx_scorefloat64958060.00%958060.4996720.2891379.97767e-060.01032490.09974290.499130.7506830.9019420.9897120.999987
person_infofloat64958060.00%7-0.0782290.156859-0.322581-0.322581-0.322581-0.05371760.0788530.0788530.0788530.078853
finance_infofloat64958060.00%350.03676250.03968660.02380950.02380950.02380950.02380950.02380950.07142860.2142861.02381
credit_infofloat64958060.00%1000.06362620.14309800000.060.180.81
act_infofloat64958060.00%740.2361970.1571320.07692310.07692310.07692310.2051280.3461540.4871790.6153851.08974
samp_typeobject958060.00%3dev:68.16%off:16.67%val:15.16%NoneNoneNoneNonedev:68.16%off:16.67%val:15.16%

特征筛选(缺失值,IV,相关系数)

  • 使用缺失率、IV、相关系数进行特征筛选。但是考虑到后续建模过程要对变量进行分箱处理,该操作会使变量的IV变小,变量间的相关性变大,因此此处可以对IV和相关系的阈值限制适当放松,或不做限制

dev_slct1, drop_lst= toad.selection.select(dev, dev['bad_ind'], 
                                                   empty=0.7, iv=0.03, 
                                                   corr=0.7, 
                                                   return_drop=True, 
                                                   exclude=ex_lis) 
print("keep:", dev_slct1.shape[1],  
      "drop empty:", len(drop_lst['empty']), 
      "drop iv:", len(drop_lst['iv']),  
      "drop corr:", len(drop_lst['corr']))

显示结果:

keep: 12 drop empty: 0 drop iv: 1 drop corr: 0

卡方分箱

# 得到切分节点  
combiner = toad.transform.Combiner()  
combiner.fit(dev_slct1, dev_slct1['bad_ind'], method='chi',
                min_samples=0.05, exclude=ex_lis)  
# 导出箱的节点  
bins = combiner.export()  
print(bins)

显示结果:

{'td_score': [0.7989831262724624], 'jxl_score': [0.4197048501965005], 'mj_score': [0.3615303943747963], 'zzc_score': [0.4469861520889339], 'zcx_score': [0.7007847486465795], 'person_info': [-0.2610139784946237, -0.1286774193548387, -0.05371756272401434, 0.013863440860215051, 0.06266021505376344, 0.07885304659498207], 'finance_info': [0.047619047619047616], 'credit_info': [0.02, 0.04, 0.11], 'act_info': [0.1153846153846154, 0.14102564102564102, 0.16666666666666666, 0.20512820512820512, 0.2692307692307692, 0.35897435897435903, 0.3974358974358974, 0.5256410256410257]}

Bivar图,调整分箱

Bivar图,用于观察变量的单调性。

  • 画图观察每个变量在开发样本和时间外样本上的Bivar图,为方便阅读,这里只以单变量act_info做示范

# 根据节点实施分箱  
dev_slct2 = combiner.transform(dev_slct1)
val2 = combiner.transform(val[dev_slct1.columns])
off2 = combiner.transform(off[dev_slct1.columns])
# 分箱后通过画图观察
from toad.plot import  bin_plot, badrate_plot
bin_plot(dev_slct2, x='act_info', target='bad_ind')
bin_plot(val2, x='act_info', target='bad_ind')  
bin_plot(off2, x='act_info', target='bad_ind')

开发样本:

测试样本:

跨时间样本:

  • 由于前3箱的变化趋势与整体不符(整体为递减趋势),因此在接下来的步骤中将其合并。第4~6箱合并,最后3箱进行合并。从而得到严格递减的变化趋势。

bins['act_info']

显示结果:

[0.1153846153846154,
 0.14102564102564102,
 0.16666666666666666,
 0.20512820512820512,
 0.2692307692307692,
 0.35897435897435903,
 0.3974358974358974,
 0.5256410256410257]
adj_bin = {'act_info': [0.16666666666666666,0.35897435897435903,]}  
combiner.set_rules(adj_bin)

dev_slct3 = combiner.transform(dev_slct1)
val3 = combiner.transform(val[dev_slct1.columns])
off3 = combiner.transform(off[dev_slct1.columns])

# 画出Bivar图
bin_plot(dev_slct3, x='act_info', target='bad_ind')  
bin_plot(val3, x='act_info', target='bad_ind')  
bin_plot(off3, x='act_info', target='bad_ind') 

显示结果:

开发样本

测试样本

验证样本

绘制负样本占比关联图

data = pd.concat([dev_slct3,val3,off3], join='inner')   
badrate_plot(data, x='samp_type', target='bad_ind', by='act_info') 

显示结果:

<matplotlib.axes._subplots.AxesSubplot at 0x1b19b5c56c8>
  • 上图中,图中的线没有交叉,故不需要对该特征的分组进行合并,即使有少量交叉也不会对结果造成明显的影响,只有当错位比较严重的情况下才进行调整

data = pd.concat([dev_slct3,val3,off3], join='inner')   
badrate_plot(data, x='samp_type', target='bad_ind', by='person_info') 

显示结果:

<matplotlib.axes._subplots.AxesSubplot at 0x1b19b608b08>
  • 上图中,有变量错位情况,属于可以容忍范围,也可以考虑将变量person_info中编号为3,4,5的箱合并

WOE编码,并验证IV

t = toad.transform.WOETransformer()  
dev_slct3_woe = t.fit_transform(dev_slct3, dev_slct3['bad_ind'], 
                                      exclude=ex_lis) 
val_woe = t.transform(val3[dev_slct3.columns])  
off_woe = t.transform(off3[dev_slct3.columns])  
data = pd.concat([dev_slct3_woe, val_woe, off_woe])
  • 计算训练样本与测试样本的PSI

psi_df = toad.metrics.PSI(dev_slct3_woe, val_woe).sort_values(0)  
psi_df = psi_df.reset_index()  
psi_df = psi_df.rename(columns = {'index': 'feature', 0: 'psi'}) 
psi_df

显示结果:

featurepsi
0uid0.000000e+00
1samp_type0.000000e+00
2td_score8.778656e-07
3zcx_score4.183912e-06
4jxl_score2.901553e-05
5zzc_score3.764148e-05
6mj_score5.005908e-05
7bad_ind4.128345e-03
8credit_info9.489392e-02
9act_info1.237395e-01
10person_info1.278102e-01
11finance_info1.341445e-01
  • 删除PSI大于0.13的特征

    • 通常单个特征的PSI值建议在0.1以下,根据具体情况可以适当调整

    • 本案例数据为演示数据变量PSI普遍较大,因此选择0.13作为阈值

psi_013 = list(psi_df[psi_df.psi<0.13].feature)
# 避免不参与计算的几个特征被删掉,把 uid,samp_type,bad_ind添加回来并去重
psi_013.extend(ex_lis)
psi_013 = list(set(psi_013))
data = data[psi_013]
dev_woe_psi = dev_slct3_woe[psi_013]
val_woe_psi = val_woe[psi_013]
off_woe_psi = off_woe[psi_013]
print(data.shape)

显示结果:

(95806, 11)
  • 卡方分箱后部分变量的IV降低,且整体相关程度增大,需要再次筛选特征

    • 使用的IV和相关系数阈值较实际建模场景都偏小,主要是因为演示数据并非真实数据

dev_woe_psi2, drop_lst = toad.selection.select(dev_woe_psi,
                                               dev_woe_psi['bad_ind'],
                                               empty=0.6,   
                                               iv=0.001, 
                                               corr=0.5, 
                                               return_drop=True, 
                                               exclude=ex_lis)  
print("keep:", dev_woe_psi2.shape[1],  
      "drop empty:", len(drop_lst['empty']),  
      "drop iv:", len(drop_lst['iv']),  
      "drop corr:", len(drop_lst['corr'])) 

显示结果:

keep: 7 drop empty: 0 drop iv: 4 drop corr: 0

特征筛选

  • 使用逐步回归进行特征筛选,使用线性回归模型,并选择KS作为评价指标

    • estimator: 用于拟合的模型,支持'ols', 'lr', 'lasso', 'ridge'

    • direction: 逐步回归的方向,支持'forward', 'backward', 'both' (推荐)

      • Forward selection:将自变量逐个引入模型,引入一个自变量后查看该模型是否发生显著性变化

        • 如果发生了显著性变化,那么则将该变量引入模型中,否则忽略该变量,直至遍历所有变量

        • 即将变量按照贡献度从大到小排列,依次加入

      • Backward elimination:与Forward selection选择相反,将所有变量放入模型

        • 尝试将某一变量进行剔除,查看剔除后对整个模型是否有显著性变化

        • 如没有显著性变化则剔除,有则保留,直到留下所有对模型有显著性变化的因素

        • 也就是将自变量按贡献度从小到大,依次剔除

      • both:将前向选择与后向消除同时进行

        • 模型中每加入一个自变量,可能使某个已放入模型的变量显著性减小

        • 显著性小于阈值时,可将该变量从模型中剔除

        • 即每增加一个新的显著变量的同时,检验模型中所有变量的显著性,剔除不显著变量,从而得到最优变量组合

    • criterion: 评判标准,支持'aic', 'bic', 'ks', 'auc'

    • max_iter: 最大循环次数

    • return_drop: 是否返回被剔除的列名

    • exclude: 不需要被训练的列名,比如ID列和时间列

dev_woe_psi_stp = toad.selection.stepwise(dev_woe_psi2,  
                                                  dev_woe_psi2['bad_ind'],  
                                                  exclude=ex_lis,  
                                                  direction='both',   
                                                  criterion='ks',  
                                                  estimator='ols',
                                              intercept=False)
val_woe_psi_stp = val_woe_psi[dev_woe_psi_stp.columns]  
off_woe_psi_stp = off_woe_psi[dev_woe_psi_stp.columns]  
data = pd.concat([dev_woe_psi_stp, val_woe_psi_stp, off_woe_psi_stp]) 
print(data.shape)

显示结果:

(95806, 6)
  • 查看剩下的特征列

dev_woe_psi_stp.columns

显示结果:

Index(['uid', 'samp_type', 'bad_ind', 'credit_info', 'act_info',
       'person_info'],
      dtype='object')

模型训练

  • 定义函数用于模型训练

def lr_model(x, y, valx, valy, offx, offy, C):  
    model = LogisticRegression(C=C, class_weight='balanced')      
    model.fit(x,y)
      
    y_pred = model.predict_proba(x)[:,1]  
    fpr_dev,tpr_dev,_ = roc_curve(y, y_pred)  
    train_ks = abs(fpr_dev - tpr_dev).max()  
    print('train_ks : ', train_ks)

    y_pred = model.predict_proba(valx)[:,1]  
    fpr_val,tpr_val,_ = roc_curve(valy, y_pred)  
    val_ks = abs(fpr_val - tpr_val).max()  
    print('val_ks : ', val_ks)
    
    y_pred = model.predict_proba(offx)[:,1]  
    fpr_off,tpr_off,_ = roc_curve(offy, y_pred)  
    off_ks = abs(fpr_off - tpr_off).max()  
    print('off_ks : ', off_ks)
      
    from matplotlib import pyplot as plt  
    plt.plot(fpr_dev, tpr_dev, label='dev')  
    plt.plot(fpr_val, tpr_val, label='val')  
    plt.plot(fpr_off, tpr_off, label='off')  
    plt.plot([0,1], [0,1], 'k--'
    plt.xlabel('False positive rate')  
    plt.ylabel('True positive rate')  
    plt.title('ROC Curve')
    plt.legend(loc='best')
    plt.show()

def xgb_model(x, y, valx, valy, offx, offy):  
    model = xgb.XGBClassifier(learning_rate=0.05,  
                              n_estimators=400,  
                              max_depth=2,  
                              class_weight='balanced',  
                              min_child_weight=1,  
                              subsample=1,   
                              nthread=-1,  
                              scale_pos_weight=1,  
                              random_state=1,  
                              n_jobs=-1,  
                              reg_lambda=300)  
    model.fit(x, y)  
      
    y_pred = model.predict_proba(x)[:,1]  
    fpr_dev,tpr_dev,_ = roc_curve(y, y_pred)  
    train_ks = abs(fpr_dev - tpr_dev).max()  
    print('train_ks : ', train_ks)  

    y_pred = model.predict_proba(valx)[:,1]  
    fpr_val,tpr_val,_ = roc_curve(valy, y_pred)  
    val_ks = abs(fpr_val - tpr_val).max()  
    print('val_ks : ', val_ks)  

    y_pred = model.predict_proba(offx)[:,1]  
    fpr_off,tpr_off,_ = roc_curve(offy, y_pred)  
    off_ks = abs(fpr_off - tpr_off).max()  
    print('off_ks : ', off_ks)  

    from matplotlib import pyplot as plt  
    plt.plot(fpr_dev, tpr_dev, label='dev')
    plt.plot(fpr_val, tpr_val, label='val') 
    plt.plot(fpr_off, tpr_off, label='off')  
    plt.plot([0,1], [0,1], 'k--')  
    plt.xlabel('False positive rate')  
    plt.ylabel('True positive rate')  
    plt.title('ROC Curve')  
    plt.legend(loc='best')  
    plt.show()
  • 定义函数调用模型训练的方法

def bi_train(data, dep='bad_ind', exclude=None):  
    from sklearn.preprocessing import StandardScaler  
    std_scaler = StandardScaler()  
    # 变量名  
    lis = list(data.columns)  
    for i in exclude:  
        lis.remove(i)      
    devv = data[(data['samp_type']=='dev')]
    vall = data[(data['samp_type']=='val')]
    offf = data[(data['samp_type']=='off')]
    x, y = devv[lis], devv[dep]
    valx, valy = vall[lis], vall[dep]
    offx, offy = offf[lis], offf[dep]
    # 逻辑回归正向
    print("逻辑回归正向:")
    lr_model(x, y, valx, valy, offx, offy, 0.1)
    # 逻辑回归反向 
    print("逻辑回归反向:")
    lr_model(offx, offy, valx, valy, x, y, 0.1) 
    # XGBoost正向
    print("XGBoost正向:")
    xgb_model(x, y, valx, valy, offx, offy) 
    # XGBoost反向
    print("XGBoost反向:")
    xgb_model(offx, offy, valx, valy, x, y)
  • 上面函数中,XGBoost模型和逻辑回归模型各调用了两次,分别为正向调用和逆向调用

    • 正向调用通过对开发样本的学习得到模型,并在时间外样本上检验效果

    • 逆向调用使用时间外样本作为训练集,检验当前模型的效果上限

    • 如逆向模型训练集KS值明显小于正向模型训练集KS值,说明当前时间外样本分布与开发样本差异较大,需要重新划分样本集。(样本量较小时经常发生)

  • 调用上面函数

bi_train(data, dep='bad_ind', exclude=ex_lis)

显示结果:

逻辑回归正向:
train_ks :  0.41733648227995124
val_ks :  0.3593935758405114
off_ks :  0.3758086175640308
逻辑回归反向:
train_ks :  0.3892612859630226
val_ks :  0.3717891855920369
off_ks :  0.4061965880072622
XGBoost正向:
train_ks :  0.42521927400747045
val_ks :  0.3595542266920359
off_ks :  0.37437103192850807
XGBoost反向:
train_ks :  0.3939473708822855
val_ks :  0.3799497614606668
off_ks :  0.3936270948436908
  • 从结果中看出:

    • XGBoost模型的效果并没有明显高于逻辑回归模型,因此当前特征不需要再进行组合。

    • 逆向调用LR模型的训练集结果,没有显著好于正向调用的时间外样本结果,该模型在当前特征空间下几乎没有更多的优化空间

    • 正向LR模型的结果训练集KS值,与时间外样本KS值的差值在5%以内,故不需要调整跨时间稳定性较差的变量

计算指标评估模型,生成模型报告

  • 假设当前模型已经进行过精细化调整了,接下来使用单个逻辑回归模型进行拟合。全部使用默认参数。

dep = 'bad_ind'  
lis = list(data.columns)  
for i in ex_lis:  
    lis.remove(i)
devv = data[data['samp_type']=='dev']
vall = data[data['samp_type']=='val']
offf = data[data['samp_type']=='off' ]
x, y = devv[lis], devv[dep]
valx, valy = vall[lis], vall[dep]
offx, offy = offf[lis], offf[dep]
lr = LogisticRegression()
lr.fit(x, y)
  • 分别计算F1-score KS和AUC

from toad.metrics import KS, F1, AUC 

prob_dev = lr.predict_proba(x)[:,1]  
print('训练集')  
print('F1:', F1(prob_dev,y))  
print('KS:', KS(prob_dev,y))  
print('AUC:', AUC(prob_dev,y)) 

prob_val = lr.predict_proba(valx)[:,1]  
print('验证集')  
print('F1:', F1(prob_val,valy))  
print('KS:', KS(prob_val,valy))  
print('AUC:', AUC(prob_val,valy)) 

prob_off = lr.predict_proba(offx)[:,1]  
print('时间外样本')  
print('F1:', F1(prob_off,offy))  
print('KS:', KS(prob_off,offy))  
print('AUC:', AUC(prob_off,offy))

print('模型PSI:',toad.metrics.PSI(prob_dev,prob_off))  
print('特征PSI:','\n',toad.metrics.PSI(x,offx).sort_values(0))  

显示结果:

训练集
F1: 0.02962459026532253
KS: 0.41406129833591426
AUC: 0.7713247123864264
验证集
F1: 0.027689429373246022
KS: 0.36127808343721585
AUC: 0.7225727568398459
时间外样本
F1: 0.032454090150250414
KS: 0.3807135163445966
AUC: 0.7435613582904539
模型PSI: 0.34091667386100255
特征PSI: 
 credit_info    0.098585
act_info       0.124820
person_info    0.127833
dtype: float64
  • 生成模型时间外样本的KS报告

toad.metrics.KS_bucket(prob_off,offy,
                       bucket=15,
                       method='quantile') 

显示结果:

minmaxbadsgoodstotalbad_rategood_rateoddsbad_propgood_proptotal_propcum_bad_ratecum_bad_rate_revcum_bads_propcum_bads_prop_revcum_goods_propcum_goods_prop_revcum_total_propcum_total_prop_revks
00.0018700.0031872.0963.0965.00.0020730.9979270.0020770.0060980.0615450.0604070.0020730.0205320.0060981.0000000.0615451.0000000.0604071.0000000.055448
10.0037910.0040681.01054.01055.00.0009480.9990520.0009490.0030490.0673610.0660410.0014850.0217190.0091460.9939020.1289060.9384550.1264480.9395930.119760
20.0042630.0053823.0793.0796.00.0037690.9962310.0037830.0091460.0506810.0498280.0021310.0232890.0182930.9908540.1795870.8710940.1762750.8735520.161294
30.0063610.0083267.01341.01348.00.0051930.9948070.0052200.0213410.0857030.0843820.0031220.0244700.0396340.9817070.2652900.8204130.2606570.8237250.225656
40.0086120.0087616.0958.0964.00.0062240.9937760.0062630.0182930.0612260.0603440.0037050.0266700.0579270.9603660.3265160.7347100.3210020.7393430.268589
50.0090220.0108919.0799.0808.00.0111390.9888610.0112640.0274390.0510640.0505790.0047170.0284870.0853660.9420730.3775800.6734840.3715810.6789980.292215
60.0110060.01477417.01464.01481.00.0114790.9885210.0116120.0518290.0935640.0927070.0060670.0298830.1371950.9146340.4711450.6224200.4642880.6284190.333950
70.0148070.01803210.0581.0591.00.0169200.9830800.0172120.0304880.0371320.0369950.0068680.0330680.1676830.8628050.5082760.5288550.5012830.5357120.340593
80.0183790.02220044.02571.02615.00.0168260.9831740.0171140.1341460.1643130.1636930.0093190.0342660.3018290.8323170.6725890.4917240.6649770.4987170.370760
90.0222350.03028026.0798.0824.00.0315530.9684470.0325810.0792680.0510000.0515810.0109200.0427880.3810980.6981710.7235890.3274110.7165570.3350230.342492
100.0309720.03758233.01167.01200.00.0275000.9725000.0282780.1006100.0745830.0751170.0124930.0448320.4817070.6189020.7981720.2764110.7916740.2834430.316465
110.0379680.05833943.01007.01050.00.0409520.9590480.0427010.1310980.0643570.0657280.0146750.0510820.6128050.5182930.8625300.2018280.8574020.2083260.249725
120.0622690.07602942.0908.0950.00.0442110.9557890.0462560.1280490.0580300.0594680.0165900.0557510.7408540.3871950.9205600.1374700.9168700.1425980.179706
130.0943980.09439885.01243.01328.00.0640060.9359940.0683830.2591460.0794400.0831300.0205320.0640061.0000000.2591461.0000000.0794401.0000000.083130-0.000000

生成评分卡

  • 将数据集合并后,利用ScoreCard函数重新训练并生成评分卡

    • 该函数内嵌逻辑回归模型 参数 C '正则化强度', class_weight 与sklearn中逻辑回归参数一致

    • combiner: 传入训练好的 toad.Combiner 对象

    • transer: 传入先前训练的 toad.WOETransformer 对象

    • pdo、rate、base_odds、base_score: e.g. pdo=60, rate=2, base_odds=20,base_score=750 实际意义为当比率为1/20,输出基准评分750,当比率为基准比率2倍时,基准分下降60分

from toad.scorecard import ScoreCard  
card = ScoreCard(combiner=combiner, 
                    transer=t, C=0.1, 
                    class_weight='balanced', 
                    base_score=750,
                    base_odds=20,
                    pdo=60,
                    rate=2)  
card.fit(x,y)  
final_card = card.export(to_frame=True)  
final_card

显示结果:

namevaluescore
0credit_info[-inf ~ 0.02)158.04
1credit_info[0.02 ~ 0.04)122.51
2credit_info[0.04 ~ 0.11)73.94
3credit_info[0.11 ~ inf)43.98
4act_info[-inf ~ 0.16666666666666666)94.24
5act_info[0.16666666666666666 ~ 0.35897435897435903)115.71
6act_info[0.35897435897435903 ~ inf)125.38
7person_info[-inf ~ -0.2610139784946237)172.76
8person_info[-0.2610139784946237 ~ -0.1286774193548387)142.93
9person_info[-0.1286774193548387 ~ -0.05371756272401434)123.74
10person_info[-0.05371756272401434 ~ 0.013863440860215051)120.79
11person_info[0.013863440860215051 ~ 0.06266021505376344)108.83
12person_info[0.06266021505376344 ~ 0.07885304659498207)90.32
13person_info[0.07885304659498207 ~ inf)75.46

小结

  • 掌握KS值的计算方法

    • KS= max(TPR-FPR)TPR和FPR曲线分隔最开的位置就是最好的”截断点“,

  • 知道评分映射方法

    • $$score = 650+50 log_2(P{正样本}/ P{负样本})$$

  • 知道LightGBM基本原理

    • XGBoost 和 LightGBM 都基于GBDT

    • XGBoost在GBDT基础上做了二阶泰勒级数展开,效率更高,模型更精准

    • LightGBM在XGBoost基础上进一步优化,直接支持类别特征,直接支持高效并行,基于直方图的决策树算法效率更高

  • 掌握使用lightGBM进行特征筛选的方法

    • 利用lightGBM输出特征重要性

    • 将样本用时间排序分组, 做跨时间交叉验证

  • 应用toad构建评分卡模型

    • 探索性数据分析

    toad.detector.detect(data_all)
    • 特征筛选(缺失值,IV,相关系数)

    dev_slct1, drop_lst= toad.selection.select(dev, dev['bad_ind'], 
                                                       empty=0.7, iv=0.03, 
                                                       corr=0.7, 
                                                       return_drop=True, 
                                                       exclude=ex_lis) 
    • Bivar图,观察变量单调性

    combiner.transform(dev_slct1)
    • 负样本占比关联图

    badrate_plot(data, x='samp_type', target='bad_ind', by='act_info') 
    • WOE

    t = toad.transform.WOETransformer()  
    dev_slct3_woe = t.fit_transform(dev_slct3, dev_slct3['bad_ind'], 
                                          exclude=ex_lis) 
    • 特征筛选

    dev_woe_psi_stp = toad.selection.stepwise(dev_woe_psi2,  
                                                      dev_woe_psi2['bad_ind'],  
                                                      exclude=ex_lis,  
                                                      direction='both',   
                                                      criterion='ks',  
                                                      estimator='ols',
                                                  intercept=False)  
    • 模型训练

    • 生成模型报告

    toad.metrics.KS_bucket(prob_off,offy,
                           bucket=10,
                           method='quantile') 
    • 生成评分卡

    from toad.scorecard import ScoreCard  
    card = ScoreCard(combiner=combiner, 
                        transer=t, C=0.1, 
                        class_weight='balanced', 
                        base_score=600,
                        base_odds=35,
                        pdo=60,
                        rate=2)

标签:info,val,评分,dev,风控,ks,train,构建,data
From: https://blog.csdn.net/2201_75415080/article/details/143723278

相关文章

  • 【金融风控】特征评估与筛选详解
    内容介绍掌握单特征分析的衡量指标知道IV,PSI等指标含义知道多特征筛选的常用方法掌握Boruta,VIF,RFE,L1等特征筛选的使用方法【理解】单特征分析什么是好特征从几个角度衡量:覆盖度,区分度,相关性,稳定性覆盖度采集类,授权类,第三方数据在使用前都会分析覆盖度采......
  • 【金融风控】特征构造及代码详解
    介绍知道未来信息的概念,及处理未来信息的方法掌握从原始数据构造出新特征的方法掌握特征变换的方法掌握缺失值处理的方法【理解】数据准备风控建模特征数据数据来源人行征信数据查询原因包括:贷款审批、贷后管理、信用卡审批、担保资格审查、司法调查、......
  • 构建交互式聊天界面:react-chat-element 实战小计
    react聊天组件库:react-chat-elements需求场景:用户可以通过多元的用户交互方式,如文件、图片、声音以及文字等输入相关信息,AI给出对应的回答react-chat-element介绍react-chat-elements是一个专为React开发者设计的聊天组件库,旨在简化聊天界面的开发过程,适用于构建社交应用、客......
  • 前端构建工具对比
    工具特性解析1.Webpack输入输出灵活:支持单文件和多文件输入输出,可以进行复杂的模块化输出,并且支持全目录结构。文件捆绑和格式支持:具有强大的捆绑功能,并支持ESM、CJS、UMD等模块格式输出。CSS和资源处理:具备强大的CSS预编译和资源处理能力,并支持插件扩展以进行图片、......
  • Dockerfile构建镜像(练习一Apache镜像)(5-1)
    目录指令详解本章实例:1.创建工作目录2.在工作目录中创建并编写Dockerfile文件(1)保证拥有centos基础镜像3.编写相关执行脚本##添加启动镜像启动执行脚本#设置centos.repo仓库文件Docker主机需要搭建yum仓库4.使用Dockerfile生成镜像 5.使用新镜像运行容器测试#查......
  • Spring Boot编程训练系统:构建高效的编程环境
    摘要随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了编程训练系统的开发全过程。通过分析编程训练系统管理的不足,创建了一个计算机管理编程训练系统的方案。文章介绍了编程训练系统的系统分析部分,包括可行性分析等,系统设计部......
  • SpringBoot校园德育活动预约和评分管理zmh4l 带论文文档1万字以上
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表系统内容:学生,活动信息,活动报名,取消报名,德育分奖励,智育分奖励开题报告内容项目名称:SpringBoot校园德育活动预约和评分管理系统项目背景:随着教育信息化的......
  • 基于surging的木舟平台如何构建起微服务
    一、概述   木舟平台分为微服务平台和物联网平台,上面几篇都是介绍如何通过网络组件接入设备,那么此篇文章就细致介绍下在木舟平台下如何构建微服务。     木舟(Kayak)是什么?      木舟(Kayak)是基于.NET6.0软件环境下的surging微服务引擎进行开发的,平台......
  • Docker 构建 PHP 7.1 和 MySQL 支持的 Web 环境
    在现代Web开发中,Docker已成为构建和部署应用程序的重要工具。本文将介绍如何使用Dockerfile创建一个包含PHP7.1、Apache和MySQL支持的Web环境。这个设置非常适合运行需要MySQL数据库的PHP应用程序。Dockerfile详解让我们逐步分析这个Dockerfile:#使用......
  • Qt构建与解析Json示例
    本文以B站UP主“明王讲QT”的【QT开发专题-天气预报】中的章节内容作为学习Qt中构建、解析Json的参考方法。1、Json文本{"info":{"asian":true,"captical":"beijing","founded":1949},"name":"China&qu......