web-dev-qa-db-ja.com

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

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) ]
82
Polymerase

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

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)ほとんどの場合、ボトルネックはメインメモリからキャッシュにデータを移動する(およびキャッシュ内のデータを可能な限り使用する-キャッシュミスを減らす)ため、メモリ効率が直接速度に影響することに注意してください-メインメモリへのアクセスを減らすため)。ここでは詳しく説明しません。

73
Arun

やってみなよ。

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のコメントに基づく。

22
G. Grothendieck

質問に答えるには:

  • はい、_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出力用の実験的(そしてまだ非常にアルファな)ツリービューアーであるRprofinstall_github("brodieg/treeprof"))を使用してこれを見つけました。

enter image description here

上記は現在、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)
_
20
BrodieG