这个问题是关于理解我应该在哪里进行R上的数据争夺研究。特别是在dplyr
,dtplyr
和data.table
之间。我主要使用dplyr
,但是当数据太大时,我会使用data.tabe
,这种情况很少见。因此,现在dtplyr
v1.0可以作为data.table
的接口了,从表面上看,我似乎不再需要担心再次使用data.table
接口。
那么,data.table
哪些最有用的功能或方面目前无法使用dtplyr
来完成,而dtplyr
可能永远无法完成? / strong>
从表面上看,dplyr
受益于data.table
,听起来dtplyr
将会超越dplyr
。 dplyr
完全成熟后,是否有任何理由使用dtplyr
?
注意:我并不是在问dplyr
与data.table
(就像data.table vs dplyr: can one do something well the other can't or does poorly?一样),但是考虑到在一个特定问题上一个人比另一个人更受青睐,为什么不{ {1}}是要使用的工具。
答案 0 :(得分:10)
我将尽力提供最好的指南,但这并不容易,因为需要熟悉{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}时,尤其是处理大型数据集时,我们会尽可能一直使用 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用户使用list(.SD)
,{dtlyr}可能不支持嵌套操作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的创建者
(请注意,对大量行进行循环设置比对大量列进行循环设置更为常见。)
最近我需要使用较大的数据和类似的连接模式进行快速连接,因此我使用了按引用更新的功能,而不是普通连接。由于它们需要更多代码,因此我将其包装在专用软件包中,并进行非标准评估,以将其称为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
,其中后者更快,但需要更多代码。
为简便起见,让我们看一些 pseudo 代码。逻辑是相同的。
针对一列或几列
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 subset frollapply
。 rolling functions, rolling aggregates, sliding window, moving average i
,j
或by
操作(您几乎可以在其中使用任何表达式)进行更复杂的操作,我认为翻译越难,特别是与按引用更新,setkey
和其他本机data.table函数(例如frollapply
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},但目前不活跃。
我希望它能对所有人有所帮助。祝你有美好的一天☺☺
答案 1 :(得分:0)
更新联接列 .SD的一些技巧 许多f函数 而且上帝知道还有什么,因为#rdatatable不仅是一个简单的库,而且不能用很少的功能来总结
这是一个完整的生态系统
从我开始R的那一天起,我就再也不需要dplyr了。因为data.table太好了
答案 2 :(得分:0)
想到了非等额联接和滚动联接。似乎没有任何计划在dplyr中完全包含等效功能,因此dtplyr没有任何翻译内容。
还存在dplyr所不具备的重塑功能(优化的dcast和melt等效于reshape2中的相同功能)。
当前所有的* _if和* _at函数也无法使用dtplyr进行翻译,但这些功能正在开发中。