data.tableのdplyr、私は本当にdata.tableを使用していますか?


91

データテーブルの上で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) ]

7
なぜデータテーブル構文を使用しないのですか?エレガントで効率的です。質問は非常に広いので、実際には答えられません。はい、dplyrデータテーブルにはメソッドがありますが、データテーブルにも独自の同等のメソッドがあります
Rich Scriven 2014

7
データテーブルの構文またはコースを使用できます。しかし、どういうわけか、dplyr構文はよりエレガントだと思います。構文の好みに関係なく。私が本当に知りたいのは、データテーブルの能力を100%活用するために、純粋なデータテーブル構文を使用する必要があるかどうかです。
ポリメラーゼ2014

3
sおよび対応するsでdplyr使用される最近のベンチマークについては、ここ(およびその中の参照)を参照してください。data.framedata.table
ヘンリック

2
@ Polymerase-その質問に対する答えは間違いなく「はい」だと思います
Rich Scriven

1
@Henrik:後で、データフレーム構築のコードのみが表示され、data.table構築に使用されたコードは表示されなかったため、そのページを誤って解釈したことに気付きました。気付いたとき、コメントを削除しました(見ていなかったらいいのに)。
IRTFM 2014

回答:


77

これらのパッケージの両方の哲学は特定の側面で異なるため、簡単で単純な答えはありません。したがって、いくつかの妥協は避けられません。対処/検討する必要のある懸念事項のいくつかを次に示します。

i(==filter()およびslice()dplyr内の)を含む操作

DTたとえば10列と仮定します。これらのdata.table式を検討してください。

DT[a > 1, .N]                    ## --- (1)
DT[a > 1, mean(b), by=.(c, d)]   ## --- (2)

(1)DTwhere列の行数を示しますa > 1。(2)は、(1)と同じ式に対してmean(b)グループ化された結果c,dを返しますi

一般的に使用される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)の両方で、最初に10列すべての行をfilter()返すため、(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つのことを行うことです十分。(少なくとも現在のところ)後の操作filter()でフィルタリングしたすべての列が必要かどうかを判断する方法はありません。このようなタスクを効率的に実行したい場合は、事前に考える必要があります。この場合、私は個人的にそれが直感に反していると思います。

(5)と(6)では、a不要な列をサブセット化することに注意してください。しかし、それを回避する方法がわかりません。場合filter()関数は戻り値に列を選択するための引数を持っていた、我々はこの問題を回避することができますが、その関数は(もdplyrの設計上の選択である)ただ一つのタスクを行うことはありません。

参照によるサブ割り当て

dplyrは参照によって更新されることはありません。これは、2つのパッケージ間のもう1つの大きな(哲学的)違いです。

たとえば、data.tableでは次のことができます。

DT[a %in% some_vals, a := NA]

これ は、条件を満たす行のみをa 参照して列を更新します。現時点では、dplyrはdata.table全体を内部的にコピーして、新しい列を追加します。@BrodieGはすでに彼の回答でこれについて言及しています。

ただし、FR#617を実装すると、ディープコピーをシャローコピーに置き換えることができます。関連するもの:dplyr:FR#614。それでも、変更する列は常にコピーされることに注意してください(したがって、少し遅くなり、メモリ効率が低下します)。参照によって列を更新する方法はありません。

その他の機能

  • data.tableでは、結合中に集計できます。これは理解しやすく、中間の結合結果が実現されないため、メモリ効率が高くなります。例については、この投稿を確認してください。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]自動的にバイナリサーチを用いた高速サブセットに同じ列から連続サブセット上で使用される最初の実行、にインデックスを作成します。この機能は進化し続けます。この機能の概要については、この要点を確認してください。

    filter()data.tablesに実装されている方法から、この機能を利用していません。

  • dplyrの機能は、同じ構文を使用してデータベースへのインターフェイスも提供することです。これは、現在data.tableでは提供されていません。

したがって、これら(およびおそらく他のポイント)を検討し、これらのトレードオフが許容できるかどうかに基づいて決定する必要があります。

HTH


(1)ほとんどの場合、ボトルネックはデータをメインメモリからキャッシュに移動することです(そしてキャッシュ内のデータを可能な限り利用することです-キャッシュミスを減らすため)メモリ効率が高いことは速度に直接影響することに注意してください(特にデータが大きくなるにつれて) -メインメモリへのアクセスを減らすため)。ここでは詳しく説明しません。


4
絶対に素晴らしい。それをありがとう
David Arenburg 2014

6
これは良い答えですが、dplyrがSQLに使用するのと同じアプローチを使用して効率的なプラスを実装することは可能です(可能性は低いですが)。つまり、式を作成し、オンデマンドで1回だけ実行します。dplyrは私にとって十分に高速であり、クエリプランナー/オプティマイザーの実装は比較的難しいため、これが近い将来に実装される可能性は低いです。filter()summarise()
ハドリー2014

メモリ効率が高いことは、別の重要な領域でも役立ちます。メモリが不足する前に実際にタスクを完了します。大規模なデータセットを操作するとき、私はパンダだけでなくdplyrでもその問題に直面しましたが、data.tableはジョブを正常に完了します。
ザキ

25

やってみなよ。

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

ポリメラーゼのコメントに基づいて改訂


2
microbenchmarkパッケージを使用しdplyrて、元の(データフレーム)バージョンでOPのコードを実行するとdiamonds、中央値0.012秒かかりましたdiamondsが、データテーブルに変換してから中央値0.024秒かかりました。G.Grothendieckのdata.tableコードの実行には0.013秒かかりました。少なくとも、私のシステムで、それはのように見えるdplyrdata.table同じ性能について持っています。しかしdplyr、データフレームが最初にデータテーブルに変換されるときに、なぜ遅くなるのでしょうか。
eipi10 2014

親愛なるG.グロタンディーク、これは素晴らしいことです。このベンチマークユーティリティを見せてくれてありがとう。ところで、dplyrのarrange(desc(Count))と同等にするために、データテーブルバージョンで[order(-Count)]を忘れました。これを追加した後でも、datatableは(2.9ではなく)約x1.8高速です。
ポリメラーゼ2014

@ eipi10ここでデータテーブルバージョンを使用してベンチを再実行できますか(最後のステップでdesc Countによる並べ替えを追加):diamondsDT [cut!= "Fair"、list(AvgPrice = mean(price)、MedianPrice = as.numeric(median (価格))、カウント= .N)、by =カット] [注文(
ポリメラーゼ

それでも0.013秒。順序付け操作は、4行しかないファイナルテーブルを並べ替えるだけなので、ほとんど時間がかかりません。
eipi10 2014

1
dplyr構文からデータテーブル構文への変換には一定のオーバーヘッドがあるため、問題のサイズを変えてみる価値があるかもしれません。また、dplyrに最も効率的なデータテーブルコードを実装していない可能性があります。パッチはいつでも歓迎されます
ハドリー2014

22

あなたの質問に答えるには:

  • はい、使用しています 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行のみ)にメモリを割り当てるだけなので、割り当てる必要のあるメモリの量に大きな違いが生じます。

参考までに、誰かが気にかけている場合は、出力に実験的な(そしてまだ非常にアルファな)ツリービューアであるtreeprofinstall_github("brodieg/treeprof"))を使用してこれを見つけましたRprof

ここに画像の説明を入力してください

上記は現在MacAFAIKでのみ機能することに注意してください。また、残念ながら、Rprofこのタイプの通話はpackagename::funname匿名として記録されるため、実際にdatatable::は内部のすべての通話grouped_dtが原因である可能性がありますが、簡単なテストからdatatable::copyは大きなもののように見えました。

とは言う[.data.tableものの、通話のオーバーヘッドがそれほど多くないことがすぐにわかりますが、グループ化のための完全に独立したブランチもあります。


編集:コピーを確認するには:

> 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)

これはすごい、ありがとう。つまり、dplyr :: group_by()は、内部データコピーステップのために、(純粋なデータテーブル構文と比較して)メモリ要件を2倍にするということですか?つまり、データテーブルオブジェクトのサイズが1GBであり、元の投稿と同様のdplyrチェーン構文を使用しています。結果を得るには、少なくとも2GBの空きメモリが必要ですか?
ポリメラーゼ2014

2
開発版で修正したような気がしますか?
ハドリー2014

@hadley、私はCRANバージョンから作業していました。devを見ると、問題に部分的に対処しているように見えますが、実際のコピーは残っています(テストされていません。R/ groupsed-dt.rの行c(20、30:32)を見るだけです。おそらく今は高速ですが、私は遅いステップがコピーである賭ける。
BrodieG

3
data.tableの浅いコピー関数も待っています。それまでは、速いより安全な方がいいと思います。
ハドリー2014

2

tidyverseの一部であるdtplyrを使用できるようになりました。これにより、通常どおりdplyrスタイルのステートメントを使用できますが、遅延評価を利用して、ステートメントを内部でdata.tableコードに変換します。翻訳のオーバーヘッドは最小限ですが、そうでない場合でも、data.tableのほとんどの利点を引き出すことができます。公式gitのレポで詳細こことtidyverseページ

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.