数据分析中的变量选择——德国信贷数据集(variable selection in data analysis-German credit datasets)

最近看了一本《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))

原文地址:https://www.cnblogs.com/ttweixiao-IT-program/p/15457553.html