1. 程式人生 > >【有監督分箱】方法二: Best-KS分箱

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

銜接上一篇工作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 >= piece: break if len(date_df) > 0: #這裡有個疑問,我感覺有問題 #這裡為啥要獲取第一條資料,total的數量 length1 = date_df.loc[0,total_name] if length1 >= rate*total_all: if 0 not in t_list: t_list.append(0) else: t_list.remove(0) t_list.sort() return t_list def get_combine(t_list, date_df, piece): ''' :param t_list: 這個值分箱間隔點 :param date_df: 資料來源 :param piece: 分箱的箱數,表示第幾箱。 :return: 列舉所有的分箱可能組合 ''' t1 = 0 t2 = len(date_df)-1 list0 = t_list[1:len(t_list)-1] combine = [] if len(t_list)-2 < piece: c = len(t_list)-2 else: c = piece-1 #獲取list0的所有子序列。子序列長度是c list1 = list(itertools.combinations(list0, c)) if list1: #向list1收尾新增資料,頭部新增t1-1,尾部新增t2 combine = map(lambda x: sorted(x + (t1-1,t2)),list1) return combine def cal_iv(date_df,items,bad_name,good_name,total_name): ''' :param date_df: :param items: :param bad_name: :param good_name: :param total_name: :return: 返回計算的IV值 ''' iv0 = 0 bad0 = np.array(map(lambda x: sum(date_df.ix[x[0]:x[1],bad_name]),items)) good0 = np.array(map(lambda x: sum(date_df.ix[x[0]:x[1],good_name]),items)) bad_rate0 = np.array(map(lambda x: sum(date_df.ix[x[0]:x[1],bad_name])*1.0/sum(date_df.ix[x[0]:x[1],total_name]),items)) if 0 in bad0: return iv0 if 0 in good0: return iv0 good_per0 = good0*1.0/sum(date_df[good_name]) bad_per0 = bad0*1.0/sum(date_df[bad_name]) woe0 = map(lambda x: math.log(x,math.e),good_per0/bad_per0) if sorted(woe0, reverse=False) == list(woe0) and sorted(bad_rate0, reverse=True) == list(bad_rate0): iv0 = sum(woe0*(good_per0-bad_per0)) elif sorted(woe0, reverse=True) == list(woe0) and sorted(bad_rate0, reverse=False) == list(bad_rate0): iv0 = sum(woe0*(good_per0-bad_per0)) return iv0 def choose_best_combine(date_df,combine,bad_name,good_name,total_name): ''' :param date_df: 資料來源 :param combine: 所有的分箱可能 :param bad_name: :param good_name: :param total_name: :return: 通過最大IV值,來得到最優的分箱方法 ''' z = [0]*len(combine) for i in range(len(combine)): item = combine[i] z[i] = (zip(map(lambda x: x+1,item[0:len(item)-1]),item[1:])) #計算最大的IV值 iv_list = map(lambda x: cal_iv(date_df,x,bad_name,good_name,total_name),z) iv_max = max(iv_list) if iv_max == 0: return '' index_max = iv_list.index(iv_max) combine_max = z[index_max] #返回最好的分箱組合 #[(0, 180), (181, 268), (269, 348), (349, 450), (451, 605)] 類似於這種資料 return combine_max def verify_woe(x): if re.match('^\d*\.?\d+$', str(x)): return x else: return 0 def best_df(date_df, items, na_df, rate, factor_name, total_name, bad_name, good_name,total_all,good_all,bad_all): ''' :param date_df: :param items: 分箱間隔,陣列[(0, 180), (181, 268), (269, 348), (349, 450), (451, 605)] :param na_df: :param rate: :param factor_name: :param total_name: :param bad_name: :param good_name: :param total_all: :param good_all: :param bad_all: :return:分箱之後的指標儲存為dataframe,並返回。 ''' df0 = pd.DataFrame() if items: piece0 = map(lambda x: '['+str(date_df.ix[x[0],factor_name])+','+str(date_df.ix[x[1],factor_name])+']',items) bad0 = map(lambda x: sum(date_df.ix[x[0]:x[1],bad_name]),items) good0 = map(lambda x: sum(date_df.ix[x[0]:x[1],good_name]),items) if len(na_df) > 0: piece0 = np.array(list(piece0) + map(lambda x: '['+str(x)+','+str(x)+']',list(na_df[factor_name]))) bad0 = np.array(list(bad0) + list(na_df[bad_name])) good0 = np.array(list(good0) + list(na_df[good_name])) else: piece0 = np.array(list(piece0)) bad0 = np.array(list(bad0)) good0 = np.array(list(good0)) #bad0,good0都是list資料結構 total0 = bad0 + good0 #計算每一個箱子的總數量佔比 total_per0 = total0*1.0/total_all #當前箱子的黑名單比例 bad_rate0 = bad0*1.0/total0 #當前箱子的白名單比例 good_rate0 = 1 - bad_rate0 #當前箱子的白名單在整體白名單資料的比例 good_per0 = good0*1.0/good_all #當前箱子黑名單在在整體黑名單資料的比例 bad_per0 = bad0*1.0/bad_all #先將這些資料儲存為數框 df0 = pd.DataFrame(zip(piece0,total0,bad0,good0,total_per0,bad_rate0,good_rate0,good_per0,bad_per0),columns=['Bin','Total_Num','Bad_Num','Good_Num','Total_Pcnt','Bad_Rate','Good_Rate','Good_Pcnt','Bad_Pcnt']) #通過bad_rate進行排序 df0 = df0.sort_values(by='Bad_Rate',ascending=False) df0.index = range(len(df0)) bad_per0 = np.arr