特征工程-使用随机森林填补缺失值

2021-12-30 16:08:03 浏览数 (1)

一、前言

特征工程在传统的机器学习中是非常重要的一个步骤,我们对机器学习算法的优化通常是有限的。如果在完成任务时发现不管怎么优化算法得到的结果都不满意,这个时候就可以考虑回头在做一下特征工程。

二、缺失值填补

在特征工程中,对缺失值的处理是很常见的一个问题。处理方法通常如下:

  1. 删除有缺省值的数据
  2. 使用数据中该特征的均值填充缺失值
  3. 使用数据中该特征的中位数填充缺失值
  4. 使用数据中该特征的众数填充缺失值
  5. 使用机器学习模型对缺失值进行填充

上面的方法各有优点,我们可以根据自己的需求来选择策略。在数据集比较大时,最后一种方式是综合表现比较好的。今天我们就来讲讲使用随机森林来进行缺失值的填补。

三、数据预处理

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

今天的内容就是这些

0 人点赞