---
title: "特征工程"
output: 
  html_document:
    number_sections: TRUE
    toc: TRUE
    toc_float: TRUE
    code_folding: show
    fig_width: 7
    fig_height: 4.5
    theme: united
    highlight: tango
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE)
```


# 简介
<font size=4 face="微软雅黑">

***
&emsp;&emsp;在开始特征工程之前呢，请先思考一下到底是什么决定了模型的好坏？是一份高大上的代码？是一个复杂的算法？是拥有一份整齐的数据？还是……？都不是！

&emsp;&emsp;在数据科学界流传着一句经久不衰的话：数据和特征决定了机器学习的上限，而模型和算法只是逼近这个上限而已。那为什么决定性因素是特征工程呢？其实很简单，特征工程的目的就是最大限度地从原始数据中获取最重要的特征代入算法，从而训练出一个最好的模型，正所谓"Garbage in, garbage out"。不过呢，做好特征工程并非易事，业务、经验等均决定了特征工程的好坏，很多情况下还得就事论事。

&emsp;&emsp;特征工程主要包括两大部分：数据清洗、特征选择和降维，其中每部分均包含了很多内容，下面我就介绍一些常用的内容供大家学习和入门。

</font>


# 数据清洗
<font size=4 face="微软雅黑">

***
&emsp;&emsp;在工作中大部分的数据还是非常糟糕的，缺失值、异常值、数据维度过大都是经常发生的，这些问题在很大可能会严重影响到模型的质量和数据挖掘的效率，所以数据清洗是必不可缺的操作。

</font>


## 缺失值识别和处理
<font size=4 face="微软雅黑">

&emsp;&emsp;在任何规模的项目中，数据都可能由于未作答问题、设备故障或误编码数据的缘故而不完整。在R中，缺失值以符号 NA(Not Available) 表示。

&emsp;&emsp;缺失值不仅会在前期的探索性数据分析中产生负面影响，而且还可能导致部分模型无法构建，从而导致整个建模挖掘过程不能够完成。所以说缺失值对于整个流程的影响还是挺大的，我们必须得进行识别并进行相应的处理。

</font>

### 缺失值识别 {.tabset .tabset-fade .tabset-pills}
<font size=4 face="微软雅黑">

在R中有很多中识别缺失值的方法，它们各有千秋。下面我就给大家演示几种常用的缺失值识别的方法：

</font>

#### dataset
<font size=4 face="微软雅黑">

在进行缺失值识别之前，先获取R中VIM程序包里面自带的sleep数据集，并使用DT程序包中的 **datatable()**函数展示我们后续需要的数据。

</font>

<font size=2 face="微软雅黑">

<span style="color:red">注意：如果没有下载VIM和DT程序包，请先使用`install.packages("VIM")`和`install.packages("DT")`两个命令下载两个程序包之后再加载。</span>

</font>

```{r}

# install.packages("VIM")      # 下载VIM程序包
# install.packages("DT")       # 下载DT程序包
library(VIM)                   # 加载VIM程序包，使用sleep数据
library(DT)                    # 加载DT程序包，使用datatable()函数

data("sleep", package = "VIM") # 获取VIM程序包中的sleep数据集
datatable(sleep)               # 展示sleep数据集

```

#### anyNA()
<font size=4 face="微软雅黑">

**anyNA()**函数是R中快速识别数据集中是否存在NA的最简单的方法，可以作用于任何类型的数据集。直接把数据集代入函数，便可返回TURE(含有缺失值)或FALSE(不含缺失值)。

</font>

```{r}

anyNA(sleep)

```

<font size=4 face="微软雅黑">

上面的代码返回的结果是TURE，说明该数据集存在缺失值。但是并不知道数据中哪个位置是缺失值，也不知道还有多少个缺失值，下面我们会对缺失值逐步深入的探索。

</font>

#### is.na()
<font size=4 face="微软雅黑">

**is.na()**函数也是R中常用的判断是否存在缺失值的函数，同样可以作用于任何数据类型。相对于**anyNA()**函数而言，该函数会在缺失值所在的位置会返回TRUE，否则返回FALSE。为了节省篇幅，我们只去探索前五行数据含有缺失值的情况。

</font>

```{r}

