數據分析中的變量編碼——德國信貸數據集(data coding in data analysis-German credit datasets)


 

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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM