R语言的链式操作
2023年5月25日
编程
如果说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()` 函数)才在数据库中执行。而 `%!>%` 运算符是即时执行的,每调用一次该运算符,都会进行数据的运算。这样当然结果会更加可预测。在一些特殊情况下,比如中间步骤需要展示一些警告信息,该操作符会比较有用。
感谢您的阅读。本网站 MyZone 对本文保留所有权利。