模型预测概率的修正

在很多场景中,我们只对模型的排序性能感兴趣,不太关注其预测概率,因为只要模型的排序性能没有下降,根据排序性设置一定的阈值就能够满足业务的需要。而模型预测概率的绝对值,除了依赖于模型的排序性,还和所应用场景的样本分布(发生比)密切相关。所以如果要使用预测概率,需根据具体的样本分布(发生比)对其进行修正。

原理

下面主要以逻辑回归为例,如下模型:

其中

是变量的woe值,

是模型系数,且截距项

理论上等于建模样本整体发生比的对数。

在模型排序性能不变(即各变量的区分度不变)的假设下,即使新样本分布(发生比)发生变化,但各变量的woe值可以认为是不变的,看woe的计算公式:

从公式可以看出,若变量的区分度不变,变量对应发生比与样本整体发生比的变化是同步的,所以假设woe值是不变的。

因此,这里所说的修正,就是对样本整体发生比的修正,修正值为:

实践

1

01

数据集准备

1、数据集data:已经过处理,只保留目标变量和7个特征,下面主要看如何修正预测概率,对于建模的过程和建模效果不做过多处理。(data数据集可公众号消息留言,提供下载链接)。

import pandas as pd
import numpy as np
import statsmodels.api as sm 
import matplotlib.pyplot as plt
data = pd.read_csv('\\data.csv')
data.info()
data.y.value_counts()

目标变量y的取值为0,0.5,1,0,0表示未违约或最大违约天数小于等于3;1表示最大违约天数大于60;0.5表示最大违约大于3且小于等于60,属于灰色样本。

2、数据集model:从data数据集中剔除掉y=0.5的灰色样本,生成model数据集,其发生比为2008/14170:

model = data[data.y!=0.5]
model.y.value_counts()

3、数据集train:对model中y=0样本采取欠抽样提高0样本比例,生成train数据集,其发生比为2008/7085:

good_indx = model.index[model.y==0]
bad_indx = model.index[model.y==1]
np.random.seed(1111)
smp_good_indx = np.random.choice(good_indx, int(round(0.5*len(good_indx))), replace=False)
smp_indx = np.concatenate((smp_good_indx, bad_indx), axis=0)
train = model.loc[smp_indx, :]
train.y.value_counts()

1

02

模型训练

用数据集train训练逻辑回归(ContVarChi2BinBatch,txtContVarBin,CalcWoe等函数见底部链接):

### 分箱
bin_dict = ContVarChi2BinBatch(train, 'cus_num', 'y', BinMax=5, BinPcntMin=0.05, SplitNum=100, spe_attri=[-1], singleIndicator = False)
train_bin = txtContVarBin(train, 'cus_num', 'y', bin_dict, testIndicator=0)
### 计算woe
train_woe, woe_dict = CalcWoe(train_bin, 'cus_num', 'y') 
### 
x_train = train_woe.drop(['cus_num', 'y'], axis=1)
y_train = train_woe['y']
### statstrains.api 逻辑回归
logit = sm.Logit(y_train, sm.add_constant(x_train))
logit_result = logit.fit()
logit_result.summary()

其中const的系数,即截距为-1.2607,与ln(2008/7085)是近似相等的。

1

03

生成评分卡

1、根据train数据集上训练的模型结果,生成标准评分卡:

