欢迎访问设·集合!

设·集合

您现在的位置是:首页 > 平面软件 > PS

风控模型—群体稳定性指标(PSI)深入理解应用

设·集合小编 发布时间:2023-03-07 13:14:57 1604次最后更新:2024-03-08 10:55:42

风控业务背景

在风控中,稳定性压倒一切。原因在于,一套风控模型正式上线运行后往往需要很久(通常一年以上)才会被替换下线。如果模型不稳定,意味着模型不可控,对于业务本身而言就是一种不确定性风险,直接影响决策的合理性。这是不可接受的。

本文将从稳定性的直观理解、群体稳定性指标(Population Stability Index,PSI)的计算逻辑、PSI背后的含义等多维度展开分析。

目录
Part 1. 稳定性的直观理解
Part 2. 群体稳定性指标(Population Stability Index,PSI)的理解
Part 3. 相对熵(KL散度)的理解
Part 4. 相对熵与PSI之间的关系
Part 5. PSI指标的业务应用
Part 6. PSI的计算代码(Python)
致谢
版权声明
参考资料

Part 1. 稳定性的直观理解

在日常生活中,我们可能会看到每月电表、水表数值的变化。直观理解上的系统稳定,通常是指某项指标波动小(低方差),指标曲线几乎是一条水平的直线。此时,我们就会觉得系统运行正常稳定,很有安全感。

在数学上,我们通常可以用变异系数(Coefficient of Variation,CV来衡量这种数据波动水平。变异系数越小,代表波动越小,稳定性越好。

变异系数的计算公式为:变异系数 C·V =( 标准偏差 SD / 平均值Mean )× 100%

那么,是不是只用用变异系数就可以了呢?方便、直观。——答案是否定的。在机器学习建模时,我们基于假设“历史样本分布等于未来样本分布”。因此,我们通常认为:

模型或变量稳定 <=> 未来样本分布与历史样本分布之间的偏差小。

然而,实际中由于受到客群变化(互金市场用户群体变化快)、数据源采集变化(比如爬虫接口被风控了)等等因素影响,实际样本分布将会发生偏移,就会导致模型不稳定。

Part 2. 群体稳定性指标(Population Stability Index,PSI)的理解

如果你有基础的风控建模经验,想必会很熟悉PSI(Population Stability Index)指标。PSI反映了验证样本在各分数段的分布与建模样本分布的稳定性。在建模中,我们常用来筛选特征变量、评估模型稳定性

那么,PSI的计算逻辑是怎样的呢?很多博客文章都会直接告诉我们,稳定性是有参照的,因此需要有两个分布——实际分布(actual)和预期分布(expected)。其中,在建模时通常以训练样本(In the Sample, INS)作为预期分布,而验证样本通常作为实际分布。验证样本一般包括样本外(Out of Sample,OOS)和跨时间样本(Out of Time,OOT)。

我们从直觉上理解,是不是可以把两个分布重叠放在一起,比较下两个分布的差异有多大

图 1 - 分数分布差异对比

PSI的计算公式也告诉我们,的确是这样的:

PSI = SUM( (实际占比 - 预期占比)* ln(实际占比 / 预期占比) )

大多数同学到了这里,遵循拿来主义,直接应用。但是,大家有没有思考过以下几个问题:

Q1: 为什么要在计算公式中引入对数—— ln(实际占比 / 预期占比)

Q2: 为什么log的底数是自然常数e,而不是其他常数(比如2)?(信息论中基常常选择为2,因此信息的单位为比特bits;而机器学习中基常选择为自然常数,因此单位常被称为奈特nats

Q3: 如果只是衡量两个分布的差异,为什么不直接定义成以下公式呢?这个公式含义是把各个分数段里的人群占比差异进行累加放大。

自定义的稳定性指标 = SUM(实际占比 - 预期占比)

让我们先保留这些疑问,回到PSI的计算公式,以图2表格数据为例介绍下计算步骤。

  • step1:将变量预期分布(excepted)进行分箱(binning)离散化,统计各个分箱里的样本占比。
    注意:
    a) 分箱可以是等频、等距或其他方式,分箱方式不同,将导致计算结果略微有差异;
    b) 对于连续型变量(特征变量、模型分数等),分箱数需要设置合理,一般设为10或20;对于离散型变量,如果分箱太多可以提前考虑合并小分箱;分箱数太多,可能会导致每个分箱内的样本量太少而失去统计意义;分箱数太少,又会导致计算结果精度降低。
  • step2: 按相同分箱区间,对实际分布(actual)统计各分箱内的样本占比
  • step3:计 算各分箱内的A - ELn(A / E),计算index = (实际占比 - 预期占比)* ln(实际占比 / 预期占比) 。
  • step4: 将各分箱的index进行求和,即得到最终的PSI。
图 2 - PSI计算表格

在计算得到PSI指标后,这个数字又代表什么业务含义呢?PSI数值越小,两个分布之间的差异就越小,代表越稳定。

图 3 - PSI数值的业务含义

Part 3. 相对熵(KL散度)的理解

