我不能在 data.table 中使用 dtplyr 做啥

Posted

技术标签:

【中文标题】我不能在 data.table 中使用 dtplyr 做啥【英文标题】:What can't I do with dtplyr that I can in data.table我不能在 data.table 中使用 dtplyr 做什么 【发布时间】:2020-03-22 02:06:52 【问题描述】:

我是否应该将我的学习精力投入到 R 中的数据处理上,特别是在 dplyrdtplyrdata.table 之间?

我主要使用dplyr,但是当数据太大时我会使用data.table,这种情况很少见。所以现在dtplyr v1.0 已经作为data.table 的接口出来了,从表面上看,我似乎再也不用担心使用data.table 接口了。

那么,data.table 的哪些最有用的功能或方面目前无法使用dtplyr 完成,而这可能永远不会使用dtplyr 完成?

从表面上看,dplyrdata.table 的优势听起来像是dtplyr 将超越dplyr。一旦dtplyr 完全成熟,还有什么理由使用dplyr

注意:我不是在询问 dplyrdata.table(如在 data.table vs dplyr: can one do something well the other can't or does poorly? 中),但考虑到对于特定问题,一个优先于另一个,为什么 dtplyr 不是解决问题的工具?使用。

【问题讨论】:

有没有在dplyr 可以做得很好而在data.table 中做得不好的事情?如果没有,切换到data.table 会比dtplyr 更好。 来自dtplyr 自述文件,'一些data.table 表达式没有直接的dplyr 等效项。例如,没有办法用dplyr 表示交叉或滚动连接。和 '为了匹配dplyr 语义,mutate() 默认不会就地修改。这意味着大多数涉及mutate() 的表达式必须制作一个如果您直接使用data.table 则不需要的副本。第二部分有一种解决方法,但考虑到mutate 的使用频率,这在我看来是一个很大的缺点。 【参考方案1】:

我会尽力提供最好的指导,但这并不容易,因为需要熟悉 data.table、dplyr、dtplyr 以及 base R 的所有内容。我使用 data.table 和许多 tidy-world 包(除了 dplyr)。两者都喜欢,尽管我更喜欢 data.table 的语法而不是 dplyr 的语法。我希望所有 tidy-world 软件包在必要时都使用 dtplyr 或 data.table 作为后端。

与任何其他翻译一样(想想 dplyr-to-sparkly/SQL),有些东西可以或不能翻译,至少现在是这样。我的意思是,也许有一天 dtplyr 可以让它 100% 翻译,谁知道呢。下面的列表并不详尽,也不是 100% 正确,因为我会根据我对相关主题/包/问题/等的了解尽力回答。

重要的是,对于那些不完全准确的答案,我希望它可以为您提供一些关于您应该注意data.table的哪些方面的指导,并将其与dtplyr进行比较并自行找出答案.不要认为这些答案是理所当然的。

而且,我希望这篇文章可以作为所有 dplyr、data.table 或 dtplyr 用户/创作者讨论和协作的资源之一,并使 #RStats 变得更好。

data.table 不仅用于快速和内存高效的操作。有很多人,包括我自己,更喜欢 data.table 的优雅语法。它还包括其他快速操作,如时间序列函数,如用 C 编写的 rolling-family(即frollapply)。它可以与任何函数一起使用,包括 tidyverse。我经常使用 data.table + purrr!

操作的复杂性

这很容易翻译

library(data.table)
library(dplyr)
library(flights)
data <- data.table(diamonds)

# dplyr 
diamonds %>%
  filter(cut != "Fair") %>% 
  group_by(cut) %>% 
  summarize(
    avg_price    = mean(price),
    median_price = as.numeric(median(price)),
    count        = n()
  ) %>%
  arrange(desc(count))

# data.table
data [
  ][cut != 'Fair', by = cut, .(
      avg_price    = mean(price),
      median_price = as.numeric(median(price)),
      count        = .N
    )
  ][order( - count)]

data.table 非常快速且内存效率很高,因为(几乎?)所有内容都是从 C 中以 update-by-reference 的关键概念从头开始构建的,key(想想 SQL ),以及它们在包中各处的不懈优化(即fifelsefread/fread,base R 采用的基数排序顺序),同时确保语法简洁一致,这就是我认为它优雅的原因。

从Introduction to data.table开始,子集、分组、更新、连接等主要数据操作操作被保存在一起

简洁一致的语法...

流畅地执行分析,无需映射每个操作的认知负担...

通过准确了解每个操作所需的数据,自动在内部非常有效地优化操作,从而生成非常快速且内存高效的代码

以最后一点为例,

# Calculate the average arrival and departure delay for all flights with “JFK” as the origin airport in the month of June.
flights[origin == 'JFK' & month == 6L,
        .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]

我们首先在 i 中进行子集查找匹配的行索引,其中起点机场等于“JFK”,月份等于 6L。我们尚未对与这些行对应的整个 data.table 进行子集化。

现在,我们查看 j 并发现它只使用了两列。我们要做的是计算它们的均值()。因此,我们只对与匹配行对应的那些列进行子集化,并计算它们的 mean()。

由于查询的三个主要组成部分(i、j 和 by)都在 [...] 中,data.table 可以看到所有三个部分并在之前一起优化查询评估,而不是每个单独的。因此,为了速度和内存效率,我们能够避免使用整个子集(即,对除 arr_delay 和 dep_delay 之外的列进行子集化)。

鉴于,为了获得 data.table 的好处,dtplr 的翻译在这方面必须是正确的。操作越复杂,翻译就越困难。对于像上面这样的简单操作,它当然可以很容易地翻译。对于复杂的,或者dtplyr不支持的,你要自己摸索,要对比翻译后的语法和benchmark,熟悉相关的包。

对于复杂的操作或不支持的操作,我也许可以在下面提供一些示例。再说一次,我只是尽力而为。对我温柔一点。

引用更新

我不会进入介绍/细节,但这里有一些链接

主要资源:Reference semantics

更多详情:Understanding exactly when a data.table is a reference to (vs a copy of) another data.table

Update-by-reference,在我看来,data.table 最重要的特性就是它如此快速和高效的内存。 dplyr::mutate 默认不支持。由于我不熟悉 dtplyr,我不确定 dtplyr 可以支持或不支持多少操作以及哪些操作。如上所述,它还取决于操作的复杂性,进而影响翻译。

在 data.table

中有两种方法可以使用 update-by-reference

data.table 的赋值运算符:=

set-family:setsetnamessetcolordersetkeysetDTfsetdiff 等等

:=set 更常用。对于复杂的大型数据集,update-by-reference 是获得最高速度和内存效率的关键。简单的思维方式(不是 100% 准确,因为细节比这复杂得多,因为它涉及硬/浅拷贝和许多其他因素),假设您正在处理 10GB 的大型数据集,每列 10 列和 1GB .要操作一列,您只需要处理 1GB。

关键是,使用 update-by-reference,您只需要处理所需的数据。这就是为什么在使用 data.table 时,尤其是处理大型数据集时,我们会尽可能地使用 update-by-reference。例如,操作大型建模数据集

# Manipulating list columns

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- data.table(df)

# data.table
dt [,
    by = Species, .(data   = .( .SD )) ][,  # `.(` shorthand for `list`
    model   := map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )) ][,
    summary := map(model, summary) ][,
    plot    := map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                           geom_point())]

