datatableの上にdplyr構文を使用すると、dplyrの構文を使用しながらdatatableの速度の利点をすべて得ることができますか?言い換えれば、dplyr構文でクエリを実行した場合、データテーブルを誤って使用しますか?または、純粋なデータテーブル構文を使用してそのすべての能力を活用する必要がありますか。
アドバイスを事前に感謝します。コード例:
library(data.table)
library(dplyr)
diamondsDT <- data.table(ggplot2::diamonds)
setkey(diamondsDT, cut)
diamondsDT %>%
filter(cut != "Fair") %>%
group_by(cut) %>%
summarize(AvgPrice = mean(price),
MedianPrice = as.numeric(median(price)),
Count = n()) %>%
arrange(desc(Count))
結果:
# cut AvgPrice MedianPrice Count
# 1 Ideal 3457.542 1810.0 21551
# 2 Premium 4584.258 3185.0 13791
# 3 Very Good 3981.760 2648.0 12082
# 4 Good 3928.864 3050.5 4906
ここに私が思いついたデータテーブルの等価性があります。 DTのグッドプラクティスに準拠しているかどうかはわかりません。しかし、私はコードが舞台裏でdplyr構文よりも本当に効率的かどうか疑問に思います:
diamondsDT [cut != "Fair"
] [, .(AvgPrice = mean(price),
MedianPrice = as.numeric(median(price)),
Count = .N), by=cut
] [ order(-Count) ]
これらの両方のパッケージの哲学は特定の側面で異なるため、単純明快な答えはありません。したがって、いくつかの妥協は避けられません。対処/検討する必要がある懸念事項の一部を以下に示します。
i
を含む操作(== filter()
およびdplyrのslice()
)たとえば、10列のDT
を想定します。これらのdata.table式を検討してください。
_DT[a > 1, .N] ## --- (1)
DT[a > 1, mean(b), by=.(c, d)] ## --- (2)
_
(1)DT
の列数_a > 1
_の行数を示します。 (2)i
の(1)と同じ式に対して_c,d
_でグループ化されたmean(b)
を返します。
一般的に使用されるdplyr
式は次のとおりです。
_DT %>% filter(a > 1) %>% summarise(n()) ## --- (3)
DT %>% filter(a > 1) %>% group_by(c, d) %>% summarise(mean(b)) ## --- (4)
_
明らかに、data.tableコードは短くなっています。さらに、それらはよりメモリ効率がよい1。どうして? (3)と(4)の両方で、filter()
は最初に10列すべてに対してrowsを返すため、(3)では必要なのは行数だけで、(4)では連続した操作のために列_b, c, d
_が必要です。これを克服するには、select()
列を先験的にする必要があります。
_DT %>% select(a) %>% filter(a > 1) %>% summarise(n()) ## --- (5)
DT %>% select(a,b,c,d) %>% filter(a > 1) %>% group_by(c,d) %>% summarise(mean(b)) ## --- (6)
_
2つのパッケージの主な哲学的違いを強調することが不可欠です。
_
data.table
_では、これらの関連する操作をまとめておきたいので、(同じ関数呼び出しから)_j-expression
_を見て、(1)の列が必要ないことを認識できます。i
の式は計算され、_.N
_は、行数を与える論理ベクトルの単なる合計です。サブセット全体が実現されることはありません。 (2)では、列_b,c,d
_のみがサブセットで具体化され、他の列は無視されます。しかし、
dplyr
では、関数に関数を1つだけ実行させるwellという哲学があります。 (少なくとも現在)filter()
の後の操作がフィルタリングしたすべての列を必要とするかどうかを知る方法はありません。このようなタスクを効率的に実行する場合は、先を考える必要があります。この場合、私は個人的にそれを反直感的だと思います。
(5)および(6)で、必要ではない列a
をサブセット化することに注意してください。しかし、私はそれを避ける方法がわかりません。 filter()
関数に返す列を選択する引数がある場合、この問題を回避できますが、関数は1つのタスクのみを実行しません(これはdplyr設計の選択でもあります)。
dplyrは参照によりnever更新します。これは、2つのパッケージのもう1つの大きな(哲学的な)違いです。
たとえば、data.tableでは次のことができます。
_DT[a %in% some_vals, a := NA]
_
条件を満たす列だけで列a
を参照で更新します。現時点では、dplyrは、data.table全体を内部的にコピーして、新しい列を追加します。 @BrodieGは、これについて彼の回答ですでに言及しています。
ただし、 FR#617 が実装されている場合、ディープコピーはシャローコピーに置き換えることができます。関連: dplyr:FR#614 ただし、変更した列は常にコピーされることに注意してください(したがって、少し遅くなり、メモリ効率が低下します)。参照によって列を更新する方法はありません。
Data.tableでは、結合中に集約できます。これは理解しやすく、中間結合の結果が実現されないため、メモリ効率が良くなります。 this post をチェックしてください。 dplyrのdata.table/data.frame構文を使用して(現時点では)実行できません。
data.tableの ローリング結合 機能は、dplyrの構文でもサポートされていません。
最近、data.tableにオーバーラップ結合を実装して、間隔範囲( 例はこちら )で結合します。これは、現時点では別個の関数foverlaps()
であり、したがってパイプ演算子(magrittr/pipeR?-自分で試したことはありません)。
しかし最終的に、私たちの目標は、それを_[.data.table
_に統合して、グループ化、参加中の集約など、上記で説明したのと同じ制限を持つ他の機能を活用できるようにすることです。
1.9.4以降、data.tableは、通常のR構文の高速バイナリ検索ベースのサブセットのために、セカンダリキーを使用した自動インデックス付けを実装しています。例:_DT[x == 1]
_および_DT[x %in% some_vals]
_は、最初の実行時に自動的にインデックスを作成します。インデックスは、同じ列の連続するサブセットで使用され、バイナリ検索を使用して高速サブセットになります。この機能は進化し続けます。この機能の概要については、 this Gist を確認してください。
filter()
がdata.tablesに実装されている方法からは、この機能を利用していません。
Dplyrの機能は、同じ構文を使用して データベースへのインターフェイス も提供することです。これは、現時点ではdata.tableにはありません。
したがって、これら(およびおそらく他のポイント)を検討し、これらのトレードオフが受け入れられるかどうかに基づいて決定する必要があります。
HTH
(1)ほとんどの場合、ボトルネックはメインメモリからキャッシュにデータを移動する(およびキャッシュ内のデータを可能な限り使用する-キャッシュミスを減らす)ため、メモリ効率が直接速度に影響することに注意してください-メインメモリへのアクセスを減らすため)。ここでは詳しく説明しません。
やってみなよ。
library(rbenchmark)
library(dplyr)
library(data.table)
benchmark(
dplyr = diamondsDT %>%
filter(cut != "Fair") %>%
group_by(cut) %>%
summarize(AvgPrice = mean(price),
MedianPrice = as.numeric(median(price)),
Count = n()) %>%
arrange(desc(Count)),
data.table = diamondsDT[cut != "Fair",
list(AvgPrice = mean(price),
MedianPrice = as.numeric(median(price)),
Count = .N), by = cut][order(-Count)])[1:4]
この問題では、data.tableはdata.tableを使用したdplyrよりも2.4倍高速です。
test replications elapsed relative
2 data.table 100 2.39 1.000
1 dplyr 100 5.77 2.414
改訂 Polymeraseのコメントに基づく。
質問に答えるには:
data.table
_を使用していますdata.table
_構文の場合ほど効率的ではありません多くの場合、これはdplyr
構文が必要な人にとっては許容できる妥協案になりますが、プレーンデータフレームのdplyr
よりも遅くなる可能性があります。
大きな要因の1つは、グループ化時にdplyr
がデフォルトで_data.table
_をコピーすることです。考慮します(マイクロベンチマークを使用):
_Unit: microseconds
expr min lq median
diamondsDT[, mean(price), by = cut] 3395.753 4039.5700 4543.594
diamondsDT[cut != "Fair"] 12315.943 15460.1055 16383.738
diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price)) 9210.670 11486.7530 12994.073
diamondsDT %>% filter(cut != "Fair") 13003.878 15897.5310 17032.609
_
フィルタリングは同等の速度ですが、グループ化はそうではありません。犯人は_dplyr:::grouped_dt
_のこの行だと思います:
_if (copy) {
data <- data.table::copy(data)
}
_
ここで、copy
のデフォルトはTRUE
です(そして、簡単にFALSEに変更することはできません)。これはおそらく100%の違いを考慮していませんが、diamonds
のサイズに関する一般的なオーバーヘッドだけでは完全な違いではないでしょう。
問題は、一貫した文法を持つために、dplyr
が2つのステップでグループ化を行うことです。最初に、グループに一致する元のデータテーブルのコピーにキーを設定し、後でグループ化します。 _data.table
_は、最大の結果グループ(この場合は1行のみ)にメモリを割り当てるだけなので、割り当てるメモリの量に大きな違いが生じます。
参考までに、誰も気にしないなら、treeprof
出力用の実験的(そしてまだ非常にアルファな)ツリービューアーであるRprof
(install_github("brodieg/treeprof")
)を使用してこれを見つけました。
上記は現在、Macs AFAIKでのみ機能します。また、残念ながら、Rprof
は_packagename::funname
_型の呼び出しを匿名として記録するため、実際には_datatable::
_内の_grouped_dt
_呼び出しのすべてであり、クイックテストでは、_datatable::copy
_が大きいように見えました。
そうは言っても、_[.data.table
_呼び出しの周りにそれほどオーバーヘッドがないことはすぐにわかりますが、グループ化のための完全に独立したブランチもあります。
[〜#〜] edit [〜#〜]:コピーの確認:
_> tracemem(diamondsDT)
[1] "<0x000000002747e348>"
> diamondsDT %>% group_by(cut) %>% summarize(AvgPrice = mean(price))
tracemem[0x000000002747e348 -> 0x000000002a624bc0]: <Anonymous> grouped_dt group_by_.data.table group_by_ group_by <Anonymous> freduce _fseq eval eval withVisible %>%
Source: local data table [5 x 2]
cut AvgPrice
1 Fair 4358.758
2 Good 3928.864
3 Very Good 3981.760
4 Premium 4584.258
5 Ideal 3457.542
> diamondsDT[, mean(price), by = cut]
cut V1
1: Ideal 3457.542
2: Premium 4584.258
3: Good 3928.864
4: Very Good 3981.760
5: Fair 4358.758
> untracemem(diamondsDT)
_