数据分析中的变量编码——德国信贷数据集(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')

 

 
posted @ 2021-10-05 17:44  ttweixiao9999  阅读(1488)  评论(0编辑  收藏  举报