### 评分卡
def scorecard(df):
    df_p = df.copy()

    df_p['x1_woe'] = np.nan
    df_p['x1_woe'][df_p['x1']<=5.03] = -1.610462
    df_p['x1_woe'][(df_p['x1']>5.03) & (df_p['x1']<=10.3)] = -0.883790
    df_p['x1_woe'][(df_p['x1']>10.3) & (df_p['x1']<=16.19)] = -0.259779
    df_p['x1_woe'][(df_p['x1']>16.19) & (df_p['x1']<=21.15)] = 0.218075
    df_p['x1_woe'][df_p['x1']>21.15] = 0.806426

    df_p['x2_woe'] = np.nan
    df_p['x2_woe'][df_p['x2']<=0.01] = -0.097708
    df_p['x2_woe'][df_p['x2']>0.01] = 1.142635

    df_p['x3_woe'] = np.nan
    df_p['x3_woe'][df_p['x3']<=-1] = -0.198114
    df_p['x3_woe'][(df_p['x3']>-1) & (df_p['x3']<=20.38)] = 0.506548
    df_p['x3_woe'][(df_p['x3']>20.38) & (df_p['x3']<=83.08)] = 0.064844
    df_p['x3_woe'][df_p['x3']>83.08] = -0.293702


    df_p['x4_woe'] = np.nan
    df_p['x4_woe'][df_p['x4']<=0.4954] = -0.349128
    df_p['x4_woe'][(df_p['x4']>0.4954) & (df_p['x4']<=0.5505)] = 0.155449
    df_p['x4_woe'][(df_p['x4']>0.5505) & (df_p['x4']<=1.9816)] = -0.193918
    df_p['x4_woe'][df_p['x4']>1.9816] = 0.330673

    df_p['x5_woe'] = np.nan
    df_p['x5_woe'][df_p['x5']<=22.18] = -0.428135
    df_p['x5_woe'][(df_p['x5']>22.18) & (df_p['x5']<=26.17)] = -0.023622
    df_p['x5_woe'][(df_p['x5']>26.17) & (df_p['x5']<=28.07)] = 0.310403
    df_p['x5_woe'][(df_p['x5']>28.07) & (df_p['x5']<=35.1)] = 0.108008
    df_p['x5_woe'][df_p['x5']>35.1] = -0.177175

    df_p['x6_woe'] = np.nan
    df_p['x6_woe'][df_p['x6']<=-1] = -0.038834
    df_p['x6_woe'][(df_p['x6']>-1) & (df_p['x6']<=0.0024)] = -0.180664
    df_p['x6_woe'][df_p['x6']>0.0024] = 0.293899

    df_p['x7_woe'] = np.nan
    df_p['x7_woe'][df_p['x7']<=621.62] = 0.055095
    df_p['x7_woe'][df_p['x7']>621.62] = -0.749705

    df_p['lnodds'] = -1.260650 + 0.854068*df_p['x1_woe'] + 0.513575*df_p['x2_woe'] + \
                         0.329542*df_p['x3_woe'] + 0.650766*df_p['x4_woe'] + \
                         0.563031*df_p['x5_woe'] + 0.664592*df_p['x6_woe'] + \
                         0.664559*df_p['x7_woe']

    # 计算预测概率              
    df_p['p'] = 1/(1+np.exp(-df_p['lnodds'])) 
    # 转换评分:特定比率1:10,特定分值600,翻番分数60:
    A=400.68431431  
    B=86.56170245  
    df_p['score'] = A - B*df_p['lnodds']
    df_p['score'] = df_p['score'].astype(int)

    return df_p

2、用标准评分卡对train数据集打分:

train_p = scorecard(train)

显而易见,训练样本上的预测概率和样本违约率是一致的,通过下面不太严谨的方法来验证一下,先对数据进行排序,然后对评分进行划分,看每段样本的违约率(通过y计算)和每段样本的预测概率的平均值是否一致:

train_p = train_p.sort_values('p', ascending=False)
train_p['cut'] = pd.cut(train_p['score'], bins=[0, 370, 420, 470, 520, 570, 620, 670, 1000])
train_p_distr1 = train_p.groupby('cut').size()
train_p_distr2 = train_p.groupby('cut')['y', 'p'].mean()
fig = plt.figure()
ax1 = fig.add_subplot(111)
train_p_distr1.plot(kind='bar', ax=ax1)
ax2 = ax1.twinx()
train_p_distr2.plot(ax=ax2)

可以看出,除了因为两侧样本较少可能产生统计偏差外,样本的违约率和预测概率基本是一致的。

1

04

新样本预测概率的修正

1、将数据集model数据集当作新样本(为方便演示先忽略和train数据集的关系),先用上述标准评分卡对其进行打分,然后根据新样本和原样本的发生比对预测概率进行修正,对比一下修正前后的差别。

修正代码如下:

def scorecard_adjust1(df_p):

    # 修正 np.log(2008/14170) - np.log(2008/7085)
    df_p['ad_lnodds'] = np.log(df_p['p']/(1-df_p['p'])) + np.log(2008/14170) - np.log(2008/7085)

    # 计算修正的预测概率              
    df_p['ad_p'] = 1/(1+np.exp(-df_p['ad_lnodds']))

    return df_p

打分:

model_p = scorecard(model)
model_p = scorecard_adjust1(model_p)

对比修正前后的预测概率:

model_p = model_p.sort_values('p', ascending=False)
model_p['cut'] = pd.cut(model_p['score'], bins=[0, 370, 410, 450, 490, 530, 570, 610, 650, 690, 1000])
model_p_distr1 = model_p.groupby('cut').size()
model_p_distr2 = model_p.groupby('cut')['y', 'p', 'ad_p'].mean()
fig = plt.figure()
ax1 = fig.add_subplot(111)
model_p_distr1.plot(kind='bar', ax=ax1)
ax2 = ax1.twinx()
model_p_distr2.plot(ax=ax2)

