美文网首页
信贷风控建模实战(三)——评分卡建模之逻辑回归

信贷风控建模实战(三)——评分卡建模之逻辑回归

作者: 老羊_肖恩 | 来源:发表于2023-02-20 13:28 被阅读0次

1. 逻辑回归的评分映射逻辑

  逻辑回归(Logistic Regression)是统计学习中的经典分类方法,是一种有监督的机器学习模型,属于广义线性模型的一种。主要用来解决二分类问题,也可以用来解决多分类问题。使用逻辑回归模型可以得到一个[0,1]区间的结果,在风控场景下可以理解为用户违约的概率,在进行评分卡建模时我们需要把违约的概率映射为用户的评分。业内主要采用一种比率缩放的评分映射方法。
  比如:假设现在期望每个用户的基础分为650分,当这个用户非预期的概率是预期概率的两倍时,加50分,非预期概率是预期概率的4倍时加100分,反之则扣分。因此可以得出业内标准的评分映射逻辑:
score = 650 + 50\log_2 (\frac{P_{正样本}}{P_{负样本}})其中score时映射后的评分输出,P_{正样本}是样本非预期的概率,P_{负样本}是样本预期的概率。接下来我们看看逻辑回归中,正负样本是怎么对应起来的。首先在二项逻辑回归模型中,正负样本的条件概率分布如下:
P(正样本|x) = \frac{\exp(w \bullet x + b)}{1 + \exp(w \bullet x + b)}

P(负样本|x) = \frac{1}{1 + \exp(w \bullet x + b)}故我们可以得到如下的逻辑回归方程
\ln(\frac{P_{正样本}}{P_{负样本}}) = w \bullet x + b由换地公式我们可知
\log_2 (\frac{P_{正样本}}{P_{负样本}}) = \frac{\ln (\frac{P_{正样本}}{P_{负样本}}) }{\ln2} = \frac{w \bullet x + b}{\ln 2}
因此我们可以最终得到我们的评分映射公式:
score = 600 + 50 \frac{w \bullet x + b}{\ln 2}其中wb可以由最终训练完的逻辑回归模型的coef_系数和intercept_得到。

2. 训练逻辑回归模型

  我们以某互联网信贷平台的脱敏数据为例,简单展示基于逻辑回归模型构建评分卡的全过程。其数据字段信息如下:

字段名称 中文描述
obs_mth 申请日期所在月份的最后一天
bad_ind 好坏标识
uid 用户id
td_score 同盾分
jxl_score 聚信立分
mj_score 摩羯分
rh_score 人行征信分
zzc_score 中智诚分
zcx_score 中诚信分
person_info 个人信息
finance_info 金融信息
credit_info 信用信息
act_info 行为信息

  通过观察这些字段我们可以知道,这个数据集是一部分依赖于该企业外部征信的评分,一部分是用户在平台上的基本信息的整合,因此这里简单跳过特征工程的环节,直接利用上述信息进行评分卡制作。

加载依赖包
# 依赖包加载
import pandas as pd
import numpy as np
from sklearn import metrics
from sklearn.linear_model import LogisticRegression
import math
数据载入
# 数据导入
data = pd.read_csv('Acard.txt')
data.head()

数据预览结果如下图所示

