私は最善のガイドを提供しようとしますが、{data.table}、{dplyr}、{dtplyr}、および{base R}のすべてに精通している必要があるため、簡単ではありません。私は{data.table}と多くの{tidy-world}パッケージ({dplyr}を除く)を使用しています。両方が大好きですが、私はdplyrよりもdata.tableの構文を好みます。すべての整頓された世界のパッケージが必要なときにいつでもバックエンドとして{dtplyr}または{data.table}を使用することを願っています。
他の変換と同様に(dplyr-to-sparkly / SQLを考えてください)、少なくとも現時点では、変換できるものとできないものがあります。つまり、ある日{dtplyr}が100%翻訳できるようになるかもしれません。以下のリストは完全なものではなく、100%正しいものです。関連するトピック/パッケージ/問題/その他に関する私の知識に基づいて、最善を尽くしてお答えします。
重要な点として、完全に正確ではない回答については、{data.table}のどの側面に注意を払うべきかについてのガイドを提供し、それを{dtplyr}と比較して、自分で回答を見つけられることを願っています。これらの答えを当たり前のことと考えないでください。
また、この投稿が{dplyr}、{data.table}、または{dtplyr}のすべてのユーザー/作成者のリソースの1つとしてディスカッションやコラボレーションに使用され、#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からゼロから構築されているため、update-by-reference、キー(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))]
クエリの3つの主要コンポーネント(i、j、by)が一緒に[...]内にあるため、data.table は3つすべてを表示し、個別にではなく、評価前にクエリをすべて最適化できます。したがって、速度とメモリ効率の両方について、サブセット全体(つまり、arr_delayとdep_delay以外の列のサブセット)を回避できます。
そのため、{data.table}のメリットを享受するには、その点で{dtplr}の翻訳が正しい必要があります。操作が複雑になるほど、翻訳は難しくなります。上記のような単純な操作の場合、簡単に翻訳できます。複雑なもの、または{dtplyr}でサポートされていないものについては、上記のように自分自身を見つける必要があります。翻訳された構文とベンチマークを比較し、よく知られた関連パッケージである必要があります。
複雑な操作またはサポートされていない操作の場合、以下の例をいくつか提供できるかもしれません。繰り返しますが、私は最善を尽くしています。私に優しくしてください。
参照による更新
イントロ/詳細には入りませんが、ここにいくつかのリンクがあります
主なリソース:リファレンスセマンティクス
詳細:data.tableが別のdata.tableへの参照(のコピー)である場合を正確に理解する
私の意見では、更新による参照は、{data.table}の最も重要な機能であり、それにより、高速かつメモリ効率が高くなっています。dplyr::mutate
デフォルトではサポートしていません。私は{dtplyr}に詳しくないので、{dtplyr}でサポートできる操作とサポートできない操作の量がわかりません。上記のように、それはまた、翻訳に影響を与える操作の複雑さにも依存します。
{data.table}でupdate-by-referenceを使用する方法は2つあります
:=
と比較して、より一般的に使用されset
ます。複雑で大規模なデータセットの場合、参照による更新が最高の速度とメモリ効率を実現する鍵となります。簡単な考え方(100%正確ではありません。ハード/浅いコピーやその他の多くの要因が関係するため、詳細はこれよりもはるかに複雑であるため)は、10列とそれぞれ1GBの10GBの大きなデータセットを扱っているとしましょう。 。1つの列を操作するには、1GBのみを処理する必要があります。
重要な点は、update-by-referenceを使用すると、必要なデータのみを処理する必要があるということです。そのため、{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)
tidyverseユーザーが使用しているため、ネスト操作は{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>
update-by-referenceの多くのユースケースがあり、{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 + update-by-reference
最近、比較的大きなデータと同様の結合パターンを使用した高速結合が必要になったため、通常の結合ではなく、参照による更新機能を使用します。彼らはより多くのコードを必要とするので、私はそれを私が呼ぶところの再利用性と可読性のための非標準の評価でプライベートパッケージにそれらをラップします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
また、試験し、それは両方の{} data.tableより多くのメモリを使用しての、9000ミリ秒〜で最も遅いのだ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
後者の方がコードが多くなりますが高速です。
簡潔にするために、いくつかの疑似コードを見てみましょう。ロジックは同じです。
1つまたはいくつかの列
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オブジェクトの最後に参照によって行を追加します
参照による更新の要約
これらはupdate-by-referenceのいくつかの使用例です。他にもたくさんあります。
ご覧のとおり、大きなデータを処理する高度な使用法には、大きなデータセットに対して参照による更新を使用する多くのユースケースと手法があります。{data.table}での使用はそれほど簡単ではなく、{dtplyr}がそれをサポートしているかどうかは、自分で確認できます。
この投稿ではupdate-by-referenceに焦点を当てています。これは、高速でメモリ効率の高い操作のための{data.table}の最も強力な機能だと思います。そうは言っても、それを非常に効率的にする他の多くの側面があり、それは{dtplyr}によってネイティブにサポートされていないと思います。
その他の重要な側面
サポートされているものとサポートされていないものは、操作の複雑さ、およびupdate-by-referenceやのようなdata.tableのネイティブ機能が関係しているかどうかにも依存しますsetkey
。そして、変換されたコードがより効率的なもの(data.tableユーザーが書くもの)であるかどうかも、別の要因です(つまり、コードは変換されますが、効率的なバージョンですか?)。多くのものが相互に関連しています。
setkey
。キーと高速バイナリ検索ベースのサブセットを参照してください
- セカンダリインデックスと自動インデックス
- データ分析のための.SDの使用
- 時系列関数:考えます
frollapply
。ローリング関数、ローリング集計、スライディングウィンドウ、移動平均
- ローリング結合、非等価結合、(一部)「クロス」結合
- {data.table}は速度とメモリ効率の基盤を構築しました。将来的には、拡張して多くの関数を含めることができます(上記の時系列関数の実装方法など)。
- 一般的に、data.tableの上、より複雑な操作
i
、j
またはby
操作が、私はそれはと組み合わせる場合は特に、難しい翻訳を考える(あなたはそこのほぼすべての式を使用することができます)更新・バイ・リファレンス、setkey
およびその他のネイティブdata.tableのような機能frollapply
- もう1つのポイントは、ベースRまたはティディバースの使用に関連しています。私はdata.table + tidyverse(dplyr / readr / tidyrを除く)の両方を使用しています。大規模な操作の場合、たとえば、
stringr::str_*
ファミリー関数とベースR関数のベンチマークを行うことがよくあり、ベースRの方がある程度高速であり、それらを使用しています。ポイントは、tidyverseやdata.tableだけに固執しないでください。他のオプションを調べて、仕事を完了してください。
これらの側面の多くは、上記のポイントと相互に関連しています
特にこれらが組み合わされている場合、{dtplyr}がこれらの操作をサポートしているかどうかを確認できます。
小規模または大規模なデータセットを処理するときのもう1つの便利なトリックは、対話型セッション中に、{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
。