Rのパッケージpurrr
は、リスト/ベクトルに対して反復処理を行う関数を提供するパッケージである。
これどう呼ぶのが正しいのだろうか。ひとまず「ぴゅるるる」と呼ぼう。
Rでループ処理を行うときは、処理速度が極端に遅いため極力for
を使わないようにすべしというのは、ある程度Rを使っている方であればご存知かと思う。
他にはapply
もあるが、個人的にあまり使いやすさを感じたことはない。
purrr
は、速度そして可読性の両方を兼ね備えたパッケージである。
基本的な使い方
purrr
のメインとなる関数はpurrr::map
である。purrr::map(x, f)
として、リスト/ベクトル/行列/データフレームx
に対して関数f
を繰り返し作用させ、計算結果をリストに格納する。
基本的にはx %>% purrr::map(f)
のように、リスト/ベクトルを渡して関数で処理するというパイプラインにするのが一般的かと思う。
例えば、標準正規分布から10個の乱数を取ってきて、それらを5つのリストに格納するコードは以下のようにかける。
library(magrittr) 1:5 %>% purrr::map(rnorm, n = 5)
[[1]] [1] 1.2644874 -0.1058268 1.1207076 0.9976939 0.1906772 [[2]] [1] 0.3033158 1.4178929 2.4092054 1.6336716 2.7202050 [[3]] [1] 2.293005 3.507343 3.918410 2.050489 2.193373 [[4]] [1] 3.697953 4.015712 5.237348 4.090736 4.329179 [[5]] [1] 6.763691 4.920744 4.854612 4.336114 5.076095
私の場合、このpurrr
を使うシチュエーションは主に2つある。
1. 同じグラフをそれぞれのカテゴリー/グループに対してサクッと作成し、サクッと取り出せるようにしたいとき
1つ目は、同じ種類のグラフ (ヒストグラム、散布図など) を、違うカテゴリー/グループごとに作成したいときである。
ggplot2
のfacet_grid
などを使って、1つの図にまとめる方法もあるが、カテゴリーの数が多すぎるとグラフが小さくなり問題である。
自分の好きなカテゴリーのグラフを手軽に取り出したいときもある。
そこでpurrr
の出番である。
irisデータを用いて、アヤメの種類ごとガクの長さ (sepal length) のヒストグラムを作成することを考える。
library(ggplot2) draw_hist <- function(df, column) { g <- ggplot(data = df, aes(x = !!dplyr::enquo(column))) + geom_histogram() return(g) } hist <- iris %>% split(.$Species) %>% purrr::map(draw_hist, column = Sepal.Length)
こうすることで、それぞれのアヤメの種類ごとにガクの長さを、例えばhist$setosa
のようにして、すぐにsetosaのヒストグラムを呼び出すことができる。
もっというと、iris
をlong形式に変換し、各カテゴリー・各変数ごとにsplit
してpurrr::map
してしまえば、全てのグループと変数の組み合わせについてヒストグラムを作成することができる。
hist_all <- tibble::rowid_to_column(iris, var = "id") %>% tidyr::pivot_longer(-c(id, Species), names_to = "variable", values_to = "value") %>% split(list(.$Species, .$variable)) %>% purrr::map(draw_hist, column = value) hist_all$setosa.Petal.Width
データサイズが大きくなったり、分割するカテゴリーが増えたりするとメモリー不足にならないか心配であるが、とりあえずこれまではうまく動いている。
ちなみに上の自作関数の中にある!!dplyr::enquo
はnon-standard evaluation (NSE) と呼ばれる黒魔術で、今は無視してほしい。*1
2. 違うデータフレームを続けて結合したいとき
2つ目は、あるデータセットに対して、複数の異なるデータセットを次々と結合させたいときである。 例えば、上で使用したアヤメのデータセットが、アヤメの種類 (Species) 、ガクの長さ (Sepal.Length) 、ガクの幅 (Sepal.Width) 、花弁の長さ (Petal.Length )、花弁の幅 (Petal.Width) の5つに分割されており、これらは全てあるid変数で結合できるとしよう。
df_iris <- tibble::rowid_to_column(iris, var = "id") df_species <- dplyr::select(df_iris, c(id, Species)) df_sepallength <- dplyr::select(df_iris, c(id, Sepal.Length)) df_sepalwidth <- dplyr::select(df_iris, c(id, Sepal.Width)) df_petallength <- dplyr::select(df_iris, c(id, Petal.Length)) df_petalwidth <- dplyr::select(df_iris, c(id, Petal.Width))
(このコードもpurrr
でスマートに書けそうな気がするが、ひとまず力技で押し通した)
これらを元通りに結合するとき、1つのやり方は素直にdplyr::left_join
を繰り返し行うことだろう。
df <- dplyr::left_join(df_species, df_sepallength, by = "id") %>% dplyr::left_join(., df_sepalwidth, by = "id") %>% dplyr::left_join(., df_petallength, by = "id") %>% dplyr::left_join(., df_petalwidth, by = "id") head(df)
id Species Sepal.Length Sepal.Width Petal.Length Petal.Width 1 1 setosa 5.1 3.5 1.4 0.2 2 2 setosa 4.9 3.0 1.4 0.2 3 3 setosa 4.7 3.2 1.3 0.2 4 4 setosa 4.6 3.1 1.5 0.2 5 5 setosa 5.0 3.6 1.4 0.2 6 6 setosa 5.4 3.9 1.7 0.4
ただ、コードとしては冗長な感は否めない。どのデータフレームをjoinしたか、混乱することもしばしばある。
私も最近知ったのだが、これはpurrr::reduce
を用いるとスッキリと書くことができる。
df <- list(df_species, df_sepallength, df_sepalwidth, df_petallength, df_petalwidth) %>% purrr::reduce(dplyr::left_join, by = "id") head(df)
id Species Sepal.Length Sepal.Width Petal.Length Petal.Width 1 1 setosa 5.1 3.5 1.4 0.2 2 2 setosa 4.9 3.0 1.4 0.2 3 3 setosa 4.7 3.2 1.3 0.2 4 4 setosa 4.6 3.1 1.5 0.2 5 5 setosa 5.0 3.6 1.4 0.2 6 6 setosa 5.4 3.9 1.7 0.4
繰り返しにはforeach
もよい?
for
は使うなと冒頭で述べたが、私はちょくちょくforeach
を使う。並列処理もforeach
で行うことができ、便利である。
標準正規分布から10個の乱数を取ってきて、それらを5つのリストに格納するコードは、foreach
を用いて以下のようにも書ける。
library(foreach) foreach(i = 1:5) %do% { rnorm(n = 10) }
が、purrr
を用いた方が処理速度は速い。それはそうか。
rbenchmark::benchmark( use_purrr = { 1:100 %>% purrr::map(rnorm, n = 1000) }, use_foreach = { foreach(i = 1:100) %do% { rnorm(n = 1000) } } )
test replications elapsed relative user.self sys.self 2 use_foreach 100 2.959 3.595 2.935 0.014 1 use_purrr 100 0.823 1.000 0.810 0.011
foreach
についてはまた別のポストで書いてみたい。
特に並列処理を行うとき、なぜnamespace::function
と普段から書いておいたほうが良いかがわかる。
*1:いつも雰囲気で何とかしている。