最近看了一本《Python金融大数据风控建模实战:基于机器学习》(机械工业出版社)这本书,看了其中第7章:变量选择 内容,总结了主要内容以及做了代码详解,分享给大家。
1. 主要知识点
变量选择是特征工程中非常重要的一部分。特征工程是一个先升维后降维的过程。升维的过程是结合业务理解尽可能多地加工特征,是一个非常耗时且需要发散思维的过程。而变量选择就是降维的过程,因为传统评分卡模型为了保证模型的稳定性与Logisitc回归模型的准确性,往往对入模变量有非常严格的考量,并希望入模变量最好不要超过20个,且要与业务强相关。这时变量选择显得尤为重要,特征加工阶段往往可以得到几百维或更高维度的特征,而从诸多特征中只保留最有意义的特征也是重要且耗时的过程。
变量选择的方法很多,常用的方法有过滤法(Filter)、包装法(Wrapper)、嵌入法(Embedding),并且在上述方法中又有单变量选择、多变量选择、有监督选择、无监督选择。
2. 代码
数据的使用还是德国信贷数据集,具体数据集介绍和获取方法请看 数据清洗与预处理代码详解——德国信贷数据集(data cleaning and preprocessing - German credit datasets)
注意:
import variable_bin_methods as varbin_meth
import variable_encode as var_encode
中 variable_bin_methods 和 variable_encode 分别是 第5章 变量编码 和 第6章 变量分箱 中的代码。
主代码:
1 # -*- coding: utf-8 -*- 2 """ 3 第7章:变量选择 4 数据获取 5 """ 6 import os 7 import pandas as pd 8 import numpy as np 9 from sklearn.model_selection import train_test_split 10 import variable_bin_methods as varbin_meth 11 import variable_encode as var_encode 12 import matplotlib 13 import matplotlib.pyplot as plt 14 # matplotlib.use('Qt5Agg') 15 matplotlib.rcParams['font.sans-serif'] = ['SimHei'] 16 matplotlib.rcParams['axes.unicode_minus'] = False 17 from sklearn.linear_model import LogisticRegression 18 from sklearn.feature_selection import VarianceThreshold 19 from sklearn.feature_selection import SelectKBest, f_classif 20 from sklearn.feature_selection import RFECV 21 from sklearn.svm import SVR 22 from sklearn.feature_selection import SelectFromModel 23 import seaborn as sns 24 from sklearn.tree import DecisionTreeClassifier 25 from feature_selector import FeatureSelector 26 import warnings 27 warnings.filterwarnings("ignore") # 忽略警告 28 29 30 # 数据读取 31 def data_read(data_path, file_name): 32 df = pd.read_csv(os.path.join(data_path, file_name), delim_whitespace=True, header=None) 33 # 变量重命名 34 columns = [ 35 'status_account', 'duration', 'credit_history', 'purpose', 'amount', 36 'svaing_account', 'present_emp', 'income_rate', 'personal_status', 37 'other_debtors', 'residence_info', 'property', 'age', 'inst_plans', 38 'housing', 'num_credits', 'job', 'dependents', 'telephone', 39 'foreign_worker', 'target' 40 ] 41 df.columns = columns 42 # 将标签变量由状态1,2转为0,1;0表示好用户,1表示坏用户 43 df.target = df.target - 1 44 # 数据分为data_train和 data_test两部分,训练集用于得到编码函数,验证集用已知的编码规则对验证集编码 45 data_train, data_test = train_test_split(df, test_size=0.2, random_state=0, stratify=df.target) 46 return data_train, data_test 47 48 49 # 离散变量与连续变量区分 50 def category_continue_separation(df, feature_names): 51 categorical_var = [] 52 numerical_var = [] 53 if 'target' in feature_names: 54 feature_names.remove('target') 55 # 先判断类型,如果是int或float就直接作为连续变量 56 numerical_var = list(df[feature_names].select_dtypes( 57 include=['int', 'float', 'int32', 'float32', 'int64', 'float64']).columns.values) 58 categorical_var = [x for x in feature_names if x not in numerical_var] 59 return categorical_var, numerical_var 60 61 62 if __name__ == '__main__': 63 path = os.getcwd() 64 data_path = os.path.join(path, 'data') 65 file_name = 'german.csv' 66 # 读取数据 67 data_train, data_test = data_read(data_path, file_name) 68 69 print("训练集中好样本数 = ", sum(data_train.target == 0)) 70 print("训练集中坏样本数 = ", data_train.target.sum()) 71 72 # 区分离散变量与连续变量 73 feature_names = list(data_train.columns) 74 feature_names.remove('target') 75 # 通过判断输入数据的类型来区分连续变量和连续变量 76 categorical_var, numerical_var = category_continue_separation(data_train, feature_names) 77 78 print("连续变量个数 = ", len(numerical_var)) 79 print("离散变量个数 = ", len(categorical_var)) 80 for s in set(numerical_var): 81 print('变量 ' + s + ' 可能取值数量 = ' + str(len(data_train[s].unique()))) 82 # 如果连续变量的取值个数 <= 10,那个就把它列入到离散变量中 83 if len(data_train[s].unique()) <= 10: 84 categorical_var.append(s) 85 numerical_var.remove(s) 86 # 同时将后加的数值变量转为字符串 87 # 这里返回的是true和false,数据类型是series 88 index_1 = data_train[s].isnull() 89 if sum(index_1) > 0: 90 data_train.loc[~index_1, s] = data_train.loc[~index_1, s].astype('str') 91 else: 92 data_train[s] = data_train[s].astype('str') 93 index_2 = data_test[s].isnull() 94 if sum(index_2) > 0: 95 data_test.loc[~index_2, s] = data_test.loc[~index_2, s].astype('str') 96 else: 97 data_test[s] = data_test[s].astype('str') 98 print("现连续变量个数 = ", len(numerical_var)) 99 print("现离散变量个数 = ", len(categorical_var)) 100 101 # 连续变量分箱 102 dict_cont_bin = {} 103 for i in numerical_var: 104 # print(i) 105 dict_cont_bin[i], gain_value_save, gain_rate_save = varbin_meth.cont_var_bin( 106 data_train[i], 107 data_train.target, 108 method=2, 109 mmin=3, 110 mmax=12, 111 bin_rate=0.01, 112 stop_limit=0.05, 113 bin_min_num=20) 114 115 # 离散变量分箱 116 dict_disc_bin = {} 117 del_key = [] 118 for i in categorical_var: 119 dict_disc_bin[i], gain_value_save, gain_rate_save, del_key_1 = varbin_meth.disc_var_bin( 120 data_train[i], 121 data_train.target, 122 method=2, 123 mmin=3, 124 mmax=8, 125 stop_limit=0.05, 126 bin_min_num=20) 127 if len(del_key_1) > 0: 128 del_key.extend(del_key_1) 129 # 删除分箱数只有1个的变量 130 if len(del_key) > 0: 131 for j in del_key: 132 del dict_disc_bin[j] 133 134 # ---------------------- 训练数据分箱 ------------------- # 135 # 连续变量分箱映射 136 df_cont_bin_train = pd.DataFrame() 137 for i in dict_cont_bin.keys(): 138 df_cont_bin_train = pd.concat([ 139 df_cont_bin_train, 140 varbin_meth.cont_var_bin_map(data_train[i], dict_cont_bin[i])], axis=1) 141 # 离散变量分箱映射 142 df_disc_bin_train = pd.DataFrame() 143 for i in dict_disc_bin.keys(): 144 df_disc_bin_train = pd.concat([ 145 df_disc_bin_train, 146 varbin_meth.disc_var_bin_map(data_train[i], dict_disc_bin[i])], axis=1) 147 148 # --------------------- 测试数据分箱 --------------------- # 149 # 连续变量分箱映射 150 df_cont_bin_test = pd.DataFrame() 151 for i in dict_cont_bin.keys(): 152 df_cont_bin_test = pd.concat([ 153 df_cont_bin_test, 154 varbin_meth.cont_var_bin_map(data_test[i], dict_cont_bin[i])], axis=1) 155 156 # 离散变量分箱映射 157 df_disc_bin_test = pd.DataFrame() 158 for i in dict_disc_bin.keys(): 159 df_disc_bin_test = pd.concat([ 160 df_disc_bin_test, 161 varbin_meth.disc_var_bin_map(data_test[i], dict_disc_bin[i])], axis=1) 162 163 # 组成分箱后的训练集与测试集 164 df_disc_bin_train['target'] = data_train.target 165 data_train_bin = pd.concat([df_cont_bin_train, df_disc_bin_train], axis=1) 166 df_disc_bin_test['target'] = data_test.target 167 data_test_bin = pd.concat([df_cont_bin_test, df_disc_bin_test], axis=1) 168 169 data_train_bin.reset_index(inplace=True, drop=True) 170 data_test_bin.reset_index(inplace=True, drop=True) 171 172 # #WOE编码 173 var_all_bin = list(data_train_bin.columns) 174 var_all_bin.remove('target') 175 176 # 训练集WOE编码 177 df_train_woe, dict_woe_map, dict_iv_values, var_woe_name = var_encode.woe_encode( 178 data_train_bin, 179 data_path, 180 var_all_bin, 181 data_train_bin.target, 182 'dict_woe_map', 183 flag='train') 184 185 # 测试集WOE编码 186 df_test_woe, var_woe_name = var_encode.woe_encode(data_test_bin, 187 data_path, 188 var_all_bin, 189 data_test_bin.target, 190 'dict_woe_map', 191 flag='test') 192 y = np.array(data_train_bin.target) 193 194 # ------------------------ 过滤法特征选择 ---------------------------- # 195 # --------------- 方差筛选 ----------------- # 196 # 获取woe编码后的数据 197 df_train_woe = df_train_woe[var_woe_name] 198 # 得到进行woe编码后的变量个数 199 len_1 = df_train_woe.shape[1] 200 # VarianceThreshold 方差阈值法,用于特征选择,过滤器法的一种,去掉那些方差没有达到阈值的特征。默认情况下,删除零方差的特征 201 # 实例化一个方差筛选的选择器,阈值设置为0.01 202 select_var = VarianceThreshold(threshold=0.01) 203 # 让这个选择器拟合df_train_woe数据 204 select_var_model = select_var.fit(df_train_woe) 205 # 将df_train_woe数据缩小为选定的特征 206 df_1 = pd.DataFrame(select_var_model.transform(df_train_woe)) 207 # 获取所选特征的掩码或整数索引,保留的索引,即可以看到哪些特征被保留 208 save_index = select_var.get_support(True) 209 print("保留下来的索引: = ", save_index) 210 211 # 获取保留下来的变量名字 212 var_columns = [list(df_train_woe.columns)[x] for x in save_index] 213 df_1.columns = var_columns 214 # 删除变量的方差 215 var_delete_variance = select_var.variances_[[x for x in range(len_1) if x not in save_index]] 216 print("删除变量的方差值 = ", var_delete_variance) 217 var_delete_variance_columns = list((df_train_woe.columns)[x] for x in range(len_1) if x not in save_index) 218 print("删除变量的列名 = ", var_delete_variance_columns) 219 220 # ------------------------ 单变量筛选 ------------------------- # 221 # 参数:SelectKBest(score_func= f_classif, k=10) 222 # score_func:特征选择要使用的方法,默认适合分类问题的F检验分类:f_classif。 k :取得分最高的前k个特征,默认10个。 223 # f_calssif计算ANOVA中的f值。方差分析ANOVA F用于分类任务的标签和/特征之间的值 224 # 当样本xx属于正类时,xixi会取某些特定的值(视作集合S+S+),当样本xx属于负类时,xixi会取另一些特定的值(S−S−)。 225 # 我们当然希望集合S+S+与S−S−呈现出巨大差异,这样特征xixi对类别的预测能力就越强。落实到刚才的方差分析问题上,就变成了我们需要检验假设H0:μS+=μS−H0:μS+=μS− ,我们当然希望拒绝H0H0,所以我们希望构造出来的ff值越大越好。也就是说ff值越大,我们拒绝H0H0的把握也越大,我们越有理由相信μS+≠μS−μS+≠μS−,越有把握认为集合S+S+与S−S−呈现出巨大差异,也就说xixi这个特征对预测类别的帮助也越大! 226 # 我们可以根据样本的某个特征xi的f值来判断特征xi对预测类别的帮助,f值越大,预测能力也就越强,相关性就越大,从而基于此可以进行特征选择。 227 select_uinvar = SelectKBest(score_func=f_classif, k=15) 228 # 传入特征集df_train_woe和标签y拟合数据 229 select_uinvar_model = select_uinvar.fit(df_train_woe, y) 230 # 转换数据,返回特征过滤后保留下的特征数据集 231 df_1 = select_uinvar_model.transform(df_train_woe) 232 # 看得分 233 len_1 = len(select_uinvar_model.scores_) 234 # 得到原始列名 235 var_name = [str(x).split('_BIN_woe')[0] for x in list(df_train_woe.columns)] 236 # 画图 237 plt.figure(figsize=(10, 6)) 238 fontsize_1 = 14 239 # barh 函数用于绘制水平条形图 240 plt.barh(np.arange(0, len_1), select_uinvar_model.scores_, color='c', tick_label=var_name) 241 plt.xticks(fontsize=fontsize_1) 242 plt.yticks(fontsize=fontsize_1) 243 plt.xlabel('得分', fontsize=fontsize_1) 244 plt.show() 245 246 # ------------------------- 分析变量相关性 ------------------------ # 247 # 计算相关矩阵。 dataFrame.corr可以返回各类型之间的相关系数DataFrame表格 248 correlations = abs(df_train_woe.corr()) 249 # 相关性绘图 250 fig = plt.figure(figsize=(10, 6)) 251 fontsize_1 = 10 252 # 绘制热力图 253 sns.heatmap(correlations, 254 cmap=plt.cm.Greys, 255 linewidths=0.05, 256 vmax=1, 257 vmin=0, 258 annot=True, 259 annot_kws={'size': 6, 'weight': 'bold'}) 260 plt.xticks(np.arange(len(var_name)) + 0.5, var_name, fontsize=fontsize_1, rotation=20) 261 plt.yticks(np.arange(len(var_name)) + 0.5, var_name, fontsize=fontsize_1) 262 plt.title('相关性分析') 263 # plt.xlabel('得分',fontsize=fontsize_1) 264 plt.show() 265 266 # -------------------- 包装法变量选择:递归消除法 -------------------- # 267 # 给定学习器, Epsilon-Support Vector Regression.实例化一个SVR估算器 268 estimator = SVR(kernel="linear") 269 # 递归消除法, REFCV 具有递归特征消除和交叉验证选择最佳特征数的特征排序。用来挑选特征 270 # REF(Recursive feature elimination) 就是使用机器学习模型不断的去训练模型,每训练一个模型,就去掉一个最不重要的特征,直到特征达到指定的数量 271 # sklearn.feature_selection.RFECV(estimator, *, step=1, min_features_to_select=1, cv=None, scoring=None, verbose=0, n_jobs=None) 272 # estimator: 一种监督学习估计器。 273 # step: 如果大于或等于1,则step对应于每次迭代要删除的个特征个数。如果在(0.0,1.0)之内,则step对应于每次迭代要删除的特征的百分比(向下舍入)。 274 # cv: 交叉验证拆分策略 275 select_rfecv = RFECV(estimator, step=1, cv=3) 276 # Fit the SVM model according to the given training data. 277 select_rfecv_model = select_rfecv.fit(df_train_woe, y) 278 df_1 = pd.DataFrame(select_rfecv_model.transform(df_train_woe)) 279 # 查看结果 280 # 选定特征的掩码。哪些特征入选最后特征,true表示入选 281 print("SVR support_ = ", select_rfecv_model.support_) 282 # 利用交叉验证所选特征的数量。挑选了几个特征 283 print("SVR n_features_ = ", select_rfecv_model.n_features_) 284 # 特征排序,使ranking_[i]对应第i个特征的排序位置。选择的(即估计的最佳)特征被排在第1位。 285 # 每个特征的得分排名,特征得分越低(1最好),表示特征越好 286 print("SVR ranking = ", select_rfecv_model.ranking_) 287 288 # --------------------- 嵌入法变量选择 -------------------------- # 289 # 选择学习器 290 # C 正则化强度 浮点型,默认:1.0;其值等于正则化强度的倒数,为正的浮点数。数值越小表示正则化越强。 291 # penalty 是正则化类型 292 lr = LogisticRegression(C=0.1, penalty='l2') 293 # 嵌入法变量选择 294 # SelectFromModel(estimator, *, threshold=None, prefit=False, norm_order=1, max_features=None) 295 # estimator用来构建变压器的基本估算器 296 # prefit: bool, default False,预设模型是否期望直接传递给构造函数。如果为True,transform必须直接调用和SelectFromModel不能使用cross_val_score, 297 # GridSearchCV而且克隆估计类似的实用程序。否则,使用训练模型fit,然后transform进行特征选择 298 # threshold 是用于特征选择的阈值 299 select_lr = SelectFromModel(lr, prefit=False, threshold='mean') 300 select_lr_model = select_lr.fit(df_train_woe, y) 301 df_1 = pd.DataFrame(select_lr_model.transform(df_train_woe)) 302 # 查看结果,.threshold_ 是用于特征选择的阈值 303 print("逻辑回归 threshold_ = ", select_lr_model.threshold_) 304 # get_support 获取选出的特征的索引序列或mask 305 print("逻辑回归 get_support = ", select_lr_model.get_support(True)) 306 307 # -------------- 基学习器选择预训练的决策树来进行变量选择 -------------- # 308 # 先训练决策树 309 # riterion = gini/entropy 可以用来选择用基尼指数或者熵来做损失函数。 310 # max_depth = int 用来控制决策树的最大深度,防止模型出现过拟合。 311 # fit需要训练数据和类别标签 312 cart_model = DecisionTreeClassifier(criterion='gini', max_depth=3).fit(df_train_woe, y) 313 # Return the feature importances. 314 print("决策树 feature_importances_ = ", cart_model.feature_importances_) 315 # 用预训练模型进行变量选择 316 select_dt_model = SelectFromModel(cart_model, prefit=True) 317 df_1 = pd.DataFrame(select_dt_model.transform(df_train_woe)) 318 # 查看结果 319 print("决策树 get_support(True) = ", select_dt_model.get_support(True))