在机器学习中,样本、变量、标签、模型等概念会频繁出现,本章从变量角度给出实际应用中的处理方法。机器学习模型通常只能处理结构化数据,而非结构化数据要转化为结构化数据才可以用于模型训练。在机器学习中,变量、字段、属性、特征、输入、预测因子、自变量是同一个意思,样本、观测、实例、记录是同一个意思,结果、预测变量、输出、目标、因变量、响应、标签是同一个意思。
在统计学中,将变量按照取值是否连续分为离散变量和连续变量。有时离散变量是字符串类型,而建模中的预测模型绝大多数只能对数值类型进行建模分析(CatBoost模型可以直接对类别变量建模)。因此,为了让模型可以正常运行,必须要提前对离散变量进行编码转换,以进行数值化,其原则是保证编码后变量的距离可计算且符合原始变量之间的距离测量。
# 第5章 变量编码
'''
变量编码:one-hot编码、标签编码、自定义字典映射、woe编码
程序的运行逻辑是:数据读取—>划分训练集与测试集—>训练集变量编码—>保存编码规则—>测试集变量编码
需要用到的Python包有:Pandas包、scikit-learn包和Pickle包,用到的函数如下:
data_read ():用于读取数据
onehot_encode():One-hot编码函数
label_encode():标签编码函数
dict_encode():自定义映射函数,用于有序变量编码
woe_encode():WOE编码函数
'''
'''
os是Python环境下对文件,文件夹执行操作的一个模块
pickle可以把字典、列表等结构化数据存到本地文件,读取后返回的还是字典、列表等结构化数据。而file.write、file.read存取的对象是字符串。
本章使用的OneHotEncoder、LabelEncoder来自sklearn.preprocessing,train_test_split来自sklearn.model_selection
'''
import os
import pandas as pd
import numpy as np
import pickle
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings("ignore") # 忽略警告
# 注意sklearn版本要在v.20.0以上,不同版本函数的位置会不同。
def data_read(data_path,file_name):
'''
csv文件是一种用,和换行符区分数据记录和字段的一种文件结构,可以用excel表格编辑,也可以用记事本编辑,是一种类excel的数据存
储文件,也可以看成是一种数据库。pandas提供了pd.read_csv()方法可以读取其中的数据并且转换成DataFrame数据帧。python的强大
之处就在于他可以把不同的数据库类型,比如txt/csv/.xls/.sql转换成统一的DataFrame格式然后进行统一的处理。真是做到了标准化。
pd.read_csv()函数参数:
os.path.join()函数:连接两个或更多的路径名组件
sep:如果不指定参数,则会尝试使用逗号分隔。
delimiter :定界符,备选分隔符(如果指定该参数,则sep参数失效)
delim_whitespace : 指定空格是否作为分隔符使用,等效于设定sep=’\s+’。如果这个参数设定为True那么delimiter 参数失效。
header :指定行数用来作为列名,数据开始行数。如果文件中没有列名,则默认为0【第一行数据】,否则设置为None。
'''
df = pd.read_csv( os.path.join(data_path, file_name), delim_whitespace = True, header = None )
##变量重命名
columns = ['status_account','duration','credit_history','purpose', 'amount',
'svaing_account', 'present_emp', 'income_rate', 'personal_status',
'other_debtors', 'residence_info', 'property', 'age',
'inst_plans', 'housing', 'num_credits',
'job', 'dependents', 'telephone', 'foreign_worker', 'target']
'''
修改列名的两种方式为:
直接使用df.columns的方式重新命名,不过这种方式需要列出所有列名。
使用rename方法,注意如果需要原地修改需要带上inplace=True的参数,否则原dataframe列名不会发生改变。
'''
df.columns = columns
##将标签变量由状态1,2转为0,1;0表示好用户,1表示坏用户
df.target = df.target - 1
'''
数据分为data_train和 data_test两部分,训练集用于得到编码函数,验证集用已知的编码规则对验证集编码。
这里是采用的是scikit-learn的model_selection模块中的train_test_split()函数实现数据切分,函数原型为:
sklearn.model_selection.train_test_split(*arrays, **options)
主要参数说明:
arrays:为需要切分的原始数据,可以是列表、Numpy arrays、稀疏矩阵、pandas的数据框。
test_size:划分的测试数据的占比,为0-1的数,默认为0.25,即训练数据为原始数据的75%,测试数据为原始数据的25%。
train_size:与test_size设置一个参数即可,并满足加和为1的关系。
random_state:随机数设置,可以保证每次切分得到的数据是相同的,这样在比较不用算法的性能时更加严谨,保证了数据集的一致性。
如果不设置,每次将随机选择随机数,产生不同的切分结果。
shuffle:是否在切分前打乱数据原有的顺序,默认为进行随机洗牌。
stratify:设置是否采用分层抽样,默认为none,不分层。分层抽样可以保证正负样本的比例与原始的数据集一致。如果设置为none,
则切分时采用随机采样方式。如果需要进行分层采样,则需要指定按哪个变量分层,一般按照标签进行采样。
如在本程序中,使用target标签进行采样。
'''
data_train, data_test = train_test_split(df, test_size=0.2, random_state=0,stratify=df.target)
return data_train, data_test
# one—hot编码
def onehot_encode(df,data_path_1,flag='train'):
'''
reset_index()函数中,drop=True: 把原来的索引index列去掉,丢掉;drop=False:保留原来的索引(以前的可能是乱的)
inplace=True:不创建新的对象,直接对原始对象进行修改;
inplace=False:对数据进行修改,创建并返回新的对象承载其修改结果。
'''
df = df.reset_index(drop=True)
# 判断数据集是否存在缺失值
'''
Python pandas判断缺失值一般采用 isnull(),然而生成的却是所有数据的true/false矩阵,对于庞大的数据dataframe,很难一眼看出
来哪个数据缺失,一共有多少个缺失数据,缺失数据的位置,而df.isnull().any()则会判断哪些”列”存在缺失值。
下面这行代码表示有缺失值的列数大于0。
'''
if sum(df.isnull().any()) > 0 :
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
'''
pandas形成的dataframe数据集,如果有些列是数字,有些列是字母,或者有些是bool型,那么如果我们有时候只需要选取特定数据类
型的列,例如:DataFrame.select_dtypes(include[‘int’], exclude=None)其中 include 包含的是需要获取的列的类型,exclude
包含的不需要获取的数据类型
'''
var_numerics = df.select_dtypes(include=numerics).columns
var_str = [ i for i in df.columns if i not in var_numerics ]
# 数据类型的缺失值用-7777填补
if len(var_numerics) > 0:
'''
DataFrame.fillna(value=None, method=None, axis=None, inplace=False, limit=None, downcast=None, **kwargs)
函数功能:使用指定的方法填充NA / NaN值
参数:
value:变量,字典,Series,orDataFrame用于填充缺失值(例如0),或者指定为每个索引(对于Series)或列(对于DataFrame)
使用哪个字典/Serise/DataFrame的值。(不在字典/Series/DataFrame中的值不会被填充)这个值不能是一个列表。
method:{‘backfill’,‘bfill’,‘pad’,‘ffill’,None},默认值None;在Series中使用方法填充空白(‘backfill’,
‘bfill’向前填充,‘pad’, ‘ffill’向后填充)
axis:{0 or ‘index’, 1 or ‘columns’}
inplace:boolean, 默认值 False。如果为Ture,在原地填满。注意:这将修改次对象上的任何其他视图(例如,DataFrame
中的列的无复制贴片)
limit:int,默认值 None;如果指定了方法,则这是连续的NaN值的前向/后向填充的最大数量。换句话说,如果连续NaN数量
超过这个数字,它将只被部分填充。如果未指定方法,则这是沿着整个轴的最大数量,其中NaN将被填充。 如果不是
无,则必须大于0。
downcast:dict,默认是 None;如果可能的话,把 item->dtype 的字典将尝试向下转换为适当的相等类型的字符串(例如,
如果可能的话,从float64到int64)
下面这行代码对于var_numerics中的列的作用效果是,如果有缺失值,则用-7777填充。
'''
df.loc[:,var_numerics] = df[var_numerics].fillna(-7777)
# 字符串类型的缺失值用NA填补
if len(var_str) > 0:
df.loc[:,var_str] = df[var_str].fillna('NA')
if flag == 'train':
'''
数据读取成功后用scikit-learn中的preprocessing模型下的OneHotEncoder()函数对不可排序的离散变量进行数值化转化,其原型如下:
classsklearn.preprocessing.OneHotEncoder(n_values=None, categorical_features=None,categories=None,sparse=True,dtype=
<class 'numpy.float64'>,handle_unknown='error')
主要参数说明:
n_values:指定每个属性值的上界,默认为auto,自动判断每个属性值的上界。当有字符型变量时只能用auto,否则报错。当变量均
为数值型时可以自行指定上界,上界直接影响了编码的位数,可以将这个参数设置为int或array来指定上界。例如变量的
最大值为3,指定上界为5,则采用5位二进制对该变量进行编码,3被编码为00010,第四位表示3,可见取值的表达式是从
0开始的。
catagorical_feature:指定需要编码的离散变量,默认为all,即将全部变量作为离散变量进行变量编码;如果自行指定需要编码的
变量,则需要给定一个list或vector,即给定列的索引标号。
sparse:返回值是否需要稀疏编码,默认为True,即返回稀疏矩阵。当离散变量的取值非常多时,编码后的维度会非常高,此时采用
稀疏编码会很方便存储。如果设置为False,则返回array数组。
dtype:指定编码后的数据类型,默认为float。
handle_unknown:当某个数据在训练集中没有出现过但在测试集中出现了,用该参数设计程序的报错机制,error为抛出异常,ignore
为忽略。
categories:用于在拟合时查询训练数据的特征种类。
下面这行代码指定df数据OneHotEncoder后的数据类型为int。
fit(self,X,y=None)方法:用于训练编码规则,与机器学习模型一样,OneHotEncoder也是一种模型,通过训练得到编码规则后,就可以将
模型保存下来用于新数据的编码。
'''
enc = OneHotEncoder(dtype='int').fit(df)
# 保存编码模型
'''
"r":以读方式打开,只能读文件 , 如果文件不存在,会发生异常
"w":以写方式打开,只能写文件, 如果文件不存在,创建该文件;如果文件已存在,先清空,再打开文件
"rb":以二进制读方式打开,只能读文件 , 如果文件不存在,会发生异常
"wb":以二进制写方式打开,只能写文件, 如果文件不存在,创建该文件;如果文件已存在,先清空,再打开文件
'''
save_model = open(os.path.join(data_path_1 ,'onehot.pkl'), 'wb')
'''
pickle.dump(obj, file, protocol)
参数:
obj:序列化对象,将对象obj保存到文件file中去;
file:file表示保存到的类文件对象,file必须有write()接口,file可以是一个以’w’打开的文件或者是一个StringIO对象,
也可以是任何可以实现write()接口的对象;
protocol:序列化模式,默认是 0(ASCII协议,表示以文本的形式进行序列化),protocol的值还可以是1和2(1和2表示以二
进制的形式进行序列化。其中,1是老式的二进制协议;2是新二进制协议)。
'''
pickle.dump(enc, save_model, 0)
save_model.close()
'''
transform(X)方法:得到编码模型后,用该方法对新样本进行编码,类似于机器学习模型中的predic方法对新数据进行预测。
'''
df_return = pd.DataFrame( enc.transform(df).toarray())
'''
get_feature_names([input_features])方法:返回编码后的特征名称,有助于查看变量的不同取值在编码后由哪一列表示。
'''
df_return.columns = enc.get_feature_names(df.columns)
elif flag =='test':
# 测试数据编码
'''
"r":以读方式打开,只能读文件 , 如果文件不存在,会发生异常
"w":以写方式打开,只能写文件, 如果文件不存在,创建该文件;如果文件已存在,先清空,再打开文件
"rb":以二进制读方式打开,只能读文件 , 如果文件不存在,会发生异常
"wb":以二进制写方式打开,只能写文件, 如果文件不存在,创建该文件;如果文件已存在,先清空,再打开文件
'''
read_model = open(os.path.join(data_path_1 ,'onehot.pkl'),'rb')
'''
python的pickle模块实现了基本的数据序列和反序列化。通过pickle模块的序列化操作我们能够将程序中运行的对象信息保存到文件
中去,永久存储;通过pickle模块的反序列化操作,我们能够从文件中创建上一次程序保存的对象。
pickle(file):从file中读取一个字符串,并将它重构为原来的python对象。
file:类文件对象,有read()和readline()接口。
'''
onehot_model = pickle.load(read_model)
read_model.close()
# 如果训练集无缺失值,测试集有缺失值则将该样本删除
var_range = onehot_model.categories
var_name = df.columns
del_index = []
for i in range(len(var_range)):
if 'NA' not in var_range[i]and 'NA' in df[var_name[i]].unique():
index = np.where( df[var_name[i]] == 'NA')
del_index.append(index)
elif -7777 not in var_range[i] and -7777 in df[var_name[i]].unique():
index = np.where( df[var_name[i]] == -7777)
del_index.append(index)
# 删除样本
if len(del_index) > 0:
del_index = np.unique(del_index)
df = df.drop(del_index)
'''
.format()函数将{}中的用format函数中()中的内容替代,下面这句程序假设del_index=7785,则会输出:
训练集无缺失值,但测试集有缺失值,第{7785}条样本被删除
'''
print('训练集无缺失值,但测试集有缺失值,第{0}条样本被删除'.format(del_index))
df_return = pd.DataFrame(onehot_model.transform( df).toarray())
df_return.columns = onehot_model.get_feature_names(df.columns)
elif flag == 'transform':
# 编码数据值转化为原始变量
read_model = open(os.path.join(data_path_1,'onehot.pkl'),'rb')
onehot_model = pickle.load(read_model)
read_model.close()
# 逆变换
'''
inverse_transform(X):用于对编码后的特征进行还原,即逆编码函数。
'''
df_return = pd.DataFrame( onehot_model.inverse_transform(df) )
'''
join()是一个字符串方法,它返回被子字符串连接的字符串.
用法:string_name.join(iterable)
string_name:这是被连接的子字符串。
输入:list1 = ['1','2','3','4']
s = "-"
s = s.join(list1)
print(s)
输出:1-2-3-4
Python rsplit()方法通过指定分隔符对字符串进行分割并返回一个列表,默认分隔符为所有空字符,包括空格、换行(\n)、制表符(\t)等。
类似于split()方法,只不过是从字符串最后面开始分割。
'''
df_return.columns = np.unique( ['_'.join(i.rsplit('_')[:-1] ) for i in df.columns])
return df_return
# 标签编码
def label_encode(df,data_path_1,flag='train'):
if flag == 'train':
enc = LabelEncoder().fit( df )
# 保存编码模型
save_model = open(os.path.join(data_path_1 ,'labelcode.pkl'), 'wb')
pickle.dump(enc, save_model, 0)
save_model.close()
df_return = pd.DataFrame( enc.transform(df))
df_return.name = df.name
elif flag =='test':
# 测试数据编码
read_model = open(os.path.join(data_path_1 ,'labelcode.pkl'),'rb')
label_model = pickle.load(read_model)
read_model.close()
df_return = pd.DataFrame(label_model.transform( df))
df_return.name = df.name
elif flag == 'transform':
# 编码数据值转化为原始变量
read_model = open(os.path.join(data_path_1 ,'labelcode.pkl'),'rb')
label_model = pickle.load(read_model)
read_model.close()
# 逆变换
df_return = pd.DataFrame( label_model.inverse_transform(df) )
return df_return
# 自定义映射
def dict_encode(df,data_path_1):
# 自定义映射
embarked_mapping = {}
embarked_mapping['status_account'] = {'NA': 1, 'A14': 2, 'A11':3,'A12': 4,'A13':5}
embarked_mapping['svaing_account'] = {'NA': 1, 'A65': 1, 'A61':3,'A62': 5,'A63':6,'A64':8}
embarked_mapping['present_emp'] = {'NA': 1, 'A71': 2, 'A72':5,'A73': 6,'A74':8,'A75':10}
embarked_mapping['property'] = {'NA': 1, 'A124': 1, 'A123':4,'A122': 6, 'A121':9 }
'''
reset_index()函数中,drop=True: 把原来的索引index列去掉,丢掉;drop=False:保留原来的索引(以前的可能是乱的)
inplace=True:不创建新的对象,直接对原始对象进行修改;
inplace=False:对数据进行修改,创建并返回新的对象承载其修改结果。
'''
df = df.reset_index(drop=True)
# 判断数据集是否存在缺失值
if sum(df.isnull().any()) > 0 :
df = df.fillna('NA')
# 字典映射
var_dictEncode = []
for i in df.columns:
col = i + '_dictEncode'
df[col] = df[i].map(embarked_mapping[i])
var_dictEncode.append(col)
return df[var_dictEncode]
# WOE编码
def woe_cal_trans(x, y, target=1):
'''
计算总体的正负样本数
p_total计算正样本数
n_total计算负样本数
value_num计算划分个数
'''
p_total = sum(y == target)
n_total = len(x)-p_total
value_num = list(x.unique())
woe_map = {}
iv_value = 0
for i in value_num:
# 计算该变量取值箱内的正负样本总数
y1 = y[np.where(x == i)[0]]
p_num_1 = sum(y1 == target)
n_num_1 = len(y1) - p_num_1
# 计算占比
bad_1 = p_num_1 / p_total
good_1 = n_num_1 / n_total
if bad_1 == 0:
bad_1 = 1e-5
elif good_1 == 0:
good_1 = 1e-5
woe_map[i] = np.log(bad_1 / good_1)
iv_value += (bad_1 - good_1) * woe_map[i]
'''
下面这行代码将x按照woe_map字典映射
'''
x_woe_trans = x.map(woe_map)
x_woe_trans.name = x.name + "_woe"
return x_woe_trans, woe_map, iv_value
def woe_encode(df,data_path_1,varnames, y, filename,flag='train'):
"""
参数:
df: pandas dataframe,待编码数据
data_path_1 :存取文件路径
varnames: 变量列表
y: 目标变量
filename:编码存取的文件名
flag: 选择训练还是测试
返回值:
df: pandas dataframe, 编码后的数据,包含了原始数据
woe_maps: dict,woe编码字典
iv_values: dict, 每个变量的IV值
"""
df = df.reset_index(drop=True)
# 判断数据集是否存在缺失值
if sum(df.isnull().any()) > 0 :
numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
var_numerics = df.select_dtypes(include=numerics).columns
var_str = [ i for i in df.columns if i not in var_numerics ]
# 数据类型的缺失值用-7777填补
if len(var_numerics) > 0:
df.loc[:,var_numerics] = df[var_numerics].fillna(-7777)
##字符串类型的缺失值用NA填补
if len(var_str) > 0:
df.loc[:,var_str] = df[var_str].fillna('NA')
if flag == 'train':
iv_values = {}
woe_maps = {}
var_woe_name = []
for var in varnames:
# var = 'loan_amnt_BIN'
x = df[var]
# 变量映射
'''
woe_cal_trans()函数用于对单个变量计算WOE值、WOE映射字典和IV值
'''
x_woe_trans, woe_map, info_value = woe_cal_trans(x, y)
var_woe_name.append(x_woe_trans.name)
'''
pd.concat(objs, axis=0, join='outer', join_axes=None, ignore_index=False,keys=None, levels=None, names=None,
verify_integrity=False)
参数:
objs:用来保存需要用来进行连接的Series/DataFrame,可以是列表或者dict类型
axis:表示希望进行连接的轴向,默认为0,也就是纵向拼接
join:有多个选择,inner,outer,这里默认值是outer,下面会根据实例来比较下
join_axes:默认为空,可以设置值指定为其他轴上使用的索引
ignore_index:连接后原来两个DF的index值会被保存,如果该索引没有实际的意义可以设置为True来进行重分配index号
'''
df = pd.concat([df, x_woe_trans], axis=1)
woe_maps[var] = woe_map
iv_values[var] = info_value
# 保存woe映射字典
save_woe_dict = open(os.path.join(data_path_1 ,filename+'.pkl'), 'wb')
pickle.dump(woe_maps, save_woe_dict, 0)
save_woe_dict.close()
return df, woe_maps, iv_values ,var_woe_name
elif flag == 'test':
# 测试数据编码
read_woe_dict = open(os.path.join(data_path_1 ,filename+'.pkl'),'rb')
woe_dict = pickle.load(read_woe_dict)
read_woe_dict.close()
# 如果训练集无缺失值,测试集有缺失值则将该样本删除
'''
Python字典keys()方法以列表形式(并非直接的列表,若要返回列表值还需调用list函数)返回字典中的所有的键。
输入:
D = {'Name': 'Runoob', 'Age': 7}
print("字典所有的键为: %s" % D.keys())
print("转换为列表形式为:%s" % list(D.keys()))
输出:
字典所有的键为: D_keys(['Age', 'Name'])
转换为列表形式为:['Age', 'Name']
'''
woe_dict.keys()
del_index = []
for key,value in woe_dict.items():
if 'NA' not in value.keys() and 'NA' in df[key].unique():
index = np.where(df[key] == 'NA')
del_index.append(index)
elif -7777 not in value.keys() and -7777 in df[key].unique():
index = np.where(df[key] == -7777)
del_index.append(index)
##删除样本
if len(del_index) > 0:
del_index = np.unique(del_index)
df = df.drop(del_index)
print('训练集无缺失值,但测试集有缺失值,该样本{0}删除'.format(del_index))
# WOE编码映射
var_woe_name = []
for key,value in woe_dict.items():
val_name = key+ "_woe"
df[val_name] = df[key].map(value)
var_woe_name.append(val_name)
return df, var_woe_name
if __name__ == '__main__':
path = 'D:\\code\\chapter5\\'
data_path = os.path.join(path ,'data')
file_name = 'german.csv'
# 读取数据
data_train, data_test = data_read(data_path,file_name)
# 不可排序变量
var_no_order = ['credit_history','purpose', 'personal_status', 'other_debtors',
'inst_plans', 'housing', 'job','telephone', 'foreign_worker']
# one-hot编码
# 训练数据编码
data_train.credit_history[882] = np.nan
data_train_encode = onehot_encode(data_train[var_no_order],data_path,flag='train')
# 测试集数据编码
data_test.credit_history[529] = np.nan
data_test.purpose[355] = np.nan
data_test_encode = onehot_encode(data_test[var_no_order],data_path,flag='test')
# 查看编码逆变化后的原始变量名
df_encoded = data_test_encode.loc[0:4]
data_inverse = onehot_encode(df_encoded,data_path,flag='transform')
# 哑变量编码
data_train_dummies = pd.get_dummies(data_train[var_no_order])
data_test_dummies = pd.get_dummies(data_test[var_no_order])
data_train_dummies.columns
'''
可排序变量
注意,如果分类变量的标签为字符串,这是需要将字符串数值化才可以进行模型训练,标签编码其本质是为标签变量数值化而提出的方法,
因此,其值支持单列数据的转化操作,并且转化后的结果是无序的。
因此有序变量统一用字典映射的方式完成。
'''
var_order = ['status_account','svaing_account', 'present_emp', 'property']
# 标签编码
# 训练数据编码
data_train_encode = label_encode(data_train[var_order[1]],data_path,flag='train')
# 验证集数据编码
data_test_encode = label_encode(data_test[var_order[1]],data_path,flag='test')
df_encoded = data_test_encode
data_inverse = label_encode(df_encoded,data_path,flag='transform')
# 自定义映射
# 训练数据编码
data_train.credit_history[882] = np.nan
data_train_encode = dict_encode(data_train[var_order],data_path)
#测试集数据编码
data_test.status_account[529] = np.nan
data_test_encode = dict_encode(data_test[var_order],data_path)
# WOE编码
# 训练集WOE编码
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')
# 测试集WOE编码
df_test_woe, var_woe_name = woe_encode(data_test,data_path,var_no_order, data_train.target, 'dict_woe_map',flag='test')