在进行数据分析和机器学习之前,我们通常的做法,需要选择变量,但在实际业务场景中,变量的个数有可能很多,其中包含连续变量和非连续变量(离散变量),同时也需要将这些自变量和因变量的关联程度在进行分析前,做个初步的了解。
1.为什么需要将连续变量调整为离散变量?
在机器学习过程中,经常会将连续变量离散化,主要是基于以下的考虑:(1)离散特征的增加和减少都很容易,易于模型的快速迭代;(2)稀疏向量内积乘法运算速度快,计算结果方便存储,容易扩展;(3)离散化后的特征对异常数据有很强的鲁棒性:比如一个特征是年龄>30是1,否则0。如果特征没有离散化,一个异常数据“年龄300岁”会给模型造成很大的干扰;(4)单变量离散化为N个后,每个变量有单独的权重,相当于为模型引入了非线性,能够提升模型表达能力,加大拟合;(5)离散化后可以进行特征交叉,由M+N个变量变为M*N个变量,进一步引入非线性,提升表达能力;(6)特征离散化后,模型会更稳定,比如如果对用户年龄离散化,20-30作为一个区间,不会因为一个用户年龄长了一岁就变成一个完全不同的人。当然处于区间相邻处的样本会刚好相反,所以怎么划分区间是门学问;(7)特征离散化以后,起到了简化了模型的作用,降低了模型过拟合的风险。
2.如何衡量自变量与因变量的关联程度?
挑选变量过程是个比较复杂的过程,需要考虑的因素很多,比如:变量的预测能力,变量之间的相关性,变量的简单性(容易生成和使用),变量的强壮性(不容易被绕过),变量在业务上的可解释性(被挑战时可以解释的通)等等。但是,其中最主要和最直接的衡量标准是变量的预测能力。
IV的全称是Information Value,中文意思是信息价值,或者信息量。WOE的全称是“Weight of Evidence”,即证据权重。WOE是对原始自变量的一种编码形式。要对一个变量进行WOE编码,需要首先把这个变量进行分组处理(也叫离散化、分箱等等,说的都是一个意思)。分组后,对于第i组,WOE的计算公式如下:
WOE表示的实际上是“当前分组中响应客户占所有响应客户的比例”和“当前分组中没有响应的客户占所有没有响应的客户的比例”的差异。
3.实例卡方分箱和IV值计算
import pandas as pdimport numpy as npimport math'''exampledata = pd.read_csv('sample_data.csv', sep="", na_values=['', '?'])data = pd.read_csv('E:/breast_cancer.csv', sep=',')temp = data[['radius_mean','diagnosis']]temp2=ChiMerge(temp,'radius_mean' , 'diagnosis', confidenceVal=5.841, bin=5, sample = None)'''# 定义一个卡方分箱(可设置参数置信度水平与箱的个数)停止条件为大于置信水平且小于bin的数目''' 运行前需要 import pandas as pd 和 import numpy as np df:传入一个数据框仅包含一个需要卡方分箱的变量与正负样本标识(正样本为1,负样本为0) variable:需要卡方分箱的变量名称(字符串) flag:正负样本标识的名称(字符串) confidenceVal:置信度水平(默认是不进行抽样95%) bin:最多箱的数目 sample: 为抽样的数目(默认是不进行抽样),因为如果观测值过多运行会较慢'''def ChiMerge(df, variable, flag, confidenceVal=3.841, bin=10, sample = None): #进行是否抽样操作 if sample != None: df = df.sample(n=sample) else: df #进行数据格式化录入 total_num = df.groupby([variable])[flag].count() # 统计需分箱变量每个值数目 total_num = pd.DataFrame({'total_num': total_num}) # 创建一个数据框保存之前的结果 positive_class = df.groupby([variable])[flag].sum() # 统计需分箱变量每个值正样本数 positive_class = pd.DataFrame({'positive_class': positive_class}) # 创建一个数据框保存之前的结果 regroup = pd.merge(total_num, positive_class, left_index=True, right_index=True, how='inner') # 组合total_num与positive_class regroup.reset_index(inplace=True) regroup['negative_class'] = regroup['total_num'] - regroup['positive_class'] # 统计需分箱变量每个值负样本数 regroup = regroup.drop('total_num', axis=1) np_regroup = np.array(regroup) # 把数据框转化为numpy(提高运行效率) print('已完成数据读入,正在计算数据初处理')#处理连续没有正样本或负样本的区间,并进行区间的合并(以免卡方值计算报错) i = 0 while (i <= np_regroup.shape[0] - 2): if ((np_regroup[i, 1] == 0 and np_regroup[i + 1, 1] == 0) or ( np_regroup[i, 2] == 0 and np_regroup[i + 1, 2] == 0)): np_regroup[i, 1] = np_regroup[i, 1] + np_regroup[i + 1, 1] # 正样本 np_regroup[i, 2] = np_regroup[i, 2] + np_regroup[i + 1, 2] # 负样本 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): chi = (np_regroup[i, 1] * np_regroup[i + 1, 2] - np_regroup[i, 2] * np_regroup[i + 1, 1]) ** 2 * (np_regroup[i, 1] + np_regroup[i, 2] + np_regroup[i + 1, 1] + np_regroup[i + 1, 2]) / ((np_regroup[i, 1] + np_regroup[i, 2]) * (np_regroup[i + 1, 1] + np_regroup[i + 1, 2]) * ( np_regroup[i, 1] + np_regroup[i + 1, 1]) * (np_regroup[i, 2] + np_regroup[i + 1, 2])) chi_table = np.append(chi_table, chi) print('已完成数据初处理,正在进行卡方分箱核心操作')#把卡方值最小的两个区间进行合并(卡方分箱核心) while (1): if (len(chi_table) <= (bin - 1) and min(chi_table) >= confidenceVal): break chi_min_index = np.argwhere(chi_table == min(chi_table))[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, 2] = np_regroup[chi_min_index, 2] + np_regroup[chi_min_index + 1, 2] np_regroup[chi_min_index, 0] = np_regroup[chi_min_index + 1, 0] np_regroup = np.delete(np_regroup, chi_min_index + 1, 0) if (chi_min_index == np_regroup.shape[0] - 1): # 最小值试最后两个区间的时候 # 计算合并后当前区间与前一个区间的卡方值并替换 chi_table[chi_min_index - 1] = (np_regroup[chi_min_index - 1, 1] * np_regroup[chi_min_index, 2] - np_regroup[chi_min_index - 1, 2] * np_regroup[chi_min_index, 1]) ** 2 * (np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index - 1, 2] + np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) / ((np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index - 1, 2]) * (np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) * (np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index, 1]) * (np_regroup[chi_min_index - 1, 2] + np_regroup[chi_min_index, 2])) # 删除替换前的卡方值 chi_table = np.delete(chi_table, chi_min_index, axis=0) else: # 计算合并后当前区间与前一个区间的卡方值并替换 chi_table[chi_min_index - 1] = (np_regroup[chi_min_index - 1, 1] * np_regroup[chi_min_index, 2] - np_regroup[chi_min_index - 1, 2] * np_regroup[chi_min_index, 1]) ** 2 * (np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index - 1, 2] + np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) / ((np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index - 1, 2]) * (np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) * (np_regroup[chi_min_index - 1, 1] + np_regroup[chi_min_index, 1]) * (np_regroup[chi_min_index - 1, 2] + np_regroup[chi_min_index, 2])) # 计算合并后当前区间与后一个区间的卡方值并替换 chi_table[chi_min_index] = (np_regroup[chi_min_index, 1] * np_regroup[chi_min_index + 1, 2] - np_regroup[chi_min_index, 2] * np_regroup[chi_min_index + 1, 1]) ** 2 * (np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2] + np_regroup[chi_min_index + 1, 1] + np_regroup[chi_min_index + 1, 2]) / ((np_regroup[chi_min_index, 1] + np_regroup[chi_min_index, 2]) * (np_regroup[chi_min_index + 1, 1] + np_regroup[chi_min_index + 1, 2]) * (np_regroup[chi_min_index, 1] + np_regroup[chi_min_index + 1, 1]) * (np_regroup[chi_min_index, 2] + np_regroup[chi_min_index + 1, 2])) # 删除替换前的卡方值 chi_table = np.delete(chi_table, chi_min_index + 1, axis=0) print('已完成卡方分箱核心操作,正在保存结果')#把结果保存成一个数据框 result_data = pd.DataFrame() # 创建一个保存结果的数据框 result_data['variable'] = [variable] * np_regroup.shape[0] # 结果表第一列:变量名 list_temp = [] for i in np.arange(np_regroup.shape[0]): if i == 0: x = '0' + ',' + str(np_regroup[i, 0]) elif i == np_regroup.shape[0] - 1: x = str(np_regroup[i - 1, 0]) + '+' else: x = str(np_regroup[i - 1, 0]) + ',' + str(np_regroup[i, 0]) list_temp.append(x) result_data['interval'] = list_temp # 结果表第二列:区间 result_data['flag_0'] = np_regroup[:, 2] # 结果表第三列:负样本数目 result_data['flag_1'] = np_regroup[:, 1] # 结果表第四列:正样本数目 return result_datadef iv_value(file,feature,sep,target): ###sep格式为(10,15,20) data = pd.read_csv(file, sep=',') #data = pd.read_csv('E:/breast_cancer.csv', sep=',') # woe #sep_value = sep.split(',') sep_value =str(sep).replace('(','').replace(')','').split(',') sep_len = len(sep_value) dict_bin = {} class_bin = {} len_dict_bin = {} len_dict_bin_0 = {} len_dict_bin_1 = {} woe_bin = {} iv_bin = {} if sep_len == 1: dict_bin[0] = data.loc[data[feature] <= float(sep_value[0]), :] dict_bin[1] = data.loc[data[feature] > float(sep_value[0]), :] dict_bin[2] = sum(data[feature].isnull()) len_dict_bin[0] = len(dict_bin[0]) len_dict_bin[1] = len(dict_bin[1]) len_dict_bin[2] = len(dict_bin[2]) class_bin[0] = "(0," + sep_value[0] + "]" class_bin[1] = "(" + sep_value[0] + "...)" class_bin[2] = "NA" else: for index, item in enumerate(sep_value):####区间 if index == 0: dict_bin[0] = data.loc[data[feature] <= float(item), :] len_dict_bin[0] = len(dict_bin[0]) class_bin[0] = "(0," + str(float(item)) + "]" else: dict_bin[index] = ( data.loc[(data[feature] >= float(sep_value[index - 1])) & (data[feature] < float(item)), :]) len_dict_bin[index] = len(dict_bin[index]) class_bin[index] = "(" + str(sep_value[index - 1]) + "," + str(sep_value[index]) + "]" dict_bin[index + 1] = data.loc[data[feature] > float(item), :] dict_bin[index + 2] = data.loc[data[feature].isnull()] len_dict_bin[index + 1] = len(dict_bin[index + 1]) len_dict_bin[index + 2] = len(dict_bin[index + 2]) class_bin[index + 1] = "(" + str(sep_value[index]) + "...)" class_bin[index + 2] = "NA" #print(class_bin) for index, item in enumerate(dict_bin): len_dict_bin_0[index] = len(dict_bin[index][dict_bin[index][target] == 0]) len_dict_bin_1[index] = len(dict_bin[index][dict_bin[index][target] == 1]) len_data_0 = len(data[data[target] == 0]) len_data_1 = len(data[data[target] == 1]) for index, item in enumerate(dict_bin): try: woe_bin[index] = math.log(math.e, (float(len_dict_bin_1[index]) / float(len_data_1)) / ( float(len_dict_bin_0[index]) / float(len_data_0))) iv_bin[index] = ((float(len_dict_bin_1[index]) / float(len_data_1)) - ( float(len_dict_bin_0[index]) / float(len_data_0))) * math.log(math.e, ( float(len_dict_bin_1[index]) / float(len_data_1)) / (float(len_dict_bin_0[index]) / float(len_data_0))) except Exception as e: iv_bin[index] = 0 iv_sum=0.0 #print(iv_bin) for key in iv_bin: try: iv_sum=iv_sum+float(iv_bin[key]) except Exception as e: print (e) #print(iv_sum) #return iv_sum dict_result = {} len_dict_bin_0[" "] = len_data_0 len_dict_bin_1[" "] = len_data_1 woe_bin[" "] = "" iv_bin[" "]=sum(iv_bin.values()) class_bin[" "] = "" len_dict_bin[" "] = len(data) dict_result["bad"] = len_dict_bin_0 dict_result["good"] = len_dict_bin_1 dict_result["all"] = len_dict_bin dict_result["woe"] = woe_bin dict_result["iv"] = iv_bin dict_result["class"] = class_bin df = pd.DataFrame(dict_result) dict_result["%good"] = (df['good'] / df['all']).map('{:.2%}'.format); dict_result["%bad"] = (df['bad'] / df['all']).map('{:.2%}'.format); df["%good"] = dict_result["%good"] df["%bad"] = dict_result["%bad"] # 调整列的顺序 df = df.loc[:, ['class', 'good', 'bad', '%good', '%bad', 'all', 'woe', 'iv']] #print(iv_sum) #print(df) return iv_sum,df