purrrパッケージまとめ

今回は反復処理をより簡潔に行えるRパッケージのpurrrパッケージのまとめ。パッケージ自体の存在は知っていたし、かなり主流な手法として多くの人が使っていることは知っていたが、それほど使う機会がなかったし、for構文などで済ませてきたので必要性を感じていなかった。けれども、下のビデオをたまたま見つけ、その有用性に気づけたので、この際にまとめておく。

youtu.be

2017のRカンファレンスこののビデオでは、コミュニティの中で有名なCharlotte Wickhamさんがでデータサイエンス初学者を対象に反復処理にフォーカスして講演しています。

purrrとは

purrr.tidyverse.org

purrrパッケージはloop処理やapply()関数の決定版となるパッケージで、コードの簡潔さや覚えやすさ、利便性の高さから現在Rユーザーにとってはなくてはならないパッケージとなっている。

インストール

install.packages("purrr")
library(purrr)

github.com

チートシートも公開されています。

library(repurrrsive)
data("sw_people")

例として今回はrepurrrsiveパッケージに梱包されている、sw_peopleデータを用います。

map(): 基本

map(.x, .f, ...)

purrrパッケージのもっとも基本の関数。.xにリストやベクトルを挿入し、.fで繰り返し実行する関数を指定する。

v = c(5,6,7)
> map(v, sqrt)
[[1]]
[1] 2.236068

[[2]]
[1] 2.44949

[[3]]
[1] 2.645751

上記はそれぞれの要素の平方根を出力している。気をつけなければいけないのが、map()の出力は常にリスト形式であること。リスト以外のアウトプットを望む場合には、map_*()として指定しなければいけない。

> map_dbl(v, sqrt)
[1] 2.236068 2.449490 2.645751

上のようにmap_dbl()とすることでリストではなくベクトル形式で帰ってくる。他にもmap_lgl()map_chr()などがある。

library(repurrrsive)
data("sw_people")

次に見るのは、リスト内の要素にアクセスする方法。例としてrepurrrsiveパッケージよりスターウォーズのキャラクターデータが記録されているsw_peopleデータを用いる。

> # for文を用いてそれぞれの要素にアクセスし取り出す方法
> for (i in 1:length(sw_people)){
+   print(sw_people[[i]]$name)
+ }
[1] "Luke Skywalker"
[1] "C-3PO"
[1] "R2-D2"
[1] "Darth Vader"
[1] "Leia Organa"
[1] "Owen Lars"
[1] "Beru Whitesun lars"
[1] "R5-D4"
[1] "Biggs Darklighter"
[1] "Obi-Wan Kenobi"
[1] "Anakin Skywalker"
[1] "Wilhuff Tarkin"
[1] "Chewbacca"
[1] "Han Solo"
[1] "Greedo"
[1] "Jabba Desilijic Tiure"
[1] "Wedge Antilles"
[1] "Jek Tono Porkins"
[1] "Yoda"
[1] "Palpatine"
....
# map()を用いる方法
> map_chr(sw_people, "name")
 [1] "Luke Skywalker"        "C-3PO"                 "R2-D2"                 "Darth Vader"           "Leia Organa"          
 [6] "Owen Lars"             "Beru Whitesun lars"    "R5-D4"                 "Biggs Darklighter"     "Obi-Wan Kenobi"       
[11] "Anakin Skywalker"      "Wilhuff Tarkin"        "Chewbacca"             "Han Solo"              "Greedo"               
[16] "Jabba Desilijic Tiure" "Wedge Antilles"        "Jek Tono Porkins"      "Yoda"                  "Palpatine"            
[21] "Boba Fett"             "IG-88"                 "Bossk"                 "Lando Calrissian"      "Lobot"                
[26] "Ackbar"                "Mon Mothma"            "Arvel Crynyd"          "Wicket Systri Warrick" "Nien Nunb"            
[31] "Qui-Gon Jinn"          "Nute Gunray"           "Finis Valorum"         "Jar Jar Binks"         "Roos Tarpals"         
[36] "Rugor Nass"            "Ric Olié"              "Watto"                 "Sebulba"               "Quarsh Panaka"        
[41] "Shmi Skywalker"        "Darth Maul"            "Bib Fortuna"           "Ayla Secura"           "Dud Bolt"             
[46] "Gasgano"               "Ben Quadinaros"        "Mace Windu"            "Ki-Adi-Mundi"          "Kit Fisto"            
[51] "Eeth Koth"             "Adi Gallia"            "Saesee Tiin"           "Yarael Poof"           "Plo Koon"             
[56] "Mas Amedda"            "Gregar Typho"          "Cordé"                 "Cliegg Lars"           "Poggle the Lesser"    
[61] "Luminara Unduli"       "Barriss Offee"         "Dormé"                 "Dooku"                 "Bail Prestor Organa"  
[66] "Jango Fett"            "Zam Wesell"            "Dexter Jettster"       "Lama Su"               "Taun We"              
[71] "Jocasta Nu"            "Ratts Tyerell"         "R4-P17"                "Wat Tambor"            "San Hill"             
[76] "Shaak Ti"              "Grievous"              "Tarfful"               "Raymus Antilles"       "Sly Moore"            
[81] "Tion Medon"            "Finn"                  "Rey"                   "Poe Dameron"           "BB8"                  
[86] "Captain Phasma"        "Padmé Amidala"        

