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