# dplyr
df %>% 
  group_by(Species) %>% 
  nest() %>% 
  mutate(
    model   = map(data, ~ lm(Sepal.Length ~ Sepal.Width, data = . )),
    summary = map(model, summary),
    plot    = map(data, ~ ggplot( . , aes(Sepal.Length, Sepal.Width)) +
                          geom_point())
  )

由于 tidyverse 用户使用 tidyr::nest,dtlyr 可能不支持嵌套操作 list(.SD)?所以我不确定后续操作是否可以翻译为data.table的方式更快,内存更少。

注意:data.table 的结果以“毫秒”为单位,dplyr 以“分钟”为单位

df <- purrr::map_dfr(1:1e5, ~ iris)
dt <- copy(data.table(df))

bench::mark(
  check = FALSE,

  dt[, by = Species, .(data = list(.SD))],
  df %>% group_by(Species) %>% nest()
)
# # A tibble: 2 x 13
#   expression                                   min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#   <bch:expr>                              <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
# 1 dt[, by = Species, .(data = list(.SD))] 361.94ms 402.04ms   2.49      705.8MB     1.24     2     1
# 2 df %>% group_by(Species) %>% nest()        6.85m    6.85m   0.00243     1.4GB     2.28     1   937
# # ... with 5 more variables: total_time <bch:tm>, result <list>, memory <list>, time <list>,
# #   gc <list>

