【问题标题】:Faster alternatives to ddply and group_byddply 和 group_by 的更快替代方案
【发布时间】:2018-08-09 11:44:49
【问题描述】:

我正在尝试找出循环遍历 data.frame myData 的最佳方法,按两列分组 c1c2。 具体来说,我想遍历c1c2 的每个独特组合,并将某个customFunction 应用于myData 中的其他列。这个customFunction 依赖于someStatsFunction,它输出一个data.frame

我通常会使用函数plyr::ddply,但我的真实数据集有超过 1800 万行,这并不奇怪这需要太长时间。所以我决定改变使用dplyr::group_bydplyr::do的管道方法。尽管使用dplyr 可以加快问题的速度(参见下面的最小示例),但仍然需要相当长的时间。我听说data.table 框架可以加快速度(参见示例here),但我不知道如何使用它。我想知道是否有人可以使用data.table 翻译下面的问题,以便我也可以对其进行基准测试。

library(plyr)  
library(dplyr)  
library(rbenchmark)  

someStatsFunction  <-  function (x) {
    data.frame(name = 'something', mean = mean(x), sd = sd(x), statx = sqrt(mean(abs(x)))/sd(x)^2)
}

customFunction  <-  function (data) {
    if (!all(sort(data$time) == data$time)) {
        stop('Column \'time\' is not ordered')
    }
    someStatsFunction(data$response)
}

myData  <-  data.frame(c1 = rep(rep(1:50, each = 30), 10), c2 = rep(rep(1:30, 50), 10), response = rnorm(30 * 50 * 10), time = 1:(30 * 50 * 10))

benchmark('testPlyr' = {
            testPlyr   <-  plyr::ddply(myData, .(c1, c2), customFunction)
          },
          'testDplyr' = {
            testDplyr  <-  myData %>% dplyr::group_by(c1,c2) %>% dplyr::do(customFunction(.))
          },
          replications = 3,
          columns      = c('test', 'replications', 'elapsed', 'relative', 'user.self', 'sys.self'))

这是我得到的输出:

       test replications elapsed relative user.self sys.self
2 testDplyr            3   7.416     1.00     7.368    0.060
1  testPlyr            3   8.378     1.13     8.364    0.012

谢谢,
D

更新在@minem 的answer之后

首先,我对上面的示例进行了一些修复,因为代码不正确。

其次,我扩展了上面的最小可重现示例,以更好地反映(稍微)我的情况。 someStatsFunction 可能依赖于 data.table 中的多个列,并根据从这些多列派生的一些重要的统计信息组合来处理一堆数字。我还增加了myData 的大小(因此,如果与原始示例相比,下面的示例现在需要更长的时间)。无论如何,我想我设法复制了从plyrdplyr 获得的输出。它使用 data.table 运行得更快,这真的很酷(参见下面的基准测试)。但是,代码似乎有点笨拙:

library(plyr)  
library(dplyr)  
library(data.table)  
library(rbenchmark)  

someStatsFunction  <-  function (y, x) {
    x    <-  as.integer(x)
    mod  <-  coef(summary(lm(y ~ x)))
    data.frame(stats1  = 'something',
             intercept = mod[1],
             slope     = mod[2],
             meanx     = mean(x),
             statx     = sqrt(mean(abs(x)))/sd(y)^2)
}

customFunction  <-  function (data) {
    if (!all(sort(data$time) == data$time)) {
        stop('Column \'time\' is not ordered')
    }
    someStatsFunction(y = data$response, x = data$time)
}

myData  <-  data.frame(c1 = rep(rep(1:50, each = 30), 1095), c2 = rep(rep(1:30, 50), 1095), response = rnorm(30 * 50 * 1095), time = rep(seq(as.Date('1981-01-01'), as.Date('1983-12-31'), by = '1 day'), each = 50*30))

benchmark('testPlyr' = {
            testPlyr   <-  plyr::ddply(myData, .(c1, c2), customFunction)
        },
          'testDplyr' = {
            testDplyr  <-  myData %>% dplyr::group_by(c1,c2) %>% dplyr::do(customFunction(.))
        },
          'testDtb' = {
            vNames   <-  c('stats1', 'intercept', 'slope', 'meanx', 'statx')
            dt       <- as.data.table(myData)
            testDtb  <- dt[order(time)][, 
            (vNames) := as.list(someStatsFunction(response, time)), 
            by = .(c1, c2)][, 
            head(.SD, 1), by = .(c1, c2)][, 
            c('response', 'time') := NULL, ]
        },
    replications = 3,
    columns      = c('test', 'replications', 'elapsed', 'relative', 'user.self', 'sys.self'))

这是我得到的输出:

       test replications elapsed relative user.self sys.self
2 testDplyr            3  28.209    3.101    20.841    7.317
3   testDtb            3   9.098    1.000    10.958    0.385
1  testPlyr            3  28.224    3.102    21.741    7.167

速度有了如此显着的提高。但是,在应用 someStatsFunction 之前,我必须先对数据进行排序(即消除在 customFunction 处的 if 语句的需要),然后使用 myData 中的列 responsetime 运行函数.此外,来自

的原始输出
dt[order(time)][, (vNames) := as.list(someStatsFunction(response, time)), by = .(c1, c2)]

给出一个不返回 1500 个值的表(即 c1c2 的 30*50 组合),而是多次重复 c1c2 的组合。此外,它确实返回了原始的 responsetime 列,尽管我只希望将 c1c2 的唯一组合绑定到来自 someStatsFunction 的统计信息(如使用 plyr 和/或dplyr),因此是我的最终代码

testDtb  <- dt[order(time)][, 
(vNames) := as.list(someStatsFunction(response, time)), 
by = .(c1, c2)][, 
head(.SD, 1), by = .(c1, c2)][, 
c('response', 'time') := NULL, ]

