R语言的链式操作

如果说R语言做数据分析中有什么神器,那必定是来自 **magrittr** 包的“管道操作符”莫属。说实话,当习惯了使用管道操作符之后,再用 Pandas 经常会觉得麻烦,虽然 Pandas 也支持链式操作,但有些操作还是不支持的。本文介绍一下R语言中几种常用的管道操作符,一定会对数据分析体验有一个质的飞跃。

# 最重要的操作符: `%>%`

虽然R语言出了一个原生的管道运算符 `|>` ,而且字符数要少一个,但是功能是完全无法和 `%>%` 相比的。而且做数据分析的时候免不了要加载 **tidyverse** ,所以不如直接使用 `%>%` 运算符。该运算符非常重要,值得在 IDE 中给它安排一个快捷键。

## 示例

为了展示其作用,这里举一个例子。假设现在我们要对 iris 数据集进行分析。


| Sepal.Length | Sepal.Width | Petal.Length | Petal.Width | Species |
| -------------- | ------------- | -------------- | ------------- | --------- |
| 5.1          | 3.5         | 1.4          | 0.2         | setosa  |
| 4.9          | 3.0         | 1.4          | 0.2         | setosa  |
| 4.7          | 3.2         | 1.3          | 0.2         | setosa  |
| 4.6          | 3.1         | 1.5          | 0.2         | setosa  |
| 5.0          | 3.6         | 1.4          | 0.2         | setosa  |
| 5.4          | 3.9         | 1.7          | 0.4         | setosa  |
| 4.6          | 3.4         | 1.4          | 0.3         | setosa  |
| 5.0          | 3.4         | 1.5          | 0.2         | setosa  |

首先我们要选出 Species 属性是 setosa 的部分,然后用 **ggplot** 绘制散点图。如果使用原生函数,有两种模式。一种是创建中间变量

```r
iris_setosa <- subset(iris, Species == "setosa")
ggplot(iris_setosa, aes(Sepal.Length, Sepal.Width)) + geom_point()
```

另一种是函数嵌套

```r
ggplot(subset(iris, Species == "setosa"), aes(Sepal.Length, Sepal.Width)) + geom_point()
```

这两种模式随着数据处理过程的增长,都有不方便的地方。前者会创建越来越多的中间变量,会占用大量内存;而且中间变量的命名是个问题,变量名不可避免地越来越长,既不便于书写,也不容易记忆。后者会让代码越来越长,非常不便于阅读,而且代码出了问题也不便于定位错误。

而如果用管道操作符,以上问题迎刃而解。

```r
subset(iris, Species == "setosa") %>%
    ggplot(aes(Sepal.Length, Sepal.Width)) + geom_point()
```

既没有创建中间变量,也不会让代码变得非常长,甚至还缩短了代码长度,可谓是一箭双雕还送了只鸟。

如果和 **dplyr** 、 **purrr** 等包函数搭配使用,则非常方便

```r
iris %>%
    filter(Species == "setosa") %>%
    mutate(Sum.Length = Sepal.Length + Petal.Length) %>%
    ggplot(aes(Sum.Length)) + geom_histogram(binwidth = 0.2)
```

以上代码执行了多个操作,但是没有产生任何中间变量,可读性也依然保持的很好。

## 原理分析

实际上,管道运算符 `%>%` 属于一个二元运算符,既然是二元运算符那就有左、右两个操作数,不妨设为 `L` 和 `R` 。对于 `L` 没有什么要求,只要是个 R 语言中的 object 就可以。但是,`R` 需要是一个可执行的函数。既然是函数,那就需要传递参数。如果是直接以 `L %>% R` 的模式写成的,那么 `L` 会被传递为 `R` 的第一个参数,其他写在 `R` 中的参数(假设是 `R(...)` )将依次作为后续参数传递,也就是相当于 `R(L, ...)` 。

那么如果 `R` 中不止第一个位置需要 `L` ,又或者 `L` 不是放在 `R` 的第一个参数位置上呢?此时可以使用 `.` 代替 `L` 进行参数传递。例如如果要对处理过的 iris 数据集中的 Sepal.Length 和 Sepal.Width 做回归,此时要将数据放在 `data` 的位置上,那么就可以使用以下几种方式:

