构建机器学习模型时(尤其是逻辑回归),一般都会对连续型特征进行特征分箱,特征分箱优点有:
1、分箱后的特征对异常数据有更强的鲁棒性。如入模特征异常大时,分箱后只是作为一个类别入模,如果将值直接入模,则会对模型造成较大的干扰。
2、可以处理缺失值,将缺失值直接作为一个类别放入模型当中。
3、特征离散化后可以起到简化模型的作用,降低模型的过拟合作用。
4、特征离散化之后,每个变量有单独的权重,可以为逻辑回归模型引入了非线性,能够提升模型表达能力,加大拟合。
如何对连续型入模特征进行分箱
1、无监督中的等频、等距分箱
2、有监督的分箱
代码事例
# 计算卡方值函数 def chi3(arr): ''' 计算卡方值 arr:频数统计表,二维numpy数组。 ''' assert(arr.ndim==2) #计算每行总频数 R_N = arr.sum(axis=1) #每列总频数 C_N = arr.sum(axis=0) #总频数 N = arr.sum() # 计算期望频数 C_i * R_j / N。 E = np.ones(arr.shape)* C_N / N E = (E.T * R_N).T square = (arr-E)**2 / E #期望频数为0时,做除数没有意义,不计入卡方值 square[E==0] = 0 #卡方值 v = square.sum() return v # 确定卡方分箱点 def chiMerge(df,col,target,max_groups=None,threshold=None): ''' 卡方分箱 df: pandas dataframe数据集 col: 需要分箱的变量名(数值型) target: 类标签 max_groups: 最大分组数。 threshold: 卡方阈值,如果未指定max_groups,默认使用置信度95%设置threshold。 return: 包括各组的起始值的列表. ''' freq_tab = pd.crosstab(df[col],df[target]) #转成numpy数组用于计算。 freq = freq_tab.values #初始分组切分点,每个变量值都是切分点。每组中只包含一个变量值. #分组区间是左闭右开的,如cutoffs = [1,2,3],则表示区间 [1,2) , [2,3) ,[3,3+)。 cutoffs = freq_tab.index.values #如果没有指定最大分组 if max_groups is None: #如果没有指定卡方阈值,就以95%的置信度(自由度为类数目-1)设定阈值。 if threshold is None: #类数目 cls_num = freq.shape[-1] threshold = chi2.isf(0.05,df= cls_num - 1) while True: minvalue = None minidx = None #从第1组开始,依次取两组计算卡方值,并判断是否小于当前最小的卡方 for i in range(len(freq) - 1): v = chi3(freq[i:i+2]) if minvalue is None or (minvalue > v): #小于当前最小卡方,更新最小值 minvalue = v minidx = i #如果最小卡方值小于阈值,则合并最小卡方值的相邻两组,并继续循环 if (max_groups is not None and max_groups< len(freq) ) or (threshold is not None and minvalue < threshold): #minidx后一行合并到minidx tmp = freq[minidx] + freq[minidx+1] freq[minidx] = tmp #删除minidx后一行 freq = np.delete(freq,minidx+1,0) #删除对应的切分点 cutoffs = np.delete(cutoffs,minidx+1,0) else: #最小卡方值不小于阈值,停止合并。 break return cutoffs # 生成分箱后的新变量 def value2group(x,cutoffs): ''' 将变量的值转换成相应的组 x: 需要转换到分组的值 cutoffs: 各组的起始值 return: x对应的组,如group1 从group1开始 ''' # 切分点从小到大排序 cutoffs = sorted(cutoffs) num_groups = len(cutoffs) # 异常情况:小于第一组的起始值。这里直接放到第一组 # 异常值建议在分组之前先处理妥善。 if x < cutoffs[0]: return 'group1' for i in range(1,num_groups): if cutoffs[i-1] <= x < cutoffs[i]: return 'group{}'.format(i) # 最后一组,也可能会包括一些非常大的异常值。 return 'group{}'.format(num_groups)
对data数据框中的特定指标开展分箱,并形成新的分箱后的指标
# 需要分箱的指标 column_cart_list = ['column1','column2','column3','column4'] # 调取分箱函数形成新指标(设置分成8箱) for column_cart in column_cart_list: cutoffs = chiMerge(data_test, column_cart, max_groups = 8) new_column_name = column_cart + '_cart' data_test[new_column_name] = data_test[column_cart].apply(value2group, args=(cutoffs,))
拿到分箱指标后,进行WOE编码(数值编码),WOE值主要作为入模型特征,计算方式如下
优点:
1、可以提高模型的性能:根据公式以每一箱中的相对全体的log odds的超出作为编码依据,能够提高模型的预测精度,同时公式也符合LR的思想
2、分层抽样中的WOE不变性:如果建模需要对好坏样本进行分层抽样,则抽样后计算的WOE与没分层计算的WOE是一致的。
3、其次可以统一变量的一个尺度,一般是【-4,4】之间
缺点:
1、根据公式来看可以看出每一个bin中必须包含bad和good样本
2、对多类别标签无效:如果是多分类,分箱后的WOE无法计算
计算的python代码
# 进行WOE编码 def calWOE(df ,var ,target): ''' 计算WOE编码 param df:数据集pandas.dataframe param var:已分组的列名,无缺失值(缺失) param target:响应变量(0,1) return:编码字典 ''' eps = 0.000001 #避免除以0 gbi = pd.crosstab(df[var],df[target]) + eps gb = df[target].value_counts() + eps gbri = gbi/gb gbri['woe'] = np.log(gbri[1]/gbri[0]) return gbri['woe'].to_dict()
基于woe编码后的结果计算特征的IV值
IV值主要表示特征的预测能力,判断特征重要性的,因为做完特征工程后,会衍生出很多特征,有的变量是没用的,或者说对模型贡献非常非常小,所以我们需要筛选最重要的特征。
IV值计算与WOE编码的关系
计算代码:
# 进行值的计算 def calIV(df,var,target): ''' 计算IV值 param df:数据集pandas.dataframe param var:已分组的列名,无缺失值(缺失) param target:响应变量(0,1) return:IV值 ''' eps = 0.000001 # 避免除以0 gbi = pd.crosstab(df[var],df[target]) + eps gb = df[target].value_counts() + eps gbri = gbi/gb gbri['woe'] = np.log(gbri[1]/gbri[0]) gbri['iv'] = (gbri[1] - gbri[0])*gbri['woe'] return gbri['iv'].sum()
同样基于循环指标计算对应指标的IV值
colums_iv_list = ['column1','column2'] # woe可能为负,但iv值不可能为负(is_invergt_case为y标签) for colums_iv in colums_iv_list: woe_map = calWOE(data_test, colums_iv, 'is_invergt_case') iv = calIV(data_test, colums_iv, 'is_invergt_case') print('当前分箱指标的IV值:', colums_iv, iv)
一般来说IV>=0.2 是有较高的重要性,0.1~0.2之间是有较弱的重要性,小于0.1几乎没有重要性,但是当IV异常高的时候,比如超过1的时候,那么此时的分箱方式可能是不稳定的
变量分布的稳定性:合适的变量,各箱的占比不会很悬殊。如果某变量有一箱的占比远远低于其他箱,则该变量的稳定性也比较弱。
单变量分析是从重要性及分布的稳定性两个角度来考虑。通常先选择 IV 高于阈值的变量,再挑选出分箱比较均匀的变量。如果变量的 IV 普遍较低,那么需要放宽 IV 的选择条件,比如原来是大于0.2的入模,发现得到的 IV 都很低 ,低到0.0几的时候,可以用0.02进行粗筛选。
3、无监督的K-means聚类分箱案例
IV值计算函数
# 同样注意保证每个分箱下既有正例样本又有负例样本,否则除0会报错 def CalcIV(Xvar, Yvar): fea_seg_list,ivi_list,woei_list,iv_list0 =[],[],[],[] N_0 = np.sum(Yvar==0) # 负样本量 N_1 = np.sum(Yvar==1) # 正样本量 N_0_group = np.zeros(np.unique(Xvar).shape) N_1_group = np.zeros(np.unique(Xvar).shape) a=0 for i in range(len(np.unique(Xvar))): # 当前分箱负样本量 N_0_group[i] = Yvar[(Xvar == np.unique(Xvar)[i]) & (Yvar == 0)].count() # 当前分箱正样本量 N_1_group[i] = Yvar[(Xvar == np.unique(Xvar)[i]) & (Yvar == 1)].count() N_2 = np.sum((Xvar == np.unique(Xvar)[i])) b=N_1_group[i]/N_1 g=N_0_group[i]/N_0 # 每一个分箱的iv值 ivi=(b - g) * np.log(b/g) # 每一个分箱的woe值 woei = np.log((N_1_group[i]/N_0_group[i])/(N_1/N_0)) fea_seg_list.append(np.unique(Xvar)[i]) ivi_list.append(ivi) woei_list.append(woei) # 特征的iv值 iv = np.sum((N_1_group/N_1-N_0_group/N_0) * np.log((N_1_group/N_1)/(N_0_group/N_0))) lens = len(ivi_list) iv_list0 = lens*[iv] return fea_seg_list,ivi_list,woei_list,iv_list0 def caliv_batch(df, Kvar, Yvar): iv_df = pd.DataFrame() df_Xvar = df.drop([Kvar, Yvar], axis=1) ivlist = [] for col in df_Xvar.columns: fea_seg_list,ivi_list,woei_list,iv_list0 = CalcIV(df[col], df[Yvar]) feaname_list = len(ivi_list)*[col] iv_ret_tmp = pd.DataFrame({'col':feaname_list,'var':fea_seg_list,'iv':ivi_list,'woe':woei_list,'iv_all':iv_list0}) iv_df = pd.concat([iv_df,iv_ret_tmp],ignore_index=True) return iv_df
利用K-mean聚类进行指标分箱
### 使用KMeans进行聚类 from sklearn.cluster import KMeans typelabel2={} for col in num_feats_6: # num_feats_6是进行聚类的指标列表 typelabel2[col]=col+'_1' k =6 # k是聚类的类别数 keys = list(typelabel2.keys()) result1 = pd.DataFrame() # 需要进行分箱的数据值 data = df[num_feats_6].copy() # 对每一个连续值进行聚类分箱 for i in range(len(keys)): # 调用k-means算法,进行聚类离散化 print(u'正在进行“%s”的聚类...' % keys[i]) kmodel = KMeans(n_clusters = k, n_jobs = 4) # n_jobs是并行数,一般等于CPU数较好 kmodel.fit(data[[keys[i]]]) print(kmodel.cluster_centers_) r1 = pd.DataFrame(kmodel.cluster_centers_, columns = [typelabel2[keys[i]]]) # 聚类中心 r2 = pd.Series(kmodel.labels_).value_counts() # 分类统计 r2 = pd.DataFrame(r2, columns = [typelabel2[keys[i]]+'n']) # 转为DataFrame,记录各个类别的数目 r = pd.concat([r1, r2], axis = 1).sort_values(typelabel2[keys[i]]) #匹配聚类中心和类别数目 r.index = [1, 2, 3, 4,5,6] #-------- print('r-1',r) r[typelabel2[keys[i]]] = r[typelabel2[keys[i]]].rolling(2).mean() # rolling_mean()用来计算相邻2列的均值,主要计算分箱的边界点。 print('r-2', r) r.iloc[0,0] = 0.0 # 这两句代码将原来的聚类中心改为边界点。 print('r-3', r) result1 = result1.append(r.T) result1 = result1.sort_index() # 以Index排序,即以A,B,C,D,E,F顺序排,得出分段的切分阈值 result1.to_excel('processedfile6.xlsx',index=False) data22 =df[['docuno_id']+num_feats_6].copy() #连续特征进行分箱 for index,row in result1.iterrows(): for key,value in typelabel2.items(): if index == value: data22.loc[data22[key]<row[2],key+'new']="-inf~{}".format(str(row[2])) data22.loc[(data22[key]>=row[2])&(data22[key]<row[3]),key+'new']="{}~{}".format(str(row[2]),str(row[3])) data22.loc[(data22[key]>=row[3])&(data22[key]<row[4]),key+'new']="{}~{}".format(str(row[3]),str(row[4])) data22.loc[(data22[key]>=row[4])&(data22[key]<row[5]),key+'new']="{}~{}".format(str(row[4]),str(row[5])) data22.loc[(data22[key]>=row[5])&(data22[key]<row[6]),key+'new']="{}~{}".format(str(row[5]),str(row[6])) data22.loc[(data22[key]>=row[6]),key+'new']="{}~inf".format(str(row[6])) # 最中保留分箱后的特征 series_newcols =[] for col in num_feats_6: series_newcols.append(col+'new') data33=data22[['docuno_id']+series_newcols].copy() data33.rename(columns={col :col[:-3] for col in data33.columns if col !='docuno_id'},inplace=True)
最后计算所有特征的IV值(注意关联y特征值)
# 计算所有特征的iv值 #计算iv data_iv = data3.copy() Kvar = 'docuno' Yvar = 'is_invergt_case' ret = caliv_batch(data_iv,Kvar,Yvar) ret # ret.to_excel('特征iv值.xlsx',index=False)