【dplyr vs data.table】data.tableを使ったデータのマージ

# はじめに
Rで機械学習等分析をやる上で欠かせないのが`data.frame`ですが、大規模(数万レコード〜）データを扱いだすと段々と苦しくなってきます。そこで登場するのが`data.table`です。ただ、`data.frame`に慣れた人にとっては、`data.table`の独特な記法を難しく感じることもあるかと思います。基本的な使い方とともにあまり解説の見つからなかったデータマージのところを自分メモも含めて書いておきます。

# 基本的なdata.tableの使い方

## data.tableの作成・型変換等
データの作成等においてはdata.frameとdata.tableの間で大きな違いはありません。ただ、データのインポートに関していうと`fread()`は`read.table()`等と比較してとんでもなく高速に読み込むのでこれだけでも価値があります。

| 内容 | data.frame | data.table |
|:--|:--|:--|
|データの作成|data.frame()|data.table()|
|型の変換|as.data.frame()   | as.data.table()  |
|データのインポート|read.table()   |fread()|

```r:data.tableによるインポート
dt <- fread("./data.csv") # 基本的な使い方
df <- fread("./data.csv", data.table = F) # data.frameとして読込
```
もし`data.table`の使い方が今ひとつよくわからない、という場合は`fread()`に`data.table = F`というオプションをつけることで`data.frame`として読み込むことが可能です。ただ単に高速にインポートしたい場合はこれでいいでしょう。freadはcsvやtsv等、いろんなデータ形式をよしなにインポートしてくれますが、日本語が苦手などの弱点もあるので、詳しくはこの記事の最後につける各種リンクを参照ください。


## data.tableの要素へのアクセス、抽出等


| 内容 | data.frame | data.table |
|:--|:--|:--|
|要素へのアクセス(行)|df[n,]|dt[n]|
|要素へのアクセス(列)|df[,n]|dt[,n, with = F]|
|要素の抽出|df[df$x == 3,]|dt[x == 3]|

要素へのアクセスにおいて行を選択する場合、`dt[1:3]`というようにインデックスでアクセスできますが、その際にコンマはいりません（あってもOK）。注意すべきは列選択をする際には`with = F`オプションが必要なことです。

## data.table記法 (代入・列削除・列追加)

| 内容 | data.frame | data.table |
|:--|:--|:--|
|列追加|df$new <- a|dt[, new:= a]|
|列削除|df$old <- NULL|dt[,old:=NULL]|
|値の変更|df$old <- a|dt[,old:=a]

data.table記法の最大の特徴は上記の値の代入や列操作にあります。`data.frame`は代入によって追加・削除をするのに対して、`data.table`は`:=`という代入子を使うことで直接データを書き換えられるところにあります。なれると病みつきになりそうです。

# データのフィルタリング/集約 (`dplyr`との比較)

データのフィルタリングや集約には`dplyr`を使うことが多いかと思いますが、`data.table`を使うことでより高速に、完結に書くこともできます。

| 内容 | dplyr | data.table |
|:--|:--|:--|
|フィルタリング|df %>% filter(条件式1 & 条件式2)|dt[条件式1 & 条件式2]|
|集約|df %>% summarise(max = max(x))|dt[, max(x)]|
|グルーピング|df %>% group_by(x) %>% summarise(max = max(y))|dt[,max(y), by = x]

`dplyr`でパイプを使ったフィルタリングや集約はとても便利ですが、`data.table`を使うとより完結に書くこともできます。ここは慣れの部分でしょう。なお`data.table`と`dplyr`は併用可能なので、パイプを使って`data.talbe`を処理することも可能です。


# データのマージ (`dplyr`との比較)

　ようやく本題です。`dplyr`を使うことでSQL的にjoinすることができますが、`data.table`でも似たようなことができます。

| 内容 | dplyr | data.table::merge() |data.table記法|
|:--|:--|:--|:--|
|Inner Join|inner_join(left, right, by = "key")|merge(left, right, by = "key", all = F)|left[right, on = "key", nomatch = 0]
|Left Join|left_join(left, right, by = "key")|merge(left, right, by = "key", all.x = T)|right[left, on = "key"]
|Right Join|right_join(left, right, by = "key")|merge(left, right, by = "key", all.y = T)|left[right, on = "key"]
|Full Join|full_join(left, right, by = "key")|merge(left, right, by = "key", all = T)|-

`data.table`には`merge()`が用意されており、オプション(all, all.x, all.y)を記述することで各種joinを実現しています。 keyは`dplyr`/`data.table`ともに設定しなければ、両方のデータに共通する列名を元によしなにマージしてくれます。[^1]また、data.tableにおいてはキーを予めセットする`setkey()`があり、これを予め設定しておくとonオプションは不要です。

さらに`data.table`に関していうと、inner/left/rightに限定されますが、data.table記法によってより簡単に書くことができます。[^2]

## data.table記法のトリッキーな使い方
[ここでの議論の中で](https://stackoverflow.com/questions/34598139/left-join-using-data-table)、メモリ効率を重視したよりよいマージ方法が提案されています。前項よりもさらに限定されますが、マージすべき列が1つだけの場合、下記のように書くことで代入をすることなく、新たなメモリ領域を確保することもなくマージすることができます。つまりより高速に、よりメモリ効率よくマージができます。

```r:data.table記法を使ったマージ
left <- data.table(x = 1:6, y = 4:9)
right <- data.table(x = 1:4, z = 6:9)
left[right, on = "x", z:= z]
```
data.table記法によるマージは限定的[^3]なので、`merge()`と併用してまで使うべきかどうかは可読性等を考えると微妙なところではありますが、覚えておいて損はないでしょう。[^4]

## 複数キーを使ったマージ

上記の例ではbyやonで指定したキーは1つだけでしたが、複数の列をキーとしてマージすることもできます。具体的には`key = c("x", "y")`や`on = c("x", "y")`と指定することでできます。

# `dplyr`と`data.table`の速度比較

これまで`dplyr`と`data.table`の書き方についてご紹介しましたが、それではどちらの方がマージが早いのでしょうか？

```r:data作成
dt.left <- data.table(id = seq(4e+06),
                cat.l = sample(letters[1:26], 4e+06, replace = T),
                val.l = runif(4e+06))
dt.right <- data.table(id = seq(4e+06),
                       cat.r = sample(letters[1:26], 4e+06, replace = T),
                       val.r = runif(4e+06))
```

```r:dplyr
system.time(res_dplyr <- left_join(dt.left, dt.right, by = "id"))

   ユーザ   システム       経過  
     1.389      0.198      1.621 
```

```r:data.table::merge()
system.time(res_merge <- merge(dt.left, dt.right, by = "id"))

   ユーザ   システム       経過  
     0.229      0.060      0.290 
```

```r:data.table記法
system.time(res_data.table <- dt.left[dt.right, on = "id"])

   ユーザ   システム       経過  
     0.167      0.032      0.209 
```

```r:data.table記法（参考)
## マージされるのはcat.rだけです
system.time(dt.left[dt.right, on = "id", cat.r:=cat.r])

   ユーザ   システム       経過  
     0.132      0.010      0.143 
```
単純なJoinに関しては`dplyr`と比較して`data.table::merge()`やdata.table記法の方が圧倒的に早いことがわかりました。`dplyr`自体も`base::merge()`と比較して高速なので[^5]、いかにdata.tableが早いかがわかりますね。トリッキーな記法は早いことは早いですが、制約が多い割にそこまで早くもないので、`data.table::merge()`かdata.table記法(+代入）で良さそうです。


## 参考リンク
- data.tableの基本的な使い方: [大規模データの高速処理 ーdata.table、dplyrー](http://kohske.github.io/ESTRELA/201410/index.html)
- data.table記法とmergeの比較: [JOINing data in R using data.table](https://rstudio-pubs-static.s3.amazonaws.com/52230_5ae0d25125b544caab32f75f0360e775.html)
- data.table記法を使った効率的なマージ: [Left join using data.table
](https://stackoverflow.com/questions/34598139/left-join-using-data-table)
- マージの速度比較: [dplyrのjoinがmergeに比べて３倍速い](http://d.hatena.ne.jp/dichika/20140104/p1)

[^1]: 予期せぬマージを防ぐためにも愚直に書いたほうがいいでしょう。
[^2]: 直感的にはleft joinの時にleftに来る`data.table`を右に書く(right[left])のが気持ちわるいですが
[^3]: Inner/left/rightのみ
[^4]: なおこの書き方だとleft/right joinが逆になります。つまりright[left, on = "x", y:=y]だとright joinになります。
[^5]: http://d.hatena.ne.jp/dichika/20140104/p1