美文网首页利用python进行数据分析
风控模型中的K-S理解以及python实现

风控模型中的K-S理解以及python实现

作者: 26f30aca5431 | 来源:发表于2018-08-22 10:36 被阅读251次

    笔者在工作中计算单变量的ks值时,发现几个分布不同的变量好y计算的ks值相同,凭借统计直觉,发现一定存在问题,笔者从数据和计算ks代码两个方向进行排除。最后定位到计算使用stats.ks_2samp()函数计算ks值时,如果变量存在缺失值,计算得到ks值有误,下面笔者就来好好梳理一下ks值的前世今生。

    ks检验介绍

    笔者刚入门机器学习开始做的例子就是金融场景下风控模型。那时评价模型的好坏就用传统的机器学习评价标准,比如说准确率、精确率和AUC,对风控模型的ks指标还一无所知,倒是作为统计科班出身的童鞋,第一次见到ks想到的就是数理统计中的Kolmogorov-Smirnov检验(柯尔莫哥洛夫-斯米尔洛夫)。后来实习过程中,发现老板们在看风控模型结果最关注的结果就是ks指标,才开始对ks指标逐渐重视起来。在衡量模型效果时,对评分卡或者机器学习模型给出的违约概率和y值计算ks值,给出模型效果来确定模型的好坏(一般0.3左右可以使用,0.4以上模型效果较高,超过0.7,可能模型有问题,这时候需要去debug一下是不是出现了特征信息泄露以及一些其他的问题)。笔者在实际应用中,发现市面上关于ks系统介绍的文章比较少,本文就系统的介绍一下ks的前世今生,以及在在风控模型下的多种实现。

    KS检验-统计角度

    首先,我们了解下统计上KS检验的概念。这里维基百科Kolmogorov–Smirnov test已经解释的很详细了。如果让我一句话解释KS检验的话:我会说KS检验是比较一个频率分布f(x)与理论分布g(x)或者两个观测值分布的检验方法。提到检验,我们第一步就是明确我们检验的原假设和备择假设。其原假设H0: 两个数据分布一致或者数据符合理论分布,定义D=max| f(x)- g(x)|,当实际观测值D>D(n,α)则拒绝H0,否则则接受H0假设。D(n,α)可以查表得到Critical Values for the Two-sample Kolmogorov-Smirnov test (2-sided) 。KS检验与t-检验之类的其他方法不同是KS检验不需要知道数据的分布情况,算是一种非参数检验方法。代价就是数据分布指定的情况下KS效果不如指定的检验好。在样本量比较小的时候,KS检验一般在分析两组数据之间是否不同时相当常用。具体的ks检验的Case可以看文章KS-检验(Kolmogorov-Smirnov test) -- 检验数据是否符合某种分布

    KS检验-风控角度

    从统计角度,我们知道KS是分析两组数据分布是否相同的检验指标。在金融领域中,我们的y值和预测得到的违约概率刚好是两个分布未知的两个分布。好的信用风控模型一般从准确性、稳定性和可解释性来评估模型。一般来说。好人样本的分布同坏人样本的分布应该是有很大不同的,KS正好是有效性指标中的区分能力指标:KS用于模型风险区分能力进行评估,KS指标衡量的是好坏样本累计分布之间的差值。好坏样本累计差异越大,KS指标越大,那么模型的风险区分能力越强。

    KS的计算步骤如下:

    • 计算每个评分区间的好坏账户数(计算的是特征的KS的话,是每个特征对应的好坏账户数)。
    • 计算每个评分区间的累计好账户数占总好账户数比率(good%)和累计坏账户数占总坏账户数比率(bad%)。
    • 计算每个评分区间累计坏账户占比与累计好账户占比差的绝对值(累计good%-累计bad%),然后对这些绝对值取最大值即得此评分卡的KS值。

    KS检验-python实现

    上面介绍了KS的统计原理以及实现方法,下面我们从三个不同的角度去实现KS的计算

    ks_2samp实现

    我们直接调用stats.ks_2samp()函数来计算。链接scipy.stats.ks_2samp¶为ks_2samp()实现源码,笔者按照源码实现了下,方便查看其中的cdf计算结果。

    def ks_calc_2samp(data,score_col,class_col):
        '''
        功能: 计算KS值,输出对应分割点和累计分布函数曲线图
        输入值:
        data: 二维数组或dataframe,包括模型得分和真实的标签
        score_col: 一维数组或series,代表模型得分(一般为预测正类的概率)
        class_col: 一维数组或series,代表真实的标签({0,1}或{-1,1})
        输出值:
        'ks': KS值,'cdf_df': 好坏人累积概率分布以及其差值gap
        '''
        Bad = data.ix[data[class_col[0]]==1,score_col[0]]
        Good = data.ix[data[class_col[0]]==0, score_col[0]]
        data1 = Bad.values
        data2 = Good.values
        n1 = data1.shape[0]
        n2 = data2.shape[0]
        data1 = np.sort(data1)
        data2 = np.sort(data2)
        data_all = np.concatenate([data1,data2])
        cdf1 = np.searchsorted(data1,data_all,side='right')/(1.0*n1)
        cdf2 = (np.searchsorted(data2,data_all,side='right'))/(1.0*n2)
        ks = np.max(np.absolute(cdf1-cdf2))
        cdf1_df = pd.DataFrame(cdf1)
        cdf2_df = pd.DataFrame(cdf2)
        cdf_df = pd.concat([cdf1_df,cdf2_df],axis = 1)
        cdf_df.columns = ['cdf_Bad','cdf_Good']
        cdf_df['gap'] = cdf_df['cdf_Bad']-cdf_df['cdf_Good']
        return ks,cdf_df
    
    crosstab实现

    我们知道计算ks的核心就是好坏人的累积概率分布,我们采用pandas.crosstab函数来计算累积概率分布。

    def ks_calc_cross(data,score_col,class_col):
        '''
        功能: 计算KS值,输出对应分割点和累计分布函数曲线图
        输入值:
        data: 二维数组或dataframe,包括模型得分和真实的标签
        score_col: 一维数组或series,代表模型得分(一般为预测正类的概率)
        class_col: 一维数组或series,代表真实的标签({0,1}或{-1,1})
        输出值:
        'ks': KS值,'crossdens': 好坏人累积概率分布以及其差值gap
        '''
        ks_dict = {}
        crossfreq = pd.crosstab(data[score_col[0]],data[class_col[0]])
        crossdens = crossfreq.cumsum(axis=0) / crossfreq.sum()
        crossdens['gap'] = abs(crossdens[0] - crossdens[1])
        ks = crossdens[crossdens['gap'] == crossdens['gap'].max()]
        return ks,crossdens
    
    roc_curve实现

    我们同时发现在sklearn库中的roc_curve函数计算roc和auc时,计算过程中已经得到好坏人的累积概率分布,同时我们利用sklearn.metrics.roc_curve来计算ks值

    from sklearn.metrics import roc_curve,auc
    def ks_calc_auc(data,score_col,class_col):
        '''
        功能: 计算KS值,输出对应分割点和累计分布函数曲线图
        输入值:
        data: 二维数组或dataframe,包括模型得分和真实的标签
        score_col: 一维数组或series,代表模型得分(一般为预测正类的概率)
        class_col: 一维数组或series,代表真实的标签({0,1}或{-1,1})
        输出值:
        'ks': KS值
        '''
        fpr,tpr,threshold = roc_curve((1-data[class_col[0]]).ravel(),data[score_col[0]].ravel())
        ks = max(tpr-fpr)
        return ks
    

    案例测试

    模拟数据data_test_1(数据中不含有NAN)
    data_test_1 = {'y30':[1,1,1,1,1,1,0,0,0,0,0,0],'a':[1,2,4,2,2,6,5,3,0,5,4,18]}
    data_test_1 = pd.DataFrame(data_test_4)
    

    计算结果:

    ks_2samp,cdf_2samp = ks_calc_2samp(data_test_1, ['a'], ['y30'])
    ks_2samp
    cdf_2samp
    
    Out[7]: 0.5
    Out[8]: 
         cdf_Bad  cdf_Good       gap
    0   0.166667  0.166667  0.000000
    1   0.666667  0.166667  0.500000
    2   0.666667  0.166667  0.500000
    3   0.666667  0.166667  0.500000
    4   0.833333  0.500000  0.333333
    5   1.000000  0.833333  0.166667
    6   0.000000  0.166667 -0.166667
    7   0.666667  0.333333  0.333333
    8   0.833333  0.500000  0.333333
    9   0.833333  0.833333  0.000000
    10  0.833333  0.833333  0.000000
    11  1.000000  1.000000  0.000000
    
    ks_cross,cdf_cross = ks_calc_cross(data_test_1, ['a'], ['y30'])
    ks_cross
    cdf_cross
    
    Out[10]: 0.5
    Out[11]: 
    y30         0         1       gap
    a                                
    0    0.166667  0.000000  0.166667
    1    0.166667  0.166667  0.000000
    2    0.166667  0.666667  0.500000
    3    0.333333  0.666667  0.333333
    4    0.500000  0.833333  0.333333
    5    0.833333  0.833333  0.000000
    6    0.833333  1.000000  0.166667
    18   1.000000  1.000000  0.000000
    
    
    ks_auc = ks_calc_auc(data_test_1, ['a'], ['y30'])
    ks_auc
    
    Out[12]: 0.5
    

    三种方法计算得到的ks值均相同,且ks_calc_cross和ks_calc_2samp计算得到的cdf相同

    模拟数据data_test_2(数据中含有NAN)
    data_test_2 = {'y30':[1,1,1,1,1,1,0,0,0,0,0,0,0],'a':[1,2,0,2,2,7,4,5,4,0,4,18,np.nan]}
    data_test_2 = pd.DataFrame(data_test_2)
    

    计算结果:

    ks_2samp,cdf_2samp = ks_calc_2samp(data_test_2, ['a'], ['y30'])
    ks_2samp
    Out[15]: 0.69047619047619047
    
    cdf_2samp
    Out[16]: 
         cdf_Bad  cdf_Good       gap
    0   0.166667  0.142857  0.023810
    1   0.333333  0.142857  0.190476
    2   0.833333  0.142857  0.690476
    3   0.833333  0.142857  0.690476
    4   0.833333  0.142857  0.690476
    5   1.000000  0.714286  0.285714
    6   0.166667  0.142857  0.023810
    7   0.833333  0.571429  0.261905
    8   0.833333  0.571429  0.261905
    9   0.833333  0.571429  0.261905
    10  0.833333  0.714286  0.119048
    11  1.000000  0.857143  0.142857
    12  1.000000  1.000000  0.000000
    
    
    ks_cross,cdf_cross = ks_calc_cross(data_test_2, ['a'], ['y30'])
    ks_cross
    Out[18]: 
    y30         0         1       gap
    a                                
    2.0  0.166667  0.833333  0.666667
    
    cdf_cross
    Out[19]: 
    y30          0         1       gap
    a                                 
    0.0   0.166667  0.166667  0.000000
    1.0   0.166667  0.333333  0.166667
    2.0   0.166667  0.833333  0.666667
    4.0   0.666667  0.833333  0.166667
    5.0   0.833333  0.833333  0.000000
    7.0   0.833333  1.000000  0.166667
    18.0  1.000000  1.000000  0.000000
    
    ks_auc = ks_calc_auc(data_test_2, ['a'], ['y30'])
    ks_auc
    
    Traceback (most recent call last):
    ValueError: Input contains NaN, infinity or a value too large for dtype('float64').
    

    三种方法计算得到的ks值均不相同。

    • 其中ks_calc_2samp计算得到的ks因为searchsorted()函数(有兴趣的同学可以自己模拟数据看下这个函数),会将Nan值默认排序为最大值,从而改变了数据的原始累积分布概率,导致计算得到的ks和真实的ks有误差。
    • 其中ks_calc_cross计算时忽略了NAN,计算得到了数据正确的概率分布,计算的ks与我们手算的ks相同
    • ks_calc_auc函数由于内置函数无法处理NAN值,直接报错了,所以如果需要ks_calc_auc计算ks值时,需要提前去除NAN值。

    总结

    在实际情况下,我们一般计算违约概率的ks值,这时是不存在NAN值的。所以以上三种方法计算ks值均可。但是当我们计算单变量的ks值时,有时数据质量不好,存在NAN值时,继续采用ks_calc_auc和ks_calc_2samp就会存在问题。

    解决办法有两个 1. 提前去除数据中的NAN值 2. 直接采用ks_calc_cross计算。

    如果关于这篇文章有什么问题,请联系京东金融-金融科技业务部-数据与金融市场研发部-白云飞知乎号
    @白云飞Harry

    相关文章

      网友评论

        本文标题:风控模型中的K-S理解以及python实现

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