is.na(sleep[1:5, ])

```

<font size=4 face="微软雅黑">

根据返回的结果，可以看出前五行数据中存在缺失值。但是对于含有少量缺失值的大数据而言，就不能很轻易地发现TRUE了，即不能轻易判断有无缺失值的存在。针对这个缺点，我们可以在外层嵌套一个**sum()**函数对返回的结果求和(TRUE=1,FALSE=0)，这样就可以根据返回的数值大小来判断含有缺失值的个数。

</font>

```{r}

sum(is.na(sleep))

```

<font size=4 face="微软雅黑">

嵌套函数之后，返回的结果是38，即sleep数据集中含有38个缺失值。

</font>

#### complete.case()
<font size=4 face="微软雅黑">

**complete.case()**函数也可以进行缺失值的识别，但是其与**is.na()**函数的识别方式有所不同。

* **is.na()**函数针对的是缺失值的具体位置进行识别，而**complete.case()**函数是针对的数据的行
* **is.na()**函数在缺失值所在的位置返回TRUE，而**complete.case()**函数在含有缺失值的行返回 FALSE

</font>

```{r}

complete.cases(sleep)

```

<font size=4 face="微软雅黑">

此函数返回的62个逻辑向量分别代表数据中每一个观测是否完整(FALSE表示不完整,TRUE表示完整)。从返回结果的第一个和第二个逻辑向量中可以看出：数据的第一行不完整(含有缺失值)，第二行完整(没有缺失值)。对照先前**is.na()**返回的结果，发现对于缺失值的识别结果是一致的。

</font>

#### aggr()
<font size=4 face="微软雅黑">

上面介绍了三种缺失值识别的方法，这三种方法虽然可以轻松地识别缺失值，但是并不能直接返回数据集中每列各含有多少个缺失值。而在工作中一般都是需要去了解数据集中每列数据的缺失情况，下面就要介绍VIM程序包中的**aggr()**函数，该函数不仅可以返回每列含有缺失值的个数，还可以可视化展示，更加生动形象。

在此函数中，主要使用以下5个参数：

aggr(x, delimiter = NULL, plot = TRUE, ...)

* x：一个向量、矩阵或者数据框
* plot：控制返回的结果是否使用图形。参数值可以是TRUE或FALSE，默认plot = TRUE
* prop：控制左图中返回缺失值的个数或者比例。参数值可以是TRUE或FALSE，默认prop = TRUE
* numbers：控制右图中是否显示具体数值。参数值可以是TRUE或FALSE，默认number = FALSE
* combined：控制左图是否与右图合并。参数值可以是TRUE和FALSE，默认combined = FALSE

</font>

```{r}

aggr(x = sleep, plot = FALSE) # 查看数据每列含有缺失值的个数

```

<font size=4 face="微软雅黑">

从返回的结果中可以看出：NonD、Dream、Sleep、Span 和 Gest 这5列中分别含有14、12、4、4、4个缺失值。

</font>

```{r}

# 可视化数据中每列含有缺失值的个数
aggr(x = sleep, plot = TRUE, prop = FALSE, numbers = TRUE, combined = TRUE) 

```

<font size=4 face="微软雅黑">

这次返回的是一个可视化的结果：左图横轴是各个变量名，纵轴是缺失值的个数；右图是一副合并的图，上方是左图，图右边的数字代表其所对应的色块代表的缺失值的个数。

</font>


### 缺失值处理 {.tabset .tabset-fade .tabset-pills}
<font size=4 face="微软雅黑">

上面我们介绍了三种缺失值识别的方法，接下来就该对其进行相应处理了。常用的缺失值的处理方法有直接剔除、简单方法(均值、中位数、特定值等)填补、高级方法(多重插补、rpart 插补、knn插补等)进行填补。以上提到的方法也只是其中的一部分，并且它们各有自己适用的情况，并没有严格上的最优方法。

</font>

<font size=2 face="微软雅黑">

<span style="color:red">注意：以下仅仅是为了演示缺失值处理的方式，并不考虑处理意义。</span>

</font>

#### na.omit()
<font size=4 face="微软雅黑">

**na.omit()**函数可以说是最简单的处理缺失值的方法，作用就是直接剔出含有缺失值的行。主要适用于样本量足够和剔除后不会引入偏差。优点就是简单方便，缺点就是可能会剔除关键的行，从而减少了数据的主要信息。

</font>

```{r}

