经过一个周末的思考,我再把这个问题的解决方案优化了一步。
首先,各位同学别怪我在这个问题上的较真,因为“split”的问题是R语言数据处理中的一个普遍性问题,大神Hadley Wickham在其文章The Split-Apply-Combine Strategy for Data Analysis中就将Split-Apply-Combine的操作方法提升到了策略的高度,并因此有了plyr软件包。故而,将split的各种可能的瓶颈研究清晰,并提出针对性的优化方案,将终究会使大家未来的数据研究工作受益。
这次我的思路是从factor这一环节展开的。split()对factor参数与非factor参数的效率可谓天差地远:
- k <- round(runif(500000, min=0, max=1), digits=5) * 1e5
- v <- round(runif(500000, min=0, max=1), digits=1) * 1e1
- k.f <- as.factor(k)
- system.time(split(v,k))
- 用户 系统 流逝
- 1.21 0.00 1.22
- system.time(split(v,k.f))
- 用户 系统 流逝
- 0.03 0.00 0.03
复制代码由此,可知split环节的瓶颈其实是在as.factor环节。
而之前“ntsean”同学的发言让我无意中发现integer数据类型和double数据类型在as.factor环节也有着非常大的差别,比如:
- k1 <- sample(1:100000, 500000, replace = T)
- typeof(k)
- [1] "double"
- typeof(k1)
- [1] "integer"
- system.time(as.factor(k))
- 用户 系统 流逝
- 1.2 0.0 1.2
- system.time(as.factor(k1))
- 用户 系统 流逝
- 0.15 0.00 0.15
复制代码至此,我推测数据类型是制约as.factor环节的一个重要因素,也是可以优化的一个重要方向。
我仔细观察了一下as.factor和factor两个函数,这两个函数都是用R语言编写的,并非用C语言。虽然我没有完全搞清楚其机理,但我猜测将integer和double的数据类型分开不同的程序处理仅仅是为了容错性上的考虑,而并非是技术性上的限制。
于是我将as.factor源程序中的一段抽取了出来,单独写了一个函数:
- factor.num <- function(x) {
- levels <- sort(unique.default(x))
- f <- match(x, levels)
- levels(f) <- as.character(levels)
- class(f) <- "factor"
- f
- }
复制代码测试了一下,对double数据类型的操作是没有问题的,且效率提升了很多:
- system.time(as.factor(k))
- 用户 系统 流逝
- 1.18 0.00 1.19
- system.time(factor.num(k))
- 用户 系统 流逝
- 0.24 0.01 0.25
- identical(as.factor(k), factor.num(k))
- [1] TRUE
复制代码这下好了,我将在28楼的测试思路延续到新的含有factor.num()的解决方案上来,且我将每种方法均用30次测试,观测均值和中位数。第一种方法是“suimong”的lapply方法,第二种方法是“ntsean”的tapply方法,第三种是我用split.num()的解决办法(在本帖28楼有说明),第四种方法是本楼用factor.num()函数的方法。最后将结果检验了一下,没有问题。
OK,执行效率再进一步,在我的i3 CPU上基本0.6秒左右可以完成,不同的电脑上可能会更快。
通过这个例子,让我对R语言的优化有了很直观的感悟,其中一条,有些函数由于考虑容错性、兼容性的要求,往往代码会比较多,在一些比较极端的对优化要求很高的场合,如果针对具体数据的特点,去掉函数不必要的容错代码,有可能大幅提高执行效率。
不知我这一思路有没有错漏,多谢各位指正!
好了,以我的水平,已经在这个问题上发挥到极致了,现在我真的可以狗带了