16

我应该把我的学习精力投入到 R 中的数据争论上,特别是在dplyrdtplyr之间data.table吗?

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

  • 那么目前无法使用的最有用的功能或方面data.table什么,并且可能永远不会使用?dtplyrdtplyr

  • 从表面上看,它dplyr的好处data.table听起来像是dtplyr会超车dplyr的。dplyr一旦dtplyr完全成熟,会有什么理由使用吗?

注意:我不是在问dplyrvs data.table(如data.table vs dplyr:一个人可以做得很好,而另一个人不能或做得不好?),但考虑到对于特定问题,一个人比另一个人更受欢迎,为什么不呢?不是dtplyr要使用的工具。

4

3 回答 3

35

我会尽力提供最好的指导,但这并不容易,因为需要熟悉所有的 {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} 的优雅语法。它还包括其他快速操作,如时间序列函数,如frollapply用 C 编写的 rolling-family (ie)。它可以与任何函数一起使用,包括 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)的关键概念,以及它们在包中各处的无情优化(即fifelse, fread/fread,基数 R 采用的基数排序顺序),同时确保语法简洁一致,这就是为什么我认为它很优雅。

Introduction 到 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} 不支持的,你必须像上面提到的那样自己去寻找,必须比较翻译的语法和基准,熟悉相关的包。

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

引用更新

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

主要资源:参考语义

更多详细信息:准确了解 data.table 何时是对另一个 data.table 的引用(相对于副本)

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

在 {data.table} 中有两种方法可以通过引用更新

  • {data.table} 的赋值运算符:=

  • set-family: set, setnames, setcolorder, setkey, setDT, fsetdiff, 等等

:=与 相比更常用set。对于复杂的大型数据集,按引用更新是获得最高速度和内存效率的关键。简单的思维方式(不是 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())
  )

list(.SD){dtlyr} 可能不支持嵌套操作,因为 tidyverse 用户使用tidyr::nest? 所以我不确定后续操作是否可以翻译为 {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>

引用更新有很多用例,甚至 {data.table} 用户也不会一直使用它的高级版本,因为它需要更多代码。{dtplyr} 是否支持这些开箱即用,您必须自己找出答案。

相同功能的多次引用更新

主要资源:使用 lapply() 优雅地分配 data.table 中的多个列

这涉及到更常用的:=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

我最近需要使用相对较大的数据和类似的连接模式进行快速连接,所以我使用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_reference和都多setkey_n_update,但比 {data.table} 的 normal_join 使用更少的内存。它消耗了大约 2.0GB 的内存。我没有包括它,因为我只想专注于 {data.table}。

主要发现

  • setkey + update和分别update比 快 ~11 和 ~6.5 倍normal join
  • 在第一次加入时,性能setkey + update类似于,update因为开销在setkey很大程度上抵消了其自身的性能提升
  • 在第二次和后续连接上,setkey不需要,setkey + 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
  • 预填充您的表,使用按引用更新技术
  • 使用键的子集(即setkey

相关资源:在 data.table 对象的末尾通过引用添加一行

引用更新总结

这些只是update-by-reference 的一些用例。还有很多。

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

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

其他关键方面

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

  • setkey. 请参阅基于键和快速二分搜索的子集
  • 二级索引和自动索引
  • 使用 .SD 进行数据分析
  • 时间序列函数:think frollapply. 滚动函数、滚动聚合、滑动窗口、移动平均
  • 滚动连接非等连接(一些)“交叉”连接
  • {data.table} 已经在速度和内存效率方面打下了基础,未来它可以扩展到包含许多功能(比如它们如何实现上面提到的时间序列功能)
  • 一般来说,对 data.table 的操作或操作越复杂ijby可以在其中使用几乎任何表达式),我认为翻译越难,尤其是当它与update-by-referencesetkey和其他本机 data.table结合使用时像这样的功能frollapply
  • 另一点与使用 base 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}但它目前处于非活动状态。

我希望它对每个人都有帮助。祝你有美好的一天☺☺

于 2019-11-28T03:21:53.583 回答
5

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

还有重塑(优化的 dcast 和熔化等效于 reshape2 中的相同功能),这也不在 dplyr 中。

所有的 *_if 和 *_at 函数目前也不能用 dtplyr 进行翻译,但它们正在开发中。

于 2019-11-27T16:28:23.433 回答
1

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

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

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

于 2019-11-27T03:27:23.160 回答