1. ホーム
  2. r

[解決済み】data.table vs dplyr:一方がうまくできない、またはうまくできないことを行うことができますか?

2022-03-23 04:27:02

質問

概要

比較的馴染みのある data.table とはあまり関係ありません。 dplyr . 私は、いくつかの dplyr ヴィネット やSOで出てきた例などを参考に、今のところ私の結論はこうです。

  1. data.tabledplyr は、グループ数が多い場合(>10-10万)や、その他の状況(以下のベンチマークを参照)を除いて、速度が同等です。
  2. dplyr は、よりアクセスしやすいシンタックスです。
  3. dplyr 潜在的なDBとの相互作用を抽象化する(またはする)
  4. 細かい機能の違いがあります(下記の「例/使い方」を参照)

私の中では、2.はかなり使い慣れたものなので、あまり重要視していません。 data.table しかし、初めて両者を使うユーザーにとっては大きな要素になることは理解しています。 どちらがより直感的か、という議論は、すでにこの製品に慣れている人の立場からの質問とは無関係なので、避けたいと思います。 data.table . また、quot;more intuitive" leads the faster analysisという議論も避けたいと思います(確かにそうですが、ここでも私が最も興味があるのはそこではありません)。

質問

知りたいことは

  1. どちらのパッケージに慣れている人にとっても、どちらかのパッケージでコーディングする方がずっと簡単な分析タスクがありますか (必要なキーストロークと必要な難解さのレベルの組み合わせ。それぞれ少ない方が良い)。
  2. あるパッケージと別のパッケージの間で、実質的に(すなわち2倍以上)効率的に実行される分析タスクがあるかどうか。

最近のSO質問 というのも、それまで私は dplyr で既にできること以上のことを提供することはできません。 data.table . 以下は dplyr の解答(Q末尾のデータ)です。

dat %.%
  group_by(name, job) %.%
  filter(job != "Boss" | year == min(year)) %.%
  mutate(cumu_job2 = cumsum(job2))

という私のハッキングの試みよりはずっとましです。 data.table を解決することができます。 とはいえ、良い data.table の解決策もかなり良いものです(Jean-Robert, Arunに感謝、ここで私は厳密な最適解よりも単一記述を好んだことに注意)。

setDT(dat)[,
  .SD[job != "Boss" | year == min(year)][, cumjob := cumsum(job2)], 
  by=list(id, job)
]

後者の構文は非常に難解に見えるかもしれませんが、実は data.table (つまり、もっと難解なトリックを使わない)。

理想を言えば、このような良い例を見てみたいですね。 dplyr または data.table の方が、実質的に簡潔で、性能も優れています。

使用方法
  • dplyr は、任意の数の行を返すグループ化された操作を許可しません ( eddiの質問 に実装されるようです。 dplyr 0.5 また、@beginneR は、以下のような回避策を示しています。 do を@eddiさんの質問への回答で紹介しています)。
  • data.table サポート ローリングジョイン (@dholstius さんありがとうございます) と同様に オーバーラップジョイン
  • data.table という形の式を内部的に最適化します。 DT[col == value] または DT[col %in% values] に対して 速度 を通して 自動インデックス作成 を使用する バイナリサーチ と同じRの基本構文を使用しながら こちらをご覧ください には、より詳細な情報と小さなベンチマークがあります。
  • dplyr は、関数の標準的な評価版(例えば regroup , summarize_each_ のプログラム的な使用を簡素化することができます)。 dplyr (のプログラム的な使用に注意)。 data.table は間違いなく可能です。ただ、少なくとも私の知る限りでは、置換や引用など、いくつかの慎重な考慮が必要です)
ベンチマーク

データ

これは質問コーナーで紹介した最初の例に対するものです。