相对熵(relative entropy),又被称为Kullback-Leibler散度(Kullback-Leibler divergence)或信息散度(information divergence),是两个概率分布间差异的非对称性度量。

划重点——KL散度不满足对称性。

我们先不考虑相对熵的计算逻辑,先看它的物理含义是什么?

相对熵可以衡量两个随机分布之间的 `距离“
1)当两个随机分布相同时,它们的 相对熵为零;当两个随机分布的差别增大时,它们的相对熵也会增大。
2)注意?:相对熵是一个从 信息论角度量化距离的指标,与 数学概念上的距离有所差异。数学上的距离需要满足:非负性、 对称性、同一性、传递性等;而 相对熵不满足对称性

看到这里,是不是感觉和PSI的概念非常相似?

当两个随机分布完全一样时,PSI = 0;反之,差异越大,PSI越大。

直觉告诉我们相对熵和PSI应该存在着某种隐含的关系,真相正在慢慢浮出水面。

我们再来看相对熵的计算公式。在信息理论中,相对熵等价于两个概率分布的信息熵(Shannon entropy)的差值。

其中,P(x)表示数据的真实分布,而Q(x)表示数据的观察分布。上式可以理解为:

概率分布携带着信息,可以用信息熵来衡量。
若用观察分布Q(x)来描述真实分布P(x),还需要多少额外的信息量?
图 4 - KL散度具有非对称性

因此,KL散度是单向描述信息熵差异。为了加强大家理解,接下来以数值案例介绍相对熵如何计算。

假如一个字符发射器,随机发出0和1两种字符,真实发出概率分布为A,但实际不知道A的具体分布。通过观察,得到概率分布B与C,各个分布的具体情况如下:

图 5 - 三种数据分布

可以计算出得到KL散度如下:

由上式可知,相对熵KL(A||C) > KL(A||B),说明A和B之间的概率分布在信息量角度更为接近。而通过概率分布可视化观察,我们也认为A和B更为接近,两者吻合。

Part 4. 相对熵与PSI之间的关系

接下来,我们从数学上来分析相对熵和PSI之间的关系。

将PSI计算公式变形后可以分解为2项,其中:

第1项:实际分布(A)与预期分布(E)之间的KL散度——
第2项:预期分布(E)与实际分布(A)之间的KL散度——

因此,PSI本质上是实际分布(A)与预期分布(E)的KL散度的一个对称化操作。其双向计算相对熵,并把两部分相对熵相加,从而更为全面地描述两个分布的差异。

至此,已经回答了上文的问题Q1——为什么要在计算公式中引入对数?

可能有人还会问,那为什么不把PSI定义成下式呢?

按笔者理解,一是为了保证数学公式的优雅;二是只要在同一尺度上比较,同时放大2倍也无所谓。

再回到上文的问题Q2——为什么log的底数是自然常数e,而不是其他常数(比如2)?按笔者理解,两者都可以,从信息量角度而言,底数为2或许更为合理。

至于问题Q3——为什么不定义为“SUM(实际占比 - 预期占比)”?这是因为有正有负,会存在相互抵消的现象。另一方面,也确实没有从相对熵角度来定义更为合理。

图 6 - PSI与KL散度之间的关系

Part 5. PSI指标的业务应用

我们在业务上一般怎么用呢?一般以训练集(INS)的样本分布作为预期分布,进而跨时间窗按月/周来计算PSI,得到如图6所示的Monthly PSI Report,进而剔除不稳定的变量。同理,在模型上线部署后,也将通过PSI曲线报表来观察模型的稳定性。

入模变量保证稳定性,变量监控
模型分数保证稳定性,模型监控

根据建模经验,给出一些建议:

1. 实际评估需要分不同粒度:时间粒度(按月、按样本集)、订单层次(放贷层、申请层)、人群(若没有分群建模,可忽略)。

2. 先在放贷样本上计算PSI,剔除不稳定的特征;再对申请样本抽样(可能数据太大),计算PSI再次筛选。之前犯的错误就是只在放贷样本上评估,后来在全量申请订单上评估时发现并不稳定,导致返工。

3. 时间窗尽可能至今为止,有可能建模时间窗稳定,但近期时间窗出现不稳定。

4. PSI只是一个宏观的指标,建议先看变量数据分布(EDD),看分位数跨时间变化来检验数据质量。我们无法得知PSI上升时,数据分布是左偏还是右偏。因此,建议把PSI计算细节也予以保留,便于在模型不稳定时,第一时间排查问题。

图 7 - Monthly PSI Report

Part 6. PSI的计算代码(Python)

import math
import numpy as np
import pandas as pd

def calculate_psi(base_list, test_list, bins=20, min_sample=10):
    try:
        base_df = pd.DataFrame(base_list, columns='score')
        test_df = pd.DataFrame(test_list, columns='score') 
        
        # 1.去除缺失值后,统计两个分布的样本量
        base_notnull_cnt = len(list(base_df'score'.dropna()))
        test_notnull_cnt = len(list(test_df'score'.dropna()))

        # 空分箱
        base_null_cnt = len(base_df) - base_notnull_cnt
        test_null_cnt = len(test_df) - test_notnull_cnt
        
        # 2.最小分箱数
        q_list = 
        if type(bins) == int:
            bin_num = min(bins, int(base_notnull_cnt / min_sample))
            q_list = x / bin_num for x in range(1, bin_num)
            break_list = 
            for q in q_list:
                bk = base_df'score'.quantile(q)
                break_list.append(bk)
            break_list = sorted(list(set(break_list))) # 去重复后排序
            score_bin_list = -np.inf   break_list   np.inf
        else:
            score_bin_list = bins
        
        # 4.统计各分箱内的样本量
        base_cnt_list = base_null_cnt
        test_cnt_list = test_null_cnt
        bucket_list = `MISSING`
        for i in range(len(score_bin_list)-1):
            left  = round(score_bin_listi 0, 4)
            right = round(score_bin_listi 1, 4)
            bucket_list.append(`(`   str(left)   ','   str(right)   '')
            
            base_cnt = base_df(base_df.score > left) & (base_df.score <= right).shape0
            base_cnt_list.append(base_cnt)
            
            test_cnt = test_df(test_df.score > left) & (test_df.score <= right).shape0
            test_cnt_list.append(test_cnt)
        
        # 5.汇总统计结果 
        stat_df = pd.DataFrame({`bucket`: bucket_list, `base_cnt`: base_cnt_list, `test_cnt`: test_cnt_list})
        stat_df'base_dist' = stat_df'base_cnt' / len(base_df)
        stat_df'test_dist' = stat_df'test_cnt' / len(test_df)
        
        def sub_psi(row):
            # 6.计算PSI
            base_list = row'base_dist'
            test_dist = row'test_dist'
            # 处理某分箱内样本量为0的情况
            if base_list == 0 and test_dist == 0:
                return 0
            elif base_list == 0 and test_dist > 0:
                base_list = 1 / base_notnull_cnt   
            elif base_list > 0 and test_dist == 0:
                test_dist = 1 / test_notnull_cnt
                
            return (test_dist - base_list) * np.log(test_dist / base_list)
        
        stat_df'psi' = stat_df.apply(lambda row: sub_psi(row), axis=1)
        stat_df = stat_df'bucket', 'base_cnt', 'base_dist', 'test_cnt', 'test_dist', 'psi'
        psi = stat_df'psi'.sum()
        
    except:
        print('error!!!')
        psi = np.nan 
        stat_df = None
    return psi, stat_df

运行演示:

>>> psi, stat_df = calculate_psi(base_list=list(dfdf'draw_month' == '2020-05'var), 
                   test_list=list(dfdf'draw_month' == '2021-02'var), 
                   bins=20, min_sample=10)
>>> stat_df
             bucket  base_cnt  base_dist  test_cnt  test_dist           psi
0           MISSING         0   0.000000         0   0.000000  0.000000e 00
1     (-inf,0.0199       238   0.046777       511   0.065454  6.274898e-03
2   (0.0199,0.0217       266   0.052280       477   0.061099  1.374765e-03
3   (0.0217,0.0235       249   0.048939       467   0.059818  2.183942e-03
4   (0.0235,0.0249       262   0.051494       380   0.048674  1.587606e-04
5   (0.0249,0.0265       280   0.055031       453   0.058025  1.585503e-04
6   (0.0265,0.0279       230   0.045204       329   0.042142  2.148738e-04
7   (0.0279,0.0297       251   0.049332       410   0.052517  1.992932e-04
8   (0.0297,0.0316       254   0.049921       388   0.049699  9.929669e-07
9   (0.0316,0.0336       248   0.048742       386   0.049443  1.000043e-05
10  (0.0336,0.0358       257   0.050511       395   0.050596  1.416201e-07
11  (0.0358,0.0382       263   0.051690       363   0.046497  5.499267e-04
12  (0.0382,0.0405       249   0.048939       343   0.043935  5.396963e-04
13  (0.0405,0.0431       258   0.050708       316   0.040476  2.305601e-03
14   (0.0431,0.046       256   0.050314       319   0.040861  1.967526e-03
15   (0.046,0.0492       255   0.050118       321   0.041117  1.781819e-03
16  (0.0492,0.0528       252   0.049528       314   0.040220  1.937663e-03
17  (0.0528,0.0578       255   0.050118       391   0.050083  2.398626e-08
18  (0.0578,0.0641       262   0.051494       369   0.047265  3.623085e-04
19  (0.0641,0.0749       249   0.048939       452   0.057897  1.505794e-03
20     (0.0749,inf       254   0.049921       423   0.054182  3.489647e-04

致谢

加强对各类指标的理解,才能巩固风控模型基本功。感谢LP帮助。

版权声明

欢迎转载分享请在文章中注明作者和原文链接,感谢您对知识的尊重和对本文的肯定。

原文作者:求是汪在路上(知乎ID)
原文链接: https:// zhuanlan.zhihu.com/p/79 682292/

?著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处,侵权转载将追究相关责任

参考资料

关于作者

在某互联网金融公司从事风控建模、反欺诈、数据挖掘等方面工作,目前致力于将实践经验固化分享,量化成长轨迹。欢迎交流

广告位

热心评论

评论列表