一、前言
特征工程在传统的机器学习中是非常重要的一个步骤,我们对机器学习算法的优化通常是有限的。如果在完成任务时发现不管怎么优化算法得到的结果都不满意,这个时候就可以考虑回头在做一下特征工程。
二、缺失值填补
在特征工程中,对缺失值的处理是很常见的一个问题。处理方法通常如下:
- 删除有缺省值的数据
- 使用数据中该特征的均值填充缺失值
- 使用数据中该特征的中位数填充缺失值
- 使用数据中该特征的众数填充缺失值
- 使用机器学习模型对缺失值进行填充
上面的方法各有优点,我们可以根据自己的需求来选择策略。在数据集比较大时,最后一种方式是综合表现比较好的。今天我们就来讲讲使用随机森林来进行缺失值的填补。
三、数据预处理
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或1,这样就可以解决上面的问题了。
比如我们原始数据如下:
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、代码实现
根据上面的思路,我们知道了如何处理多分类的特征。而对于数字特征,我们不需要进行额外处理,因此我们需要遍历特征的列,然后判断是否是我们要处理的列。具体代码如下:
代码语言:javascript复制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的作用。来看下面这段代码:
代码语言:javascript复制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)
上面代码输出如下:
代码语言:javascript复制['gender=female' 'gender=male' 'gender=unknow']
[[0. 1. 0.]
[1. 0. 0.]
[0. 0. 1.]
[0. 1. 0.]
[0. 1. 0.]]
可以看到,这个和我们上面思路提到的转换是一样的。因为dv接收的是字典序列,因此我们需要先使用下面代码:
代码语言:javascript复制colum = data.map(lambda x: {feature_name: x})
这样就可以将当前列转换成字典序列类型。然后调用dv.fit_transform就可以实现转换。
(2)问题②
这部分代码是为了让原本gender为nan的数据转换后gender=female和gender=male也应为nan。但是对于存在缺失值的数据,转换过程中会出现下面的问题:
代码语言:javascript复制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)
上面我们添加了一个带有缺失值的数据,输出结果如下:
代码语言:javascript复制['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、代码实现
这部分是在实现上面对多分类的处理后进行的,完整代码如下:
代码语言:javascript复制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
这样我们就实现了随机森林填充缺失值的操作。完整代码如下:
代码语言:javascript复制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
今天的内容就是这些