update-by-reference 有很多用例,甚至 data.table 用户也不会一直使用它的高级版本,因为它需要更多代码。 dtplyr 是否支持这些开箱即用的功能,您必须自己了解。

相同功能的多个引用更新

主要资源:Elegantly assigning multiple columns in data.table with lapply()

这涉及到更常用的:=set

dt <- data.table( matrix(runif(10000), nrow = 100) )

# A few variants

for (col in paste0('V', 20:100))
  set(dt, j = col, value = sqrt(get(col)))

for (col in paste0('V', 20:100))
  dt[, (col) := sqrt(get(col))]

# I prefer `purrr::map` to `for`
library(purrr)
map(paste0('V', 20:100), ~ dt[, (.) := sqrt(get(.))])

根据 data.table Matt Dowle 的创建者

(请注意,在大量行上循环设置可能比在大量列上循环设置更常见。)

Join + setkey + 引用更新

我最近需要使用相对较大的数据和类似的连接模式进行快速连接,因此我使用 update-by-reference 的强大功能,而不是普通连接。由于它们需要更多代码,因此我将它们包装在私有包中,并使用非标准评估以实现可重用性和可读性,我称之为 setjoin

我在这里做了一些基准测试:data.table join + update-by-reference + setkey

总结

# For brevity, only the codes for join-operation are shown here. Please refer to the link for details

# Normal_join
x <- y[x, on = 'a']

# update_by_reference
x_2[y_2, on = 'a', c := c]

# setkey_n_update
setkey(x_3, a) [ setkey(y_3, a), on = 'a', c := c ]

注意:dplyr::left_join 也经过了测试,它是最慢的,大约 9,000 毫秒,比 data.table 的 update_by_referencesetkey_n_update 使用更多内存,但比 data.table 使用更少的内存的 normal_join。它消耗了大约 2.0GB 的内存。我没有包含它,因为我只想专注于 data.table。

主要发现

setkey + updateupdate 分别比 normal join 快 ~11 和 ~6.5 倍 在首次加入时,setkey + update 的性能与 update 相似,因为 setkey 的开销在很大程度上抵消了其自身的性能提升 在第二次和后续连接中,由于不需要 setkeysetkey + updateupdate 快约 1.8 倍(或比 normal join 快约 11 倍)

示例

对于性能和内存效率高的联接,请使用updatesetkey + update,后者速度更快,但需要更多代码。

为了简洁起见,让我们看一些代码。逻辑是一样的。

一列或几列

a <- data.table(x = ..., y = ..., z = ..., ...)
b <- data.table(x = ..., y = ..., z = ..., ...)

# `update`
a[b, on = .(x), y := y]
a[b, on = .(x),  `:=` (y = y, z = z, ...)]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), y := y ]
setkey(a, x) [ setkey(b, x), on = .(x),  `:=` (y = y, z = z, ...) ]

对于许多列

cols <- c('x', 'y', ...)
# `update`
a[b, on = .(x), (cols) := mget( paste0('i.', cols) )]
# `setkey + update`
setkey(a, x) [ setkey(b, x), on = .(x), (cols) := mget( paste0('i.', cols) ) ]

用于快速和内存高效连接的包装器...其中许多...具有类似的连接模式,将它们包装为上面的 setjoin - 与update - 有或没有setkey

setjoin(a, b, on = ...)  # join all columns
setjoin(a, b, on = ..., select = c('columns_to_be_included', ...))
setjoin(a, b, on = ..., drop   = c('columns_to_be_excluded', ...))
# With that, you can even use it with `magrittr` pipe
a %>%
  setjoin(...) %>%
  setjoin(...)

对于setkey,可以省略参数on。也可以包含它以提高可读性,尤其是与他人协作时。

大行操作