datatable(na.omit(sleep)) # 直接删除含有缺失值的行，并展示返回的数据

```

#### impute()
<font size=4 face="微软雅黑">

对缺失值进行简单填补时，一般采用Hmice程序包中的**impute()**函数。简单的填补方法包括均值、中位数、特定值填补等方法，这些方法同样相对简单。主要适用于需要填补的自变量对于因变量的影响较小。

优点就是简单方便，并且可能会产生相当好的效果，缺点就是从一定程度上修改了数据，可能会破坏数据的信息，尤其是在缺失值过多的情况下。

在此函数中，主要使用以下2个参数：

impute(x, fun = median, ...)

* x：一个向量
* fun：如果需要填补的列是数值，则使用函数的名称(mean、median等)作为参数值；如果是非数值，则默认填补为出现频次最多的特征值，或者自定义填充值

</font>

```{r}

# install.packages("Hmice")
library(Hmisc)                                      # impute()

sleep$Sleep <- impute(x = sleep$Sleep, fun = mean)  # 用Sleep列的均值填补Sleep列的缺失值
sleep$Span  <- impute(x = sleep$Span, fun = median) # 用Span列的均值填补Span列的缺失值
sleep$NonD  <- impute(x = sleep$NonD, fun = 8)      # 用特定值“8”填补NonD列的缺失值

aggr(sleep, plot = F)                               # 展示此时数据缺失情况

```

<font size=4 face="微软雅黑">

上面返回的结果中可以看出：填补之后的数据集中仅有Dream和Gest两列存在缺失值，之前含有缺失值的3列已被成功填补。

</font>

#### mice
<font size=4 face="微软雅黑">

mice的全称是Multivariate Imputation by Chained Equations，这个程序包提供了多种高大上的缺失值填补方法，一般用于填补复杂的缺失问题。

其使用两步进行插值：

* 使用**mice()**生成返回数据的m个完整副本，并对每个副本进行相应地建模处理，生成不同的填补值进行填补
* 使用**complete()**返回这些完整的数据集中的一个(默认)或多个

在此函数中，主要使用以下3个参数：

* data：一个不完整的矩阵或者数据框
* m：返回的完整数据集的个数
* method：指定每列数据的估算方法

</font>

<font size=2 face="微软雅黑">

<span style="color:red">注：该函数中包含了很多参数，大家可以在加载mice程序包之后使用`help(mice)`查看帮助文档，获得更详细的信息。</span>

</font>

```{r}

# install.packages("mice")
library(mice)                                         # mice() / complete()
data("sleep", package = "VIM")

set.seed(45)                                          # 设置随机种子，保持每次插值的一致性
sleep.imp <- mice(data = sleep, m = 3, method = "rf") # 基于随机森林模型进行mice插值
sleep.mice <- complete(sleep.imp)                     # 返回完整数据

anyNA(sleep.mice)                                     # 查看此时的数据是否存在缺失值

```


#### knnImputation()
<font size=4 face="微软雅黑">

上面介绍了几种相对简单粗暴的填补缺失值的方法，下面我们使用高级一点的方法对缺失值进行填补。首先介绍 knn 插值法，此方法是使用 DMwR 程序包中的**knnImputation()**函数。此函数使用knn算法来填补缺失值(如果大家没学过knn算法也无所谓，后面的章节中会讲到)。

具体过程就是利用欧式距离，找出距离含有缺失值的观测最近的k个样本。然后根据这个k个邻居利用设定的方法("weighAvg"或"median")计算出填充值，最后利用此填充值进行填补。

优点就是可以一次性的填补所有的缺失值，并且可以设置邻居的个数，相当于拥有多个填补值；缺点就是计算量比较大，对于大量数据就无能为力了，太消耗时间。

在此函数中，主要使用以下4个参数：

knnImputation(data, k = 10, scale = T, meth = "weighAvg", distData = NULL)

* data：需要填补的数据框
* k：邻居的数量，默认 k = 10
* scale：在寻计算距离时是否进行标准化。参数值可以是 TRUE 和 FALSE，默认 scale = TRUE
* meth：计算填充值的方法。参数值可以是 median 和 weighAvg，默认 meth = "weighAvg"

</font>

```{r}

