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

时间:2019-11-26 15:46:51

标签: r dplyr data.table dtplyr

这个问题是关于理解我应该在哪里进行R上的数据争夺研究。特别是在dplyrdtplyrdata.table之间。我主要使用dplyr,但是当数据太大时,我会使用data.tabe,这种情况很少见。因此,现在dtplyr v1.0可以作为data.table的接口了,从表面上看,我似乎不再需要担心再次使用data.table接口。

那么,data.table哪些最有用的功能或方面目前无法使用dtplyr来完成,而dtplyr可能永远无法完成? / strong>

从表面上看,dplyr受益于data.table,听起来dtplyr将会超越dplyrdplyr完全成熟后,是否有任何理由使用dtplyr

注意:我并不是在问dplyrdata.table(就像data.table vs dplyr: can one do something well the other can't or does poorly?一样),但是考虑到在一个特定问题上一个人比另一个人更受青睐,为什么不{ {1}}是要使用的工具。

3 个答案:

答案 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)构建的),以及它们在程序包中无处不在的优化(即fifelsefread/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-家族:setsetnamessetcolordersetkeysetDTfsetdiff,还有更多

:=相比,

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_referencesetkey_n_update使用更多的内存,但是使用更少的内存比{data.table}的normal_join大。它消耗了约2.0GB的内存。我没有包含它,因为我只想专注于{data.table}。

主要发现

  • setkey + updateupdate分别比normal join快11倍和6.5倍左右。
  • 在第一次加入时,setkey + update的性能类似于update,因为setkey的开销在很大程度上抵消了其自身的性能提升
  • 在第二次和后续联接中,由于不需要setkey,因此setkey + updateupdate快约1.8倍(或比normal join快约11倍)

Image

示例

对于性能和内存高效的联接,请使用updatesetkey + 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
  • Secondary indices and auto indexing
  • Using .SD for Data Analysis
  • 时间序列函数:思考frollapplyrolling functions, rolling aggregates, sliding window, moving average
  • rolling joinnon-equi join(some) "cross" join
  • {data.table}为提高速度和内存效率奠定了基础,将来,它可以扩展为包括许多功能(例如它们如何实现上述时间序列功能)
  • 通常,对data.table的ijby操作(您几乎可以在其中使用任何表达式)进行更复杂的操作,我认为翻译越难,特别是与按引用更新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},但目前不活跃。

我希望它能对所有人有所帮助。祝你有美好的一天☺☺

答案 1 :(得分:0)

更新联接列 .SD的一些技巧 许多f函数 而且上帝知道还有什么,因为#rdatatable不仅是一个简单的库,而且不能用很少的功能来总结

这是一个完整的生态系统

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

答案 2 :(得分:0)

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

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

当前所有的* _if和* _at函数也无法使用dtplyr进行翻译,但这些功能正在开发中。