在做風(fēng)控策略分析時(shí)雄妥,我們經(jīng)常要對(duì)某個(gè)變量進(jìn)行離散化赵讯,查看不同階段的好壞用戶分布情況巡揍,好的離散化方法可以讓我們找到比較好的策略分界點(diǎn)镊辕。
本片文章主要講述卡方分箱
一油够、理論
基本思想:卡方分箱是依賴于 卡方檢驗(yàn)的分箱方法,在統(tǒng)計(jì)指標(biāo)上選擇卡方統(tǒng)計(jì)量(chi-Square)進(jìn)行判別征懈∈В卡方分箱的基本思想是判斷相鄰的兩個(gè)區(qū)間是否有分布差異,如果兩個(gè)相鄰的區(qū)間具有非常類似的分布卖哎,則這兩個(gè)區(qū)間可以合并鬼悠;否則,它們應(yīng)當(dāng)保持分開亏娜』牢眩基于卡方統(tǒng)計(jì)量的結(jié)果進(jìn)行自下而上的合并,直到滿足分箱的限制條件為止维贺。
卡方分箱的實(shí)現(xiàn)步驟:
1. 預(yù)先設(shè)定一個(gè)卡方的閾值或者分箱個(gè)數(shù)的閾值它掂。
在做分箱處理時(shí)可以使用兩種限制條件:
分箱個(gè)數(shù):限制最終的分箱個(gè)數(shù)結(jié)果,每次將樣本中具有最小卡方值的區(qū)間與相鄰的最小卡方區(qū)間進(jìn)行合并溯泣,直到分箱個(gè)數(shù)達(dá)到限制條件為止虐秋。
卡方閾值:根據(jù)自由度和顯著性水平得到對(duì)應(yīng)的卡方閾值,如果分箱的各區(qū)間最小卡方值小于卡方閾值垃沦,則繼續(xù)合并客给,直到最小卡方值超過設(shè)定閾值為止。
通過顯著性水平和自由度計(jì)算出這個(gè)閾值栏尚,然后數(shù)據(jù)的卡方值與這個(gè)閾值進(jìn)行比較起愈,如果卡方值大于閾值只恨,就可以推翻原假設(shè)(兩個(gè)相鄰區(qū)間的分布無差異);如果卡方值小于閾值,則不能推翻原假設(shè)(兩個(gè)相鄰區(qū)間的分布無差異),即可合并抬虽。
顯著性水平官觅,當(dāng)置信度90%時(shí)顯著性水平為10%,ChiMerge算法推薦使用置信度為0.90阐污、0.95休涤、0.99。
自由度笛辟,比分類數(shù)量小1功氨。例如:有3類,自由度為2。
類別和屬性獨(dú)立時(shí),有90%的可能性,計(jì)算得到的卡方值會(huì)小于4.6(在excel中用CHIINV(0.1,2)算出)手幢。大于閾值4.6的卡方值就說明屬性和類不是相互獨(dú)立的捷凄,不能合并。如果閾值選的大,區(qū)間合并就會(huì)進(jìn)行很多次,離散后的區(qū)間數(shù)量少围来、區(qū)間大跺涤。
2. 初始化:根據(jù)要離散化的數(shù)據(jù)對(duì)實(shí)例進(jìn)行排序,每個(gè)實(shí)例屬于一個(gè)區(qū)間
3. 合并區(qū)間:
計(jì)算每一對(duì)相鄰區(qū)間的卡方值
將卡方值最小的一對(duì)區(qū)間合并(合并需要符合以下兩個(gè)條件之一)
4.評(píng)估指標(biāo)
分完箱之后需要評(píng)估监透,常用的評(píng)估手段是計(jì)算出WOE和IV值桶错。對(duì)于WOE和IV值的含義,看 數(shù)據(jù)挖掘模型中的IV和WOE詳解
分箱的注意點(diǎn)
對(duì)于連續(xù)型變量胀蛮,
? 使用ChiMerge進(jìn)行分箱(默認(rèn)分成5個(gè)箱)
? 檢查分箱后的bad rate單調(diào)性院刁;倘若不滿足,需要進(jìn)行相鄰兩箱的合并粪狼,直到bad rate為止
? 上述過程是收斂的退腥,因?yàn)楫?dāng)箱數(shù)為2時(shí),bad rate自然單調(diào)
? 分箱必須覆蓋所有訓(xùn)練樣本外可能存在的值鸳玩!
? 原始值很多時(shí)阅虫,為了減小時(shí)間的開銷,通常選取較少(例如50個(gè))初始切分點(diǎn)不跟。但是要注意分布不均勻!
對(duì)于類別型變量米碰,
? 當(dāng)類別數(shù)較少時(shí)窝革,原則上不需要分箱
? 當(dāng)某個(gè)或者幾個(gè)類別的bad rate為0時(shí),需要和最小的非0 的bad rate的箱進(jìn)行合并
? 當(dāng)該變量可以完全區(qū)分目標(biāo)變量時(shí)吕座,需要認(rèn)真檢查該變量的合理性
要求分箱完之后:
(1)不超過5箱
(2)Bad Rate單調(diào)
(3)每箱同時(shí)包含好壞樣本
(4)特殊值如-1虐译,單獨(dú)成一箱
連續(xù)型變量可直接分箱
類別型變量:
(a)當(dāng)取值較多時(shí),先用bad rate編碼吴趴,再用連續(xù)型分箱的方式進(jìn)行分箱
(b)當(dāng)取值較少時(shí):
(b1)如果每種類別同時(shí)包含好壞樣本漆诽,無需分箱
(b2)如果有類別只包含好壞樣本的一種,需要合并
二、代碼
2.1 R包--discretization
discretization包厢拭,是一個(gè)用來做有監(jiān)督離散化的工具集兰英,主要用于卡方分箱算法,它提供了幾種常用的離散化工具函數(shù)供鸠,可以按照自上而下或自下而上畦贸,實(shí)施離散化算法。
項(xiàng)目主頁: https://cran.r-project.org/web/packages/discretization/
提供了幾個(gè)主要的離散化的工具函數(shù):
chiM楞捂,ChiM算法進(jìn)行離散化
chi2, Chi2算法進(jìn)行離散化薄坏,在chiM的基礎(chǔ)上進(jìn)行優(yōu)化
mdlp琐谤,最小描述長度原理(MDLP)進(jìn)行離散化
modChi2戴卜,改進(jìn)的Chi2方法離散數(shù)值屬性
disc.Topdown,自上而下的離散化
extendChi2叔壤,擴(kuò)展Chi2算法離散數(shù)值屬性
chiM算法進(jìn)行離散化(根據(jù)卡方閾值來設(shè)定合并停止條件)
ChiM()函數(shù)繁堡,使用ChiMerge算法基于卡方檢驗(yàn)進(jìn)行自下而上的合并涵但。通過卡方檢驗(yàn)判斷相鄰閾值的相對(duì)類頻率,是否有明顯不同帖蔓,或者它們是否足夠相似矮瘟,從而合并為一個(gè)區(qū)間。
chiM(data,alpha)函數(shù)解讀塑娇。
* 第一個(gè)參數(shù)data澈侠,是輸入數(shù)據(jù)集,要求最后一列是分類屬性埋酬。
* 第二個(gè)參數(shù)alpha哨啃,表示顯著性水平。
* 自由度写妥,通過數(shù)據(jù)計(jì)算獲得是2拳球,一共3個(gè)分類減去1。
2.2 自定義函數(shù)ChiMerge
#初始化劃分
SplitData <- function(df,col,numOfSplit,special_attribute=NULL){
library(dplyr)
#當(dāng)連續(xù)變量的初始取值集合太多時(shí)(>100),我們先對(duì)其進(jìn)行初步劃分
#:param df: 按照col排序后的數(shù)據(jù)集
#:param col: 待分箱的變量
#:param numOfSplit: 切分的組別數(shù)
#:param special_attribute: 在切分?jǐn)?shù)據(jù)集的時(shí)候珍特,某些特殊值需要排除在外
#:return: 在原數(shù)據(jù)集上增加一列祝峻,把原始細(xì)粒度的col重新劃分成粗粒度的值,便于分箱中的合并處理
df2 <- df
if(length(special_attribute)>0){
df2 <- filter(df,! col %in% special_attribute)
}
N <- dim(df2)[1] #行數(shù)
n <- floor(N/numOfSplit) #每組樣本數(shù)
splitPointIndex <- seq(1,numOfSplit-1,1)*n #分割點(diǎn)的下標(biāo)
rawValues <- sort(df2[,col]) #對(duì)取值進(jìn)行升序排序
splitPoint <- rep(0,length(rawValues))
for(i in splitPointIndex){
splitPoint[i] <- rawValues[i] #分割點(diǎn)的取值
}
splitPoint <- sort(unique(splitPoint)) #對(duì)取值進(jìn)行升序排序
if(splitPoint[1]==0){
splitPoint<- splitPoint[-1]
}
return(splitPoint)
}
#計(jì)算每個(gè)值的好壞比率
BinBadRate <- function(df,col,target,grantRateIndicator=0){
library(dplyr)
#:param df:需要計(jì)算好壞比率的數(shù)據(jù)集
#:param col:需要計(jì)算好壞比率的特征
#:param target:好壞標(biāo)簽
#:param grantRateIndicator:1返回總體的壞樣本率扎筒,0不返回
#:return:每箱的壞樣本率以及總體的壞樣本率(當(dāng)grantRateIndicator=1時(shí))
#total <- df %>% group_by(col) %>% summarise(total=n())
#bad <- df %>% group_by(col) %>% summarise(bad=sum(target))
total <- data.frame(table(df[,col]))
names(total) <- c(col,'total')
bad <- data.frame(tapply(df[,target],df[,col],FUN = sum))
bad$bad <- row.names(bad)
names(bad) <- c('bad',col)
regroup <- left_join(total,bad,by=col)
#regroup$bad_rate <- bad/total
regroup <- mutate(regroup,bad_rate = bad/total)
dicts <- regroup[,'bad_rate'] #每箱對(duì)應(yīng)的壞樣本率組成的向量
names(dicts) <- regroup[,col]
if(grantRateIndicator==0){
return(list(dicts,regroup))
}else{
N =sum(regroup[,'total'])
B = sum(regroup[,'bad'])
overallRate = B*1.0/N
return(list(dicts,regroup,overallRate))
}
}
#計(jì)算卡方值
Chi2 <- function(df,total_col,bad_col){
library(dplyr)
df2 <- df
# 求出df中莱找,總體的壞樣本率和好樣本率
badRate <- sum(df2[,bad_col])/sum(df2[,total_col])
# 當(dāng)全部樣本只有好或者壞樣本時(shí),卡方值為0
if(badRate %in% c(0,1)){
return(0)
}
good=df2[,total_col]-df2[,bad_col]
df2 <- cbind(df2,good)
goodRate = sum(df2[,'good'])/ sum(df2[,total_col])
# 期望壞(好)樣本個(gè)數(shù)=全部樣本個(gè)數(shù)*平均壞(好)樣本占比
badExpected=df2[,total_col]*badRate
goodExpected=df2[,total_col]*goodRate
df2 <- cbind(df2,badExpected)
df2 <- cbind(df2,goodExpected)
badChi <- sum(((df2[,bad_col]-df2[,'badExpected'])^2)/df2[,'badExpected'])
goodChi <- sum(((df2[,'good']-df2[,'goodExpected'])^2)/df2[,'goodExpected'])
chi2 <- badChi+goodChi
return(chi2)
}
AssignBin <- function(x,cutOffPoints,special_attribute=NULL){
# :param x: 某個(gè)變量的某個(gè)取值
# :param cutOffPoints:上述變量的分箱結(jié)果嗜桌,用切分點(diǎn)表示
# :param special_attribute:不參與分箱的特殊取值
# :return:分箱后的對(duì)應(yīng)的第幾個(gè)箱奥溺,從0開始
# for example, if cutOffPoints = c(10,20,30), if x = 7, return Bin 0. If x = 35, return Bin 3
numBin = length(cutOffPoints)+1+length(special_attribute)
if(x %in% special_attribute){
i <- which(special_attribute==x)
return(paste('Bin',0-i))
}
if(x<= cutOffPoints[1]){
return('Bin 0')
}else if(x>cutOffPoints[length(cutOffPoints)]){
return(paste("Bin",numBin-1))
}else{
for(i in seq(1,numBin-1)){
if(cutOffPoints[i] < x & x<= cutOffPoints[i+1]){
return(paste('Bin',i))
}
}
}
}
AssignGroup <- function(x,bin){
# '
# :param x:某個(gè)變量的某個(gè)取值
# :param bin:上述變量的分箱結(jié)果
# :return:x在分箱結(jié)果下的映射
#
N = length(bin)
if(x<=min(bin)){
return(min(bin))
}else if(x>max(bin)){
return(10e10)
}else{
for(i in 1:N-1){
if(bin[i]<x && x<=bin[i+1]){
return(bin[i+1])
}
}
}
}
ChiMerge <- function(df,col,target,max_interval=5,special_attribute=NULL,minBinPcnt=0,numOfSplit=100){
# '''
# 通過指定最大分箱數(shù),使用卡方值分割連續(xù)變量
# :param df:包含目標(biāo)變量和分箱變量的數(shù)據(jù)框
# :param target:目標(biāo)變量骨宠,取值0或1
# :param col:需要分箱的變量
# :param max_interval:最大分箱數(shù)浮定,如果原始變量的取值個(gè)數(shù)低于該參數(shù)相满,不執(zhí)行這個(gè)函數(shù)
# :param special_attribute:不參與分箱的變量取值,注意:必須是向量形式
# :param minBinPcnt:最小箱的占比,默認(rèn)為0
# :param numOfSplit:當(dāng)連續(xù)變量的初始取值集合太多時(shí)(>100),我們先對(duì)其進(jìn)行初步劃分桦卒,切分的組別數(shù)
# :return :分箱結(jié)果
# '''
library(dplyr)
colLevels=sort(unique(df[,col])) #升序排序變量值
N_distinct = length(colLevels) #不同取值的個(gè)數(shù)
if(N_distinct<=max_interval){ #如果原始變量的取值個(gè)數(shù)低于max_interval,不執(zhí)行這個(gè)函數(shù)
print(paste(col,'變量的取值個(gè)數(shù)低于設(shè)定的最大分箱數(shù)max_interval:',max_interval))
return(colLevels[-length(colLevels)]) #去掉最后一個(gè)值
}else{
if(length(special_attribute)>=1){
df1 <- filter(df,col %in% special_attribute)
df2 <- filter(df,!col %in% special_attribute)
}else{
df2 <- df
}
N_distinct <- length(unique(df2[,col])) #該變量的不同取值個(gè)數(shù)
#步驟一:通過col對(duì)數(shù)據(jù)集進(jìn)行分組立美,求出每組的總樣本數(shù)和壞樣本數(shù)
if(N_distinct>numOfSplit){
split_x <- SplitData(df2,col,numOfSplit)
#temp <- cut(df2[,col],breaks = split_x,include.lowest = TRUE)
temp <- apply(df2[col],1,AssignGroup,split_x)
df2 <- cbind(df2,temp)
}else{
temp <- df2[,col]
df2 <- cbind(df2,temp)
}
#總體bad rate將被用來計(jì)算expected bad count
ha <- BinBadRate(df2,'temp',target)
regroup <- ha[[2]]
binBadRate<- ha[[1]]
#首先,每個(gè)單獨(dú)的屬性值將被分為單獨(dú)的一組
#對(duì)屬性值進(jìn)行排序闸盔,然后兩兩組別進(jìn)行合并
colLevels<- sort(unique(df2[,'temp']))
groupIntervals <- list()
for(i in 1:length(colLevels)){
groupIntervals[i] <-list(colLevels[i])
}
# #步驟二悯辙,建立循環(huán),不斷合并最優(yōu)的相鄰的兩個(gè)組別迎吵,直到:
# #1.最終分裂出來的分箱數(shù)<=預(yù)設(shè)的最大分箱數(shù)
# #2.每箱的占比不低于預(yù)設(shè)值(可選)
# #3.每箱同時(shí)包含好壞樣本
# #如果有特殊屬性躲撰,那么最終分裂出來的分箱數(shù)=預(yù)設(shè)的最大分箱數(shù)-特殊屬性的個(gè)數(shù)
split_intervals= max_interval-length(special_attribute)
while(length(groupIntervals)>=split_intervals){ #終止條件
#每次循環(huán)時(shí),計(jì)算合并相鄰組別后的卡方值击费。具有最小卡方值值的合并方案拢蛋,是最優(yōu)方案
chisqList <- rep(100000000,length(groupIntervals)-1)
for(k in 1:(length(groupIntervals)-1)){
temp_group <- c(groupIntervals[[k]],groupIntervals[[k+1]])
df2b <- filter(regroup, temp %in% temp_group)
chisq = Chi2(df2b,'total','bad')
chisqList[k] <- chisq
}
best_combined <- order(chisqList)[1] #找到最小值的位置
#合并
groupIntervals[[best_combined]] = c(groupIntervals[[best_combined]],groupIntervals[[best_combined+1]])
# after combining two intervals, we need to remove one of them
groupIntervals[[best_combined+1]] <- NULL
}
for(i in 1:length(groupIntervals)){
groupIntervals[[i]]<- sort(groupIntervals[[i]])
}
cutOffPoints <- rep(0,length(groupIntervals)-1)
for(i in 1:(length(groupIntervals)-1)){
cutOffPoints[i] <- max(groupIntervals[[i]])
}
# 檢查是否有箱沒有好或者壞樣本。如果有蔫巩,需要跟相鄰的箱進(jìn)行合并谆棱,直到每箱同時(shí)包含好壞樣本
groupedvalues <- apply(df2['temp'],1,AssignBin,cutOffPoints,special_attribute)
temp_Bin<-groupedvalues
df2 <- cbind(df2,temp_Bin)
#返回(每箱壞樣本率列表,和包含“列名圆仔、壞樣本數(shù)垃瞧、總樣本數(shù)、壞樣本率的數(shù)據(jù)框”)
middle <- BinBadRate(df2,'temp_Bin',target)
binBadRate <- middle[[1]]
regroup <- middle[[2]]
minBadRate <- min(binBadRate)
maxBadRate <- max(binBadRate)
while(minBadRate ==0 || maxBadRate == 1){
# 找出全部為好/壞樣本的箱
indexForBad01 <- filter(regroup,bad_rate %in% c(0,1))[,'temp_Bin']
bin <- indexForBad01[1]
return(bin)
# 如果是最后一箱坪郭,則需要和上一個(gè)箱進(jìn)行合并个从,也就意味著分裂點(diǎn)cutOffPoints中的最后一個(gè)需要移除
if(bin==max(regroup[,'temp_Bin'])){
cutOffPoints <- cutOffPoints[1:length(cutOffPoints)-1]
}else if(bin == min(regroup[,'temp_Bin'])){
# 如果是第一箱,則需要和下一個(gè)箱進(jìn)行合并歪沃,也就意味著分裂點(diǎn)cutOffPoints中的第一個(gè)需要移除
cutOffPoints[1] <- NULL
}else{
# 如果是中間的某一箱嗦锐,則需要和前后中的一個(gè)箱進(jìn)行合并,依據(jù)是較小的卡方值
# 和前一箱進(jìn)行合并沪曙,并且計(jì)算卡方值
currentIndex <- which(regroup[,'temp_Bin']==bin)
prevIndex <- regroup[,'temp_Bin'][currentIndex - 1]
df3 <- filter(df2,temp_Bin %in% c(prevIndex,bin))
middle <- BinBadRate(df3, 'temp_Bin', target)
binBadRate <- middle[[1]]
df2b <- middle[[2]]
chisq1 = Chi2(df2b, 'total', 'bad')
# 和后一箱進(jìn)行合并奕污,并且計(jì)算卡方值
laterIndex <- regroup[,'temp_Bin'][currentIndex + 1]
df3b <- filter(df2,temp_Bin %in% c(prevIndex,bin))
middle <- BinBadRate(df3b, 'temp_Bin', target)
binBadRate <- middle[[1]]
df2b <- middle[[2]]
chisq2 = Chi2(df2b, 'total', 'bad')
if(chisq1 < chisq2){
cutOffPoints[currentIndex - 1] <- NULL
}else{cutOffPoints[currentIndex] <- NULL}
}
# 完成合并之后,需要再次計(jì)算新的分箱準(zhǔn)則下液走,每箱是否同時(shí)包含好壞樣本
groupedvalues <- apply(df2['temp'],1,AssignBin,cutOffPoints,special_attribute)
temp_Bin = groupedvalues
df2 <- cbind(df2,temp_Bin)
middle <- BinBadRate(df2, 'temp_Bin', target)
binBadRate <- middle[[1]]
regroup <- middle[[2]]
minBadRate <- min(binBadRate)
maxBadRate <- maxmax(binBadRate)
}
if(minBinPcnt > 0){
groupedvalues <- apply(df2['temp'],1,AssignBin,cutOffPoints,special_attribute)
temp_Bin = groupedvalues
df2 <- cbind(df2,temp_Bin)
valueCounts <- data.frame(table(groupedvalues))
names(valueCounts)[2] <- 'temp'
pcnt=valueCounts[,'temp']/sum(valueCounts[,'temp'])
valueCounts <- cbind(valueCounts,pcnt)
valueCounts <- arrange(valueCounts,Var1)
minPcnt = min(valueCounts[,'pcnt'])
while(minPcnt < minBinPcnt & len(cutOffPoints) > 2){
# 找出占比最小的箱
indexForMinPcnt = filter(valueCounts,valueCounts[,'pcnt'] == minPcnt)[,'var1'][1]
# 如果占比最小的箱是最后一箱碳默,則需要和上一個(gè)箱進(jìn)行合并,也就意味著分裂點(diǎn)cutOffPoints中的最后一個(gè)需要移除
if(indexForMinPcnt==max(valueCounts[,'var1'])){
cutOffPoints[length(cutOffPoints)] <- NULL
}else if(indexForMinPcnt==min(valueCounts[,'var1'])){
# 如果占比最小的箱是第一箱育灸,則需要和下一個(gè)箱進(jìn)行合并腻窒,也就意味著分裂點(diǎn)cutOffPoints中的第一個(gè)需要移除
cutOffPoints[1] <- NULL
}else{
# 如果占比最小的箱是中間的某一箱,則需要和前后中的一個(gè)箱進(jìn)行合并磅崭,依據(jù)是較小的卡方值
# 和前一箱進(jìn)行合并,并且計(jì)算卡方值
currentIndex <- which(valueCounts[,'pcnt']==indexForMinPcnt)
prevIndex <- valueCounts[,'var1'][currentIndex-1]
df3 <- filter(df2,var1 %in% c(prevIndex, indexForMinPcnt))
middle <- BinBadRate(df3, 'temp_Bin', target)
binBadRate <- middle[[1]]
df2b <- middle[[2]]
chisq1 = Chi2(df2b, 'total', 'bad')
# 和后一箱進(jìn)行合并瓦哎,并且計(jì)算卡方值
laterIndex <- valueCounts[,'var1'][currentIndex-1]
df3b <- filter(df2,temp_Bin %in% c(laterIndex, indexForMinPcnt))
middle <- BinBadRate(df3b, 'temp_Bin', target)
binBadRate <- middle[[1]]
df2b <- middle[[2]]
chisq2 = Chi2(df2b, 'total', 'bad')
if(chisq1<chisq2){
cutOffPoints[currentIndex - 1] <- NULL
}else{cutOffPoints[currentIndex] <- NULL}
}
groupedvalues <- apply(df2['temp'],1,AssignBin,cutOffPoints,special_attribute)
temp_Bin = groupedvalues
df2 <- cbind(df2,temp_Bin)
valueCounts <- data.frame(table(groupedvalues))
names(valueCounts)[2] <- 'temp'
pcnt=valueCounts[,'temp']/sum(valueCounts[,'temp'])
valueCounts <- cbind(valueCounts,pcnt)
valueCounts <- arrange(valueCounts,Var1)
minPcnt = min(valueCounts[,'pcnt'])
}
}
cutOffPoints = c(special_attribute , cutOffPoints)
return(cutOffPoints)
}
}
2. 3自定義并行化分箱函數(shù)
有時(shí)候數(shù)據(jù)量大的時(shí)候卡方分箱的計(jì)算大會(huì)導(dǎo)致運(yùn)行速度慢砸喻,所以我們可以合理利用我們電腦的多核