# install.packages("DMwR")
library(DMwR) # knnImputation()
data("sleep", package = "VIM")

set.seed(45)
# 使用knn算法插值
sleep.knn <- knnImputation(data = sleep, k = 5, scale = TRUE, meth = "weighAvg")

anyNA(sleep.knn)

```


## 离群值识别和处理
<font size=4 face="微软雅黑">

&emsp;&emsp;离群值是指数据集的每个变量中孤立于其他数据点的对象。在数据的收集中，由于各种因素的影响，离群点是很难避免的。尤其在人工输入数据的时候，经常会出现小数点输入错误的问题，比如123.12误输入为1231.2。

&emsp;&emsp;离群点同样会对数据挖掘产生一定的影响，如果不做离群点的处理，那么模型的输入就是存在偏差的。可想而知，一个有偏差的输入，很难得出无偏差的输出。

</font>


### 离群点识别 {.tabset .tabset-fade .tabset-pills}
<font size=4 face="微软雅黑">

&emsp;&emsp;在 R 中识别离群点的方法同样有很多，比如可视化方向的箱线图、统计学方向的高杠杆值点、dffits值、学生化残差、cook距离等。不过统计学方向的识别方法是基于回归的，现在学习这个有些吃力，所以在此我就只简单的介绍通用的可视化方法。

</font>

#### dataset
<font size=4 face="微软雅黑">

在进行离群点识别之前，先获取R中datasets程序包中自带的iris数据集和ggplot2程序包中自带的diamonds数据集，并且使用DT程序包中的**datatable()**函数展示我们后续需要的数据。

</font>



```{r}

## 下载程序包
# install.packages("ggplot2")
# install.packages("plotly")
# install.packages("DT")
# install.packages("tidyr")

library(ggplot2)                      # diamonds
library(plotly)                       # plot_ly()
library(DT)                           # datatable()
library(tidyr)                        # gather()

data("diamonds", package = "ggplot2") # 获取ggplot2程序包中的diamonds数据集
datatable(diamonds)

```


#### box
<font size=4 face="微软雅黑">

绘制箱线图也有很多种函数，比如**boxplot()**、**geom_boxplot()**、**plot_ly()**等。上面我们在可视化部分已经讲解了前两种方法，这里作为拓展，我们使用第三种方法绘制交互的箱线图。

此函数返回的图形会让我们方便的了解某个变量的分布情况，并且会把离群点(上边缘以上和下边缘以下)标记出来。不过对于含有较多离群点的变量来说，其返回的结果不能清晰的看到离群点的具体个数。

绘制箱线图时使用的**plot_ly()**函数就比较复杂了，这里作为拓展，我们只解释以下绘制箱线图的代码的参数：

* data：绘图所需要的数据
* y：指定箱线图的纵轴的数据
* type：指定绘制哪个图形。比如绘制箱线图时使用“box”参数值

</font>

```{r}

# 绘制iris数据集的Sepal.Width变量的箱线图
plot_ly(data = iris, y = ~Sepal.Width, type = "box")


## 随机抽取数据的1000条，绘制不同cut的price箱线图
set.seed(45)                          # 设定随机种子
idx <- sample(1:nrow(diamonds), 1000) # 从1:53940中随机产生1000个数字
diamonds.sam <- diamonds[idx, ]       # 提取此1000行数据，赋给diamonds.sam

# 绘制siamonds.sam的price变量箱线图
plot_ly(diamonds.sam, y = ~price, type = "box")