dat <- structure(list(id = c(1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 2L, 2L, 
2L, 2L, 2L, 2L, 2L, 2L), name = c("Jane", "Jane", "Jane", "Jane", 
"Jane", "Jane", "Jane", "Jane", "Bob", "Bob", "Bob", "Bob", "Bob", 
"Bob", "Bob", "Bob"), year = c(1980L, 1981L, 1982L, 1983L, 1984L, 
1985L, 1986L, 1987L, 1985L, 1986L, 1987L, 1988L, 1989L, 1990L, 
1991L, 1992L), job = c("Manager", "Manager", "Manager", "Manager", 
"Manager", "Manager", "Boss", "Boss", "Manager", "Manager", "Manager", 
"Boss", "Boss", "Boss", "Boss", "Boss"), job2 = c(1L, 1L, 1L, 
1L, 1L, 1L, 0L, 0L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L)), .Names = c("id", 
"name", "year", "job", "job2"), class = "data.frame", row.names = c(NA, 
-16L))

解決方法は?

包括的な回答/比較を提供するためには、少なくとも以下の点をカバーする必要があります(重要度の順不同)。 Speed , Memory usage , SyntaxFeatures .

私の意図は、data.tableの観点から、それぞれをできるだけ明確にカバーすることです。

<ブロッククオート

注:明示的に言及されていない限り、dplyrを参照すると、内部がRcppを使用したC++であるdplyrのdata.frameインタフェースを参照することになります。


data.tableの構文は、その形式が統一されています -。 DT[i, j, by] . このため i , jby を一緒にするのは、デザイン上の理由です。関連する操作を一緒にしておくことで 最適化しやすい のためのオペレーションを スピード といった、より重要な メモリ使用量 を提供し、また、いくつかの 強力な機能 を、構文の一貫性を保ちつつ、実現しました。

1. 速度

かなりの数のベンチマーク(ただし、ほとんどはグループ化操作に関するもの)が、すでに質問に追加されている data.table get より速く グループ化するグループや行の数が増えるにつれ、dplyr よりも マットによるベンチマーク からのグループ化について 1,000万行~20億行 (RAMで100GB)で 1億~1,000万グループ とグループ化カラムを変化させ、これも比較します。 pandas . こちらもご覧ください 更新されたベンチマーク を含む。 Sparkpydatatable にも対応しています。

ベンチマークについては、こうした残された部分もカバーできるといいですね。

  • を含むグループ化操作 行のサブセット - すなわち DT[x > val, sum(y), by = z] 型の演算を行う。

  • などの他の操作のベンチマークを行う。 更新 ジョインズ .

  • また、ベンチマーク メモリフットプリント を、実行時間だけでなく、各操作に対して設定します。

