特征工程之分箱--卡方分箱
1.定義
分箱就是將連續(xù)變量離散化,將多狀態(tài)的離散變量合并成少狀態(tài)。
2.分箱的用處
離散特征的增加和減少都很容易,易于模型的快速迭代;
稀疏向量內(nèi)積乘法運(yùn)算速度快,計(jì)算結(jié)果方便存儲(chǔ),容易擴(kuò)展;
列表內(nèi)容離散化后的特征對(duì)異常數(shù)據(jù)有很強(qiáng)的魯棒性:比如一個(gè)特征是年齡>30是1,否則0。如果特征沒有離散化,一個(gè)異常數(shù)據(jù)“年齡300歲”會(huì)給模型造成很大的干擾;
列表內(nèi)容邏輯回歸屬于廣義線性模型,表達(dá)能力受限;單變量離散化為N個(gè)后,每個(gè)變量有單獨(dú)的權(quán)重,相當(dāng)于為模型引入了非線性,能夠提升模型表達(dá)能力,加大擬合;
離散化后可以進(jìn)行特征交叉,由M+N個(gè)變量變?yōu)镸*N個(gè)變量,進(jìn)一步引入非線性,提升表達(dá)能力;
列表內(nèi)容特征離散化后,模型會(huì)更穩(wěn)定,比如如果對(duì)用戶年齡離散化,20-30作為一個(gè)區(qū)間,不會(huì)因?yàn)橐粋€(gè)用戶年齡長了一歲就變成一個(gè)完全不同的人。當(dāng)然處于區(qū)間相鄰處的樣本會(huì)剛好相反,所以怎么劃分區(qū)間
是門學(xué)問;
特征離散化以后,起到了簡化了邏輯回歸模型的作用,降低了模型過擬合的風(fēng)險(xiǎn)。 可以將缺失作為獨(dú)立的一類帶入模型。
將所有變量變換到相似的尺度上。
3.分箱方法
分箱方法分為無監(jiān)督分箱和有監(jiān)督分箱。
1)常用的無監(jiān)督分箱方法有等頻分箱,等距分箱和聚類分箱。
2)有監(jiān)督分箱主要有best-ks分箱和卡方分箱。基于我的項(xiàng)目中重點(diǎn)應(yīng)用了卡方分箱,所以這里重點(diǎn)對(duì)卡方分箱做些總結(jié)。
4.卡方分箱的原理
卡方分箱是自底向上的(即基于合并的)數(shù)據(jù)離散化方法。
它依賴于卡方檢驗(yàn):具有最小卡方值的相鄰區(qū)間合并在一起,直到滿足確定的停止準(zhǔn)則。
基本思想:對(duì)于精確的離散化,相對(duì)類頻率在一個(gè)區(qū)間內(nèi)應(yīng)當(dāng)完全一致。
因此,如果兩個(gè)相鄰的區(qū)間具有非常類似的類分布,則這兩個(gè)區(qū)間可以合并;
否則,它們應(yīng)當(dāng)保持分開。而低卡方值表明它們具有相似的類分布。
分箱步驟:
這里需要注意初始化時(shí)需要對(duì)實(shí)例進(jìn)行排序,在排序的基礎(chǔ)上進(jìn)行合并。
卡方閾值的確定:
根據(jù)顯著性水平和自由度得到卡方值
自由度比類別數(shù)量小1。例如:有3類,自由度為2,則90%置信度(10%顯著性水平)下,卡方的值為4.6。
閾值的意義
類別和屬性獨(dú)立時(shí),有90%的可能性,計(jì)算得到的卡方值會(huì)小于4.6。
大于閾值4.6的卡方值就說明屬性和類不是相互獨(dú)立的,不能合并。如果閾值選的大,區(qū)間合并就會(huì)進(jìn)行很多次,離散后的區(qū)間數(shù)量少、區(qū)間大。
注:
1,ChiMerge算法推薦使用0.90、0.95、0.99置信度,最大區(qū)間數(shù)取10到15之間.
2,也可以不考慮卡方閾值,此時(shí)可以考慮最小區(qū)間數(shù)或者最大區(qū)間數(shù)。指定區(qū)間數(shù)量的上限和下限,最多幾個(gè)區(qū)間,最少幾個(gè)區(qū)間。
3,對(duì)于類別型變量,需要分箱時(shí)需要按照某種方式進(jìn)行排序。
5.分完箱之后評(píng)估指標(biāo)
分為箱之后,需要評(píng)估。在積分卡模型中,最常用的評(píng)估手段是計(jì)算出WOE和IV值。
https://www.cnblogs.com/wqbin/p/10547628.html
6.分箱
def Chi2(df, total_col, bad_col,overallRate):
'''
#此函數(shù)計(jì)算卡方值
:df dataFrame
:total_col 每個(gè)值得總數(shù)量
:bad_col 每個(gè)值的壞數(shù)據(jù)數(shù)量
:overallRate 壞數(shù)據(jù)的占比
: return 卡方值
'''
df2=df.copy()
df2['expected']=df[total_col].apply(lambda x: x*overallRate)
combined=list(zip(df2['expected'], df2[bad_col]))
chi=[(i[0]-i[1])**2/i[0] for i in combined]
chi2=sum(chi)
return chi2
#基于卡方閾值卡方分箱,有個(gè)缺點(diǎn),不好控制分箱個(gè)數(shù)。
def ChiMerge_MinChisq(df, col, target, confidenceVal=3.841):
'''
#此函數(shù)是以卡方閾值作為終止條件進(jìn)行分箱
: df dataFrame
: col 被分箱的特征
: target 目標(biāo)值,是0,1格式
: confidenceVal 閾值,自由度為1, 自信度為0.95時(shí),卡方閾值為3.841
: return 分箱。
這里有個(gè)問題,卡方分箱對(duì)分箱的數(shù)量沒有限制,這樣子會(huì)導(dǎo)致最后分箱的結(jié)果是分箱太細(xì)。
'''
#對(duì)待分箱特征值進(jìn)行去重
colLevels=set(df[col])
#count是求得數(shù)據(jù)條數(shù)
total=df.groupby([col])[target].count()
total=pd.DataFrame({'total':total})
#sum是求得特征值的和
#注意這里的target必須是0,1。要不然這樣求bad的數(shù)據(jù)條數(shù),就沒有意義,并且bad是1,good是0。
bad=df.groupby([col])[target].sum()
bad=pd.DataFrame({'bad':bad})
#對(duì)數(shù)據(jù)進(jìn)行合并,求出col,每個(gè)值的出現(xiàn)次數(shù)(total,bad)
regroup=total.merge(bad, left_index=True, right_index=True, how='left')
regroup.reset_index(level=0, inplace=True)
#求出整的數(shù)據(jù)條數(shù)
N=sum(regroup['total'])
#求出黑名單的數(shù)據(jù)條數(shù)
B=sum(regroup['bad'])
overallRate=B*1.0/N
#對(duì)待分箱的特征值進(jìn)行排序
colLevels=sorted(list(colLevels))
groupIntervals=[[i] for i in colLevels]
groupNum=len(groupIntervals)
while(1):
if len(groupIntervals) == 1:
break
chisqList=[]
for interval in groupIntervals:
df2=regroup.loc[regroup[col].isin(interval)]
chisq=Chi2(df2, 'total', 'bad', overallRate)
chisqList.append(chisq)
min_position=chisqList.index(min(chisqList))
if min(chisqList) >= confidenceVal:
break
if min_position==0:
combinedPosition=1
elif min_position== groupNum-1:
combinedPosition=min_position-1
else:
if chisqList[min_position-1]<=chisqList[min_position + 1]:
combinedPosition=min_position-1
else:
combinedPosition=min_position+1
groupIntervals[min_position]=groupIntervals[min_position]+groupIntervals[combinedPosition]
groupIntervals.remove(groupIntervals[combinedPosition])
groupNum=len(groupIntervals)
return groupIntervals
#最大分箱數(shù)分箱
def ChiMerge_MaxInterval_Original(df, col, target,max_interval=5):
'''
: df dataframe
: col 要被分項(xiàng)的特征
: target 目標(biāo)值 0,1 值
: max_interval 最大箱數(shù)
:return 箱體
'''
colLevels=set(df[col])
colLevels=sorted(list(colLevels))
N_distinct=len(colLevels)
if N_distinct <= max_interval:
print("the row is cann't be less than interval numbers")
return colLevels[:-1]
else:
total=df.groupby([col])[target].count()
total=pd.DataFrame({'total':total})
bad=df.groupby([col])[target].sum()
bad=pd.DataFrame({'bad':bad})
regroup=total.merge(bad, left_index=True, right_index=True, how='left')
regroup.reset_index(level=0, inplace=True)
N=sum(regroup['total'])
B=sum(regroup['bad'])
overallRate=B*1.0/N
groupIntervals=[[i] for i in colLevels]
groupNum=len(groupIntervals)
while(len(groupIntervals)>max_interval):
chisqList=[]
for interval in groupIntervals:
df2=regroup.loc[regroup[col].isin(interval)]
chisq=Chi2(df2,'total','bad',overallRate)
chisqList.append(chisq)
min_position=chisqList.index(min(chisqList))
if min_position==0:
combinedPosition=1
elif min_position==groupNum-1:
combinedPosition=min_position-1
else:
if chisqList[min_position-1]<=chisqList[min_position + 1]:
combinedPosition=min_position-1
else:
combinedPosition=min_position+1
#合并箱體
groupIntervals[min_position]=groupIntervals[min_position]+groupIntervals[combinedPosition]
groupIntervals.remove(groupIntervals[combinedPosition])
groupNum=len(groupIntervals)
groupIntervals=[sorted(i) for i in groupIntervals]
print(groupIntervals)
cutOffPoints=[i[-1] for i in groupIntervals[:-1]]
return cutOffPoints
#計(jì)算WOE和IV值
def CalcWOE(df,col, target):
'''
: df dataframe
: col 注意這列已經(jīng)分過箱了,現(xiàn)在計(jì)算每箱的WOE和總的IV
:target 目標(biāo)列 0-1值
:return 返回每箱的WOE和總的IV
'''
total=df.groupby([col])[target].count()
total=pd.DataFrame({'total':total})
bad=df.groupby([col])[target].sum()
bad=pd.DataFrame({'bad':bad})
regroup=total.merge(bad, left_index=True, right_index=True, how='left')
regroup.reset_index(level=0, inplace=True)
N=sum(regroup['total'])
B=sum(regroup['bad'])
regroup['good']=regroup['total']-regroup['bad']
G=N-B
regroup['bad_pcnt']=regroup['bad'].map(lambda x: x*1.0/B)
regroup['good_pcnt']=regroup['good'].map(lambda x: x*1.0/G)
regroup['WOE']=regroup.apply(lambda x: np.log(x.good_pcnt*1.0/x.bad_pcnt),axis=1)
WOE_dict=regroup[[col,'WOE']].set_index(col).to_dict(orient='index')
IV=regroup.apply(lambda x:(x.good_pcnt-x.bad_pcnt)*np.log(x.good_pcnt*1.0/x.bad_pcnt),axis=1)
IV_SUM=sum(IV)
return {'WOE':WOE_dict,'IV_sum':IV_SUM,'IV':IV}
#分箱以后檢查每箱的bad_rate的單調(diào)性,如果不滿足,那么繼續(xù)進(jìn)行相鄰的兩項(xiàng)合并,直到bad_rate單調(diào)為止
def BadRateMonotone(df, sortByVar, target):
#df[sortByVar]這列已經(jīng)經(jīng)過分箱
df2=df.sort_values(by=[sortByVar])
total=df2.groupby([sortByVar])[target].count()
total=pd.DataFrame({'total':total})
bad=df2.groupby([sortByVar])[target].sum()
bad=pd.DataFrame({'bad':bad})
regroup=total.merge(bad, left_index=True, right_index=True, how='left')
regroup.reset_index(level=0, inplace=True)
combined=list(zip(regroup['total'], regroup['bad']))
badRate=[x[1]*1.0/x[0] for x in combined]
badRateMonotone=[badRate[i]<badRate[i+1] for i in range(len(badRate)-1)]
Monotone = len(set(badRateMonotone))
if Monotone==1:
return True
else:
return False
#檢查最大箱,如果最大箱里面數(shù)據(jù)數(shù)量占總數(shù)據(jù)的90%以上,那么棄用這個(gè)變量
def MaximumBinPcnt(df, col):
N=df.shape[0]
total=df.groupby([col])[col].count()
pcnt=total*1.0/N
return max(pcnt)
#對(duì)于類別型數(shù)據(jù),以bad_rate代替原有值,轉(zhuǎn)化成連續(xù)變量再進(jìn)行分箱計(jì)算。比如我們這里的戶籍地代碼,就是這種數(shù)據(jù)格式
#當(dāng)然如果類別較少時(shí),原則上不需要分箱
def BadRateEncoding(df, col, target):
'''
: df DataFrame
: col 需要編碼成bad rate的特征列
:target值,0-1值
: return: the assigned bad rate
'''
total=df.groupby([col])[target].count()
total=pd.DataFrame({'total':total})
bad=df.groupby([col])[target].sum()
bad=pd.DataFrame({'bad':bad})
regroup=total.merge(bad, left_index=True, right_index=True, how='left')
regroup.reset_index(level=0, inplace=True)
regroup['bad_rate']=regroup.apply(lambda x: x.bad*1.0/x.total, axis=1)
br_dict=regroup[[col,'bad_rate']].set_index([col]).to_dict(orient='index')
badRateEnconding=df[col].map(lambda x: br_dict[x]['bad_rate'])
return {'encoding':badRateEnconding,'br_rate':br_dict}
View Code
7.自動(dòng)化分箱
在工程中,考慮到能夠自動(dòng)化對(duì)數(shù)據(jù)里所有需要分箱的連續(xù)變量進(jìn)行分箱,所以在工程上需要做些處理,需要寫個(gè)自動(dòng)化分箱腳本:
class Woe_IV:
def __init__(self,df,colList,target):
'''
:param df: 這個(gè)是用來分箱的dataframe
:param colList: 這個(gè)分箱的列數(shù)據(jù),數(shù)據(jù)結(jié)構(gòu)是一個(gè)字段數(shù)組
例如colList=[
{
'col':'openning_room_num_n3'
'bandCol':'openning_room_num_n3_band',
'bandNum':6,
‘toCsvPath':'/home/liuweitang/yellow_model/data/mk/my.txt'
},
]
:param target 目標(biāo)列0-1值,1表示bad,0表示good
'''
self.df=df
self.colList=colList
self.target=target
def to_band(self):
for i in range(len(self.colList)):
colParam=self.colList[i]
#計(jì)算出箱體分別值,返回的是一個(gè)長度為5數(shù)組[0,4,13,45,78]或者長度為6的數(shù)組[0,2,4,56,67,89]
cutOffPoints=ChiMerge_MaxInterval_Original(self.df,colParam['col'],self.target,colParam['bandNum'])
print(cutOffPoints)
indexValue=0
value_band=[]
#那么cutOffPoints第一個(gè)值就是作為一個(gè)獨(dú)立的箱
if len(cutOffPoints) == colParam['bandNum']-1:
print('len-1 type')
for i in range(0,len(cutOffPoints)):
if i==0:
self.df.loc[self.df[colParam['col']]<=cutOffPoints[i], colParam['bandCol']]=indexValue
indexValue+=1
value_band.append('0-'+str(cutOffPoints[i]))
if 0<i<len(cutOffPoints):
self.df.loc[(self.df[colParam['col']] > cutOffPoints[i - 1]) & (self.df[colParam['col']] <= cutOffPoints[i]), colParam['bandCol']] = indexValue
indexValue+=1
value_band.append(str(cutOffPoints[i - 1]+1)+"-"+str(cutOffPoints[i]))
if i==len(cutOffPoints)-1:
self.df.loc[self.df[colParam['col']] > cutOffPoints[i], colParam['bandCol']] = indexValue
value_band.append(str(cutOffPoints[i]+1)+"-")
#那么就是直接分割分箱,
if len(cutOffPoints)==colParam['bandNum']:
print('len type')
for i in range(0,len(cutOffPoints)):
if 0< i < len(cutOffPoints):
self.df.loc[(self.df[colParam['col']] > cutOffPoints[i - 1]) & (self.df[colParam['col']] <= cutOffPoints[i]), colParam['bandCol']] = indexValue
value_band.append(str(cutOffPoints[i - 1]+1)+"-"+str(cutOffPoints[i]))
indexValue += 1
if i == len(cutOffPoints)-1:
self.df.loc[self.df[colParam['col']] > cutOffPoints[i], colParam['bandCol']] = indexValue
value_band.append(str(cutOffPoints[i]+1)+"-")
self.df[colParam['bandCol']].astype(int)
#到此分箱結(jié)束,下面判斷單調(diào)性
isMonotone = BadRateMonotone(self.df,colParam['bandCol'], self.target)
#如果不單調(diào),那就打印出錯(cuò)誤,并且繼續(xù)執(zhí)行下一個(gè)特征分箱
if isMonotone==False:
print(colParam['col']+' band error, reason is not monotone')
continue
#單調(diào)性判斷完之后,就要計(jì)算woe_IV值
woe_IV=CalcWOE(self.df, colParam['bandCol'],self.target)
woe=woe_IV['WOE']
woe_result=[]
for i in range(len(woe)):
woe_result.append(woe[i]['WOE'])
iv=woe_IV['IV']
iv_result=[]
for i in range(len(iv)):
iv_result.append(iv[i])
good_bad_count=self.df.groupby([colParam['bandCol'],self.target]).label.count()
good_count=[]
bad_count=[]
for i in range(0,colParam['bandNum']):
good_count.append(good_bad_count[i][0])
bad_count.append(good_bad_count[i][1])
print(value_band)
print(good_count)
print(bad_count)
print(woe_result)
print(iv_result)
#將WOE_IV值保存為dataframe格式數(shù)據(jù),然后導(dǎo)出到csv
#這里其實(shí)還有個(gè)問題,就是
woe_iv_df=pd.DataFrame({
'IV':iv_result,
'WOE':woe_result,
'bad':bad_count,
'good':good_count,
colParam['bandCol']:value_band
})
bad_good_count=self.df.groupby([colParam['bandCol'],self.target])[self.target].count();
woe_iv_df.to_csv(colParam['toCsvPath'])
print(colParam['col']+'band finished')
View Code
應(yīng)用方便 :
openning_data=pd.read_csv('***',sep='$')
colList=[
{
'col':'openning_room_0_6_num_n3',
'bandCol':'openning_room_0_6_num_n3_band',
'bandNum':5,
'toCsvPath':'/home/liuweitang/yellow_model/eda/band_result/openning_room_0_6_num_n3_band.csv'
},
{
'col':'openning_room_6_12_num_n3',
'bandCol':'openning_room_6_12_num_n3_band',
'bandNum':5,
'toCsvPath':'/home/liuweitang/yellow_model/eda/band_result/openning_room_6_12_num_n3_band.csv'
}
]
band2=Woe_IV(openning_data,colList,'label')
band2.to_band()
View Code
8.分箱后處理
分箱后需要編碼
dummy
one-hot
label-encode
9.注意問題
對(duì)于分箱需要注意的是,分完箱之后,某些箱區(qū)間里,bad或者good分布比例極不均勻,極端時(shí),會(huì)出現(xiàn)bad或者good數(shù)量直接為0。那么這樣子會(huì)直接導(dǎo)致后續(xù)計(jì)算WOE時(shí)出現(xiàn)inf無窮大的情況,這是不合理的。這種情況,說明分箱太細(xì),需要進(jìn)一步縮小分箱的數(shù)量。
總結(jié)
以上是生活随笔為你收集整理的特征工程之分箱--卡方分箱的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 李峰叫李婷吃香肠第几页
- 下一篇: 一深一浅验孕棒是什么意思(怀孕一周怎么处