```

<font size=4 face="微软雅黑">

从上面的两个箱线图可以看出，第一个箱线图可以看到Sepal.Width中存在4个离群点。但是对于第二个箱线图，我们只能看出含有很多离群点(离群点都连成了一条线)，但是不能看出具体的个数，不过这个缺陷并不会对我们后续的离群点处理产生严重的影响。

</font>


### 离群点处理
<font size=4 face="微软雅黑">

&emsp;&emsp;针对于离群点的处理方法主要是：直接剔除含有异常值的观测、视为缺失值处理、均值修正(前后两个观测的均值替换)、用特定值替换(一般用于知道离群点出现的原因)。同样，处理的方法也是各有千秋。

&emsp;&emsp;需要注意的是，并不是说离群点必须要做处理。如果说离群点是一个正确的数据，比如说公司中不同职位的员工的工资汇总表就会出现很多离群点，所以说在很多情况下则可以直接进行挖掘。由于这个离群点的处理大都是由业务需求决定的，所以后面只演示剔除某个值以上的离群点。

&emsp;&emsp;后面的代码中会出现**%>%**，这个是dplyr包里面的管道函数。这个函数就是把前面的数据作为一个参数，传入后面的函数中，默认作为第一个参数，当然也可以自定义参数的位置。

</font>

<font size=2 face="微软雅黑">

<span style="color:red">注：**%>%**非常好用，在R中可以说是至关重要的一个函数，强烈建议大家多去使用此函数！</span>

</font>

```{r}

# install.packages("dplyr")
library(dplyr)              # filter()

## 假设需要剔除Sepal.Width的长度大于4的观测
iris.new <- iris %>% filter(Sepal.Width <= 4) # 筛选出Sepal.Width小于等于4的观测

# 绘制箱线图验证是否处理成功
plot_ly(data = iris.new, y = ~Sepal.Width, type = "box")

```


## 无量纲化
<font size=4 face="微软雅黑">

&emsp;&emsp;无量纲化就是消除量纲影响，量纲是表征物理量的性质(类别)，如时间、长度、质量等。那么不同的量纲又有什么影响呢？举个例子：若是研究成年男性的身高、年龄对于体重的影响，那么身高一般是150-190cm，而年龄则是18-100岁。若是直接将此数据对体重就行拟合，那么则很容易去强化某一变量而弱化另一变量的作用。

&emsp;&emsp;消除量纲最常用的两种方法是就是标准化和归一化。以下进行无量纲化操作使用的数据是R自带的Puromycin数据集。因为在此只为了进行单纯的无量纲化处理，所以就不去详细解释此数据集了，有兴趣的可以在R中输入`?Puromycin`命令去查看数据的详细信息。

</font>

```{r}

data("Puromycin")
datatable(Puromycin)

```

### 标准化
<font size=4 face="微软雅黑">

简单的说，标准化就是按照特征矩阵的列处理数据，通过使用z-score的方法，转换为标准正态分布。在R中使用**scale()**函数就可以简单的将数据进行标准化，该函数使用的是z-score标准化方法，该方法原理如下：
$$X_{new}=\frac{X-\mu}{\sigma}=\frac{X-Mean(X)}{StdDev(X)}$$

</font>

```{r}

## 对数据的第一列和第二列进行标准化，并把处理后的数据赋值给Puromycin.sacle
Puromycin.scale <- scale(Puromycin[, 1:2])
datatable(Puromycin.scale)

```

### 归一化
<font size=4 face="微软雅黑">

与标准化不同的是，归一化只是将样本的特征值转换到同一量纲下把数据映射到[0,1]区间内，却没有使数据标准化的作用。在R中最常用的归一化就是min-max方法，其原理如下：
$$X_{new}=\frac{X-min(X)}{max(X)-min(X)}$$

</font>

```{r}

## 笨方法——封装函数
# 封装归一化函数
normalize <- function(x) {
  return((x - min(x)) / (max(x) - min(x)))
}

## 对数据的前两列进行归一化
Puromycin.nor <-
  lapply(Puromycin[1:2], normalize) %>% # 进行标准化，返回列表
  data.frame()                          # 将列表转换为数据框

datatable(Puromycin.nor)


## 好方法
# install.packages("caret")
library(caret)

data("Puromycin")                                   # 获取数据
standard <- preProcess(Puromycin, method = 'range') # 针对数据设定归一化方法
datatable(predict(standard, Puromycin))             # 对数据进行归一化