如上所述,使用set 预填充您的表格,使用 update-by-reference 技术 使用键的子集(即setkey

相关资源:Add a row by reference at the end of a data.table object

引用更新总结

这些只是按引用更新的一些用例。还有很多。

如您所见,对于处理大数据的高级用法,有许多使用 update-by-reference 用于大数据集的用例和技术。在 data.table 中使用不是那么容易,dtplyr 是否支持,你可以自己去了解。

我在这篇文章中关注 update-by-reference,因为我认为它是 data.table 最强大的功能,可实现快速和内存高效的操作。也就是说,还有很多很多其他方面也使它如此高效,我认为 dtplyr 本身并不支持这些方面。

其他关键方面

支持/不支持什么,还取决于操作的复杂性以及是否涉及 data.table 的原生功能,如 update-by-referencesetkey。翻译后的代码是否更高效(data.table 用户会编写)也是另一个因素(即代码已翻译,但它是高效版本吗?)。很多东西都是相互关联的。

setkey。见Keys and fast binary search based subset Secondary indices and auto indexing Using .SD for Data Analysis 时间序列函数:想想frollapply。 rolling functions, rolling aggregates, sliding window, moving average rolling join, non-equi join, (some) "cross" join data.table 已经为速度和内存效率奠定了基础,未来它可以扩展到包含许多功能(例如它们如何实现上述时间序列功能) 一般来说,data.table 的ijby 操作越复杂(你可以在其中使用几乎任何表达式),我认为翻译越难,尤其是当它与 update-by-reference、setkey 和其他原生 data.table 函数,如 frollapply 另一点与使用基础 R 或 tidyverse 有关。我同时使用 data.table + tidyverse(dplyr/readr/tidyr 除外)。对于大型操作,我经常进行基准测试,例如,stringr::str_* 系列与基本 R 函数,我发现基本 R 在一定程度上更快并使用它们。重点是,不要只关注 tidyverse 或 data.table 或...,探索其他选项来完成工作。

其中许多方面与上述各点相互关联

操作的复杂性

参考更新

您可以了解 dtplyr 是否支持这些操作,尤其是当它们组合在一起时。

在处理小型或大型数据集时,另一个有用的技巧是,在交互式会话期间,data.table 真正实现了其极大地减少 编程计算 时间的承诺.

为速度和“增压行名”(未指定变量名的子集)重复使用的变量设置键。

dt <- data.table(iris)
setkey(dt, Species) 

dt['setosa',    do_something(...), ...]
dt['virginica', do_another(...),   ...]
dt['setosa',    more(...),         ...]

# `by` argument can also be omitted, particularly useful during interactive session
# this ultimately becomes what I call 'naked' syntax, just type what you want to do, without any placeholders. 
# It's simply elegant
dt['setosa', do_something(...), Species, ...]

如果您的操作只涉及第一个示例中的简单操作,dtplyr 可以完成工作。对于复杂/不受支持的,您可以使用本指南将 dtplyr 的翻译版本与经验丰富的 data.table 用户如何使用 data.table 的优雅语法以快速且内存高效的方式进行编码进行比较。翻译并不意味着它是最有效的方法,因为可能有不同的技术来处理不同的大数据案例。对于更大的数据集,您可以将 data.table 与 disk.frame、fst 和 drake 以及其他很棒的软件包结合使用,以充分利用它。还有一个big.data.table,但它目前处于非活动状态。

希望对大家有帮助。祝你有美好的一天☺☺

【讨论】:

【参考方案2】:

我想到了非 equi 连接和滚动连接。似乎没有任何计划在 dplyr 中包含等效功能,因此 dtplyr 没有什么可以翻译的。

还有 dplyr 中没有的重塑(优化的 dcast 和 melt 等效于 reshape2 中的相同功能)。

目前所有 *_if 和 *_at 函数也无法使用 dtplyr 进行翻译,但这些正在开发中。

【讨论】:

【参考方案3】:

在加入时更新列 一些.SD技巧 许多 f 函数 天知道还有什么,因为#rdatatable 不仅仅是一个简单的库,它不能用几个函数来概括

它本身就是一个完整的生态系统

从我开始使用 R 的那一天起,我就再也不需要 dplyr。因为 data.table 太棒了

【讨论】:

以上是关于我不能在 data.table 中使用 dtplyr 做啥的主要内容,如果未能解决你的问题,请参考以下文章

data.table fread 可以接受连接吗?

data.table vs dplyr:一个人可以做得很好,而另一个人不能或做得很差?

在单个 R data.table 中按组有效地定位

在 data.table 计算中使用选择中的总行数

在data.table R中滚动连接

R:在用户定义的函数中使用 get 和 data.table