用随机森林填补缺失值
1 引言
科研工作中采集到的数据集总会存在大量缺失值,通常直接删掉一整行效率更高,但有时候会遇到不能删掉必须填充的情况,因此本文将利用随机森林来填充波士顿数据集,并且将原始数据集,0和均值作为比较对象来验证随机森林在填充缺失值上的优越性。
2 导入的库
这里用到了sklearn中impute模块的simpleImputer函数来填充0和均值
from sklearn.datasets import load_boston
from sklearn.ensemble import RandomForestRegressor
from sklearn.impute import SimpleImputer # 填补缺失值的类
from sklearn.model_selection import cross_val_score
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
3 数据预处理
这里我们要对波士顿数据集做一个预处理,因为它本身是完整的,所以我们需要对其进行人为的缺失操作。
3.1 引入该数据集。
dataset = load_boston()
x_full, y_full = dataset.data, dataset.target
print(x_full.shape)
由于波士顿数据集存在道德问题,所以运行的时候会出现如下警告,并且还提供了不会产生警告的代码让我们进行研究,这里我们不管它,该警告不会影响我们代码运行的。
我们来看标签输出结果,可以明显发现是一个连续标签,因此这里肯定做回归,要用回归器,这里我们用随机森林RandomForestRegressor模型。并且发现特征矩阵尺寸是506*13
3.2 产生缺失值
首先确定我们希望放入的缺失数据的比例,一共是506*13=6578个数据,在这里我们假设是50%,那总共就要有3289个数据缺失。
n_samples = x_full.shape[0] # 样本数
n_features = x_full.shape[1] # 特征数
rng = np.random.RandomState(0) # 设置随机种子
missing_rate = 0.5 # 确定缺失比例
n_missing_samples = int(np.floor(n_samples * n_features * missing_rate))
所有缺失数据会随机遍布在数据集的各行各列当中,而一个缺失的数据会需要一个行索引和一个列索引,本文将创造一个数组,包含3289个分布在0-506中间的行索引,和3289个分布在0-13之间的列索引,从而可以利用索引来为数据中的任意3289个位置赋空值,然后本文将用0,均值,随机森林来填写这些缺失值,查看回归的结果。
miss_features = rng.randint(0, n_features, n_missing_samples) # 获取列索引
miss_samples = rng.randint(0, n_samples, n_missing_samples) # 获取行索引
x_missing = x_full.copy()
y_missing = y_full.copy()
x_missing[miss_samples, miss_features] = np.nan
x_missing = pd.DataFrame(x_missing)
这里本文使用randint函数来创建索引数组,对于一般数据集来说,缺失值可能占数据集的10%左右,对于这种缺失值很少的情况也可以使用choice函数来创建索引数组,此时产生的索引是不重复的,并且用空值nan进行填充,缺失数据集如下:
4 使用0和均值填补
这里简单介绍下SimpleImputer,它一共有三个重要参数分别是missing_values, strategy和fill_value,miss_values是要告诉python数据集的缺失值形式,比如nan,-999等,strategy是确定填充值类型,比如均值,常数,中位数和众数等,当strategy='constant’常数时才会用到fill_value用来确定填充的常数值,这里本文由于填充0,因此赋值其为0。
# 使用均值进行填补
imp_mean = SimpleImputer(missing_values=np.nan, strategy='mean') # 实例化
x_missing_mean = imp_mean.fit_transform(x_missing) # 训练fit+导出
fit_transform接口可以同时训练和导出,然后我们来查看填充后的情况。
print(pd.DataFrame(x_missing_mean))
可以很明显的发现空值nan都被均值填充了。我们再用isnull函数来查看空值情况。
print(pd.DataFrame(x_missing_mean).isnull())
这里只要是空值则会返回False,然后对每列求和也可以发现每列都等于0,这是由于布尔值False等于0因此当某列都等于0时就可以说明该列无空值。
print(pd.DataFrame(x_missing_mean).isnull().sum()) # 有无空值,有返回True,无返回False,大于0说明有空值
同理我们来看用0填充的情况:
imp_0 = SimpleImputer(missing_values=np.nan, strategy='constant', fill_value=0)
x_missing_0 = imp_0.fit_transform(x_missing)
print(pd.DataFrame(x_missing_0))
至此我们已经填充了0和均值,接下来将利用随机森林回归来填充缺失值
5 利用随机森林回归来填充
5.1 填充思路
首先,我们需要明确一个概念,即测试集的标签不能有缺失值。因此,我们可以根据这个基本原则将接下来的新标签分成两部分。一部分是我们需要预测的空值,作为测试集的标签;另一部分则是剩下的标签值,作为训练集的标签。
此外,我们还需要划分特征矩阵。将包含空值标签的行组成的矩阵作为测试集的特征矩阵,而不包含空值的标签的行组成的矩阵作为训练集的特征矩阵。需要注意的是,我们处理的是原始特征矩阵,而原始标签将与新特征矩阵合并。
其次,当有多列包含空值的特征时,需要先将含空值最少的特征作为新标签,因为缺失数据少所需要的准确信息也少,当只剩下最后一列含空值的特征时,其余的特征都已经十分完整,因此可以很好地预测这列含空值最多的特征。
最后,我们需要找出数据集中,缺失值从小到大的特征们的顺序以及其索引,这里本文依旧采用对各列空值布尔值求和的思路进行排序。注意,如果这里用sort排序会损失索引,用argsort排序则会得到排序后的值对应索引的顺序,然后用values调取该排序。
x_missing_reg = x_missing.copy()
sort_index = np.argsort(x_missing_reg.isnull().sum(axis=0)).values
至此,前置工作已经做完,接下来将展示用随机森林填充缺失值的具体代码:
for i in sort_index:
# 构建我们的新特征矩阵(没有被选中去填充的特征+原始的标签)和新标签(被选中去填充的特征)
df = x_missing_reg
# 新标签
fill_c = df.iloc[:, i]
# 新特征矩阵
df = pd.concat([df.iloc[:, df.columns != i], pd.DataFrame(y_full)], axis=1)
# 在新特征矩阵中,对含有缺失值的列,进行0的填补
df_0 = SimpleImputer(missing_values=np.nan, strategy='constant', fill_value=0).fit_transform(df)
# 找出我们的训练集和测试集
Ytrain = fill_c[fill_c.notnull()]
Ytest = fill_c[fill_c.isnull()] # 为Xtest提供索引
Xtrain = df_0[Ytrain.index, :]
Xtest = df_0[Ytest.index, :]
# 用随机森林回归来填补缺失值
rfg = RandomForestRegressor(n_estimators=100)
rfg = rfg.fit(Xtrain, Ytrain)
Ypredict = rfg.predict(Xtest) # 用predict接口将Xtest导入,得到我们的预测结果,就是我们要用来填补空值的这些值
# x_missing_reg.iloc[Ytest.index, i] = Ypredict # 用索引找到缺失值
x_missing_reg.loc[x_missing_reg.iloc[:, i].isnull(), i] = Ypredict # 用布尔值判断找到缺失值
6 四种填充方式的对比
6.1 交叉验证
目前为止,已经有了四种数据集,分别是原始数据集,填充0的数据集,填充均值的数据集以及随机森林回归填充的数据集。
接下来我们将通过交叉验证来计算各自的均方误差(mean_squared_error,简称MSE)进行比较,MSE越小说明填充效果越好。
x = [x_full, x_missing_mean, x_missing_0, x_missing_reg]
mse = [] # 越小越好
for i in x:
estimator = RandomForestRegressor(random_state=0, n_estimators=100) # 实例化
scores = cross_val_score(estimator, i, y_full, scoring='neg_mean_squared_error', cv=5).mean()
mse.append(scores*-1)
6.2 对比可视化
x = [x_full, x_missing_mean, x_missing_0, x_missing_reg]
mse = [] # 越小越好
for i in x:
estimator = RandomForestRegressor(random_state=0, n_estimators=100) # 实例化
scores = cross_val_score(estimator, i, y_full, scoring='neg_mean_squared_error', cv=5).mean()
mse.append(scores*-1)
x_labels = ['Full Data',
'Mean Imputation',
'Zero Imputation',
'Regressor Imputation']
colors = ['r', 'g', 'b', 'orange']
plt.figure(figsize=(12, 6)) # 画出画布
ax = plt.subplot(111) # 添加子图
for i in np.arange(len(mse)):
ax.barh(i, mse[i], color=colors[i], alpha=0.6, align='center')
ax.set_title('Imputation Techniques with Boston Data')
ax.set_xlim(left=np.min(mse)*0.9,
right=np.max(mse)*1.1)
ax.set_yticks(np.arange(len(mse)))
ax.set_xlabel('R Square')
ax.invert_yaxis() # 反转y轴
ax.set_yticklabels(x_labels)
plt.show()
可以发现用随机森林回归的MSE最小,说明填充效果很好,不过它现在比原数据集的MSE还要小,可能存在过拟合风险,需要注意。