2. メモリ使用量

  1. 以下の操作を含む filter() または slice() はメモリ効率が悪いことがあります(data.framesとdata.tablesの両方で)。 この記事を見る .

    <ブロッククオート

    注意点 ハドレー氏のコメント について話しています。 スピード (彼にとってdplyrは十分速いということ)、一方、ここでの大きな関心事は メモリ .

  2. data.table インターフェースでは、現時点では、カラムの変更/更新を行うことができます。 参照 (結果を変数に再代入する必要がないことに注意してください)。

     # sub-assign by reference, updates 'y' in-place
     DT[x >= 1L, y := NA]
    
    

    しかし、dplyr は決して は参照によって更新されます。dplyrに相当するのは、次のようになります(結果を再代入する必要があることに注意してください)。

     # copies the entire 'y' column
     ans <- DF %>% mutate(y = replace(y, which(x >= 1L), NA))
    
    

    これに対する懸念は 参照透過性 . data.tableオブジェクトを参照しながら更新することは、特に関数内では必ずしも望ましいことではありません。しかし、これは非常に便利な機能です。 これ これ の投稿は、面白いケースを想定しています。そして、私たちはそれを維持したいのです。

    そのため、輸出に向けた取り組みを行っています。 shallow() を提供するdata.tableの関数です。 両方の可能性 . 例えば、関数内で入力のdata.tableを変更しないことが望ましい場合、次のようにすることができます。

     foo <- function(DT) {
         DT = shallow(DT)          ## shallow copy DT
         DT[, newcol := 1L]        ## does not affect the original DT 
         DT[x > 2L, newcol := 2L]  ## no need to copy (internally), as this column exists only in shallow copied DT
         DT[x > 2L, x := 3L]       ## have to copy (like base R / dplyr does always); otherwise original DT will 
                                   ## also get modified.
     }
    
    

    を使用しないことで shallow() の場合、旧来の機能は維持されます。

     bar <- function(DT) {
         DT[, newcol := 1L]        ## old behaviour, original DT gets updated by reference
         DT[x > 2L, x := 3L]       ## old behaviour, update column x in original DT.
     }
    
    

    を作成することで シャローコピー を使って shallow() しかし、元のオブジェクトを変更したくないという要望は理解できます。私たちは、あなたが変更した列をコピーすることを保証しながら、内部ですべてを処理します。 どうしても必要なときだけ . 実装すると、これで 参照透過性 の問題を完全に解決し、ユーザーに両方の可能性を提供します。

    <ブロッククオート

    また、一度 shallow() がエクスポートされた場合、dplyrのdata.tableインタフェースはほとんどすべてのコピーを回避する必要があります。したがって、dplyrの構文を好む人は、data.tablesでそれを使用することができます。

    <ブロッククオート

    しかし、参照による(サブ)割り当てなど、data.tableが提供する多くの機能が欠けていることに変わりはないでしょう。

  3. 結合しながら集計する。

    次のような2つのdata.tablesがあるとする。

     DT1 = data.table(x=c(1,1,1,1,2,2,2,2), y=c("a", "a", "b", "b"), z=1:8, key=c("x", "y"))
     #    x y z
     # 1: 1 a 1
     # 2: 1 a 2
     # 3: 1 b 3
     # 4: 1 b 4
     # 5: 2 a 5
     # 6: 2 a 6
     # 7: 2 b 7
     # 8: 2 b 8
     DT2 = data.table(x=1:2, y=c("a", "b"), mul=4:3, key=c("x", "y"))
     #    x y mul
     # 1: 1 a   4
     # 2: 2 b   3
    
    

    そして、あなたが取得したいのは sum(z) * mul の各行に対して DT2 列で結合している間 x,y . どちらかです。

      1. アグリゲート DT1 を取得する。 sum(z) 2)結合を行い、3)乗算を行う(または)。

        data.tableの方法

        DT1[, .(z = sum(z)), keyby = .(x,y)][DT2][, z := z*mul][].

        dplyr相当

        DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>%。 right_join(DF2) %>% mutate(z = z * mul)

      1. を使用して)一挙に行う。 by = .EACHI 機能)を使用します。

        DT1[DT2, list(z=sum(z) * mul), by = .EACHI]の場合。

    メリットは何ですか?

    • 中間結果のためにメモリーを確保する必要がない。

    • グループ化・ハッシュ化を2回行う必要がない(1回は集計用、もう1回は結合用)。

    • そして、さらに重要なことは、私たちが行いたかった操作が j の(2)にある。

    確認 この記事 についての詳しい説明は by = .EACHI . 中間結果は生成されず、結合と集計はすべて一度に実行されます。

    をご覧ください。 これ , これ そして これ の投稿は、実際の利用シーンを想定しています。

    dplyr を指定する必要があります。 結合して集約するか、先に集約してから結合する。 どちらもメモリ(これはスピードにつながります)の点では効率的ではありません。

  4. 更新と結合。

    以下に示すdata.tableのコードを考えてみましょう。

     DT1[DT2, col := i.mul]
    
    

    追加・更新 DT1 カラム colmul から DT2 のある行について DT2 のキーカラムと一致する DT1 . のこの操作に正確に相当するものがあるとは思えません。 dplyr を回避することなく、すなわち *_join をコピーしなければなりません。 DT1 は、新しいカラムを追加するだけなので、不要です。

    確認 この記事 は、実際の使用シナリオをご覧ください。

要約すると、最適化の一つひとつが重要であることを認識することが重要です。例えば グレース・ホッパー は言うだろう。 ナノ秒を気にする !

3. シンタックス