```r
subset(iris, Species == "setosa") %>% lm(Sepal.Length ~ Sepal.Width, .) %>% summary()
subset(iris, Species == "setosa") %>% lm(Sepal.Length ~ Sepal.Width, data = .) %>% summary()
subset(iris, Species == "setosa") %>% lm(formula = Sepal.Length ~ Sepal.Width, data =.) %>% summary()
```

此时,参数 `L` 就不会出现在 `R` 的第一个参数位置上了。而且 `.` 是可以复用的,每个出现 `.` 的位置上,都会把 `L` 作为实参传递。例如,如果要统计 iris 四个测量值各自的总离差平方和($\sum (y_i-\bar{y})^2$)并用 **ggplot** 绘制成柱状图,就可以用下面的方式

```r
iris[,1:4] %>% 
    Map(function (x, y) {
        data.frame(
            indicator = y,
            tss = sum((x - mean(x))^2)
        )
    }, ., names(.)) %>%
    Reduce(rbind, .) %>%
    ggplot(aes(x = indicator, y = tss)) + geom_col()
```

![image.png](/media/67a7070a-e71d-4a9f-a616-5a78b1d6751c_image.png)

由于 **ggplot** 绘图使用的数据都是关系数据,所以这里不仅提取出前四列,也提取出前四列的名字,然后分别运算组合成一个包含两列的数据框,一列是 `indicator` 表示指标名,后面的 `tss` 表示该指标的总离差平方和。

# 特殊管道运算符

下面这些运算符需要明确加载 **magrittr** 包,只加载 **tidyverse** 是没有的。

## Tee Pipe `%T>%`

如果熟悉 Linux 则会了解一个命令 `tee` ,该命令可以将程序输出同时输出到控制台和标准输出,是非常有用的一个命令。而这个 Tee Pipe 也是类似的功能:它可以同时将一个 `L` 流向两个 `R` ,用法是

```r
L %T>% R1 %>% R2
```

这样在调用 `R2` 时,其左操作数不再是 `R1` 的返回值,而是 `L` 。在有些时候可以产生奇效,比如上一个例子,如果我们想在输出柱状图之前输出一下处理过的数据,就可以这样

```r
iris[,1:4] %>% 
    Map(function (x, y) {
        data.frame(
            indicator = y,
            tss = sum((x - mean(x))^2)
        )
    }, ., names(.)) %>%
    Reduce(rbind, .) %T>%
    print() %>%
    ggplot(aes(x = indicator, y = tss)) + geom_col()
```

或者用这种方式将图片同时输出到屏幕和文件

```r
iris[,1:4] %>% 
    Map(function (x, y) {
        data.frame(
            indicator = y,
            tss = sum((x - mean(x))^2)
        )
    }, ., names(.)) %>%
    Reduce(rbind, .) %>% {
        ggplot(., aes(x = indicator, y = tss)) + geom_col()
    } %T>%
    print(.) %>%
    ggsave("iris.png", .)
```

## Exposition pipe `%$%`

简单地讲,`L %$% R` 相当于 `with(L, R(...))` ,就是把 `L` 中的名字暴露给 `R` 去调用。比如要计算 Sepal.Length 和 Sepal.Width 的相关性,就可以这样

```r
iris %$% cor(Sepal.Length, Sepal.Width)
```

有的时候在调用基础包的一些函数时会比较有用。

## Assignment pipe `%<>%`

官方文档明确指出,使用该操作符 `L %<>% R` 相当于 `L <- L%>% R` ,所以个人觉得用处不大, 因为 `<-` 非常常用,而且含义也更明确,使用 `%<>%` 反而可读性降低了。

## Eager pipe `%!>%`

普通的 `%>%` 执行的是惰性运算,只有当需要进行计算的时候才进行计算。如果使用本地数据,这点可能体现的不是非常明显,当使用 **DBI** 连接到数据库的时候,会发现 **tidyverse** 中的函数(像 `select` `filter` `left_join` 等)实际上是拼接了一些 SQL 语句,当需要获取数据时(例如调用了 `collect()` 函数)才在数据库中执行。而 `%!>%` 运算符是即时执行的,每调用一次该运算符,都会进行数据的运算。这样当然结果会更加可预测。在一些特殊情况下,比如中间步骤需要展示一些警告信息,该操作符会比较有用。