【特征⼯程】卡⽅分箱原理和python代码(附带训练数据和测试结果),适合
⼆分类和多分类
背景介绍
本⽂针对有⼀定基础的数据分析⼈员,专门想了解卡⽅分箱原理和寻能直接运⾏的代码的⼈员。
分箱是特征⼯程中常见的操作,也就是将某⼀个变量划分为多个区间,⽐如对年龄分箱,1-10岁,10-40岁,40+岁。卡⽅分箱就是⽤来寻最优分割点的⽅法。
本⽂介绍了卡⽅分箱原理、python代码、使⽤数据集(有数据集构造代码)测试分箱效果⼏个部分。
注:这⾥保证代码肯定可以直接运⾏,并附上了检验分箱原理的代码。如果有注释不清楚的,欢迎⼀起讨论。
卡⽅分箱原理
卡⽅分箱是⾃底向上的(即基于合并的)数据离散化⽅法。它依赖于卡⽅检验:具有最⼩卡⽅值的相邻区间合并在⼀起,直到满⾜确定的停⽌准则。
基本思想:对于精确的离散化,相对类频率在⼀个区间内应当完全⼀致。因此,如果两个相邻的区间具有⾮常类似的类分布,则这两个区间可以合并;否则,它们应当保持分开。⽽低卡⽅值表明它们具有相似的类分布。
卡⽅检验可以⽤来评估两个分布的相似性,因此可以将这个特性⽤到数据分箱的过程中。
理想的分箱是在同⼀个区间内标签的分布是相同的。卡⽅分箱就是不断的计算相邻区间的卡⽅值(卡⽅值越⼩表⽰分布越相似),将分布相似的区间(卡⽅值最⼩的)进⾏合并,直到相邻区间的分布不同,达到⼀个理想的分箱结果。
下⾯⽤⼀个例⼦来解释:
由上图,第⼀轮中初始化是5个区间,分别计算相邻区间的卡⽅值。到1.2是最⼩的,合并2、3区间,为了⽅便,将合并后的记为第2区间,因此得到4个区间。第⼆轮中,由于合并了区间,影响该区
间与前⾯的和后⾯的区间的卡⽅值,因此重新计算1和2,2和4的卡⽅值,由于4和5区间没有影响,因此不需要重新计算,这样就得到了新的卡⽅值列表,到最⼩的取值2.5,因此该轮会合并2、4区间,并重复这样的步骤,⼀直到满⾜终⽌条件。
终⽌条件⼀般有两个,后⾯我的代码⾥的pvalue,smallest,biggest都是终⽌条件:
1. 卡⽅值,设置相邻区间的最⼩卡⽅值。这⾥需要提到⼀点,计算出卡⽅值需要查询卡⽅表,会得到⼀个置信度,即pvalue。我的程序
使⽤的scipy中直接计算置信度的⼯具scipy.stats.chi2_contingency,直接计算pvalue,因此设置最⼩卡⽅值也就是设置最⼤pvalue。如果不清楚pvalue和卡⽅值关系的⼩伙伴请看⼀下卡⽅检验(我可能表述的不清楚,但欢迎我讨论)。
如果着急的⼩伙伴看⼀下这张表。标红的是pvalue,下⾯的是卡⽅值。基本规律就是卡⽅值越⼤,pvalue越⼩。可以先这么记忆,然后事后再查⼀下两者原理上的关系。
2. 分箱个数,⼀般会设置最⼤分箱数和最⼩分箱数。最⼤分箱数防⽌算法过早停⽌;最⼩分箱数防⽌算法⽆穷尽的合并,最后合并成⼀
个区间。
另⼀个问题你可能问了,你直接把相邻区间的卡⽅值写上了,怎么算的呢?
我举⼀个例⼦,就那1,2区间来说
标签在每个区间的分布如下
然后使⽤卡⽅值计算公式就可以计算了,我就不写具体怎么算了,公式⾮常简单,毕竟⼤家过来应该都是想看⼀下代码能不能运⾏的,我别把⼤家的忍耐⼒给耗光了,不多说了,下⾯是代码。
python代码
def tagcount(series,tags):
"""
统计该series中不同标签的数量,可以针对多分类
series:只含有标签的series
tags:为标签的列表,以实际为准,⽐如[0,1],[1,2,3]
"""
result =[]
countseries = series.value_counts()
for tag in tags:
try:
result.append(countseries[tag])
except:
result.append(0)
return result
def ChiMerge3(df, num_split,tags=[1,2,3],pvalue_edge=0.1,biggest=10,smallest=3,sample=None):
"""
df:只包含要分箱的参数列和标签两列
num_split:初始化时划分的区间个数,适合数据量特别⼤的时候。
tags:标签列表,⼆分类⼀般为[0,1]。以实际为准。
pvalue_edge:pvalue的置信度值
bin:最多箱的数⽬
smallest:最少箱的数⽬
sample:抽样的数⽬,适合数据量超级⼤的情况。可以使⽤抽样的数据进⾏分箱。百万以下不需要
"""
import pandas as pd
import numpy as np
import scipy
variable = df.columns[0]
flag = df.columns[1]
#进⾏是否抽样操作
if sample !=None:
df = df.sample(n=sample)
else:
df
#将原始序列初始化为num_split个区间,计算每个区间中每类别的数量,放置在⼀个矩阵中。⽅便后⾯计算pvalue值。
percent = df[variable].quantile([1.0*i/num_split for i in range(num_split+1)],interpolation="lower").drop_duplicates(keep="last").tolist() percent = percent[1:]
np_regroup =[]
for i in range(len(percent)):
if i ==0:
tempdata = tagcount(df[df[variable]<=percent[i]][flag],tags)
tempdata.insert(0,percent[i])
elif i ==len(percent)-1:
tempdata = tagcount(df[df[variable]>percent[i-1]][flag],tags)
tempdata.insert(0,percent[i])
else:
tempdata = tagcount(df[(df[variable]>percent[i-1])&(df[variable]<=percent[i])][flag],tags)
tempdata.insert(0,percent[i])
np_regroup.append(tempdata)
np_regroup = pd.DataFrame(np_regroup)
np_regroup = np.array(np_regroup)
#如果两个区间某⼀类的值都为0,就会报错。先将这类的区间合并,当做预处理吧
i =0
while(i <= np_regroup.shape[0]-2):
check =0
for j in range(len(tags)):
if np_regroup[i,j+1]==0and np_regroup[i+1,j+1]==0:
check +=1
"""
这个for循环是为了检查是否有某⼀个或多个标签在两个区间内都是0,如果是的话,就进⾏下⾯的合并。
"""
if check>0:
np_regroup[i,1:]= np_regroup[i,1:]+ np_regroup[i+1,1:]
np_regroup[i,0]= np_regroup[i +1,0]
np_regroup = np.delete(np_regroup, i +1,0)
i = i -1
i = i +1
#对相邻两个区间进⾏置信度计算
chi_table = np.array([])
for i in np.arange(np_regroup.shape[0]-1):
temparray = np_regroup[i:i+2,1:]
pvalue = scipy.stats.chi2_contingency(temparray,correction=False)[1]
chi_table = np.append(chi_table, pvalue)
temp =max(chi_table)
#把pvalue最⼤的两个区间进⾏合并。注意的是,这⾥并没有合并⼀次就重新循环计算相邻区间的pvalue,⽽是只更新影响到的区间。while(1):
while(1):
#终⽌条件,可以根据⾃⼰的期望定制化
if(len(chi_table)<=(biggest -1)and temp <= pvalue_edge):
break
if len(chi_table)<smallest:
break
num = np.argwhere(chi_table==temp)
for i in range(num.shape[0]-1,-1,-1):
chi_min_index = num[i][0]
np_regroup[chi_min_index,1:]= np_regroup[chi_min_index,1:]+ np_regroup[chi_min_index +1,1:] np_regroup[chi_min_index,0]= np_regroup[chi_min_index +1,0]
np_regroup = np.delete(np_regroup, chi_min_index +1,0)
#最⼤pvalue在最后两个区间的时候,只需要更新⼀个,删除最后⼀个。⼤家可以画图,很容易明⽩
if(chi_min_index == np_regroup.shape[0]-1):
temparray = np_regroup[chi_min_index-1:chi_min_index+1,1:]
chi_table[chi_min_index -1]= scipy.stats.chi2_contingency(temparray,correction=False)[1]
chi_table = np.delete(chi_table, chi_min_index, axis=0)
#最⼤pvalue是最先两个区间的时候,只需要更新⼀个,删除第⼀个。
elif(chi_min_index ==0):
temparray = np_regroup[chi_min_index:chi_min_index+2,1:]
chi_table[chi_min_index]= scipy.stats.chi2_contingency(temparray,correction=False)[1]
chi_table = np.delete(chi_table, chi_min_index+1, axis=0)
#最⼤pvalue在中间的时候,影响和前后区间的pvalue,需要更新两个值。
else:
# 计算合并后当前区间与前⼀个区间的pvalue替换
temparray = np_regroup[chi_min_index-1:chi_min_index+1,1:]
chi_table[chi_min_index -1]= scipy.stats.chi2_contingency(temparray,correction=False)[1] # 计算合并后当前与后⼀个区间的pvalue替换
temparray = np_regroup[chi_min_index:chi_min_index+2,1:]
chi_table[chi_min_index]= scipy.stats.chi2_contingency(temparray,correction=False)[1] # 删除替换前的pvalue
chi_table = np.delete(chi_table, chi_min_index +1, axis=0)
#更新当前最⼤的相邻区间的pvalue
temp =max(chi_table)
print("*"*40)
print("最终相邻区间的pvalue值为:")
print(chi_table)
print("*"*40)
#把结果保存成⼀个数据框。
"""
可以根据⾃⼰的需求定制化。我保留两个结果。
1. 显⽰分割区间,和该区间内不同标签的数量的表
2. 为了⽅便pandas对该参数处理,把apply的具体命令打印出来。⽅便直接对数据集处理。
serise.apply(lambda x:XXX)中XXX的位置
"""
#将结果整合到⼀个表中,即上述中的第⼀个
interval =[]
interval_num = np_regroup.shape[0]
for i in range(interval_num):
if i ==0:
interval.append('x<=%f'%(np_regroup[i,0]))
elif i == interval_num-1:
interval.append('x>%f'%(np_regroup[i-1,0]))
else:
interval.append('x>%f and x<=%f'%(np_regroup[i-1,0],np_regroup[i,0]))
result = pd.DataFrame(np_regroup)
result[0]= interval
#整理series的命令,即上述中的第⼆个
premise ="str(0) if "
length_interval =len(interval)
for i in range(length_interval):
if i == length_interval-1:
premise = premise[:-4]
break
premise = premise + interval[i]+" else "+'str(%d+1)'%i +" if "
return result,premise
验证分箱结果
为了⽅便观察,我以⼆分类为例⼦进⾏展⽰。
⾸先我们先构造数据集,数据集的⽬的很明显,针对不同的x取值区间,y=1的概率不同。我们就是验证卡⽅分箱是否能到这个规律。#构造⼀个有40000数据量的数据
num =10000
x1 = np.random.randint(1,10,(1,num))
x2 = np.random.randint(10,30,(1,num))
x3 = np.random.randint(30,45,(1,num))
x4 = np.random.randint(45,80,(1,num))
x =list(x1[0])+list(x2[0])+list(x3[0])+list(x4[0])
y1 =[0for i in range(int(num*0.9))]+[1for i in range(int(num*0.1))]
y2 =[0for i in range(int(num*0.7))]+[1for i in range(int(num*0.3))]
y3 =[0for i in range(int(num*0.5))]+[1for i in range(int(num*0.5))]
y4 =[0for i in range(int(num*0.3))]+[1for i in range(int(num*0.7))]
y = y1+y2+y3+y4
testdata = pd.DataFrame({"x":x,"y":y})variable used in lambda
#打乱顺序,其实没必要,分箱的时候会重新对x进⾏排序
testdata = testdata.sample(frac=1)
数据集中只有⼀个变量x和标签y。我们对不同x的取值下y=1的概率(也就是取值为1的个数占总个数的⽐值)
画图结果符合我们构造的数据集的规律,卡⽅分箱的结果预期结果⼤概是如下⼏个分割点[10,30,45]。让我们来看⼀下结果吧。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论