最近看了一本《Python金融大數據風控建模實戰:基於機器學習》(機械工業出版社)這本書,看了其中第5章:變量編碼的方法 內容,總結了主要內容以及做了代碼詳解,分享給大家。
1. 主要知識點
在統計學中,將變量按照取值是否連續分為離散變量和連續變量。例如性別就是離散變量,變量中只有男、女、未知三種情況;年齡是連續變量,是1~100的整數(假設100歲是年齡的最大值)。而建模中的預測模型都只能對數值類型進行建模分析。因此,為了讓模型可以正常運行,必須要提前對離散變量進行編碼轉換,以進行數值化,其原則是保證編碼后變量的距離可計算且符合原始變量之間的距離度量。
變量編碼主要分成無監督編碼和有監督編碼。
無監督編碼即不需要標簽信息,直接對原始離散變量進行變量編碼。無監督編碼常用的3種方式:One-hot(獨熱)編碼、Dummy variable(啞變量)編碼、Label(標簽)編碼。
有監督編碼就是考慮目標變量,則變量編碼的過程可能會使離散變量的數值化過程更具有方向性,這就是有監督編碼。
2. 代碼
數據的使用還是德國信貸數據集,具體數據集介紹和獲取方法請看 數據清洗與預處理代碼詳解——德國信貸數據集(data cleaning and preprocessing - German credit datasets)
1 import os 2 import pandas as pd 3 import numpy as np 4 import pickle 5 from sklearn.preprocessing import OneHotEncoder 6 from sklearn.preprocessing import LabelEncoder 7 from sklearn.model_selection import train_test_split 8 import warnings 9 warnings.filterwarnings("ignore") # 忽略警告
10
11
12 # 注意sklearn版本要在v.20.0以上,不同版本函數的位置會不同。
13 def data_read(data_path, file_name): 14 df = pd.read_csv(os.path.join(data_path, file_name), delim_whitespace=True, header=None) 15 # 變量重命名
16 columns = ['status_account', 'duration', 'credit_history', 'purpose', 'amount', 17 'svaing_account', 'present_emp', 'income_rate', 'personal_status', 18 'other_debtors', 'residence_info', 'property', 'age', 19 'inst_plans', 'housing', 'num_credits', 20 'job', 'dependents', 'telephone', 'foreign_worker', 'target'] 21 df.columns = columns 22 # 將標簽變量由狀態1,2轉為0,1; 0表示好用戶,1表示壞用戶
23 df.target = df.target - 1
24 # 數據分為data_train和 data_test兩部分,訓練集用於得到編碼函數,驗證集用已知的編碼規則對驗證集編碼
25 # stratify(分層): none或者array/series類型的數據,表示按這列進行分層采樣。
26 data_train, data_test = train_test_split(df, test_size=0.2, random_state=0, stratify=df.target) 27 return data_train, data_test 28
29
30 # -------------------------------- one—hot編碼 ------------------------------------ #
31 def onehot_encode(df, data_path_1, flag='train'): 32 # reset_index()重置索引。不想保留原來的index,使用參數 drop=True,默認 False。
33 df = df.reset_index(drop=True) 34 print("one-hot編碼 df = ", df) 35 # 判斷數據集是否存在缺失值
36 if sum(df.isnull().any()) > 0: 37 numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64'] 38 # select_dtypes()方法返回原數據幀的子集,由include中聲明的 列組成,並且排除exclude中聲明的列。
39 var_numerics = df.select_dtypes(include=numerics).columns 40 var_str = [i for i in df.columns if i not in var_numerics] 41
42 # pandas中的df.loc[]主要是根據DataFrame的行標和列標進行數據的篩選的
43 # 其接受兩個參數:行標和列標,當列標省略時,默認獲取整行數據。兩個參數都可以以字符,切片以及列表的形式傳入。
44 # 以切片傳入行標,以列表形式傳入列標 https://zhuanlan.zhihu.com/p/139825425
45
46 # 數據類型的缺失值用-77777填補
47 if len(var_numerics) > 0: 48 # DataFrame.fillna函數:使用指定方法填充NA/NaN值
49 df.loc[:, var_numerics] = df[var_numerics].fillna(-7777) 50 # 字符串類型的缺失值用NA填補
51 if len(var_str) > 0: 52 df.loc[:, var_str] = df[var_str].fillna('NA') 53 print("填補缺失值后數據 = ", df) 54 if flag == 'train': 55 # OneHotEncoder 可以實現將分類特征的每個元素轉化為一個可以用來計算的值
56 # dtype=<class 'numpy.float64'>:表示編碼數值格式,默認為浮點型。
57 # Fit OneHotEncoder to X.
58 enc = OneHotEncoder(dtype='int').fit(df) 59 # 保存編碼模型
60 save_model = open(os.path.join(data_path_1, 'onehot.pkl'), 'wb') 61 # pickle.dump(obj, file, protocol=None,)
62 # obj表示將要封裝的對象,file表示obj要寫入的文件對象,file必須以二進制可寫模式打開,即“wb”
63 # protocol——序列化模式,默認是 0(ASCII協議,表示以文本的形式進行序列化)
64 pickle.dump(enc, save_model, 0) 65 save_model.close() 66
67 # 800 * 37(=5+11+4+3+3+3+4+2+2)
68 print("編碼后數據的大小 = ", enc.transform(df).toarray().shape) 69 # 一個Datarame是一個二維表格,類似電子表格的數據結構,包含一個經過排序的列表集,它的每一列都可以有不同的類型值
70 # 這是是創建DataFrame類型數
71 # 如果不加 toarray() 的話,輸出的是稀疏的存儲格式,即索引加值的形式,也可以通過參數指定 sparse = False 來達到同樣的效果
72 df_return = pd.DataFrame(enc.transform(df).toarray()) 73 # get_feature_names():返回一個含有特征名稱的列表,通過索引排序,如果含有one-hot表示的特征,則顯示相應的特征名
74 df_return.columns = enc.get_feature_names(df.columns) 75 print("特征名稱", df_return.columns) 76 pass
77
78 elif flag == 'test': 79 # ----------------------- 測試數據編碼 -------------------------
80 # 打開訓練集保存好的編碼模型文件,並且將數據從文件中讀取出來,最后關閉文件
81 read_model = open(os.path.join(data_path_1, 'onehot.pkl'), 'rb') 82 onehot_model = pickle.load(read_model) 83 read_model.close() 84
85 # 如果訓練集無缺失值,測試集有缺失值則將該樣本刪除
86 # The categories of each feature determined during fitting
87 # (in order of the features in X and corresponding with the output of transform).
88 var_range = onehot_model.categories_ 89 # 采用DataFrame.columns屬性以返回給定Dataframe的列標簽
90 var_name = df.columns 91 del_index = [] 92 for i in range(len(var_range)): 93 print("var_name = ", var_name[i]) 94
95 # 如果訓練集無缺失值,測試集有缺失值則將該樣本刪除
96 # 如果“NA”不是這個變量的取值,並且這個變量的取值中有它
97 # unique()函數用於獲取Series對象的唯一值。
98 if 'NA' not in var_range[i] and 'NA' in df[var_name[i]].unique(): 99 # 獲取值==“NA”所在的行值
100 index = np.where(df[var_name[i]] == 'NA') 101 del_index.append(index) 102 # 如果-7777不是這個變量的取值,並且這個變量的取值中有它
103 elif -7777 not in var_range[i] and -7777 in df[var_name[i]].unique(): 104 index = np.where(df[var_name[i]] == -7777) 105 del_index.append(index) 106 # 刪除樣本
107 if len(del_index) > 0: 108 # numpy.unique(ar, return_index=False, return_inverse=False, return_counts=False, axis=None)[source]
109 # Find the unique elements of an array.
110 del_index = np.unique(del_index) 111 # 從行或列中刪除指定的標簽,第一個參數labels:單個標簽或類似列表,要刪除的索引或列標簽。
112 # 第二個參數axis:{0或'index',1或'columns'},默認0,是從索引(0或“ index”)還是從列(1或“ columns”)中刪除標簽。
113 df = df.drop(del_index) 114 print('訓練集無缺失值,但測試集有缺失值,第{0}條樣本被刪除'.format(del_index)) 115
116 # transform(X) Transform X using one-hot encoding.
117 df_return = pd.DataFrame(onehot_model.transform(df).toarray()) 118 # get_feature_names():返回一個含有特征名稱的列表,通過索引排序,如果含有one-hot表示的特征,則顯示相應的特征名
119 df_return.columns = onehot_model.get_feature_names(df.columns) 120 pass
121
122 elif flag == 'transform': 123 # 編碼數據值轉化為原始變量
124 read_model = open(os.path.join(data_path_1, 'onehot.pkl'), 'rb') 125 onehot_model = pickle.load(read_model) 126 read_model.close() 127 # 逆變換
128 # inverse_transform(X) Convert the data back to the original representation.
129 df_return = pd.DataFrame(onehot_model.inverse_transform(df)) 130 # rsplit() 方法從右側開始將字符串拆分為列表
131 df_return.columns = np.unique(['_'.join(i.rsplit('_')[:-1]) for i in df.columns]) 132
133 return df_return 134
135
136 # ----------------------------------- 標簽編碼 ------------------------------------- #
137 def label_encode(df, data_path_1, flag='train'): 138 if flag == 'train': 139 # preprocessing.LabelEncoder() 獲取一個LabelEncoder
140 # enc.fit() 訓練LabelEncoder
141 enc = LabelEncoder().fit(df) 142 # 保存編碼模型
143 save_model = open(os.path.join(data_path_1, 'labelcode.pkl'), 'wb') 144 pickle.dump(enc, save_model, 0) 145 save_model.close() 146 # transform表示使用訓練好的LabelEncoder對數據進行編碼
147 df_return = pd.DataFrame(enc.transform(df)) 148 df_return.name = df.name 149 print("df_return.name = ", df_return.name) 150 print("labels = ", np.unique(df_return.values)) 151 pass
152
153 elif flag == 'test': 154 # 測試數據編碼
155 read_model = open(os.path.join(data_path_1, 'labelcode.pkl'), 'rb') 156 label_model = pickle.load(read_model) 157 read_model.close() 158 df_return = pd.DataFrame(label_model.transform(df)) 159 df_return.name = df.name 160
161 elif flag == 'transform': 162 # 編碼數據值轉化為原始變量
163 read_model = open(os.path.join(data_path_1, 'labelcode.pkl'), 'rb') 164 label_model = pickle.load(read_model) 165 read_model.close() 166 # 逆變換 inverse_transform(X) Convert the data back to the original representation.
167 df_return = pd.DataFrame(label_model. inverse_transform(df)) 168 return df_return 169
170
171 # --------------------------------- 自定義映射 ------------------------------- #
172 def dict_encode(df, data_path_1): 173 # 自定義映射
174 embarked_mapping = {} 175 embarked_mapping['status_account'] = {'NA': 1, 'A14': 2, 'A11': 3, 'A12': 4, 'A13': 5} 176 embarked_mapping['svaing_account'] = {'NA': 1, 'A65': 1, 'A61': 3, 'A62': 5, 'A63': 6, 'A64': 8} 177 embarked_mapping['present_emp'] = {'NA': 1, 'A71': 2, 'A72': 5, 'A73': 6, 'A74': 8, 'A75': 10} 178 embarked_mapping['property'] = {'NA': 1, 'A124': 1, 'A123': 4, 'A122': 6, 'A121': 9} 179
180 df = df.reset_index(drop=True) 181 # 判斷數據集是否存在缺失值
182 if sum(df.isnull().any()) > 0: 183 # DataFrame.fillna函數:使用指定方法填充NA/NaN值
184 df = df.fillna('NA') 185 # 字典映射
186 var_dictEncode = [] 187 for i in df.columns: 188 col = i + '_dictEncode'
189 # map方法都是把對應的數據逐個當作參數傳入到字典或函數中,得到映射后的值。
190 # 添加新的列值
191 df[col] = df[i].map(embarked_mapping[i]) 192 var_dictEncode.append(col) 193 return df[var_dictEncode] 194
195
196 # ------------------------------------- WOE編碼 ----------------------------------- #
197 def woe_cal_trans(x, y, target=1): 198 # 計算總體的正負樣本數, target=1表示壞樣本,0表示好樣本
199 p_total = sum(y == target) # 壞樣本的總個數
200 n_total = len(x)-p_total # 好樣本的總個數
201 value_num = list(x.unique()) 202 woe_map = {} 203 iv_value = 0 204 for i in value_num: 205 # 計算該變量取值箱內該變量的正負樣本總數
206 y1 = y[np.where(x == i)[0]] 207 p_num_1 = sum(y1 == target) # 當前變量取值中壞樣本的總個數
208 n_num_1 = len(y1) - p_num_1 # 當前變量取值中好樣本的總個數
209 # 計算占比
210 bad_1 = p_num_1 / p_total 211 good_1 = n_num_1 / n_total 212 # 在Badi=0或Goodi=0時,需要將Badi/Badtatol或Goodi/Goodtatol給予一個極小值
213 if bad_1 == 0: # log(x) x != 0
214 bad_1 = 1e-4
215 elif good_1 == 0: # 分母不能為0
216 good_1 = 1e-5
217 woe_map[i] = np.log(bad_1 / good_1) 218 # iv_value += (bad_1 - good_1) * woe_map[i]
219 iv_value = iv_value + (bad_1 - good_1) * woe_map[i] 220 x_woe_trans = x.map(woe_map) 221 x_woe_trans.name = x.name + "_woe"
222 return x_woe_trans, woe_map, iv_value 223
224
225 def woe_encode(df, data_path_1, varnames, y, filename, flag='train'): 226 """
227 WOE編碼映射 228 --------------------------------------- 229 Param 230 df: pandas dataframe,待編碼數據 231 data_path_1 :存取文件路徑 232 varnames: 變量列表 233 y: 目標變量 234 filename:編碼存取的文件名 235 flag: 選擇訓練還是測試 236 --------------------------------------- 237 Return 238 df: pandas dataframe, 編碼后的數據,包含了原始數據 239 woe_maps: dict,woe編碼字典 240 iv_values: dict, 每個變量的IV值 241 """
242 df = df.reset_index(drop=True) 243 y = y.reset_index(drop=True) 244 print("df.shape = ", df.shape) 245 print("y.shape = ", y.shape) 246 # 判斷數據集是否存在缺失值
247 if sum(df.isnull().any()) > 0: 248 numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64'] 249 var_numerics = df.select_dtypes(include=numerics).columns 250 var_str = [i for i in df.columns if i not in var_numerics] 251 # 數據類型的缺失值用-77777填補
252 if len(var_numerics) > 0: 253 df.loc[:, var_numerics] = df[var_numerics].fillna(-7777) 254 # 字符串類型的缺失值用NA填補
255 if len(var_str) > 0: 256 df.loc[:, var_str] = df[var_str].fillna('NA') 257 if flag == 'train': 258 iv_values = {} 259 woe_maps = {} 260 var_woe_name = [] 261 for var in varnames: 262 # var = 'foreign_worker'
263 x = df[var] 264 # 變量映射
265 x_woe_trans, woe_map, info_value = woe_cal_trans(x, y) 266 var_woe_name.append(x_woe_trans.name) 267 df = pd.concat([df, x_woe_trans], axis=1) 268 woe_maps[var] = woe_map 269 iv_values[var] = info_value 270 # 保存woe映射字典
271 save_woe_dict = open(os.path.join(data_path_1, filename+'.pkl'), 'wb') 272 pickle.dump(woe_maps, save_woe_dict, 0) 273 save_woe_dict.close() 274 return df, woe_maps, iv_values, var_woe_name 275 elif flag == 'test': 276 # 測試數據編碼
277 read_woe_dict = open(os.path.join(data_path_1, filename+'.pkl'), 'rb') 278 woe_dict = pickle.load(read_woe_dict) 279 read_woe_dict.close() 280 print(woe_dict.keys()) 281 # 如果訓練集無缺失值,測試集有缺失值則將該樣本刪除
282 del_index = [] 283 for key, value in woe_dict.items(): 284 if 'NA' not in value.keys() and 'NA' in df[key].unique(): 285 index = np.where(df[key] == 'NA') 286 del_index.append(index) 287 elif -7777 not in value.keys() and -7777 in df[key].unique(): 288 index = np.where(df[key] == -7777) 289 del_index.append(index) 290 # 刪除樣本
291 if len(del_index) > 0: 292 del_index = np.unique(del_index) 293 df = df.drop(del_index) 294 print('訓練集無缺失值,但測試集有缺失值,該樣本{0}刪除'.format(del_index)) 295
296 # WOE編碼映射
297 var_woe_name = [] 298 for key, value in woe_dict.items(): 299 val_name = key + "_woe"
300 df[val_name] = df[key].map(value) 301 var_woe_name.append(val_name) 302 return df, var_woe_name 303
304
305 if __name__ == '__main__': 306 path = os.getcwd() 307 data_path = os.path.join(path, 'data') 308 file_name = 'german.csv'
309 # 讀取數據
310 data_train, data_test = data_read(data_path, file_name) 311
312 # 不可排序變量
313 var_no_order = ['credit_history', 'purpose', 'personal_status', 'other_debtors', 314 'inst_plans', 'housing', 'job', 'telephone', 'foreign_worker'] 315 print("不可排序變量的長度 = ", len(var_no_order)) 316
317 # --------------------------- one-hot編碼 ------------------------- #
318 # 訓練數據編碼
319 data_train.credit_history[882] = np.nan 320 data_train_encode = onehot_encode(data_train[var_no_order], data_path, flag='train') 321
322 # 測試集數據編碼
323 print("data_test = ", data_test) 324 data_test.credit_history[529] = np.nan 325 data_test.purpose[355] = np.nan 326 print("-----------------------------------------") 327 print("data_test = ", data_test) 328 data_test_encode = onehot_encode(data_test[var_no_order], data_path, flag='test') 329
330 # 查看編碼逆變化后的原始變量名
331 df_encoded = data_test_encode.loc[0:4] 332 # pd.set_option("display.max_columns", None)
333 print(df_encoded) 334 df_encoded.to_csv("df_encoded.csv") 335 data_inverse = onehot_encode(df_encoded, data_path, flag='transform') 336 print("------------------------------------") 337 print(data_inverse) 338
339 # -------------------------- 啞變量編碼 -------------------------- #
340 # get_dummies 是利用pandas實現one hot encode的方式,它會忽略NA項,如果你想它不忽略,則修改以下參數,但是會在每一個變量都添加nan這一項
341 # 參數:dummy_nabool, default False. Add a column to indicate NaNs, if False NaNs are ignored.
342 data_train_dummies = pd.get_dummies(data_train[var_no_order]) 343 data_test_dummies = pd.get_dummies(data_test[var_no_order]) 344 print(data_train_dummies.columns) 345 print(data_train_dummies.shape) 346
347 # 可排序變量
348 # 注意,如果分類變量的標簽為字符串,這是需要將字符串數值化才可以進行模型訓練,標簽編碼其本質是為
349 # 標簽變量數值化而提出的方法,因此,其值支持單列數據的轉化操作,並且轉化后的結果是無序的。
350 # 因此有序變量統一用字典映射的方式完成。
351 var_order = ['status_account', 'svaing_account', 'present_emp', 'property'] 352
353 # -------------------------- 標簽編碼 --------------------------- #
354 # 訓練數據編碼
355 data_train_encode = label_encode(data_train[var_order[1]], data_path, flag='train') 356 # 驗證集數據編碼
357 data_test_encode = label_encode(data_test[var_order[1]], data_path, flag='test') 358 # 查看編碼你變化后的原始變量名
359 df_encoded = data_test_encode 360 data_inverse = label_encode(df_encoded, data_path, flag='transform') 361
362 # -------------------------- 自定義映射 ------------------------- #
363 # 訓練數據編碼
364 data_train.credit_history[882] = np.nan 365 data_train_encode = dict_encode(data_train[var_order], data_path) 366 # 測試集數據編碼
367 data_test.status_account[529] = np.nan 368 data_test_encode = dict_encode(data_test[var_order], data_path) 369
370 # --------------------------- WOE編碼 --------------------------- #
371 # 訓練集WOE編碼
372 df_train_woe, dict_woe_map, dict_iv_values, var_woe_name = woe_encode(data_train, data_path, var_no_order, data_train.target, 'dict_woe_map', flag='train') 373 # 測試集WOE編碼
374 df_test_woe, var_woe_name = woe_encode(data_test, data_path, var_no_order, data_train.target, 'dict_woe_map', flag='test')