では、次に シンタックス . ハドレーは次のようにコメントしています。 こちら :

データテーブルは非常に高速ですが、その簡潔さゆえに 学習しにくい それを使用したコードは、書いた後に読むのが難しくなる ...

この発言は非常に主観的であるため、無意味だと思います。私たちが試せるのは、次のような対比です。 構文の一貫性 . data.tableとdplyrの構文を並べて比較してみます。

以下のようなダミーデータを使って作業します。

DT = data.table(x=1:10, y=11:20, z=rep(1:2, each=5))
DF = as.data.frame(DT)

  1. 基本的な集計・更新操作。

     # case (a)
     DT[, sum(y), by = z]                       ## data.table syntax
     DF %>% group_by(z) %>% summarise(sum(y)) ## dplyr syntax
     DT[, y := cumsum(y), by = z]
     ans <- DF %>% group_by(z) %>% mutate(y = cumsum(y))
    
     # case (b)
     DT[x > 2, sum(y), by = z]
     DF %>% filter(x>2) %>% group_by(z) %>% summarise(sum(y))
     DT[x > 2, y := cumsum(y), by = z]
     ans <- DF %>% group_by(z) %>% mutate(y = replace(y, which(x > 2), cumsum(y)))
    
     # case (c)
     DT[, if(any(x > 5L)) y[1L]-y[2L] else y[2L], by = z]
     DF %>% group_by(z) %>% summarise(if (any(x > 5L)) y[1L] - y[2L] else y[2L])
     DT[, if(any(x > 5L)) y[1L] - y[2L], by = z]
     DF %>% group_by(z) %>% filter(any(x > 5L)) %>% summarise(y[1L] - y[2L])
    
    
    • data.tableの構文はコンパクトで、dplyrの構文はかなり冗長です。(a)の場合とほぼ同じです。

    • (b)の場合、(c)のように filter() を使用し、dplyrでは 要約 . しかし、一方 更新 の中にロジックを移動させる必要がありました。 mutate() . しかし、data.table では、両方の操作を同じロジックで表現しています。 x > 2 を取得しますが、最初のケースでは sum(y) 一方、2 番目のケースでは、これらの行を更新して y をその累積和で返す。

      というのは、このような意味です。 DT[i, j, by] フォーム は一貫して .

    • 同様に(c)の場合、以下のようになります。 if-else という条件では、論理的に表現することができます。 "as-is"。 をdata.tableとdplyrの両方で使用することができます。しかし、もしあなたが if 条件を満たし、そうでない場合はスキップされます。 summarise() を直接指定することができます(AFAICT)。私たちは filter() を最初に作成し、それからまとめる。 summarise() は常に 単一値 .

      同じ結果を返しますが filter() では、実際の操作を目立たなくしています。

      を使用することは大いにあり得ることです。 filter() が、私が言いたいのは、そうする必要はないはずだということです。

  2. 複数カラムの集計・更新

     # case (a)
     DT[, lapply(.SD, sum), by = z]                     ## data.table syntax
     DF %>% group_by(z) %>% summarise_each(funs(sum)) ## dplyr syntax
     DT[, (cols) := lapply(.SD, sum), by = z]
     ans <- DF %>% group_by(z) %>% mutate_each(funs(sum))
    
     # case (b)
     DT[, c(lapply(.SD, sum), lapply(.SD, mean)), by = z]
     DF %>% group_by(z) %>% summarise_each(funs(sum, mean))
    
     # case (c)
     DT[, c(.N, lapply(.SD, sum)), by = z]     
     DF %>% group_by(z) %>% summarise_each(funs(n(), mean))
    
    
    • (a)の場合、コードはほぼ等価である。 lapply() 一方 dplyr が導入されます。 *_each() への関数の束と一緒に funs() .

    • data.tableの := は列名を提供する必要がありますが、dplyrはそれを自動的に生成します。

    • (b)の場合、dplyrの構文は比較的わかりやすい。複数関数での集計・更新の改善はdata.tableのリストにある。

    • しかし、(c)の場合、dplyrは以下を返すでしょう。 n() は一度だけでなく、カラムの数だけ繰り返されます。data.tableでは、リストを返すだけでよいのです。 j . リストの各要素は結果のカラムになります。そこで、もう一度、おなじみの基本関数である c() を連結して .Nlist を返します。 list .

    注:もう一度言いますが、data.tableでは、リストを返すだけでよいのです。 j . リストの各要素は、resultのカラムになります。あなたは c() , as.list() , lapply() , list() などの基本関数を使えば、新しい関数を覚えることなく、このようなことが実現できます。

    <ブロッククオート

    特殊な変数だけを覚える必要があります -。 .N.SD は少なくとも dplyrでの同等品は n().

  3. 接合部

    dplyrは結合の種類ごとに別々の関数を提供しますが、data.tableでは同じ構文で結合を行うことができます。 DT[i, j, by] (そして理由もある)。また、同等の merge.data.table() という関数で代用できる。

     setkey(DT1, x, y)
    
     # 1. normal join
     DT1[DT2]            ## data.table syntax
     left_join(DT2, DT1) ## dplyr syntax
    
     # 2. select columns while join    
     DT1[DT2, .(z, i.mul)]
     left_join(select(DT2, x, y, mul), select(DT1, x, y, z))
    
     # 3. aggregate while join
     DT1[DT2, .(sum(z) * i.mul), by = .EACHI]
     DF1 %>% group_by(x, y) %>% summarise(z = sum(z)) %>% 
         inner_join(DF2) %>% mutate(z = z*mul) %>% select(-mul)
    
     # 4. update while join
     DT1[DT2, z := cumsum(z) * i.mul, by = .EACHI]
     ??
    
     # 5. rolling join
     DT1[DT2, roll = -Inf]
     ??
    
     # 6. other arguments to control output
     DT1[DT2, mult = "first"]
     ??
    
    
  • それぞれの結合(left, right, inner, anti, semiなど)に対して別々の関数を使う方が良いと思う人もいるかもしれませんし、data.tableの DT[i, j, by] または merge() であり、ベースRと同様である。

  • しかし、dplyrのjoinはそれだけを行います。それ以上ではありません。それ以下でもありません。

  • data.tablesは結合中に列を選択できる(2)、dplyrの場合は select() 上記のように結合する前に、まず両方のdata.frameで結合を行います。そうしないと、不要な列を後で削除するために結合を複雑にすることになり、非効率です。

  • data.tablesは 結合時のアグリゲート を使って by = .EACHI 機能(3)と、さらに 参加中更新 (4). わずかなカラムを追加/更新するために、なぜ結合結果全体を実体化するのですか?

  • data.tableは、以下の機能を備えています。 ローリングジョイン (5) - ロール 前方、LOCF , ロールバック、NOCB , 最寄 .

  • data.tableはまた mult = を選択する引数です。 最初 , 最後 または すべて が一致する(6)。

  • data.tableには allow.cartesian = TRUE 引数を使用することで、偶発的な無効結合から保護することができます。