数据预览
数据基本信息探查
# 查看数据基本情况
data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 95806 entries, 0 to 95805
Data columns (total 13 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   obs_mth       95806 non-null  object 
 1   bad_ind       95806 non-null  float64
 2   uid           95806 non-null  object 
 3   td_score      95806 non-null  float64
 4   jxl_score     95806 non-null  float64
 5   mj_score      95806 non-null  float64
 6   rh_score      95806 non-null  float64
 7   zzc_score     95806 non-null  float64
 8   zcx_score     95806 non-null  float64
 9   person_info   95806 non-null  float64
 10  finance_info  95806 non-null  float64
 11  credit_info   95806 non-null  float64
 12  act_info      95806 non-null  float64
dtypes: float64(11), object(2)
memory usage: 9.5+ MB
数据变量汇总信息
# 查看离散变量的统计信息
data.describe()
    bad_ind td_score    jxl_score   mj_score    rh_score    zzc_score   zcx_score   person_info finance_info    credit_info act_info
count   95806.000000    95806.000000    95806.000000    95806.000000    95806.000000    95806.000000    95806.000000    95806.000000    95806.000000    95806.000000    95806.000000
mean    0.018767    0.499739    0.499338    0.501640    0.498407    0.500627    0.499672    -0.078229   0.036763    0.063626    0.236197
std 0.135702    0.288349    0.288850    0.288679    0.287797    0.289067    0.289137    0.156859    0.039687    0.143098    0.157132
min 0.000000    0.000005    0.000013    0.000007    0.000005    0.000012    0.000010    -0.322581   0.023810    0.000000    0.076923
25% 0.000000    0.250104    0.249045    0.250517    0.250115    0.249501    0.248318    -0.261014   0.023810    0.000000    0.076923
50% 0.000000    0.500719    0.499795    0.503048    0.497466    0.501688    0.499130    -0.053718   0.023810    0.000000    0.205128
75% 0.000000    0.747984    0.748646    0.752032    0.747188    0.750986    0.750683    0.078853    0.023810    0.060000    0.346154
max 1.000000    0.999999    0.999985    0.999993    0.999986    0.999998    0.999987    0.078853    1.023810    1.000000    1.089744
观察正负样本分布
# 查看好坏样本的分布
data.groupby(['bad_ind']).agg({'bad_ind':np.size})
      bad_ind
bad_ind 
0.0     94008
1.0     1798
观察样本在时间上的分布
# 查看样本在月份上的分布
data.groupby(data.obs_mth).agg({'obs_mth':np.size})
            obs_mth
obs_mth 
2018-06-30  20565
2018-07-31  34030
2018-09-30  10709
2018-10-31  14527
2018-11-30  15975
挑选训练样本和时间外样本(OOT)
# 选择'2018-11-30'之外的数据作为时间内样本,用于模型训练
train = data[data.obs_mth != '2018-11-30'].reset_index().copy()
# 选择'2018-11-30'的数据作为时间外样本,用于模型验证
valid = data[data.obs_mth == '2018-11-30'].reset_index().copy()
构建训练集和验证集
# 挑选特征
feature_list = ['td_score', 'jxl_score', 'mj_score','rh_score', 'zzc_score', 'zcx_score', 
                'person_info', 'finance_info','credit_info', 'act_info']
x_train = train[feature_list].copy()
y_train = train['bad_ind'].copy()

x_valid = valid[feature_list].copy()
y_valid = valid['bad_ind'].copy()
训练逻辑回归模型
# 构建并训练逻辑回归模型
lr = LogisticRegression(C = 0.1, class_weight='balanced')
lr.fit(x_train, y_train)
训练集和验证集上的效果比对
# 在训练集上的评价指标
y_pred_train = lr.predict_proba(x_train)[:, 1]
fpr_lr_train,tpr_lr_train, _ = metrics.roc_curve(y_train, y_pred_train)
ks_train = abs(fpr_lr_train - tpr_lr_train).max()
print("the KS on train set is:", ks_train)
the KS on train set is: 0.45006177149590326
# 在验证集上的评价指标
y_pred_valid = lr.predict_proba(x_valid)[:, 1]
fpr_lr_valid,tpr_lr_valid, _ = metrics.roc_curve(y_valid, y_pred_valid)
ks_valid = abs(fpr_lr_valid - tpr_lr_valid).max()
print("Train KS is:", ks_valid)
the KS on valid set is: 0.4156029286374541
# 训练集和验证集上的ROC对比
from matplotlib import pyplot as plt
plt.plot(fpr_lr_train, tpr_lr_train, label = 'LR_train')
plt.plot(fpr_lr_valid, tpr_lr_valid, label = 'LR_valid')
plt.plot([0,1], [0,1], 'k--')
plt.xlabel('Flase positive rate')
plt.ylabel('True positive rate')
plt.title('ROC Curve')
plt.legend(loc='best')
plt.show()
ROC曲线

  通常我们使用ROC曲线(Receiver Operating Characteristic Curve)来衡量模型的整体区分度,以及通过ROC曲线的平稳度来推断模型的鲁棒性和泛化能力。

生成模型报告

  通过分箱,分别对KS值,正负样本数、正负样本占比、捕获率等进行汇总,生成最终的模型报告。

# 生成模型报告
model = lr
row_num, col_num = 0, 0
bins = 20
Y_predict = [s[1] for s in model.predict_proba(x_valid)]
Y = y_valid

rows = Y.shape[0]
lis = [(Y_predict[i], Y[i]) for i in range(rows)]
ks_lis = sorted(lis, key=lambda x : x[0], reverse=True)
bin_num = int(rows/bins + 1)

good = sum([1 for (p,y) in ks_lis if y <= 0.5])
bad = sum([1 for (p,y) in ks_lis if y > 0.5])

good_cnt, bad_cnt = 0,0
KS = []
GOOD = []
GOOD_CNT = []
BAD = []
BAD_CNT = []
BAD_PCTG = []
BAD_RATE = []
DCT_REPORT = []
report = {}
for j in range(bins):
    ds = ks_lis[j * bin_num : min(bin_num * (j + 1), rows)]
    good1 = sum([1 for (p,y) in ds if y <= 0.5])
    bad1 = sum([1 for (p,y) in ds if y > 0.5])
    bad_cnt += bad1
    good_cnt += good1
    bad_pctg = round(bad_cnt / sum(y_valid), 3)
    bad_rate = round(bad1 /(bad1 + good1), 3)
    ks = round(math.fabs(bad_cnt / bad - good_cnt / good), 3)
    KS.append(ks)
    GOOD.append(good1)
    GOOD_CNT.append(good_cnt)
    BAD.append(bad1)
    BAD_CNT.append(bad_cnt)
    BAD_PCTG.append(bad_pctg)
    BAD_RATE.append(bad_rate)
    
    report['KS'] = KS
    report['正样本个数'] = GOOD
    report['正样本累计个数'] = GOOD_CNT
    report['负样本个数'] = BAD
    report['负样本累计个数'] = BAD_CNT
    report['捕获率'] = BAD_PCTG
    report['负样本占比'] = BAD_RATE
    
pd.DataFrame(report)

报告结果如下图所示:

模型报告

通过观察上述报告,我们可以看出,模型的最大的KS值出现在编号为4的分箱中,在报告中,前4箱的样本总数为3006,其中负样本190个,捕获率达到57.9%,也就是说,如果在风控决策中拒绝掉分数最低的20%的人,就可以捕获到57.9%的负样本。

3. 评分映射

  根据我们在第一部分中推导出来的评分映射公式,我们可以在验证集上,映射算出每个样本的最终风险分。这里需要注意的是,逻辑回归的输出中正样本和负样本的定义与我们实际也业务中的正负样本的定义刚好相反,即数据集中的正样本(即bad_ind为1)是我们业务上的负样本,也就是欺诈样本。因此上述映射公式中的加号,在这里应该改为减号。

# 评分计算
def score(x, coef, intercept):
    xbeta = intercept
    for i in range(len(x) - 1):
        xbeta += x[i] * coef[i]
    score = 650-50 * xbeta / math.log(2)  
    return score

x_valid['score'] = x_valid.apply(lambda x : score(x, lr.coef_[0],lr.intercept_[0]) ,axis=1)  
x_valid.head()
image.png
# 计算KS
fpr_lr,tpr_lr,_ = metrics.roc_curve(y_valid, x_valid['score'])  
val_ks = abs(fpr_lr - tpr_lr).max()  
print('val_ks : ',val_ks) 
val_ks :  0.415602928637454

  通过对模型进行映射评分,在时间外样本上的KS值与逻辑回归模型预测的KS值一致,因此通过这种方式可以验证映射函数的逻辑是否正确。因为在映射函数逻辑正确的情况下,是不会影响模型的排序能力的, 故映射后的KS值应当与模型映射前直接输出的KS值是一致的

4.划分评分等级

  在得到用每个用户的评分之后,还可以根据评分对用户进行等级划分。如果希望某个区间的逾期率变大或变小,可以通过调整阈值参数来实现

# 划分评级
def level(score):
    level = 0
    if score <= 680:
        level = "D"
    elif score <= 730 and score > 680 : 
        level = "C"
    elif score <= 780 and score > 730:
        level = "B"
    elif  score > 780 :
        level = "A"
    return level
x_valid['level'] = x_valid.score.map(lambda x : level(x) )
x_valid['level'].groupby(x_valid['level']).count()/len(x_valid)
level
A    0.033239
B    0.167074
C    0.208951
D    0.590736
Name: level, dtype: float64

  好了,以上就是关于使用逻辑回归构建评分卡的全部内容,逻辑回归是信贷风控中解释性较强的模型,往往用到的场景比较多,且因为其效果一般都还不错,在银行和各大互联网金融公司往往会作为baseline来使用,至于评分卡建模,后期我们会介绍一种基于XGBoost的评分卡构建方法。


信贷风控建模实战系列

信贷风控建模实战(一)——建模流程总览
信贷风控建模实战(二)——策略生成及规则挖掘
信贷风控建模实战(三)——评分卡建模之逻辑回归
信贷风控建模实战(四)——评分卡建模之XGBoost
信贷风控建模实战(五)——特征工程
信贷风控建模实战(六)——异常检测
信贷风控建模实战(七)——群组划分or聚类
信贷风控建模实战(八)——风控基础概念

相关文章

网友评论

      本文标题:信贷风控建模实战(三)——评分卡建模之逻辑回归

      本文链接:https://www.haomeiwen.com/subject/akllkdtx.html