我会尽力提供最好的指导,但这并不容易,因为需要熟悉 {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、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
:= 比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 用户会编写)也是另一个因素(即代码已翻译,但它是高效版本吗?)。很多东西都是相互关联的。
其中许多方面与上述各点相互关联
您可以了解 {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},但它目前处于非活动状态。
希望对大家有帮助。祝你有美好的一天☺☺