如上图所示,修正前的预测概率线(橙色线)在样本违约线的上方,因为新样本的发生比相对训练样本的发生比减少了;而修正后的预测概率基本和样本违约率一致。

2、 同理,将数据集data数据集作为新样本,data数据集是更接近实际业务的数据分布,其中y=0.5的灰色样本定义为未违约客户,看其修正前后的预测概率对比。

def scorecard_adjust2(df_p):

    # 修正 np.log(2008/17758) - np.log(2008/7085)
    df_p['ad_lnodds'] = np.log(df_p['p']/(1-df_p['p'])) + np.log(2008/17758) - np.log(2008/7085)

    # 计算修正的预测概率              
    df_p['ad_p'] = 1/(1+np.exp(-df_p['ad_lnodds']))

    return df_p

data['y2'] = data['y']
data['y2'][data['y2']==0.5] = 0
data['y2'].value_counts(normalize=True)

data_p = scorecard(data)
data_p = scorecard_adjust2(data_p)

data_p = data_p.sort_values('p', ascending=False)
data_p['cut'] = pd.cut(data_p['score'], bins=[0, 370, 410, 450, 490, 530, 570, 610, 650, 690, 1000])
data_p_distr1 = data_p.groupby('cut').size()
data_p_distr2 = data_p.groupby('cut')['y2', 'p', 'ad_p'].mean()
fig = plt.figure()
ax1 = fig.add_subplot(111)
data_p_distr1.plot(kind='bar', ax=ax1)
ax2 = ax1.twinx()
data_p_distr2.plot(ax=ax2)

1

05

无样本情况下违约率的修正

有历史样本情况下,根据评分切分后的样本违约率,就可以知道评分的效果和使用方法。但若一个业务刚开始做还没有样本积累(冷启动),或业务已开展但无法收集到足够的数据(主要是x变量),这时如果想知道评分对应的违约率,和预测概率的修正是一样的方法,如下公式:

若业务整体违约率已知,或根据行业平均的违约率对新业务的整体违约率做个估计,这里我们假设业务整体违约率为10%,则评分修正后的评分违约率计算如下图:

所以在使用其他评分时,要尽可能的知道评分开发的细节,其中每个评分段对应的样本违约率和评分开发所使用样本的整体违约率是必不可少的两个信息。

本文分享自微信公众号 - 大数据建模的一点一滴(bigdatamodeling),作者:小石头

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-03-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 算法 | 决策树

    决策树是一种基本学习方法,可用于回归和分类。回归树的分割准则一般是平均误差,而分类树的分割准则有信息增益、信息增益率、基尼指数等,下面简单梳理决策...

    小石头
  • 算法 | 随机森林

    随机森林是集成学习的一种方法,是将多棵树进行集成的算法,随机是指训练每棵树的样本和变量具有随机性,而多棵树组合在一起就像“森林”一样。

    小石头
  • 徒手撸算法 | 逻辑回归

    逻辑回归是线性回归的改进,通过特定的连接函数将实数范围压缩到(0, 1)范围内,从而实现分类的目的。

    小石头
  • 涨姿势!看骨灰级程序员如何玩转Python

    每个人都知道这个命令。但如果你要读取很大的数据,尝试添加这个参数:nrows = 5,以便在实际加载整个表之前仅读取表的一小部分。然后你可以通过选择错误的分隔符...

    一墨编程学习
  • 帮助数据科学家理解数据的23个pandas常用代码

    返回给定轴缺失的标签对象,并在那里删除所有缺失数据(’any’:如果存在任何NA值,则删除该行或列。)。

    AiTechYun
  • Python 数据分析初阶

    这里可以单独查看其中的内容 data['nick'],计算其中的大小则使用 data['nick'].value_counts()。

    zucchiniy
  • 2 个数据处理的小功能,非常实用!

    0.25 版本开始支持 query 方法,可读性上又获得大幅提升,类似 sql 查询数据的写法,更加人性化。

    double
  • 用 Pandas 进行数据处理系列 二

    获取行操作df.loc[3:6]获取列操作df['rowname']取两列df[['a_name','bname']] ,里面需要是一个 list 不然会报错增...

    zucchiniy
  • pandas模块(很详细归类),pd.concat(后续补充)

    https://pandas.pydata.org/pandas-docs/stable/?v=20190307135750

    小小咸鱼YwY
  • Pandas入门操作

    俺也想起舞

扫码关注云+社区

领取腾讯云代金券