```

<font size=4 face="微软雅黑">

心细的同学们肯定发现两种方法代入的数据有所不同，笨方法中指定对前两列进行归一化，而好方法中则不需指定。这正是笨方法与好方法的区别：好方法可以自动识别数值进行标准化，避免繁琐的操作。

</font>


## 特征重编码
<font size=4 face="微软雅黑">

特征重编码就是把数据集中的变量值进行重塑，无论是定量特征还是定性特征都可以进行重编码。比如说把“性别”这个定性变量中的“男”、“女”重塑为“0”、“1”。这样做的目的主要就是为了让数据适应不同的算法要求，因为不同的算法对数据类型的要求不同。

</font>

### 定量特征重塑
<font size=4 face="微软雅黑">

针对于定量特征的重编码主要就是二值化。常用于概率模型中，将概率小于等于0.5的转换为0，否则转换为1。

下面我们随机生成10个0-1之间的随机数，然后进行二值化变换。

</font>

```{r}

set.seed(45)
mydata <- runif(10, 0, 1) # 生成10个0-1之间的随机数

# 对其进行二值化变换，小于等于0.5的赋值为0，否则赋值为1
mydata.new <- ifelse(mydata <= 0.5, 0, 1)

```

### 定性特征重塑
<font size=4 face="微软雅黑">

针对于定性特征的重编码主要就是构建独热(One-Hot)编码。简而言之，就是将一个包含n个特征的变量转换为n个变量，转换后的变量中只会出现0、1两个特征。

下面我们构建一个拥有三个特征的变量，然后进行独热编码。

</font>

```{r}

# 构建一个数据框，赋值为mydata，并展示数据集
mydata <- data.frame(letters = c("A", "B", "C", "A", "C")); mydata

# install.packages("dummies")
library(dummies)

# 对mydata进行独热编码，赋值为mydata.oh，并展示数据
mydata.oh <- dummy.data.frame(mydata, names = c('letters'), sep = '.'); mydata.oh

```

# 特征选择
<font size=4 face="微软雅黑">

***
&emsp;&emsp;一般而言，我们面对的数据集都会拥有很多特征，那么就可能存在有些特征对后续的分析挖掘是没有用处的，因而我们需要将其识别并剔除，这个过程就是特征选择。周志华老师的《机器学习》上面给出的特征选择的定义是：从给定的特征集合中选择出相关特征子集的过程。

&emsp;&emsp;特征选择不仅可以降低维灾难问题，而且可以降低模型的过拟合问题，从而可以在一定程度上提升模型的精确度、降低建立模型的时间。

目前有两种常用的特征选择的方法：**封装法、过滤法**

</font>


## 封装法
<font size=4 face="微软雅黑">

**封装法：**特征选择的过程依赖于模型的训练，以模型的预测能力作为特征选择的评价准则。简单的说，封装法选择恶特征就是适用于该算法的最好的特征。该方法所筛选的变量具有高质量性，但是运行速度会比较慢。

在R中我们可以使用 caret 包中的**rfe()**函数结合**rfeControl()**函数进行封装法的特征选择。

**rfeControl()**函数用来设置**rfe()**函数的控制参数，该函数中几个重要的参数如下：

* **functions：**指定模型拟合、预测和特征选择的函数。可以是lmFuncs、rfFuncs、treebagFuncs、nbFuncs等。
* **rerank：**布尔类型参数，在每次特征删除的过程中是否重新计算特征的重要性，默认为False。
* **method：**指定抽样的方法，可以是boot、cv、LOOCV和LGOCV。
* **saveDetails：**是否保存特征选择过程中的预测值和变量重要性，默认为FALSE。
* **number：**指定折数或者重抽样迭代次数,当method为cv或repeatedcv时，则默认从总体中抽取10份样本并迭代10次，否则抽取25份并迭代25次。
* **repeats：**指定抽样组数，默认抽取一组样本。
* **verbose：**是否返回每次重抽样的详细信息，默认为FALSE。
* **returnResamp：**返回重抽样的汇总信息。
* **p：**如果指定method为LGOCV时，该参数起作用，指定训练集的比重。
* **seeds：**为抽样设置随机种子。
* **allowParallel：**在并行后台已加载和允许的情况下，是否允许并行运算。

**rfe()**函数用来进行特征帅选，该函数主要使用以下几个参数：

* **x：**指定输入变量。
* **y：**指定输出变量。
* **sizes：**通过一个整数向量，指定需要保留的特征数量。
* **metric：**指定衡量最优模型的判断指标，默认使用RMSE和Rsquared衡量回归模型，使用Accuracy和Kappa系数衡量分类模型。
* **maximize：**布尔类型参数，如果metric为RMSE,则不要求metric最小化，否则要求Kappa系数、判决系数最大化和精确度达到最大化。
* **rfeControl：**指定rfe函数的控制参数。

下面我们针对鸢尾花数据集使用封装法进行变量的筛选：

</font>


```{r}

