我不能在 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 中的数据处理上,特别是在 dplyr
、dtplyr
和 data.table
之间?
我主要使用dplyr
,但是当数据太大时我会使用data.table
,这种情况很少见。所以现在dtplyr
v1.0 已经作为data.table
的接口出来了,从表面上看,我似乎再也不用担心使用data.table
接口了。
那么,data.table
的哪些最有用的功能或方面目前无法使用dtplyr
完成,而这可能永远不会使用dtplyr
完成?
从表面上看,dplyr
与data.table
的优势听起来像是dtplyr
将超越dplyr
。一旦dtplyr
完全成熟,还有什么理由使用dplyr
?
注意:我不是在询问 dplyr
与 data.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 ),以及它们在包中各处的不懈优化(即fifelse
、fread/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-referencedata.table 的赋值运算符:=
set
-family:set
、setnames
、setcolorder
、setkey
、setDT
、fsetdiff
等等
:=
比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_reference
和 setkey_n_update
使用更多内存,但比 data.table 使用更少的内存的 normal_join。它消耗了大约 2.0GB 的内存。我没有包含它,因为我只想专注于 data.table。
主要发现
setkey + update
和 update
分别比 normal join
快 ~11 和 ~6.5 倍
在首次加入时,setkey + update
的性能与 update
相似,因为 setkey
的开销在很大程度上抵消了其自身的性能提升
在第二次和后续连接中,由于不需要 setkey
,setkey + update
比 update
快约 1.8 倍(或比 normal join
快约 11 倍)
示例
对于性能和内存效率高的联接,请使用update
或setkey + 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-reference 或 setkey
。翻译后的代码是否更高效(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 的i
、j
或by
操作越复杂(你可以在其中使用几乎任何表达式),我认为翻译越难,尤其是当它与 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 做啥的主要内容,如果未能解决你的问题,请参考以下文章