详解随机森林-用随机森林回归填补缺失值【菜菜的sklearn课堂笔记】

时间:2022-11-02 17:15:39

视频作者:[菜菜TsaiTsai] 链接:[【技术干货】菜菜的机器学习sklearn【全85集】Python进阶_哔哩哔哩_bilibili]

我们现实收集到的数据往往是有缺失值的,我们可以选择含有缺失值的数据,有时候填补缺失值会比直接丢弃样本效果更好,即便我们其实并不知道缺失值的真实样貌。 在sklearn中,我们可以使用sklearn.impute.SimpleImputer来将均值、中值、众数或者其他常用的数值填补到缺失数据中 在这个案例,我们将使用均值、0、随机森林回归来填补缺失值,并验证四种状况下的拟合情况,找出对使用的数据集来说最佳的缺失值填补方法

SimpleImputer(
    ['missing_values=nan', "strategy='mean'", 'fill_value=None', 'copy=True'],
)
# 后面章节会更详细的讲
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import load_boston
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score

导入数据

dataset = load_boston()
dataset.data.shape
---
(506, 13)

# 总共506*13=6578个数据
X_full, y_full = dataset.data, dataset.target
n_samples = X_full.shape[0]
n_features = X_full.shape[1]

为完整的数据放入缺失值 首先我们要确定希望放入缺失数据的比例,在这里我们假设是50%

rng = np.random.RandomState(0)
missing_rate = 0.5
n_missing_samples = int(np.floor(n_samples * n_features * missing_rate))
n_missing_samples # 总共有3289个数据缺失
---
3289

有数据要随机遍布在数据集的各行各列中,而一个缺失的数据需要一个行索引和一个列索引。 如果我们能创造一个数组,包含3289个分布在0-506中间的行索引,和3289个分布在0-13之间的列索引,那我们就可以利用索引来为数据中的任意3289个位置赋空值,然后我们用0、均值和随机森林来填写这些缺失值,然后查看回归结果如何

missing_features = rng.randint(0,n_features,n_missing_samples)
missing_samples = rng.randint(0,n_samples,n_missing_samples)
# randint(下限,上限,n),在[下限,上限)之间随机取出n个整数
missing_samples = > rng.choice(n_samples,n_missing_samples,replace=False)

我们现在采样了3289个数据,远远超过了我们的样本量506,所以我们使用随机抽取的函数randint。但如果我们需要的数据量小于我们的样本量506,那我们可以采用no.random.choice来抽样,choice会随机抽取不重复的随机数,因此可以帮助我们让数据更加分散,确保数据不会集中在一些行中,它的区间下限固定为0,上限自己指定,依旧是$[0,上限)$

X_missing = X_full.copy()
y_missing = y_full.copy()

X_missing[missing_samples,missing_features] = np.nan

X_missing = pd.DataFrame(X_missing)
# 转换成DataFrame是为了后续方便各种操作

numpy对矩阵的运算快,但在索引等功能上不如pandas

X_missing.head()
---
	0	1	2	3	4	5	6	7	8	9	10	11	12
0	0.00632	18.0	2.31	NaN	NaN	NaN	65.2	NaN	NaN	296.0	15.3	396.9	NaN
1	0.02731	NaN	7.07	NaN	0.469	NaN	78.9	4.9671	2.0	242.0	17.8	NaN	9.14
2	NaN	NaN	NaN	NaN	0.469	7.185	61.1	4.9671	NaN	242.0	NaN	NaN	4.03
3	NaN	NaN	NaN	NaN	NaN	NaN	45.8	6.0622	NaN	222.0	18.7	NaN	NaN
4	NaN	0.0	2.18	NaN	0.458	7.147	NaN	6.0622	3.0	222.0	NaN	NaN	5.33

用均值填补缺失值

imp_mean = SimpleImputer(missing_values=np.nan,strategy='mean')# 实例化
X_missing_mean = imp_mean.fit_transform(X_missing)
# 意为把X_missing中的值导入到模型imp_mean中,计算,完毕后返回给X_missing_mean

imp_mean.fit_transform输入的可以是DataFrame也可以是ndarray

imp_mean.fit_transform(np.array([1,2,np.nan]).reshape(-1,1))
---
array([[1. ],
       [2. ],
       [1.5]])
pd.DataFrame(X_missing_mean).isnull().sum().sum()
# 如果都不是空值则都是False,加和就为0(默认以列为单位加和,得到结果为Series,然后再加和)
---
0

用0值填补缺失值

imp_0 = SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0)

