我将尽力提供最佳指南,但这并不容易,因为您需要熟悉所有{data.table},{dplyr},{dtplyr}以及{base R}。我使用{data.table}和许多{tidy-world}软件包({dplyr}除外)。两者都可以,尽管我更喜欢data.table的语法而不是dplyr的语法。我希望所有整洁的程序包都将在必要时使用{dtplyr}或{data.table}作为后端。
与任何其他翻译(例如dplyr-to-sparkly / SQL)一样,至少在目前,有些东西可以翻译或不能翻译。我的意思是,知道,也许有一天{dtplyr}可以将其100%翻译。下面的列表并不详尽,也不是100%正确,因为我会根据对相关主题/包装/问题/等的了解尽我所能回答。
重要的是,对于那些并非完全准确的答案,我希望它为您提供一些有关{data.table}您应注意的方面的指南,并将其与{dtplyr}进行比较,并自己找出答案。不要将这些答案视为理所当然。
而且,我希望这篇文章可以用作所有{dplyr},{data.table}或{dtplyr}用户/创建者的资源之一,以进行讨论和协作,并使#RStats更好。
{data.table}不仅用于快速且内存高效的操作。包括我自己在内的许多人都喜欢{data.table}的优雅语法。它还包括其他快速操作,如时序函数,如frollapply
用C编写的滚动族(即)。它可以与任何函数一起使用,包括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完全从头开始构建的,其关键概念是按引用更新,关键(例如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的副本)
在我看来,{data.table}的最重要的功能是按引用更新,这就是使它如此快速和高效存储的原因。dplyr::mutate
默认情况下不支持它。由于我不熟悉{dtplyr},因此不确定{dtplyr}可以支持或不能支持多少操作。如上所述,它还取决于操作的复杂性,这又会影响翻译。
在{data.table}中使用引用更新有两种方法
:=
与相比更常用set
。对于复杂的大型数据集,按引用更新是获得最高速度和内存效率的关键。简单的思维方式(不是100%准确,因为细节涉及到硬/浅拷贝和许多其他因素,因此比这要复杂得多),例如您要处理的是10GB,10列和1GB的大型数据集。要操作一列,您只需要处理1GB。
关键是,通过引用更新,您只需要处理所需的数据。这就是为什么在使用{data.table}时,尤其是处理大型数据集时,我们尽可能一直使用按引用更新的原因。例如,处理大型建模数据集
# 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 +按引用更新
最近,我需要使用较大的数据和类似的连接模式进行快速连接,因此我使用了按引用更新的功能,而不是普通连接。由于它们需要更多代码,因此我将其包装在私有包中,并在非标准评估中将其称为可重用性和可读性setjoin
。
我在这里做了一些基准测试:data.table连接+引用更新+ 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对象的末尾添加一行
引用更新摘要
这些只是按引用更新的一些用例。还有更多。
如您所见,对于处理大数据的高级用法,有许多使用按引用更新的用例和技术对大型数据集。在{data.table}中使用它并不是那么容易,并且{dtplyr}是否支持它,您可以自己找到。
我专注于按引用更新在这篇文章中因为它是{data.table}的最强大功能,可实现快速且内存高效的操作。就是说,还有很多其他方面也使其变得如此高效,我认为{dtplyr}本身并不支持这些方面。
其他关键方面
是否支持,还取决于操作的复杂性以及它是否涉及data.table的本机功能,例如按引用更新或setkey
。而翻译后的代码是否更高效(data.table用户将编写的代码)也是另一个因素(即,代码已翻译,但这是有效版本吗?)。许多事物是相互联系的。
setkey
。请参阅密钥和基于快速二进制搜索的子集
- 二级索引和自动索引
- 使用.SD进行数据分析
- 时间序列函数:思考
frollapply
。滚动功能,滚动聚合,滑动窗口,移动平均值
- 滚动联接,非等联接,(一些)“交叉”联接
- {data.table}为提高速度和内存效率奠定了基础,将来,它可以扩展为包括许多功能(例如它们如何实现上述时间序列功能)
- 一般来说,在data.table的更复杂的操作
i
,j
或者by
操作(可以用在那里几乎所有的表情),我觉得很难翻译,特别是当它与结合更新的基准,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},但当前处于非活动状态。
希望对大家有帮助。祝你有美好的一天
dplyr
,你不能做好data.table
?如果没有,切换到data.table
会比更好dtplyr
。