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