我是否应该将学习精力投入到R中的数据纠缠中,尤其是在dplyr
,dtplyr
和data.table
之间?
我主要使用dplyr
,但是当数据太大时,我会使用data.table
,这种情况很少见。因此,现在dtplyr
v1.0已成为data.table
的界面,从表面上看,我似乎再也不需要担心使用data.table
界面了。
那么,目前无法使用data.table
完成dtplyr
最有用的功能或方面是什么,而dtplyr
可能永远无法完成?
表面上,具有dplyr
优点的data.table
听起来像dtplyr
将超越dplyr
。 dplyr
完全成熟后,会有什么理由使用dtplyr
吗?
注意:我并不是在问dplyr
vs data.table
(就像在data.table vs dplyr: can one do something well the other can't or does poorly?中一样),但是鉴于一个特定问题,一个相对于另一个更受欢迎,为什么dtplyr
不能用作工具。
最佳答案
我将尽力提供最佳指南,但这并不容易,因为需要熟悉{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}的优雅语法。它还包括其他快速操作,如时序函数,如用C编写的滚动族(即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完全从头开始构建的,其关键概念包括按引用更新,关键(例如SQL)以及其无处不在的优化。 (即
fifelse
,fread/fread
,基数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()。因此,我们仅对与匹配行相对应的那些列进行子集,然后计算它们的mean()。
由于查询的三个主要组成部分(i,j和by)都在内部,因此data.table可以查看所有这三个并在评估之前完全优化查询,而不是分别进行优化。因此,为了速度和存储效率,我们能够避免整个子集(即,除了设置arr_delay和dep_delay之外的子集)。
鉴于此,为了获得{data.table}的好处,{dtplr}的翻译在这方面必须是正确的。操作越复杂,翻译越难。对于像上面这样的简单操作,它当然可以很容易地翻译。对于复杂的代码,或者{dtplyr}不支持的代码,您必须如上所述找到自己,必须比较翻译后的语法和基准并熟悉相关的软件包。
对于复杂的操作或不受支持的操作,我也许可以在下面提供一些示例。再说一次,我只是尽力而为。对我要温柔。
引用更新
我不会介绍介绍/详细信息,但是这里有一些链接
主要资源:Reference semantics
更多详细信息:Understanding exactly when a data.table is a reference to (vs a copy of) another data.table
在我看来,{data.table}的最重要的功能是按引用更新,这就是使它如此快速和高效存储的原因。
dplyr::mutate
默认情况下不支持它。由于我不熟悉{dtplyr},因此不确定{dtplyr}可以支持或不能支持多少操作。如上所述,它还取决于操作的复杂性,这又会影响翻译。在{data.table}中使用引用更新有两种方法
{data.table}
:=
的赋值运算符set
系列:set
,setnames
,setcolorder
,setkey
,setDT
,fsetdiff
等与
:=
相比,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())
)
{dtlyr}可能不支持嵌套操作
list(.SD)
,因为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}是否支持这些现成的功能,您必须自己了解一下。
相同功能的多个引用更新
主要资源: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 +按引用更新
最近,我需要使用较大的数据和类似的连接模式进行快速连接,因此我使用了按引用更新的功能,而不是普通连接。由于它们需要更多代码,因此我将它们包装在带有非标准评估的专用程序包中,以实现可重用性和可读性,我将其称为
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
预填充表格,使用按引用更新技术
使用键的子集(即
setkey
)相关资源:Add a row by reference at the end of a data.table object
引用更新摘要
这些只是按引用更新的一些用例。还有更多。
如您所见,对于处理大数据的高级用法,有许多用例和技术对大型数据集使用了按引用更新。在{data.table}中使用它并不是那么容易,并且{dtplyr}是否支持它,您可以自己找到。
我在本文中重点介绍按引用更新,因为它是{data.table}的最强大功能,可实现快速且内存高效的操作。就是说,还有许多其他方面也使其变得如此高效,我认为{dtplyr}本身并不支持这些方面。
其他关键方面
是否支持/不支持,还取决于操作的复杂性以及它是否涉及data.table的本机功能,例如按引用更新或
setkey
。而翻译后的代码是否更高效(data.table用户将编写的代码)也是另一个因素(即,代码已翻译,但它是有效版本吗?)。许多事物是相互联系的。setkey
。见Keys and fast binary search based subsetSecondary indices and auto indexing
Using .SD for Data Analysis
时间序列函数:请考虑
frollapply
。 rolling functions, rolling aggregates, sliding window, moving averagerolling join,non-equi join,(some) "cross" join
{data.table}为提高速度和内存效率奠定了基础,将来,它可以扩展为包括许多功能(例如它们如何实现上述时间序列功能)
通常,对data.table的
i
,j
或by
操作进行更复杂的操作(您几乎可以在其中使用任何表达式),我认为翻译的难度越大,尤其是当它与by-by-update结合使用时参考,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},但是它目前是不活动的。
希望对大家有帮助。祝你有美好的一天
关于r - 我无法在data.table中使用dtplyr做什么,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59054589/