# install.packages("caret")
library(caret)

## 设置rfe的控制参数
rfeControls.rf <- rfeControl(
  functions = rfFuncs,                     # 使用随机森林算法
  method = 'cv',                           # 使用交叉验证的抽样方法
  number = 3)                              # 指定3折交叉验证

## 使用rfe函数进行特征选择
rfe.rf <- rfe(x = iris[, -5],              # 指定输入变量
              y = iris[, 5],               # 指定输出变量
              sizes = seq(1, 4, 1),        # 指定保留特征的数量
              rfeControl = rfeControls.rf) # 指定控制参数

rfe.rf                                     # 展示筛选结果

```

<font size=4 face="微软雅黑">

通过上面封装法输出的特征筛选结果，可以看出最终选取了Petal.Width、Petal.Length两个变量作为自变量。

</font>

## 过滤法
<font size=4 face="微软雅黑">

**过滤法：**与上述讲解的封装法最大的不同之处就是其特征选择过程与模型的建立无关，而是通过分析变量内部的关系进行特征的筛选。该方法在运行速度上会相对快一些，但是很可能会剔除比较重要的变量，从而导致数据信息的丢失。

在R中我们可以使用 caret 包中的**sbf()**函数结合**sbfControl()**函数进行封装法的特征选择。

**sbfControl()**函数用来设置**sbf()**函数的控制参数，该函数中几个重要的参数如下：

* **functions：**指定模型拟合、预测和特征选择的函数。可以是lmSBF、rfSBF、treebagSBF、nbSBF等。
* **method：**指定抽样的方法，可以是boot、cv、LOOCV和LGOCV。
* **saveDetails：**是否保存特征选择过程中的预测值和变量重要性，默认为FALSE。
* **number：**指定折数或者重抽样迭代次数,当method为cv或repeatedcv时，则默认从总体中抽取10份样本并迭代10次，否则抽取25份并迭代25次。
* **repeats：**指定抽样组数，默认抽取一组样本。
* **verbose：**是否返回每次重抽样的详细信息，默认为FALSE。
* **returnResamp：**返回重抽样的汇总信息。
* **p：**如果指定method为LGOCV时，该参数起作用，指定训练集的比重。
* **seeds：**为抽样设置随机种子。
* **allowParallel：**在并行后台已加载和允许的情况下，是否允许并行运算。

**sbf()**函数用来进行特征帅选，该函数主要使用以下几个参数：

* **x：**指定输入变量。
* **y：**指定输出变量。
* **sbfControl：**指定sbf函数的控制参数。

下面我们针对鸢尾花数据集使用过滤法进行变量的筛选：

</font>

```{r}

## 构建sbf函数的控制参数
sbfControls.rf <- sbfControl(
  functions = rfSBF,                       # 使用随机森林算法
  method = 'cv',                           # 使用交叉验证的抽样方法
  number = 3)                              # 指定3折交叉验证

#使用sbf函数进行特征选择
sbf.rf <- sbf(x = iris[, -5],              # 指定输入变量
              y = iris[, 5],               # 指定输出变量
              sizes = seq(1, 4, 1),        # 指定保留特征的数量
              sbfControl = sbfControls.rf) # 指定控制参数

sbf.rf                                     # 展示筛选结果

```

<font size=4 face="微软雅黑">

通过上面过滤法输出的特征筛选结果，可以看出最终选择了所有的自变量作为自变量。

**值得注意的是，特征选择是经常用到的，上面我们也只是举了两个简单的例子去帮助大家入门。实际应用中还得需要多次的调参才可能选取出最佳的特征。**

</font>


# 参考文献
<font size=4 face="微软雅黑">

[1] 卡巴科弗. R语言实战[M]. 人民邮电出版社, 2013.<br>
[2] 周志华. 机器学习 : = Machine learning[M]. 清华大学出版社, 2016.

</font>