一、前言
特征工程在传统的机器学习中是非常重要的一个步骤,我们对机器学习算法的优化通常是有限的。如果在完成任务时发现不管怎么优化算法得到的结果都不满意,这个时候就可以考虑回头在做一下特征工程。
二、缺失值填补
在特征工程中,对缺失值的处理是很常见的一个问题。处理方法通常如下:
- 删除有缺省值的数据
- 使用数据中该特征的均值填充缺失值
- 使用数据中该特征的中位数填充缺失值
- 使用数据中该特征的众数填充缺失值
- 使用机器学习模型对缺失值进行填充
上面的方法各有有点,我们可以根据自己的需求来选择策略。在数据集比较大时,最后一种方式是综合表现比较好的。今天我们就来讲讲使用随机森林来进行缺失值的填补。
三、数据预处理
3.1、处理思路
在外面开始填充数据前,我们还需要对原本的数据进行一些简单的处理。假如我们现在要对下面的数据进行填充:
name | sex | age | target |
zack | male | 20 | 1 |
rudy | male | 30 | 1 |
alice | female | 20 | 0 |
atom | male | 31 | 0 |
alex | female | 32 | 1 |
kerry | female | 0 | |
king | 20 | 1 | |
nyx | male | 20 | 1 |
petty | female | 0 |
在使用scikit-learn创建随机森林时,不允许我们训练数据的特征值为字符串,因此我们要对name、gender、city这几列进行处理,这里采取one-hot编码的策略。
注意:上面是我捏造的一些数据,至于target是什么含义我也不知道。
首先name特征在很多情况下都不会影响最后的结果,因此我们直接选择删除name特征。然后是gender和city特征,他们都是类型特征,对于gender我们可以用0代表male、用1代表female。而city是多分类的特征,我们也可以采取和gender一样的方法,0代表city_01、1代表city_02、2代表city_03。不过这样会导致city特征权重不一样,如果类别太多对结果会有很大影响。
这个时候我们就可以换一个策略,我们可以把原本的city特征拆分成三个特征,分别是city=city_01、city=city_02、city=city_03,然后特征值只有0或一,这样就可以解决上面的问题了。
比如我们原始数据如下:
name | gender | age | city | target |
zack | male | 21 | city_01 | 1 |
alice | female | 22 | city_02 | 0 |
进行转换后数据如下(忽略name特征):
name | gender=male | gender=female | age | city=city_01 | city=city_02 | city=city_03 | target |
zack | 1 | 0 | 21 | 1 | 0 | 0 | 1 |
alice | 0 | 1 | 22 | 0 | 1 | 0 | 0 |
在上面我们还有个city=city_03特征,这是因为我们要考虑整个数据集进行拆分。
这里还需要注意一点,就是gender特征可以不这样拆分,这里为了方便就不另外对gender用另外的策略了。
3.2、代码实现
根据上面的思路,我们知道了如何处理多分类的特征。而对于数字特征,我们不需要进行额外处理,因此我们需要遍历特征的列,然后判断是否是我们要处理的列。具体代码如下:
import numpy as np import pandas as pd from sklearn.feature_extraction import DictVectorizer # 创建DictVectorizer dv = DictVectorizer(sparse=False) # 读取数据 df = pd.read_csv("test.csv") # 删除name列 df = df.drop(['name'], axis=1) # 裁剪出特征值 X = df.iloc[:, 0:-1] # 遍历特征值的列 for colum in X.iteritems(): # 对非数值型列进行处理(多类别数据) if colum[1].dtype == np.object_: # 拆分出列名和数据 feature_name, data = colum # ①、将该列转换成字典 colum = data.map(lambda x: {feature_name: x}) colum = dv.fit_transform(colum) # 多分类特征名转换后的特征名,如gender->[gender=male, gender=female] features = dv.get_feature_names_out() # 将新创建的列添加进去 X[features] = colum # 删除当前列 X = X.drop([feature_name], axis=1) # ②、如果原先值是空,则吧所以新添加的列设置为nan if list(features).__contains__(feature_name): features = list(features) features.remove(feature_name) features = np.array(features) # 对于特征值是null的数据,转换后的各个特征也应为null # 如:gender为null,那gender=male为null,gender=female为null mask = X[features].sum(axis=1) == 0 X.loc[mask, features] = np.nan 复制代码
对于大部分代码,相信读者都能理解。这里来解释下代码中①、②两个部分。
3.3、代码解析
(1)问题①
在①处我们将当前列的数据转换成了字典,然后再调用DictVectorizer对象的fit_transform方法,我们直接看DictVectorizer的作用。来看下面这段代码:
from sklearn.feature_extraction import DictVectorizer # 待处理字典列表 data = [ {"gender": "male"}, {"gender": "female"}, {"gender": "unknow"}, {"gender": "male"}, {"gender": "male"} ] dv = DictVectorizer(sparse=False) # 转换数据 data = dv.fit_transform(data) print(dv.get_feature_names_out()) print(data) 复制代码
上面代码输出如下:
['gender=female' 'gender=male' 'gender=unknow'] [[0. 1. 0.] [1. 0. 0.] [0. 0. 1.] [0. 1. 0.] [0. 1. 0.]] 复制代码
可以看到,这个和我们上面思路提到的转换是一样的。因为dv接收的是字典序列,因此我们需要先使用下面代码:
colum = data.map(lambda x: {feature_name: x}) 复制代码
这样就可以将当前列转换成字典序列类型。然后调用dv.fit_transform就可以实现转换。
(2)问题②
这部分代码是为了让原本gender为nan的数据转换后gender=female和gender=male也应为nan。但是对于存在缺失值的数据,转换过程中会出现下面的问题:
from sklearn.feature_extraction import DictVectorizer data = [ {"gender": "male"}, {"gender": "female"}, {"gender": "unknow"}, {"gender": "male"}, {"gender": None} ] dv = DictVectorizer(sparse=False) data = dv.fit_transform(data) print(dv.get_feature_names_out()) print(data) 复制代码
上面我们添加了一个带有缺失值的数据,输出结果如下:
['gender' 'gender=female' 'gender=male' 'gender=unknow'] [[ 0. 0. 1. 0.] [ 0. 1. 0. 0.] [ 0. 0. 0. 1.] [ 0. 0. 1. 0.] [nan 0. 0. 0.]] 复制代码
可以发现,我们原本只期望有三列,但是却出现了四列。因此我们需要将dv.get_feature_names_out()中的多余列删除。
到处,我们的数据就处理完了。下面我们可以使用随机森林来填补缺失值。
四、使用随机森林填补缺失值
4.1、实现思路
填补缺失值的过程就是不断建立模型预测的过程。我们还是来看一组简单的数据:
height | weight | age |
181 | 70 | 20 |
178 | 18 | |
160 | 50 | |
170 | 60 | 19 |
上面的数据有两个特征存在缺失值,我们都需要进行填充。当我们要填充weight时,我们可以考虑选取weight不为空的数据。然后将其余列作为特征值,而weight作为目标值。这样我们就可以训练出一个可以预测weight的模型。
但是上面的方法有个问题,就是我们选取的是weight不为空的数据,但是这些数据的其它特征可能为空。这个时候我们就可以考虑用其它简单方法先对其余缺失值进行填充,然后训练模型填充weight的缺失值。
在填补weight的缺失值后,再用同样的方法来填补其余有缺失值的特征。
为了效果好,我们会有限选择填补缺失值数量少的列,因为这样我们就可以拿到较多的数据,可以更好地填充该列地数据。然后依次类推。
4.2、代码实现
这部分是在实现上面对多分类的处理后进行的,完整代码如下:
y = df.iloc[:, [-1]] # 按照当前列缺失值的数量进行升序排列 sortindex = np.argsort(X.isnull().sum(axis=0)).values #axis=0按列进行加和 for i in sortindex: # 将当前列作为目标值 feature_i = X.iloc[:, i] # 将其余列作为特征值(包括目标值) tmp_df = pd.concat([X.iloc[:, X.columns != i], y], axis=1) # 使用众数填充其余列缺失值 imp_mf = SimpleImputer(missing_values=np.nan, strategy='most_frequent') tmp_df_mf = imp_mf.fit_transform(tmp_df) # 将feature_i中非空的样本作为训练数据 y_notnull = feature_i[feature_i.notnull()] y_null = feature_i[feature_i.isnull()] X_notnull = tmp_df_mf[y_notnull.index, :] X_null = tmp_df_mf[y_null.index, :] # 如果没有缺失值则填充下一列 if y_null.shape[0] == 0: continue # 建立随机森林回归树进行训练 rfc = RandomForestRegressor(n_estimators=100) rfc = rfc.fit(X_notnull, y_notnull) # 对缺失值进行预测 y_predict = rfc.predict(X_null) # 填充缺失值 X.loc[X.iloc[:, i].isnull(), X.columns[i]] = y_predict 复制代码
这样我们就实现了随机森林填充缺失值的操作。完整代码如下:
import numpy as np import pandas as pd from sklearn.ensemble import RandomForestRegressor from sklearn.feature_extraction import DictVectorizer from sklearn.impute import SimpleImputer dv = DictVectorizer(sparse=False) df = pd.read_csv("test.csv") name = df['name'] df = df.drop(['name'], axis=1) X = df.iloc[:, 0:-1] # 遍历数据的列 for colum in X.iteritems(): # 对非数值型列进行处理 if colum[1].dtype == np.object_: # 拆分出列名和数据 feature_name, data = colum # 将该列转换成字典 colum = data.map(lambda x: {feature_name: x}) colum = dv.fit_transform(colum) features = dv.get_feature_names_out() # 将新创建的列添加进去 X[features] = colum # 删除当前列 X = X.drop([feature_name], axis=1) # 如果原先值是空,则吧所以新添加的列设置为nan if list(features).__contains__(feature_name): features = list(features) features.remove(feature_name) features = np.array(features) mask = X[features].sum(axis=1) == 0 X.loc[mask, features] = np.nan y = df.iloc[:, [-1]] # 按照当前列缺失值的数量进行升序排列 sortindex = np.argsort(X.isnull().sum(axis=0)).values for i in sortindex: # 将当前列作为目标值 feature_i = X.iloc[:, i] # 将其余列作为特征值(包括目标值) tmp_df = pd.concat([X.iloc[:, X.columns != i], y], axis=1) # 使用众数填充其余列缺失值 imp_mf = SimpleImputer(missing_values=np.nan, strategy='most_frequent') tmp_df_mf = imp_mf.fit_transform(tmp_df) # 将feature_i中非空的样本作为训练数据 y_notnull = feature_i[feature_i.notnull()] y_null = feature_i[feature_i.isnull()] X_notnull = tmp_df_mf[y_notnull.index, :] X_null = tmp_df_mf[y_null.index, :] # 如果没有缺失值则下一列 if y_null.shape[0] == 0: continue # 建立随机森林回归树进行训练 rfc = RandomForestRegressor(n_estimators=100) rfc = rfc.fit(X_notnull, y_notnull) # 对缺失值进行预测 y_predict = rfc.predict(X_null) # 填充缺失值 X.loc[X.iloc[:, i].isnull(), X.columns[i]] = y_predict 复制代码
今天的内容就是这些,更多内容可以关注“新建文件夹X”。
作者:ZackSock
链接:https://juejin.cn/post/7055620775985807391
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。