For循环与向量化(Vectorization)
写在前面
感谢水友们积极的提问,大猫和村长在此再次表示衷心的感谢。通过对水友们问题的汇总,我们发现大多数水友存在一些R语言的应用误区,在此出一期关于该问题的解读。
问题提出
首先思考一个典型的增长率的计算的例子。假设我们有一列时间序列,每个都记录着时刻的值。现在我们希望针对每个计算当期的增长率,其公式如下:
大家可能首先想到的是利用For循环来做。假如一个向量长度为,那么我们就把上面的增长率公式应用遍。这种思路以标量(scalar)的角度考虑问题。这样是否真的有效率?除此之外,能否有其他的思路? ”
解决方法
For循环
首先我们用R语言最底层的For循环进行函数的编写。我们定义向量长度为10000,重复运行1000次,测定计算其运行的中位数时长(为了防止极值对结果的影响)。在计算时间方面,运用microbenchmark
包,生成每次运行的时间,最后计算中位数,代码与结果如下:
library(microbenchmark)
growthRBL <- function(x) {
growth <- vector(mode = "numeric")
for (i in seq_along(x)) {
growth[i] <- x[i]/x[i-1] - 1
}
growth
}
time1 <- microbenchmark(growthRBL(1:10000), times = 1000) %>% as.data.table()
time1[, median(time)/1e6]
3.3792015
通过结果可以发现,平均每次运行所需要耗费的时长为3.3秒左右。有没有更快的方法呢?我们来看下面的思路。
Vectorized(向量化)
根据Hadley Wickham在其著作Advanced R中第一章所涉及到的内容,R最底层的数据结构只有两种:向量(vector)和列表(list),其他所有的数据格式都是通过这两种最基本的数据结构衍化而来。向量作为最基本的数据结构,其在进行底层编写的时候,进行了很大程度的优化设计。向量有时候作为一种基本的编写思路,是具有很高效率的。有鉴于此,我们通过R语言最底层的向量思维进行函数编写。
代码语言:javascript复制library(microbenchmark)
growthRBV <- function(x) {
shift <- function(y) {
c(NA, y[1:(length(y)-1)])
}
x/shift(x) - 1
}
time2 <- microbenchmark(growthRBV(1:10000), times = 1000) %>% as.data.table()
time2[, median(time)/1e6]
0.084901
我们在函数中编写了另一个函数,名曰shift
。由于我们需要做的是向量中某一个元素与前一个元素的处理结果,那么只需要将元素往后进行移位,与原来的向量进行一一对应的处理即可,这样便达到了以向量进行处理的模式。也可称之为向量化(vectorization)。
上述的运行结果更能反映这种编写的效率,可以看到运行速度提升了将近40倍,运行时间变成了0.08s左右。
关于For循环和Vectorization的深入思考
Vectorization在更多包的拓展
现在有很多的R包会对底层的一些函数进行优化,也即是对向量化的进一步优化,我们选择效率较为强大的data.table
,看看其对shift
函数的优化情况。
library(microbenchmark)
growthRDV <- function(x) {
x/data.table::shift(x) - 1
}
time3 <- microbenchmark(growthRDV(1:10000), times = 1000) %>% as.data.table()
time3[, median(time)/1e6]
0.0406515
通过结果可以发现,data.table
对于shift
函数的优化又使其效率翻倍!!运行时间继续降低至0.04s!!
更底层的For循环
R语言本身的For循环效率相对低下,究其原因在于R作为高级语言,循环本身需要先进行编译,再放入底层进行处理。更为直接的做法,如果想提升效率,则可以直接将循环放入底层进行运行。有鉴于此,C
可作为一种比较好的替代手段。R语言提供了一个很好的C
语言的接口,Rcpp
包能够比较方便调用C
的语句进行操作。(若有对Rcpp感兴趣的同学可以戳这里进行了解)
library(microbenchmark)
Rcpp::cppFunction('NumericVector growthRCL(NumericVector x){
int n = x.size();
NumericVector y(n);
y[0] = NA_REAL;
for(int i = 1; i < n; i ) {
y[i] = x[i]/x[i-1]-1;
}
return y;
}')
time4 <- microbenchmark(growthRCL(1:10000), times = 1000) %>% as.data.table()
time4[, median(time)/1e6]
0.029601
如上所示,使用Rcpp
包中的cppFunction
进行C
语句的调用。在这里会自动调用已经配置好的C
头文件,并自动编译而后运行。调用的C
语句,在R
语言中皆有相对应的数据格式。通过运行结果可以发现,Rcpp
调用的底层循环略优于data.table
的向量化,运行时间在0.03s左右。
总结
通过上面的运行效率排序可以发现:
我们也可以总结出以下两点:
- 在R语言中一般意义上的数据操作,能够向量化尽量进行向量化,For循环尽量避免使用。
- 利用data.table进行数据操作有着比R本身向量化更好的效率表现,如果自身对效率的要求更高,可以利用更底层的语言接口进行编写。
- 最后还有一点需要注意:向量化并不能解决一切问题。当遇到一些特殊情况,比如函数嵌套调用过多,或者数据迭代问题,对更为底层的语言进行调用,则会显得更为有效。