我会尽力提供最好的指导,但这并不容易,因为需要熟悉所有的 {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、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 + 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
- 预填充您的表,使用按引用更新技术
- 使用键的子集(即
setkey
)
相关资源:在 data.table 对象的末尾通过引用添加一行
引用更新总结
这些只是update-by-reference 的一些用例。还有很多。
如您所见,对于处理大数据的高级用法,有许多用例和技术使用大数据集的引用更新。在 {data.table} 中使用起来不是那么容易,{dtplyr} 是否支持它,你可以自己去了解。
我在这篇文章中专注于按引用更新,因为我认为它是 {data.table} 最强大的功能,可实现快速和内存高效的操作。也就是说,还有很多很多其他方面也使它如此高效,我认为 {dtplyr} 本身并不支持这些方面。
其他关键方面
支持/不支持什么,它还取决于操作的复杂性以及它是否涉及 data.table 的原生功能,如update-by-reference或setkey
. 翻译后的代码是否更高效(data.table 用户会编写)也是另一个因素(即代码已翻译,但它是高效版本吗?)。许多事情是相互关联的。
setkey
. 请参阅基于键和快速二分搜索的子集
- 二级索引和自动索引
- 使用 .SD 进行数据分析
- 时间序列函数:think
frollapply
. 滚动函数、滚动聚合、滑动窗口、移动平均
- 滚动连接,非等连接,(一些)“交叉”连接
- {data.table} 已经在速度和内存效率方面打下了基础,未来它可以扩展到包含许多功能(比如它们如何实现上面提到的时间序列功能)
- 一般来说,对 data.table 的操作或操作越复杂
i
(j
您by
可以在其中使用几乎任何表达式),我认为翻译越难,尤其是当它与update-by-reference、setkey
和其他本机 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}但它目前处于非活动状态。
我希望它对每个人都有帮助。祝你有美好的一天☺☺