为什么dplyr如此缓慢?

13

和大多数人一样,我对 Hadley Wickham 以及他为 R 所做的贡献印象深刻——所以我想把一些函数移到他的 tidyverse 中……但是这样做后,我不禁在想这一切的意义何在?

我的新的 dplyr 函数比它们基础版本的慢得多——我希望我做错了什么。我特别希望能从理解 non-standard-evaluation 所需的努力中获得一些回报。

那么,我做错了什么?为什么 dplyr 如此慢?

一个例子:

require(microbenchmark)
require(dplyr)

df <- tibble(
             a = 1:10,
             b = c(1:5, 4:0),
             c = 10:1)

addSpread_base <- function() {
    df[['spread']] <- df[['a']] - df[['b']]
    df
}

addSpread_dplyr <- function() df %>% mutate(spread := a - b)

all.equal(addSpread_base(), addSpread_dplyr())

microbenchmark(addSpread_base(), addSpread_dplyr(), times = 1e4)

计时结果:

Unit: microseconds
              expr     min      lq      mean median      uq       max neval
  addSpread_base()  12.058  15.769  22.07805  24.58  26.435  2003.481 10000
 addSpread_dplyr() 607.537 624.697 666.08964 631.19 636.291 41143.691 10000

使用 dplyr 函数转换数据需要的时间大约是原来方法的30倍—这肯定不是本意吧?

我想也许这个案例太简单了,如果我们有一个更加现实的情况,在其中添加列并对数据进行子集划分,那么 dplyr 就会发挥出它真正的优势--但事实上情况更糟。如下面的计时所示,这比基础方法慢了约70倍。

# mutate and substitute
addSpreadSub_base <- function(df, col1, col2) {
    df[['spread']] <- df[['a']] - df[['b']]
    df[, c(col1, col2, 'spread')]
}

addSpreadSub_dplyr <- function(df, col1, col2) {
    var1 <- as.name(col1)
    var2 <- as.name(col2)
    qq <- quo(!!var1 - !!var2)
    df %>% 
        mutate(spread := !!qq) %>% 
        select(!!var1, !!var2, spread)
}

all.equal(addSpreadSub_base(df, col1 = 'a', col2 = 'b'), 
          addSpreadSub_dplyr(df, col1 = 'a', col2 = 'b'))

microbenchmark(addSpreadSub_base(df, col1 = 'a', col2 = 'b'), 
               addSpreadSub_dplyr(df, col1 = 'a', col2 = 'b'), 
               times = 1e4)

结果:

Unit: microseconds
                                           expr      min       lq      mean   median       uq      max neval
  addSpreadSub_base(df, col1 = "a", col2 = "b")   22.725   30.610   44.3874   45.450   53.798  2024.35 10000
 addSpreadSub_dplyr(df, col1 = "a", col2 = "b") 2748.757 2837.337 3011.1982 2859.598 2904.583 44207.81 10000

3
你使用data.table吗?对我来说,它非常有用且快速。最好的! - LocoGris
7
一篇不错的阅读材料:https://dev59.com/geo6XIcBkEYKwwoYKRDG。tldr 的意思是:tidyverse 旨在编写简洁的代码,并非一定比 data.table 更快。 - RLave
6
使用特定的“清晰代码”定义。 - Roland
2
@ricardo 只需比较两种方法之间的函数调用次数即可。如果您编写关注微秒到毫秒级别的低级函数,则可能不应使用tidyverse。 - Roland
4
一则附注:我很惊讶你在 mutate 中使用了 :=,并且它居然生效了。难道 = 不是标准用法吗? - Henrik
显示剩余8条评论
1个回答

8
这些是微秒,您的数据集只有10行。除非您计划在数百万个仅有10行的数据集上进行循环,否则您的基准测试结果几乎毫无意义(在这种情况下,绑定它们作为第一步是明智的选择)。
让我们使用更大的数据集,例如大1百万倍:
df <- tibble(
  a = 1:10,
  b = c(1:5, 4:0),
  c = 10:1)

df2 <- bind_rows(replicate(1000000,df,F))

addSpread_base <- function(df) {
  df[['spread']] <- df[['a']] - df[['b']]
  df
}
addSpread_dplyr  <- function(df) df %>% mutate(spread = a - b)

microbenchmark::microbenchmark(
  addSpread_base(df2), 
  addSpread_dplyr(df2),
  times = 100)
# Unit: milliseconds
#                 expr      min       lq     mean   median       uq      max neval cld
# addSpread_base(df2) 25.85584 26.93562 37.77010 32.33633 35.67604 170.6507   100   a
# addSpread_dplyr(df2) 26.91690 27.57090 38.98758 33.39769 39.79501 182.2847   100   a

仍然相当快,差别不大。

至于你得到的结果的原因,是因为你使用了一个更复杂的函数,所以它有额外的开销。

评论者指出,dplyr并没有尝试过于追求速度,也许与 data.table 相比确实如此,而接口是第一要考虑的问题,但作者们也在努力提高速度。例如,混合评估允许(如果我理解正确)直接在分组数据上执行 C 代码进行聚合,这比基本代码要快得多,但简单的代码总是会在简单的函数中运行得更快。


我理解dplyr已经解决了大部分速度问题,因此我感到失望。此外,我认为非标准评估的情况有点混乱。在我看来,必须存在某种性能上升来回报努力攀登曲线的努力。 - ricardo
但是我找到了这个:https://rpubs.com/hadley/dplyr-benchmarks,它是5年前的,而且`dplyr`通常比基础更快,有一个更近期的基准测试讨论混合评估,但我找不到它。 - moodymudskipper
嗯,我想我做错了。我对“addSpreadSub”函数进行了基准测试,“base”在1000万行的情况下比“dplyr”快约20%。因此,这不仅仅是一个规模问题。 - ricardo
有趣的是,我不知道为什么会存在这么大的差异,而且我无法复现它。 - moodymudskipper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接