X_missing_0 = imp_0.fit_transform(X_missing)
pd.DataFrame(X_missing_0).isnull().sum().sum()
---
0

使用随机森林回归填补缺失值 任何回归都是从特征矩阵中学习,然后求解连续型标签y的过程,之所以能够实现这个过程,是因为回归算法认为,特征矩阵和标签之间存在着某种联系。 实际上,标签和特征是可以相互转换的,比如说,在一个“用地区,环境,附近学校数量”预测“房价”的问题中,我们既可以用“地区”,“环境”,“附近学校数量”的数据来预测“房价”,也可以反过来,用“环境”,“附近学校数量”和“房价”来预测“地区”。而回归填补缺失值,正是利用了这种思想。 对于一个有n个特征的数据来说,其中特征T有缺失值,我们就把特征T当做标签,其他的n-1个特征和原来的特征标签组成的特征矩阵。那对于T来说,它没有缺失的部分,就是Y_test,这部分数据既有标签也有特征,而它缺失的部分,只有特征没有标签,就是我们需要预测的部分

  • 特征T不缺失的值对应的其他n-1个特征 + 本来的标签组成X_train
  • 特征T不缺失的值组成Y_train
  • 特征T缺失的值对应的其他n-1个特征 + 本来的标签组成X_test
  • 特征T缺失的值因为是未知的,组成我们需要预测的Y_test

这种做法,对某一个特征大量缺失,其他特征却很完整的情况,非常适用。

如果数据中除了特征T,还有其他特征也有缺失值,我们从缺失最少的开始填补,因为填补缺失最少的特征需要的准确信息最小。填补一个特征时,其他特征的缺失值先用0代替,每完成一次回归预测,就将预测值放到原来的特征矩阵中,然后继续填补下一个特征。 每次填补完毕,有缺失值的特征会减少一个,所以每次循环后,需要用0来填补的特征就越来越少。当进行到最后一个特征时(这个特征应该是所有特征中缺失值最多的),已经没有任何的其他特征需要用0来进行填补了,而我们已经使用回归为其他特征填补了大量有效信息,可以用来填补缺失最多的特征。 遍历所有的特征后,数据就完整,不再有缺失值了。

X_missing_reg =X_missing.copy()

# 找出数据集中,缺失值从少到多排列的特征的顺序
sortindex = np.argsort(X_missing_reg.isnull().sum(axis=0)).values
# X_missing_reg.isnull().sum(axis=0)返回Series
# np.sort返回array,会丢失索引
# np.argsort返回从小到大排序的顺序所对应的索引
np.argsort(X_missing_reg.isnull().sum(axis=0))
---
0      6
1      8
2      0
3      9
4     12
5      2
6     10
7      4
8      7
9      5
10     1
11     3
12    11
dtype: int64

这里我们先拿出缺失数据最少的一列i来说 构建我们的新特征矩阵(没有被选中去填充的特征+原始的标签)和标签(被选中去填充的特征) 每次预测完一个特征后用得到的列替换X_missing_reg

df = X_missing_reg # 一开始也就是我们构造的X_missing
# 这里给不给copy()都一样,下面pd.concat的inplace=True,也会创建副本
fillc = df.iloc[:,i]
df = pd.concat([df.iloc[:,df.columns != i],pd.DataFrame(y_full)],axis=1)
# pd.concat([],axis),列表中写要连的DataFram,axis选择轴线,axis=0延长,axis=1增宽

如果之前使用了

df = X_missing_reg.copy()

那么我们这里可以用,所以我们也可以直接df.drop(i,axis=1),或者del df[:,i]来替代df.iloc[:,df.columns != i] 这里由于不是copy,所以选择切片并让concat返回副本的方式避免更改原数据

实例化并用0填充空值

df_0 = SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0).fit_transform(df)

找出训练集和测试集,训练集是被选中要填充的特征中(现在是我们的标签)的非空值

Ytrain = fillc[fillc.notnull()]
Ytest = fillc[fillc.isnull()]

我们需要的不是Ytest的值,需要的是Ytest的索引,用Ytest的索引找到Xtest然后用Xtrain、Ytrain训练出来的模型预测得到Ytest,填补改列的缺失值

# 在新特征矩阵上,被选出来的要填充的特征的非空值所对应的数据
Xtrain = df_0[Ytrain.index,:]
# 在新特征矩阵上,被选出来的要填充的特征的空值所对应的记录
Xtest = df_0[Ytest.index,:]

用随机森林回归来填补缺失值

