【有监督分箱】方法二: Best-KS分箱

本文介绍了有监督分箱中的Best-KS方法,详细阐述了KS值在模型风险区分能力评估中的作用,以及KS值的计算方式。Best-KS分箱通过逐步拆分过程寻找最佳切点,保证分箱后KS值不超过分箱前。文章还提供了整体代码实现。
摘要由CSDN通过智能技术生成

衔接上一篇工作:https://blog.csdn.net/hxcaifly/article/details/80203663

变量的KS值

KS(Kolmogorov-Smirnov)用于模型风险区分能力进行评估,指标衡量的是好坏样本累计部分之间的差距 。KS值越大,表示该变量越能将正,负客户的区分程度越大。通常来说,KS>0.2即表示特征有较好的准确率。强调一下,这
里的KS值是变量的KS值,而不是模型的KS值。(后面的模型评估里会重点讲解模型的KS值)。
KS的计算方式:

  1. 计算每个评分区间的好坏账户数。
  2. 计算各每个评分区间的累计好账户数占总好账户数比率(good%)和累计坏账户数占总坏账户数比率(bad%)。
  3. 计算每个评分区间累计坏账户比与累计好账户占比差的绝对值(累计good%-累计bad%),然后对这些绝对值取最大值记得到KS值。

Best-KS分箱

Best-KS分箱的算法执行过程是一个逐步拆分的过程:

  1. 将特征值值进行从小到大的排序。
  2. 计算出KS最大的那个值,即为切点,记为D。然后把数据切分成两部分。
  3. 重复步骤2,进行递归,D左右的数据进一步切割。直到KS的箱体数达到我们的预设阈值即可。
    Best-KS分箱的特点:
  4. 连续型变量:分箱后的KS值<=分箱前的KS值
  5. 分箱过程中,决定分箱后的KS值是某一个切点,而不是多个切点的共同作用。这个切点的位置是原始KS值最大的位置。

整体代码

# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
#import missingno as msno
plt.style.use('fivethirtyeight')
import warnings
import datetime
warnings.filterwarnings('ignore')
#%matplotlib inline
#from tqdm import tqdm

import re
import math
import time
import itertools
import random

from logging import Logger
from logging.handlers import TimedRotatingFileHandler
import os

#######################################################KS分箱的主体逻辑##############################################
def init_logger(logger_name,logging_path):
    if not os.path.exists(logging_path):
        os.makedirs(logging_path)
    if logger_name not in Logger.manager.loggerDict:
        logger  = logging.getLogger(logger_name)
        logger.setLevel(logging.DEBUG)
        handler = TimedRotatingFileHandler(filename=logging_path+"/%sAll.log"%logger_name,when='D',backupCount = 7)
        datefmt = '%Y-%m-%d %H:%M:%S'
        format_str = '[%(asctime)s]: %(name)s %(filename)s[line:%(lineno)s] %(levelname)s  %(message)s'
        formatter = logging.Formatter(format_str,datefmt)
        handler.setFormatter(formatter)
        handler.setLevel(logging.INFO)
        logger.addHandler(handler)
        console= logging.StreamHandler()
        console.setLevel(logging.INFO)
        console.setFormatter(formatter)
        logger.addHandler(console)
        handler = TimedRotatingFileHandler(filename=logging_path+"/%sError.log"%logger_name,when='D',backupCount=7)
        datefmt = '%Y-%m-%d %H:%M:%S'
        format_str = '[%(asctime)s]: %(name)s %(filename)s[line:%(lineno)s] %(levelname)s  %(message)s'
        formatter = logging.Formatter(format_str,datefmt)
        handler.setFormatter(formatter)
        handler.setLevel(logging.ERROR)
        logger.addHandler(handler)
    logger = logging.getLogger(logger_name)
    return logger

def get_max_ks(date_df, start, end, rate, factor_name, bad_name, good_name, total_name,total_all):
    '''
    计算最大的ks值
    :param date_df: 数据源
    :param start: 第一条数据的index
    :param end: 最后一条数据的index
    :param rate:
    :param factor_name:
    :param bad_name:
    :param good_name:
    :param total_name:
    :param total_all:
    :return:最大ks值切点的index
    '''
    ks = ''
    #获取黑名单数据
    bad = date_df.loc[start:end,bad_name]
    #获取白名单数据
    good = date_df.loc[start:end,good_name]

   #np.cumsum累加。计算黑白的数量占比,累计差
    bad_good_cum = list(abs(np.cumsum(bad/sum(bad)) - np.cumsum(good/sum(good))))  
    if bad_good_cum:
        #找到最大的ks
        max_ks = max(bad_good_cum)
        #找到最大ks的切点index。
        index_max = bad_good_cum.index(max_ks)
        t = start + index_max
        len1 = sum(date_df.loc[start:t,total_name])
        len2 = sum(date_df.loc[t+1:end,total_name])
        #这个就是rate起的效果,一旦按照最大ks切点切割数据,要保证两边的数据量都不能小于一个阈值
        if len1 >= rate*total_all:
            if len2 >= rate*total_all:
                ks = t
    #如果分割之后,任意一部分数据的数量小于rate这个阈值,那么ks就返回为空了。
    return ks