もう一度言いますが、構文は DT[i, j, by] を追加し、さらに出力を制御することができます。

  1. do() ...

    dplyrのsummariseは、単一の値を返す関数のために特別に設計されています。もし、関数が複数の値や不等な値を返す場合は、次のようにする必要があります。 do() . すべての関数の戻り値について、あらかじめ知っておく必要があります。

     DT[, list(x[1], y[1]), by = z]                 ## data.table syntax
     DF %>% group_by(z) %>% summarise(x[1], y[1]) ## dplyr syntax
     DT[, list(x[1:2], y[1]), by = z]
     DF %>% group_by(z) %>% do(data.frame(.$x[1:2], .$y[1]))
    
     DT[, quantile(x, 0.25), by = z]
     DF %>% group_by(z) %>% summarise(quantile(x, 0.25))
     DT[, quantile(x, c(0.25, 0.75)), by = z]
     DF %>% group_by(z) %>% do(data.frame(quantile(.$x, c(0.25, 0.75))))
    
     DT[, as.list(summary(x)), by = z]
     DF %>% group_by(z) %>% do(data.frame(as.list(summary(.$x))))
    
    
  • .SD に相当するのは .

  • data.tableでは、かなり多くのものを投げることができます。 j - 唯一覚えておくべきことは、リストの各要素がカラムに変換されるように、リストを返すということです。

  • dplyrでは、それができません。そのため do() 関数が常に単一の値を返すかどうか、どの程度確信が持てるかによりますが。そして、それはかなり遅いです。