rfc = RandomForestRegressor(n_estimators=50) #实例化
rfc = rfc.fit(Xtrain,Ytrain)
Ypredict = rfc.predict(Xtest)

用predict接口将Xtest导入,得到我们预测的结果(回归结果),就是我们要用来填补空值的这些值

X_missing_reg.loc[X_missing_reg.iloc[:,i].isnull(),i] = Ypredict
# iloc不支持使用boolSeries进行索引,但loc可以,可以取Series的values,或者ix.[](官方不建议使用ix)

布尔索引会直接去除掉False的值

pd.Series([1,2,1,1,3])[pd.Series([1,2,1,1,3])==1]

取Series的values,或者ix.

a = pd.DataFrame(rng.randint(0,10,(4,5)))
a.iloc[pd.Series([True,True,False,True]).values,1],a.ix[pd.Series([True,True,False,True]),1]
---
(0    0
 1    5
 3    8
 Name: 1, dtype: int32, 0    0
 1    5
 3    8
 Name: 1, dtype: int32)

到此对于i的操作就完毕了,整合在一起循环即可填补所有缺失值

for i in sortindex:
    df = X_missing_reg 
    fillc = df.iloc[:,i]
    df = pd.concat([df.iloc[:,df.columns != i],pd.DataFrame(y_full)],axis=1)
    df_0 = SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0).fit_transform(df)
    Ytrain = fillc[fillc.notnull()]
    Ytest = fillc[fillc.isnull()]
    Xtrain = df_0[Ytrain.index,:]
    Xtest = df_0[Ytest.index,:]
    rfc = RandomForestRegressor(n_estimators=50) 
    rfc = rfc.fit(Xtrain,Ytrain)
    Ypredict = rfc.predict(Xtest)
    X_missing_reg.loc[X_missing_reg.iloc[:,i].isnull(),i] = Ypredict

填补结果

X_missing_reg.head()
---
0	1	2	3	4	5	6	7	8	9	10	11	12
0	0.006320	18.00	2.3100	0.06	0.500220	6.37186	65.20	4.118102	3.88	296.0	15.300	396.9000	10.1476
1	0.027310	11.83	7.0700	0.06	0.469000	6.20716	78.90	4.967100	2.00	242.0	17.800	390.7672	9.1400
2	0.129802	14.05	3.1026	0.02	0.469000	7.18500	61.10	4.967100	4.42	242.0	16.556	393.1716	4.0300
3	0.053528	16.90	3.3406	0.20	0.452401	7.24684	45.80	6.062200	3.50	222.0	18.700	391.7752	5.2614
4	0.062054	0.00	2.1800	0.16	0.458000	7.14700	28.64	6.062200	3.00	222.0	16.854	389.4170	5.3300

X_missing_reg.isnull().sum().sum()
---
0

对填充好的数据进行建模预测

X = [X_full,X_missing_mean,X_missing_0,X_missing_reg]
mse = []
for x in X:
    estimator = RandomForestRegressor(random_state=0,n_estimators=100)#实例化
    scores = cross_val_score(estimator,x,y_full,scoring='neg_mean_squared_error',cv=5).mean()
    mse.append(scores * -1)

mse # mse越小越好
---
[21.62860460743544, 47.342475892156855, 44.379830132149095, 15.169715537895552]

[*zip(['X_full','X_missing_mean','X_missing_0','X_missing_reg'],mse)]
---
[('X_full', 21.62860460743544),
 ('X_missing_mean', 47.342475892156855),
 ('X_missing_0', 44.379830132149095),
 ('X_missing_reg', 15.169715537895552)]

用所得结果画出条形图

x_labels = ['Full data','Mean Imputation','Zero Imputation','Regressor Imputation']
colors = ['r','g','b','orange']

plt.figure(figsize=(12,6))# 画出画布,并指定使用改画布
ax = plt.subplot(111)# 添加子图
# 111第一个1指第一行,第二个1指第一列,第三个1指该子图的第一个图

for i in np.arange(len(mse)):
    ax.barh(i,mse[i],color=colors[i],alpha=0.6,align='center')
    # 因为都是ax.所以都画在subplot(111),也就在同一个图里

ax.set_title('Imputation Techniques with Boston Data')
ax.set_xlim(left=np.min(mse)*0.9,right=np.max(mse)*1.1)# 不从0显示x轴,显示最小值的0.9倍到最大值的1.1倍
ax.set_yticks(np.arange(len(mse)))# y轴显示序号
ax.set_yticklabels(x_labels)
ax.invert_yaxis()# 翻转y轴正方向,有没有没啥关系
plt.show()