コードの簡潔さは一目瞭然。要素にアクセスしたい場合は第二引数に"x"のようにダブルクオテーションを用いて指定すれば良い。このような処理が重なってしまった時にエラーが出てもデバッギングが容易になる。

names = c("Sam", "Bob", "Chris", "Emma")
> map_chr(names, ~paste0("My name is ", .x))
[1] "My name is Sam"   "My name is Bob"   "My name is Chris" "My name is Emma" 

カスタム関数を指定するときは上記のように.fに指定できる。関数内で呼び出す場所をで意義するには第一引数である.xを書けば良い。

map2(): 複数の入力に対して特定の関数を実行

map2(.x, .y. .f, ...)

.x.yとして複数のリストやベクトルを指定することが可能になる。

# 例として用いるベクトル群を指定
names = c("Sam", "Bob", "Chris", "Emma")
ages = c(8, 10, 9, 12)
gender = c("boy", "boy", "boy", "girl")
> map2(names, ages, ~paste0(.x, " is ", .y, " years old"))
[[1]]
[1] "Sam is 8 years old"

[[2]]
[1] "Bob is 10 years old"

[[3]]
[1] "Chris is 9 years old"

[[4]]
[1] "Emma is 12 years old"

上記のようにカスタム関数ないで二つ違うベクトルにアクセスしたい時は.x.yとして指定できる。

pmap(): 三つ以上のインプットに対しの関数実行

pmap(.l, .f, ...)

map()map2()と違って最初にリストを作成しなければいけないのが注意点。

children = list(names, ages, gender)
> pmap_chr(children, 
+      function(names, ages, gender){paste0(names, " is ", ages, " years old ", gender, ".")}
+      )
[1] "Sam is 8 years old boy."    "Bob is 10 years old boy."   "Chris is 9 years old boy."  "Emma is 12 years old girl."

関数の指定の際に引数を指定しなければいけない。

番外編

safely()とtranspose():

予期せぬ値がインプットリスト内に入っていた場合などに便利。通常通り反復構文で書いているとエラーが出た時にそこで演算処理を停止してしまうが、map()内でsafely()を置くと、出力が$result$errorになりトラブルシューティングがしやすい。

list_unexpected = list("unknown", 10)
> map(list_unexpected, ~.x*2)
Error in .x * 2 : non-numeric argument to binary operator

上記のように文字と数字を含むリストオブジェクトに対して"ある数を倍にして返す関数"を適用するとエラーが出るのがデフォルト。しかし、safely()を用いることでエラー箇所と出力の両方を返してくれる。

> map(list_unexpected, 
+     safely(~.x * 10, otherwise = NA_real_))
[[1]]
[[1]]$result
[1] NA

[[1]]$error
<simpleError in .x * 10: non-numeric argument to binary operator>


[[2]]
[[2]]$result
[1] 100

[[2]]$error
NULL

さらに最後にtranspose()を指定することでよりみやすくなる。

> map(list_unexpected, 
+     safely(~.x * 10, otherwise = NA_real_)) %>% transpose()
$result
$result[[1]]
[1] NA

$result[[2]]
[1] 100


$error
$error[[1]]
<simpleError in .x * 10: non-numeric argument to binary operator>

$error[[2]]
NULL

possibly()

safely()と類似した関数であるが、エラーは帰ってこない

> map(list_unexpected, 
+     possibly(~.x * 10, otherwise = NA_real_))
[[1]]
[1] NA

[[2]]
[1] 100

まとめ

すぐには使いこなせる気がしないが最終的なゴールとしてはfor文を使わずにコードをかけるところまで行きたい。とても便利だしエラー箇所などすぐにわかるのでどんどん手を動かして慣れていきたい。

参考文献