def cut_fun(x,date_df,types,rate,factor_name,bad_name,good_name,total_name,total_all):
    '''

    :param x: List,就是保存了date_df的第一条index和最后一条index的List。
    :param date_df: 数据源
    :param types: 不知道是什么意思
    :param rate: rate的含义也是一直不清楚
    :param factor_name: 待分箱的特征字段
    :param bad_name:
    :param good_name:
    :param total_name:
    :param total_all:
    :return: 数据的start index,切点index,end index。
    '''
    if types == 'upper':
        #起始从date_df的第一条开始
        start = x[0]
    else:
        start = x[0]+1
    #结束时date_df的最后一条
    end = x[1]
    t = ''
    #很明显start != end,所以就执行这个函数体
    if start != end:
        #计算得到最大ks切点index的值,并且把值存入t。
        t = get_max_ks(date_df,start,end,rate,factor_name,bad_name,good_name,total_name,total_all)
    if t:
        #把t存入x。
        x.append(t)
        #这个时候x存着[start,切点,end]
        x.sort()
    if t == 0:
        x.append(t)
        x.sort()

    return x

def cut_while_fun(t_list,date_df,rate,factor_name,bad_name,good_name,total_name,total_all):
    '''

    :param t_list: start_index,分箱切点 ,end_index
    :param date_df:
    :param rate:
    :param factor_name:
    :param bad_name:
    :param good_name:
    :param total_name:
    :param total_all:
    :return:
    '''
    if len(t_list) != 2:
        #切点左边数据
        t_up = [t_list[0],t_list[1]]
        #切点右边数据
        t_down = [t_list[1],t_list[2]]

        #递归对左边数据进行切割
        if t_list[1]-t_list[0] > 1 and sum(date_df.loc[t_up[0]:t_up[1],total_name]) >= rate * sum(date_df[total_name]):

            t_up = cut_fun(t_up,date_df,'upper',rate,factor_name,good_name,bad_name,total_name,total_all)
        else:
            t_up = []

        #递归对右边数据进行切割
        if t_list[2]-t_list[1] > 1 and sum(date_df.loc[t_down[0]+1:t_down[1],total_name]) >= rate * sum(date_df[total_name]):
            t_down = cut_fun(t_down,date_df,'down',rate,factor_name,good_name,bad_name,total_name,total_all)
        else:
            t_down = []
    else:
        t_up = []
        t_down = []
    return t_up,t_down

def ks_auto(date_df,piece,rate,factor_name,bad_name,good_name,total_name,total_all):
    '''
    :param date_df: 数据源
    :param piece: 分箱数目
    :param rate: 最小数量占比,就是把数据通过切点分成两半部分之后,要保证两部分的数量都必须不能小于这个占比rate。
    :param factor_name: 待分箱的特征名称
    :param bad_name: 黑名单特征名称
    :param good_name: 白名单特征名称
    :param total_name: 总和的特诊名称
    :param total_all: 总共数据量
    :return: 返回整个分箱的间隔点,用List保存。这里是以date_df的index为分割点的。
    '''
    t1 = 0
    #数据源的大小,条数
    t2 = len(date_df)-1
    num = len(date_df)
    #还不知道这样做的目的是什么。
    if num > pow(2,piece-1):
        num = pow(2,piece-1)

    #新定义一个list,这个list是什么含义
    t_list = [t1,t2]
    tt =[]
    i = 1
    #如果数据源的条数大于1,就表示有分箱的资格
    if len(date_df) > 1:
        #这个是为了获取date_df数据的[start_index,切点_index, end_index]
        #将数据根据ks最大处进行二分
        t_list = cut_fun(t_list,date_df,'upper',rate,factor_name,bad_name,good_name,total_name,total_all)
        tt.append(t_list)
        for t_new in tt:
            #>2说明,分箱是成功的。
            if len(t_new) > 2:
                #
                up_down = cut_while_fun(t_new,date_df,rate,factor_name,bad_name,good_name,total_name,total_all)
                t_up = up_down[0]
                if len(t_up) > 2:
                    #
                    t_list = list(set(t_list+t_up))
                    tt.append(t_up)
                t_down = up_down[1]
                if len(t_down) > 2:
                    t_list = list(set(t_list+t_down))
                    tt.append(t_down)
                i += 1
                #注意循环的停止条件
                #1. i表示通过箱数限制break
                #2. len(t_list)还不是很清楚
                if len(t_list)-1 > num:
                    break
                if i >=<
  • 9
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值