在风控模型领域,PSI(Population Stability Index)是个常见的指标,用来描述模型/特征的稳定性。因为在金融领域,稳定是个特别重要的要求,模型/特征的更新频率,比起搜广推这种业务场景来也慢很多很多。像风控模型这种,更新频率至少是按季度起算,按年算也非常常见。因此,模型上线前后的稳定性就是个特别重要的指标。
我们在建模时,模型/特征的稳定性一般可以这么衡量:
模型或特征稳定性 == 历史样本分布与未来样本分布的偏差大小。
如果历史样本分布与未来样本分布偏差小,我们就可以认为模型的稳定性较好没有比较大的偏差,反制亦然。
当然在实际情况中,未来样本的分布,总是在不断变化的。互联网各种场景中,用户的变化都非常快。举个很实际的例子,大部分做贷款业务的互金公司,包括传统的银行,在20年用户都发生了重大变化,大家的逾期率普遍都提升了很多,原因大家也都清楚,就是因为疫情的黑天鹅事件。
训练模型的时候,我们一般可以把训练数据作为历史样本(actual),验证数据作为未来样本(expected),那么PSI的计算公式如下:
P S I = ∑ i = 1 n ( A i − E i ) ∗ l n A i E i PSI = \sum_{i=1} ^n (A_i - E_i) * ln \frac{A_i}{E_i} PSI=i=1∑n(Ai−Ei)∗lnEiAi
参考文献1种提到PSI值可以这样理解:
PSI = SUM( (实际占比 - 预期占比)* ln(实际占比 / 预期占比) )
这么理解就更为直观,是一种加深理解的好方法。
说到PSI,一般都会提到KL散度,因为这两长得就很像,各种藕断丝连的关系。
KL散度(Kullback-Leibler divergence,简称KLD),在讯息系统中称为相对熵(relative entropy),在连续时间序列中称为随机性(randomness),在统计模型推断中称为讯息增益(information gain)。也称讯息散度(information divergence)。
KL散度是两个几率分布P和Q差别的非对称性的度量。 KL散度是用来度量使用基于Q的分布来编码服从P的分布的样本所需的额外的平均比特数。典型情况下,P表示数据的真实分布,Q表示数据的理论分布、估计的模型分布、或P的近似分布。(参考文献2)
D K L ( P ∣ ∣ Q ) = − ∑ p i l n q i p i D_{KL}(P||Q) = -\sum p_i ln\frac{q_i}{p_i} DKL(P∣∣Q)=−∑pilnpiqi
也可以等价于
D K L ( P ∣ ∣ Q ) = ∑ p i l n p i q i D_{KL}(P||Q) = \sum p_i ln\frac{p_i}{q_i} DKL(P∣∣Q)=∑pilnqipi
如果是连续随机变量,概率分布P和Q可按积分方式定义为
D K L ( P ∣ ∣ Q ) = ∫ − ∞ ∞ p ( x ) l n p ( x ) q ( x ) d ( x ) D_{KL}(P||Q) = \int _{-\infty} ^{\infty} p(x) ln\frac{p(x)}{q(x)} d(x) DKL(P∣∣Q)=∫−∞∞p(x)lnq(x)p(x)d(x)
可以对KL散度进行简单进行推导
随机变量X的熵可以表示为:
H ( X ) = − ∑ i p ( x i ) l o g p ( x i ) H(X) = -\sum_i p(x_i) log p(x_i) H(X)=−i∑p(xi)logp(xi)
而对于两个随机变量P, Q,概率分布分别为p(x), q(x),则p与q的交叉熵定义为:
H ( p , q ) = ∑ p ( x ) l o g 1 q ( x ) = − ∑ p ( x ) l o g q ( x ) H(p, q) = \sum p(x) log \frac{1}{q(x)} = -\sum p(x)logq(x) H(p,q)=∑p(x)logq(x)1=−∑p(x)logq(x)
在信息论中,交叉熵可认为是对预测分布q(x)用真实分布p(x)来进行编码时所需要的信息量大小。
因此,KL散度(相对熵)为:
D K L ( P ∣ ∣ Q ) = H ( p , q ) − H ( p ) = − ∑ p ( x ) l o g q ( x ) − ( − ∑ p ( x ) l o g p ( x ) ) = ∑ p ( x ) l o g p ( x ) q ( x ) \begin{aligned} D_{KL}(P||Q) &= H(p, q) - H(p) \\ &= -\sum p(x)logq(x) - (-\sum p(x)logp(x)) \\ & = \sum p(x) log \frac{p(x)}{q(x)} \end{aligned} DKL(P∣∣Q)=H(p,q)−H(p)=−∑p(x)logq(x)−(−∑p(x)logp(x))=∑p(x)logq(x)p(x)
注意KL散度并不是一个真正的度量或者距离,因为它不具有对称性
D K L ( P ∣ ∣ Q ) ≠ D K L ( Q ∣ ∣ P ) D_{KL}(P||Q) \neq D_{KL}(Q||P) DKL(P∣∣Q)=DKL(Q∣∣P)
我们再来看PSI的公式
P S I = ∑ i = 1 n ( A i − E i ) ∗ l n A i E i PSI = \sum_{i=1} ^n (A_i - E_i) * ln \frac{A_i}{E_i} PSI=i=1∑n(Ai−Ei)∗lnEiAi
拆开一下
P S I = ∑ i = 1 n A i ∗ l n A i E i + E i ∗ l n E i A i PSI = \sum_{i=1} ^n A_i * ln \frac{A_i}{E_i} + E_i * ln \frac{E_i}{A_i} PSI=i=1∑nAi∗lnEiAi+Ei∗lnAiEi
所以拆开一下,很容易看出来,PSI其实是将A(actual)与E(expected)的KL散度做了一个对称化操作,将两个KL散度进行了相加。
P S I = D K L ( A ∣ ∣ E ) + D K L ( E ∣ ∣ A ) PSI = D_{KL}(A||E) + D_{KL}(E||A) PSI=DKL(A∣∣E)+DKL(E∣∣A)
上头已经解释了很长时间的理论,下面我们看看具体代码如何实现。
import math
def genData():
ages = ['<18', '[18,25)', '[25,30)', '[30,40)', '[40,50)', '>50']
train_all = [190, 370, 550, 140, 160, 130]
train_target = [90, 70, 50, 40, 60, 80]
train_target_num = sum(train_target)
ai_list = [round(ele / train_target_num, 4) for ele in train_target]
print(ai_list)
predict_all = [1912, 3705, 5507, 1408, 1603, 1311]
predict_target = [800, 827, 548, 446, 660, 846]
predict_target_num = sum(predict_target)
ei_list = [round(ele / predict_target_num, 4) for ele in predict_target]
print(ei_list)
psi_list = [round((ai - ei) * math.log(ai / ei), 6) for ai, ei in zip(ai_list, ei_list)]
print(psi_list)
print(sum(psi_list))
genData()
上面的代码,我们模拟一个场景:
将用户的年龄段分为六段,train_target, predict_target分别为响应人群,最后计算PSI值。
最后代码输出
[0.2308, 0.1795, 0.1282, 0.1026, 0.1538, 0.2051]
[0.1938, 0.2004, 0.1328, 0.1081, 0.1599, 0.205]
[0.006465, 0.002302, 0.000162, 0.000287, 0.000237, 0.0]
0.009453000000000001
一般如果PSI值<0.1,我们认为模型/特征稳定性很好,变化不大,可以使用。
如果PSI值>0.25,则认为模型/特征稳定性不行,需要进行更新。
参考文献
1.https://zhuanlan.zhihu.com/p/79682292
2.https://zh.wikipedia.org/wiki/%E7%9B%B8%E5%AF%B9%E7%86%B5
3.https://hsinjhao.github.io/2019/05/22/KL-DivergenceIntroduction/