<ブロッククオート

もう一度言いますが、data.tableの構文は、以下のように一貫しています。 DT[i, j, by] . で式を投げ続ければいいのです。 j このようなことを気にする必要はありません。

をご覧ください。 このSOの質問 これ . dplyrの構文を使って、答えをストレートに表現できないかなぁ...。

要約すると、特に強調したのは いくつか dplyrの構文が非効率的であったり、制限があったり、操作を単純化できなかったりする事例があります。これは特に、data.tableが(上記の貼り付けやリンクのような)読みにくく、学びにくい構文についてかなりの反発を受けるからです。dplyrを取り上げたほとんどの記事は、最も簡単な操作について話しています。そして、それは素晴らしいことです。しかし、その構文と機能の制限を認識することも重要であり、私はまだそれについての記事を見たことがありません。

<ブロッククオート

data.tableにも癖があります(そのうちのいくつかは私が指摘し、私たちが修正しようとしているものです)。また、data.tableのjoinを改善しようとしているところです。 こちら .

<ブロッククオート

しかし、dplyrがdata.tableと比較して欠けている機能の数も考慮する必要があります。

4. 特徴

これまで、ほとんどの機能を指摘してきました。 こちら と、この記事でも紹介しています。さらに

  • 恐怖 - fast file readerは、以前から利用可能です。

  • fwrite - a 並列化された 高速ファイルライターが利用可能になりました。参照 この記事 は、実装の詳細な説明と #1664 は、今後の開発状況を追跡するために使用されます。

  • 自動インデックス作成 - も、Rの基本構文をそのまま内部で最適化するための便利な機能です。

  • アドホックなグループ化 : dplyr の間に変数をグループ化し、結果を自動的にソートします。 summarise() これは常に望ましいとは限りません。

  • 上記のdata.table結合における多くの利点(速度、メモリ効率、構文)。

  • 非等式結合 : 他の演算子による結合を許可する <=, <, >, >= と、data.tableの結合の他のすべての利点を併せ持つ。

  • オーバーラップレンジジョイン が最近 data.table で実装されました。チェック 本論文 は、ベンチマークを含む概要をご覧ください。

  • setorder() 関数で、data.table の参照による並べ替えを高速に行うことができます。

  • dplyrは以下を提供します。 データベースへのインターフェイス は同じ構文を使っていますが、data.table は今のところそうではありません。

  • data.table は、より高速な同等品を提供します。 りったいしゅうごうえんざん (ヤン・ゴレッキ作)・・・。 fsetdiff , fintersect , funionfsetequal 加えて all 引数で指定します(SQL と同様)。

  • data.tableは、マスキングの警告を出さずにきれいにロードされ、以下のようなメカニズムを持っています。 こちら について [.data.frame R パッケージに渡されたときの互換性 dplyr は基本関数 filter , lag[ を使用すると、問題を引き起こす可能性があります; 例えば ここで こちら .


最後に

  • データベースについて - data.tableが同様のインターフェイスを提供できない理由はありませんが、これは今のところ優先事項ではありません。しかし、これは今は優先順位が低いです。もしユーザーがその機能を強く望めば、優先順位が上がるかもしれません。

  • 並列化について - 何事も、誰かが実行に移すまでは難しいものです。もちろん、努力は必要です(スレッドセーフであること)。

    • 現在(v1.9.7 devel)、既知の時間のかかる部分を並列化することで、少しずつ性能を向上させることが進められています。 OpenMP .