dplyr でDBのデータをUNIONする
dplyrというかdbplyrというべきか
Rでデータフレームを縦方向に結合するときは rbind
か dplyr::bind_rows
がよく使われる。
だが、dplyrでDBに接続しているとき、DBにあるデータを結合するときに上記の関数ではうまくいかない。
結論から述べると、dplyr::union
を使えばOK。SQLのUNIONと同じなので覚えやすい。
dplyr::union_all
もあり、SQLのUNION ALLと同じで、重複を削除しない。
また、dplyr::union
は引数を2つしか取らないため、3つ以上のデータをUNIONしようとしても、最初の2つ以外は無視される。
これにはいくつか対処方法があり、自分がやってみた方法をメモ代わりに書いてみる。
以下に例を記す。コンソールの結果を貼っているので読みにくいかもしれないが、RのソースはこちらのGistにもあるので、お好みでそちらもどうぞ。
まずはDBの準備。 ここでは例として、attenuデータを使う。
SQLiteにattenuデータフレームをいったん格納し、 もう一度Rから読み込む。
# install.packages("DBI") # install.packages("RSQLite") library(dplyr) # DB接続 con <- DBI::dbConnect(RSQLite::SQLite(), path = ":memory:") # DBにローカルのデータフレームをコピー dplyr::copy_to(con, attenu, "attenu", temporary = FALSE) # テーブル読み込み(copy_toの返り値を使ってもよい) tbl_attenu <- dplyr::tbl(con, "attenu")
ではまず、eventカラムの値別にデータを読み込み、3種類のオブジェクトを作ってみる。 これらを縦方向に結合したい。
> # 3種類のデータを読み込んでみる > (tbl_e1 <- tbl_attenu %>% + filter(event==1)) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7 117 12 0.359 > > (tbl_e8 <- tbl_attenu %>% + filter(event==8)) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 8 5.3 111 19 0.086 2 8 5.3 116 21 0.179 3 8 5.3 290 13 0.205 4 8 5.3 112 22 0.073 5 8 5.3 113 29 0.045 > > (tbl_e11 <- tbl_attenu %>% + filter(event==11)) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 11 7.7 2714 45 0.11 2 11 7.7 2708 145 0.01 3 11 7.7 2715 300 0.01
rbind
や dplyr::bind_rows
ではうまくいかない
> rbind(tbl_e1, tbl_e8) src ops tbl_e1 List,2 List,4 tbl_e8 List,2 List,4 > dplyr::bind_rows(tbl_e1, tbl_e8) Error in bind_rows_(x, .id) : Argument 1 must be a data frame or a named atomic vector, not a tbl_dbi/tbl_sql/tbl_lazy/tbl
dplyr::union
だとうまく結合できる。
ご存知かとは思うが、UNION と UNION ALLの結果の違いも載せておく。
> # Success > dplyr::union(tbl_e1, tbl_e8) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7.0 117 12 0.359 2 8 5.3 111 19 0.086 3 8 5.3 112 22 0.073 4 8 5.3 113 29 0.045 5 8 5.3 116 21 0.179 6 8 5.3 290 13 0.205 > > # UNION と UNION ALL の違い > dplyr::union_all(tbl_e1, tbl_e1) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7 117 12 0.359 2 1 7 117 12 0.359 > dplyr::union(tbl_e1, tbl_e1) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7 117 12 0.359
で、ここからが主題なのだけど、dplyr::union
は引数を2つだけしかとらず、
3つめ以降は無視される。無視て。
以下の例では、event=11 が結合できていない。
> # 注意:UNIONは最初の2つの引数しか受け付けない > dplyr::union(tbl_e1, tbl_e8, tbl_e11) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7.0 117 12 0.359 2 8 5.3 111 19 0.086 3 8 5.3 112 22 0.073 4 8 5.3 113 29 0.045 5 8 5.3 116 21 0.179 6 8 5.3 290 13 0.205
こういうときの解決策1として、いったんリストにするという方法がある。
リストをまとめてUNIONするには、Reduce
関数を使う。他にも方法ありそう。
> # 3つ以上をUNIONしたいときは、リストをReduceでUNIONする > tbl_list <- list(tbl_e1, tbl_e8, tbl_e11) > Reduce(dplyr::union, tbl_list) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7.0 117 12 0.359 2 8 5.3 111 19 0.086 3 8 5.3 112 22 0.073 4 8 5.3 113 29 0.045 5 8 5.3 116 21 0.179 6 8 5.3 290 13 0.205 7 11 7.7 2708 145 0.010 8 11 7.7 2714 45 0.110 9 11 7.7 2715 300 0.010
うまくできている。
さて、リストとReduceと聞いて思い浮かべるのは、foreach
パッケージである。
わざわざ各々のオブジェクトを作らなくても、for文のように以下のように書くと便利だ。
> # foreachを使うと便利 > library(foreach) > events <- c(1, 8, 11) > tbl_list <- foreach::foreach( + x=events, + .combine = list, + .multicombine = T # 3つ以上をリストにする場合はTRUEにする必要がある + ) %do% { + dplyr::tbl(con, "attenu") %>% + filter(event == x) + } > Reduce(dplyr::union, tbl_list) # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7.0 117 12 0.359 2 8 5.3 111 19 0.086 3 8 5.3 112 22 0.073 4 8 5.3 113 29 0.045 5 8 5.3 116 21 0.179 6 8 5.3 290 13 0.205 7 11 7.7 2708 145 0.010 8 11 7.7 2714 45 0.110 9 11 7.7 2715 300 0.010
ここで、foreach関数の .combine
に union
を入れることで、いきなり UNION できるので非常におすすめ。
ちなみに、先ほどとは違って、ここでは .multicombine=F
とする必要があるので注意。
> # foreachを使うと便利 > tbl_result <- foreach::foreach( + x=events, + .combine = dplyr::union, + .multicombine = F # 今度はFALSEにしないと、最初の2つしかUNIONされない + ) %do% { + dplyr::tbl(con, "attenu") %>% + filter(event == x) + } > tbl_result # Source: lazy query [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7.0 117 12 0.359 2 8 5.3 111 19 0.086 3 8 5.3 112 22 0.073 4 8 5.3 113 29 0.045 5 8 5.3 116 21 0.179 6 8 5.3 290 13 0.205 7 11 7.7 2708 145 0.010 8 11 7.7 2714 45 0.110 9 11 7.7 2715 300 0.010
生成されるSQLを見てみても、しっかりUNIONされていることがわかる(ムダにサブクエリがあるのはもう仕方ない)。
> # show_queryで作られるSQLが見られる > tbl_result %>% + dplyr::show_query() <SQL> SELECT * FROM (SELECT * FROM `attenu`) WHERE (`event` = 1.0) UNION SELECT * FROM (SELECT * FROM `attenu`) WHERE (`event` = 8.0) UNION SELECT * FROM (SELECT * FROM `attenu`) WHERE (`event` = 11.0)
また、dbplyr
でクエリを作ると、ついついパイプでつなげすぎてしまい、非常に重いクエリになることがよくある。多分悪いクセである。
そんなときは、いったん dplyr::compute
で中間データをテーブルに入れてしまうと良い。
> # 結果を一度computeしてテーブルに格納しておくと便利 > tbl_result <- tbl_result %>% + dplyr::compute(name="attenu_union") > tbl_result %>% + dplyr::show_query() <SQL> SELECT * FROM `attenu_union`
クエリを見ても、格納したテーブルから始まっている。
ということは、foreach関数の .final
引数に dplyr::compute
を指定すると、
UNIONした結果を即座にテーブルに格納してくれて便利なのではないか。
> # .final を使って一気にUNION->格納もできる > tbl_result <- foreach::foreach( + x=events, + .combine = dplyr::union, + .multicombine = F, + .final = function(y){dplyr::compute(y, name="attenu_union2")} + ) %do% { + dplyr::tbl(con, "attenu") %>% + filter(event == x) + } > tbl_result # Source: table<attenu_union2> [?? x 5] # Database: sqlite 3.19.3 [] event mag station dist accel <dbl> <dbl> <chr> <dbl> <dbl> 1 1 7.0 117 12 0.359 2 8 5.3 111 19 0.086 3 8 5.3 112 22 0.073 4 8 5.3 113 29 0.045 5 8 5.3 116 21 0.179 6 8 5.3 290 13 0.205 7 11 7.7 2708 145 0.010 8 11 7.7 2714 45 0.110 9 11 7.7 2715 300 0.010
無事に出来ている。
コーディングに自信があればおすすめしたい。
というのも、筆者はコーディングがヘボく、間違ったクエリを発行して間違ったデータを長時間かけてテーブルに格納してしまう糞尿以下の存在であるため、
.final
でいきなり compute
するのは控えている。
以上、d(b)plyr でUNIONする方法をまとめた。 筆者がdbplyrを真面目にやりだしたのはここ最近であるため、もっと良い方法があればぜひ教えてほしい。