在做数据分析的时候循环遍历某一变量,并且生成一个数据框(data.frame
)作为报告是一个非常常见的事情,但是究竟怎么写才能让程序跑得比西方记者还快却非常麻烦。众所周知 R 是一个很慢而且语法怪异的语言,用「常规」的思路来写一段 R 脚本,性能可能会非常不理想,而写「性能很好」的脚本,又可能把你的代码变成自带精神污染效果的魔法道具。因此在本文当中我将分享一个用于循环遍历变量生成数据框的编程模板,以期在性能和可读性之间取得平衡。
大方向很简单,把 for loop
禁用掉,尽量使用向量化循环(lapply
),同时不使用 sapply
;不使用 「Scoping Assignment Operator」(<<-
)。
立下这量项基本原则的原因如下:
- 在 R 里面
for loop
是万恶之源,性能烂到根本没法看,所以在任何情况下都不应当用它; sapply
的行为不稳定,返回结果会做「化简」,但是这种化简并不总是我们想要的(我非常讨厌R
这种无端的「化简」行为,给data.frame
切片的时候也有莫名其妙的化简导致切片返回数据类型不稳定);- Scoping Assignment Operator 会极大影响代码的可读性,因为跨上下文变量赋值在大多数情况下都很不直观。之所以提这点是希望 ban 掉这样的一种写法:先创建一个空白的
data.frame
作为结果变量,然后每次循环遍历的时候都把本次输出的结果和这个结果变量做拼接,最后再输出。这么做真的很慢,非常慢,而且代码会被写的很丑(在 R 里面建一个 0 行的空白数据框是很恶心的事情)。
除此以外还有一项可选准则:使用 tibble
而非 data.frame
。换言之,能用 tidyverse
全家桶的地方不用原生的函数。因为 tidyverse
那一套东西是直接用 RCpp
搞的,性能比原生的方法和变量类型好很多,而且在控制台的输出也比原生 data.frame
更加直观。但是之所以把这项准则设为可选,是因为一些可以直接在 data.frame
上用的函数不能拿到 tibble
上用,隔壁课题组的同学就被这事情坑过,所以如果你对 tibble
不够熟悉也懒得学的话,可以弃之不用。
如果你不想用 tibble
的话
【例1】如果不想用 tibble 的话最基本的模板是这样的:
1 | library(magrittr) |
这里的 %>%
是 magrittR
包里面的管道操作符,x %>% f
等价于 f(x)
; x %>% f(y)
等价于 f(x, y)
。这玩意可以有效避免你的代码出现无限个圆括号套娃,增强代码可读性(数括号真的很烦,尤其是改代码的时候数括号看哪里的括号缺了一半更烦 ヽ(#`Д´)ノ)。
do.call(rbind, list(x, y))
与 rbind(x, y)
等价,只是这里我们没办法确定传入参数的个数,所以才需要使用这个函数。
【例2】有些情况下,并不是被遍历的每个变量都能输出有效结果,这个时候我们可以通过 Filter
函数来滤掉无效项:
1 | library(magrittr) |
这一例中有几个比较 tricky 的地方。首先,如果想要遍历一个 data.frame
,很多人可能直觉性的会想到用 apply(x, margin = 1)
,但实际上这种做法是非常不可取的,因为 apply 这个函数被设计的本意是用来遍历矩阵(matrix
)的,它会自动的将每行数据简化成一个向量,众所周知 R 当中的向量无法存储多个类型的数据,因此所有数据都会被简化为同种类型的数据,这会为程序带来很大的不确定性。 比较安全的做法是遍历一个数值向量 1:nrow(x)
之后在 lapply
内部使用切片器。
另外,Filter(Negate(is.null), .)
是一个性能比较好的滤除 NULL
的方法,参考了这位老哥的答案。
使用 tibble
的版本
【例3】如果你不介意多调两个包的话,可以这么写:
1 | library(magrittr, tibble, dplyr) |
tibble
函数来自 tibble
包;bind_rows
函数来自 dplyr
包,是 do.call(rbind, .)
的便利版本。
附加题:如果要生成多个 data.frame 的话
【例4】有的时候我们可能希望通过一次遍历生成多个 data.frame,最后再把这些结果拼接在一起,这个时候可以借助一个小函数:
1 | merge_df <- function(x, unit_names = NULL, safe_deduce = T) { |
虽然注释非常多,但实际上这个函数真正在做的事情非常简单,merge_df
期待你会:
- 利用
lapply
遍历一个变量; - 每次遍历时,返回一个包含若干
data.frame
的list
; - 每次返回的
list
应当具有相同的结构,即list
的names
应当一致。
merge_df
会把每次迭代返回的,name
一致的 data.frame
沿着行(row)的方向拼接成一个大的 data.frame
。
你可以手动传入 unit_names
参数来告诉这个函数每次迭代返回的 list
究竟是什么结构。如果没有提供这个参数,那么函数会进行自动推断,这时函数假定每次遍历返回的 list
结构一致,将第一次迭代返回值的结构视作接下来所有遍历结果的结构。如果第一次迭代没有返回一个 list
那么程序将抛出 NULL。
在自动推断遍历结果结构时,你可以选择进行「安全推断」(默认开启,与 R
的 API 设计调性一致但我并不喜欢这样),即第一次返回的 list 中,只有元素类型为 data.frame
才将之视作为返回结果的有效结构,否则则被抛弃。比如如果第一次遍历返回的结果是这样的:
1 | list( |
那么开启「安全推断」,函数认为迭代返回的数据结构是 c('report_c')
,否则则认迭代返回数据的结构是 c('report_a', 'report_b', 'report_c')
。
除此之外该函数还做了很多变量类型安全检查:
- 输入的数据
x
必须是一个list
; - 对于每次迭代的结果,如果与预先定义的迭代结果结构一致,且为
data.frame
,则参与结果拼合,否则被抛弃。
你可以这样使用这个函数:
1 | library(magrittr) |
【附加题】试试把上面的函数改成 tidyverse
版本的?(没有参考答案哦~)
以上就是本次的分享啦,本文是遗迹计划的一部分,希望能够为你提供一些帮助,莉莉爱你 ♥ヽ( ^ω^ ゞ )~
Comments
No comments here,
Why not write something?