衔接上一篇工作:https://blog.csdn.net/hxcaifly/article/details/80203663
变量的KS值
KS(Kolmogorov-Smirnov)用于模型风险区分能力进行评估,指标衡量的是好坏样本累计部分之间的差距 。KS值越大,表示该变量越能将正,负客户的区分程度越大。通常来说,KS>0.2即表示特征有较好的准确率。强调一下,这
里的KS值是变量的KS值,而不是模型的KS值。(后面的模型评估里会重点讲解模型的KS值)。
KS的计算方式:
- 计算每个评分区间的好坏账户数。
- 计算各每个评分区间的累计好账户数占总好账户数比率(good%)和累计坏账户数占总坏账户数比率(bad%)。
- 计算每个评分区间累计坏账户比与累计好账户占比差的绝对值(累计good%-累计bad%),然后对这些绝对值取最大值记得到KS值。
Best-KS分箱
Best-KS分箱的算法执行过程是一个逐步拆分的过程:
- 将特征值值进行从小到大的排序。
- 计算出KS最大的那个值,即为切点,记为D。然后把数据切分成两部分。
- 重复步骤2,进行递归,D左右的数据进一步切割。直到KS的箱体数达到我们的预设阈值即可。
Best-KS分箱的特点: - 连续型变量:分箱后的KS值<=分箱前的KS值
- 分箱过程中,决定分箱后的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 >=<