我无法在data.table中使用dtplyr做什么


9

我应该投资我对数据中的R扯皮,尤其具有学习努力dplyrdtplyrdata.table

  • dplyr主要使用数据,但是当数据太大而无法使用时data.table,这种情况很少发生。因此,现在dtplyrv1.0已成为的接口data.table,从表面上看,我似乎再也不需要担心使用该data.table接口。

  • 那么什么是最有用的特性或方面data.table不能被使用做dtplyr的那一刻,那可能永远不会有做dtplyr

  • 从表面上看,dplyr借助的好处,data.table它听起来像dtplyr会超越dplyr。会不会有任何理由来使用dplyr,一旦dtplyr已经完全成熟?

注意:我不是在问dplyrvs data.table(就像在data.table vs dplyr中:一个人可以做得很好而另一个不能做得不好或做得不好吗?),但是鉴于在一个特定问题上一个人比另一个人更受青睐,为什么呢?牛逼dtplyr是使用工具。


1
有什么是可以做的很好的dplyr,你不能做好data.table?如果没有,切换到data.table会比更好dtplyr
sindri_baldur

2
根据dtplyr自述文件,“某些data.table表达式没有直接对dplyr等的含义。例如,无法用来表达交叉或滚动连接dplyr。和'为了匹配dplyr语义,mutate默认情况下()不会在适当位置进行修改。这意味着大多数涉及的表达式mutate()必须制作一个副本,如果您data.table直接使用的话则不需要。在第二部分中有某种方法,但是考虑到使用频率mutate,在我看来这是一个很大的缺点。
ClancyStats

Answers:


15

我将尽力提供最佳指南,但这并不容易,因为您需要熟悉所有{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)以及它们在软件包中的无处不在的优化。 (即fifelsefread/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中的第一个子集找到匹配的行索引,其中起始机场等于“ JFK”,月份等于6L。我们尚未将与这些行相对应的整个data.table子集化。

  • 现在,我们看一下j,发现它仅使用两列。我们要做的是计算它们的mean()。因此,我们只对与匹配行相对应的那些列进行子集,然后计算它们的mean()。

由于查询三个主要组成部分(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}中使用引用更新有两种方法

  • {data.table}的赋值运算符 :=

  • set-家庭:setsetnamessetcolordersetkeysetDTfsetdiff,多

:=与相比更常用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 + updateupdate速度比是〜11和〜6.5倍normal join,分别
  • 在首次加入时,的性能setkey + update类似于update开销setkey大大抵消了其自身的性能提升
  • 在第二次和随后的联接上(setkey不需要),setkey + update速度快于update〜1.8倍(或快于normal join〜11倍)

图片

例子

对于高效的性能和内存有效的联接,请使用updatesetkey + 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(...)

setkeyon可以忽略参数。也可以包括它,以提高可读性,尤其是与他人合作时。

大行操作

  • 如上所述,使用 set
  • 预填充表格,使用引用更新技术
  • 使用键的子集(即setkey

相关资源: 通过引用在data.table对象的末尾添加一行

引用更新摘要

这些只是按引用更新的一些用例。还有更多。

如您所见,对于处理大数据的高级用法,有许多使用按引用更新的用例和技术对大型数据集。在{data.table}中使用它并不是那么容易,并且{dtplyr}是否支持它,您可以自己找到。

我专注于按引用更新在这篇文章中因为它是{data.table}的最强大功能,可实现快速且内存高效的操作。就是说,还有很多其他方面也使其变得如此高效,我认为{dtplyr}本身并不支持这些方面。

其他关键方面

是否支持,还取决于操作的复杂性以及它是否涉及data.table的本机功能,例如按引用更新setkey。而翻译后的代码是否更高效(data.table用户将编写的代码)也是另一个因素(即,代码已翻译,但这是有效版本吗?)。许多事物是相互联系的。

  • setkey。请参阅密钥和基于快速二进制搜索的子集
  • 二级索引和自动索引
  • 使用.SD进行数据分析
  • 时间序列函数:思考frollapply滚动功能,滚动聚合,滑动窗口,移动平均值
  • 滚动联接非等联接(一些)“交叉”联接
  • {data.table}为提高速度和内存效率奠定了基础,将来,它可以扩展为包括许多功能(例如它们如何实现上述时间序列功能)
  • 一般来说,在data.table的更复杂的操作ij或者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},但当前处于非活动状态。

希望对大家有帮助。祝你有美好的一天


2

我想到了非等额联接和滚动联接。似乎没有任何计划在dplyr中包含等效功能,因此dtplyr没有任何翻译内容。

还存在dplyr中不存在的重塑(优化的dcast和melt等效于reshape2中的相同功能)。

当前所有的* _if和* _at函数也无法使用dtplyr进行翻译,但是它们正在使用中。


0

更新有关连接的专栏一些.SD技巧很多f函数而且上帝知道其他事情,因为#rdatatable不仅是一个简单的库,而且不能用很少的函数来概括

它是一个完整的生态系统

自从开始R的那一天起,我就再也不需要dplyr了。因为data.table真是太好了

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.