首页 文章

dplyr在data.table上,我真的在使用data.table吗?

提问于
浏览
75

如果我在 datatable 之上使用 dplyr 语法,在使用dplyr的语法时,是否可以获得数据表的所有速度优势?换句话说,如果我使用dplyr语法查询数据表,是否会误用数据表?或者我是否需要使用纯数据表语法来利用其所有功能 .

提前感谢任何建议 . 代码示例:

library(data.table)
library(dplyr)

diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut) 

diamondsDT %>%
    filter(cut != "Fair") %>%
    group_by(cut) %>%
    summarize(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = n()) %>%
    arrange(desc(Count))

结果:

#         cut AvgPrice MedianPrice Count
# 1     Ideal 3457.542      1810.0 21551
# 2   Premium 4584.258      3185.0 13791
# 3 Very Good 3981.760      2648.0 12082
# 4      Good 3928.864      3050.5  4906

这是我想出的数据表等价 . 不确定它是否符合DT良好实践 . 但我想知道代码是否真的比场景背后的dplyr语法更有效:

diamondsDT [cut != "Fair"
        ] [, .(AvgPrice = mean(price),
                 MedianPrice = as.numeric(median(price)),
                 Count = .N), by=cut
        ] [ order(-Count) ]

3 回答

  • 64

    没有直接/简单的答案,因为这两个方案的哲学在某些方面有所不同 . 因此,一些妥协是不可避免的 . 以下是您可能需要解决/考虑的一些问题 .

    在dplyr中涉及i(== filter()和slice()的操作)

    假设 DT 有10列 . 考虑这些data.table表达式:

    DT[a > 1, .N]                    ## --- (1)
    DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)
    

    (1)给出 DT 中列 a > 1 的行数 . (2)返回 mean(b)c,d 分组, i 中的相同表达式为(1) .

    常用的 dplyr 表达式是:

    DT %>% filter(a > 1) %>% summarise(n())                        ## --- (3) 
    DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)
    

    显然,data.table代码更短 . 此外,它们还具有更高的内存效率1 . 为什么?因为在(3)和(4)中, filter() 首先返回所有10列的行,而在(3)中我们只需要行数,而在(4)中我们只需要列 b, c, d 用于连续操作 . 要克服这一点,我们必须 select() 列apriori:

    DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
    DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)
    

    必须强调两个包之间的主要哲学差异:在data.table中,我们喜欢将这些相关操作保持在一起,并且允许查看j-expression(来自相同的函数调用)并实现没有需要(1)中的任何列 . i中的表达式得到计算,而.N只是给出行数的逻辑向量的总和;整个子集永远不会实现 . 在(2)中,只有列b,c,d在子集中具体化,其他列被忽略 . 但是在dplyr中,哲学是让一个函数恰好做一件事 . (至少目前)无法判断filter()之后的操作是否需要我们过滤的所有列 . 如果您想要有效地执行此类任务,您需要提前考虑 . 在这种情况下,我个人认为它具有反竞争性 .

    请注意,在(5)和(6)中,我们仍然是 a 的子集列,我们不知道如何避免这种情况 . 如果 filter() 函数有一个参数来选择要返回的列,我们可以避免这个问题,但是该函数不会只执行一个任务(这也是一个dplyr设计选择) .

    通过引用进行子分配

    dplyr永远不会通过引用更新 . 这是两个包之间的另一个巨大(哲学)差异 .

    例如,在data.table中,您可以执行以下操作:

    DT[a %in% some_vals, a := NA]
    

    它仅通过引用来更新列 a ,满足条件的那些行 . 目前,dplyr深度复制整个data.table以添加新列 . @BrodieG在他的回答中已经提到了这一点 .

    但是当实现FR #617时,深拷贝可以被浅拷贝替换 . 也相关:dplyr: FR#614 . 请注意,您修改的列将始终被复制(因此速度较慢/内存效率较低) . 将无法通过引用更新列 .

    其他功能

    • 在data.table中,您可以在加入时进行聚合,这是更直接的理解和内存效率,因为中间连接结果永远不会实现 . 检查this post以获取示例 . 您可以't (at the moment?) do that using dplyr'的data.table / data.frame语法 .

    dplyr的语法也不支持

    • data.table的rolling joins功能 .

    • 我们最近在data.table中实现了重叠连接以加入区间范围(here's an example),这是一个单独的函数 foverlaps() ,因此可以与管道运算符一起使用(magrittr / pipeR? - 我自己从未尝试过) .

    但最终,我们的目标是将其整合到 [.data.table 中,这样我们就可以收集其他功能,例如分组,聚合等等 . 这将具有上述相同的限制 .

    • 自1.9.4以来,data.table使用辅助密钥实现自动索引,以便在常规R语法上使用基于快速二进制搜索的子集 . 例如: DT[x == 1]DT[x %in% some_vals] 将在第一次运行时自动创建索引,然后使用二进制搜索将其从同一列的连续子集用于快速子集 . 此功能将继续发展 . 有关此功能的简短概述,请查看this gist .

    从为data.tables实现 filter() 的方式来看,它没有利用此功能 .

    所以,你必须权衡这些(可能还有其他点),并根据这些是否决定你可以接受这种权衡 .

    HTH


    (1)请注意,内存效率直接影响速度(特别是当数据变大时),因为大多数情况下的瓶颈是将数据从主内存移动到缓存(并尽可能多地利用缓存中的数据 - 减少缓存未命中 - 以减少访问主存储器) . 这里不详述 .

  • 18

    就试一试吧 .

    library(rbenchmark)
    library(dplyr)
    library(data.table)
    
    benchmark(
    dplyr = diamondsDT %>%
        filter(cut != "Fair") %>%
        group_by(cut) %>%
        summarize(AvgPrice = mean(price),
                     MedianPrice = as.numeric(median(price)),
                     Count = n()) %>%
        arrange(desc(Count)),
    data.table = diamondsDT[cut != "Fair", 
                            list(AvgPrice = mean(price),
                                 MedianPrice = as.numeric(median(price)),
                                 Count = .N), by = cut][order(-Count)])[1:4]
    

    在这个问题上,似乎data.table比使用data.table的dplyr快2.4倍:

    test replications elapsed relative
    2 data.table          100    2.39    1.000
    1      dplyr          100    5.77    2.414
    

    Revised 基于Polymerase的评论 .

  • 20

    回答你的问题:

    • 是的,您正在使用 data.table

    • 但不如使用纯 data.table 语法那样有效

    在许多情况下,对于那些想要 dplyr 语法的人来说,这将是一个可接受的折衷方案,尽管它可能比使用普通数据帧的 dplyr 慢 .

    一个重要因素似乎是 dplyr 将在分组时默认复制 data.table . 考虑(使用microbenchmark):

    Unit: microseconds
                                                                   expr       min         lq    median
                                    diamondsDT[, mean(price), by = cut]  3395.753  4039.5700  4543.594
                                              diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
     diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))  9210.670 11486.7530 12994.073
                                   diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609
    

    过滤速度相当,但分组不是 . 我相信罪魁祸首是 dplyr:::grouped_dt 中的这一行:

    if (copy) {
        data <- data.table::copy(data)
    }
    

    其中 copy 默认为 TRUE (并且't easily be changed to FALSE that I can see). This likely doesn' t可以占到差异的100%,但是对于 diamonds 这个大小的东西而言,一般的开销很可能不是完全不同的 .

    问题是,为了获得一致的语法, dplyr 分两步进行分组 . 它首先在与组匹配的原始数据表的副本上设置键,然后才对其进行分组 . data.table 只为最大的结果组分配内存,在这种情况下只是一行,这样就需要分配多少内存 .

    仅供参考,如果有人关心的话,我通过使用 treeprofinstall_github("brodieg/treeprof") )找到了这个,这是一个用于 Rprof 输出的实验性(并且仍然非常阿尔法)树查看器:

    enter image description here

    注意以上目前仅适用于macs AFAIK . 另外,遗憾的是, Rprofpackagename::funname 类型的调用记录为匿名,因此它实际上可能是 grouped_dt 内部的任何和所有 datatable:: 调用,但是从快速测试看起来像 datatable::copy 是最重要的 .

    也就是说,您可以快速查看 [.data.table 调用周围没有那么多开销,但是对于分组还有一个完全独立的分支 .


    EDIT :确认复制:

    > tracemem(diamondsDT)
    [1] "<0x000000002747e348>"    
    > diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
    tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>% 
    Source: local data table [5 x 2]
    
            cut AvgPrice
    1      Fair 4358.758
    2      Good 3928.864
    3 Very Good 3981.760
    4   Premium 4584.258
    5     Ideal 3457.542
    > diamondsDT[, mean(price), by = cut]
             cut       V1
    1:     Ideal 3457.542
    2:   Premium 4584.258
    3:      Good 3928.864
    4: Very Good 3981.760
    5:      Fair 4358.758
    > untracemem(diamondsDT)
    

相关问题