我是否可以通过更简化的方式获得相同的输出?

【问题讨论】:

  • 您的示例数据太小,时间不重要。也许把它写成依赖于#rows 和#groups。另外,我想你可以简化它(没有理由有两个分组变量 c1 和 c2)
  • 谢谢弗兰克,我在 c1 和 c2 中使用了这个简单的示例,因为在我的原始数据中它们是坐标(经纬度)。我基本上需要对每对坐标的环境时间序列应用一个复杂的函数。是否将坐标粘贴到一个单独的分组变量中(出于循环的目的),然后在输出中将它们拆分回来加快速度?或者这只会创建额外的不必要的代码?另外,我有兴趣了解data.table 替代方案是否会大大加快速度......
  • 我不认为将它们结合起来会提高速度;我只是说我们可能不需要解决这个问题是额外的复杂性......我认为(但可能是错误的)只有 #groups 很重要(在加载 data.table 后可计算为uniqueN(setDT(mydata), by=c("c1", "c2")))。顺便说一句,如果您想知道优化了哪些分组计算,可以查看?GForce
  • 完美,感谢您的建议。 data.table 的概念对我来说还是很新的,所以我还不太清楚如何将上面的例子翻译成 data.table 框架。但如果我有一个工作,我会尝试使用 data.table 示例编辑我的帖子。
  • setDT(myData)[order(time), someStatsFunction(response, time), by=.(c1, c2)] 之类的东西会达到你想要的效果

标签: r performance dplyr data.table plyr


【解决方案1】:

尝试:

dt <- as.data.table(myData)
rr <- dt[, .(
  lon = c1,
  lat = c2,
  name = 'something',
  mean = mean(response),
  sd = sd(response),
  statx = sqrt(abs(response)) / sd(response) ^ 2

), keyby = .(c1, c2)]
rr
#        c1 c2 lon lat      name        mean        sd     statx
#     1:  1  1   1   1 something  0.23841637 0.9384408 0.3253456
#     2:  1  1   1   1 something  0.23841637 0.9384408 0.2421654
#     3:  1  1   1   1 something  0.23841637 0.9384408 0.5321797
#     4:  1  1   1   1 something  0.23841637 0.9384408 0.4136648
#     5:  1  1   1   1 something  0.23841637 0.9384408 1.5863249
# ---                                                        
# 14996: 50 30  50  30 something -0.04082032 0.7156352 2.3970053
# 14997: 50 30  50  30 something -0.04082032 0.7156352 0.8375551
# 14998: 50 30  50  30 something -0.04082032 0.7156352 1.7826972
# 14999: 50 30  50  30 something -0.04082032 0.7156352 1.0293926
# 15000: 50 30  50  30 something -0.04082032 0.7156352 0.1376940

【讨论】:

  • 嗨@minem,感谢您的回复。这是一个很好的解决方案,并且绝对比 dplyr 或 plyr 方法更快,并且以完全相同的格式吐出数据,这很棒。但是,您的示例不允许我将整个数据作为参数“发送”到customFunction - 这是必要的,因为此函数在应用嵌套函数 @987654323 之前会执行数据质量检查(“if”语句) @。实际上,我有一个更复杂的包装函数(如customFunction),它依赖于一系列嵌套函数来输出输出data.frame。
【解决方案2】:

感谢 @chinsoon12 提供的answer,我能够得到我想要的结果:

library(plyr)  
library(dplyr)  
library(data.table)  
library(rbenchmark)  

someStatsFunction  <-  function (y, x) {
    x    <-  as.integer(x)
    mod  <-  coef(summary(lm(y ~ x)))
    data.frame(stats1  = 'something',
             intercept = mod[1],
             slope     = mod[2],
             meanx     = mean(x),
             statx     = sqrt(mean(abs(x)))/sd(y)^2)
}

customFunction  <-  function (data) {
    if (!all(sort(data$time) == data$time)) {
        stop('Column \'time\' is not ordered')
    }
    someStatsFunction(y = data$response, x = data$time)
}

myData  <-  data.frame(c1 = rep(rep(1:50, each = 30), 1095), c2 = rep(rep(1:30, 50), 1095), response = rnorm(30 * 50 * 1095), time = rep(seq(as.Date('1981-01-01'), as.Date('1983-12-31'), by = '1 day'), each = 50*30))

benchmark('testPlyr' = {
            testPlyr   <-  plyr::ddply(myData, .(c1, c2), customFunction)
        },
          'testDplyr' = {
            testDplyr  <-  myData %>% dplyr::group_by(c1,c2) %>% dplyr::do(customFunction(.))
        },
          'testDtb' = {
            testDtb  <-  setDT(myData)[order(time), someStatsFunction(response, time), by=.(c1, c2)]
        },
    replications = 3,
    columns      = c('test', 'replications', 'elapsed', 'relative', 'user.self', 'sys.self'))

这是我获得的基准测试结果:

       test replications elapsed relative user.self sys.self
2 testDplyr            3  68.383    3.976    48.120   20.392
3   testDtb            3  17.201    1.000    17.232    0.008
1  testPlyr            3  57.938    3.368    49.676    8.304

如果您想知道不同方法的结果是否相同,请检查:

all.equal(testDplyr, testDtb)
# [1] TRUE
all.equal(testDplyr, testPlyr)
# [1] TRUE

【讨论】:

    猜你喜欢
    • 2018-12-11
    • 2012-07-05
    • 2012-01-23
    • 2013-07-13
    • 2011-02-27
    • 2010-09-22
    • 2017-05-15
    • 1970-01-01
    相关资源
    最近更新 更多