医療職からデータサイエンティストへ

統計学、機械学習に関する記事をまとめています。

モダンな繰り返し処理purrrの使い方

データの繰り返し処理やapply族を使ったデータフレームの処理などはデータ解析をする上では欠かせませんよね。

そんな時に役に立つのが{purrr}パッケージです。このパッケージは、今までfor-loopさせていた処理や、apply族を使っていた処理をスマートに処理することができます。

慣れるまでに少し時間がかかりますが、使いこなせるようになれば処理の幅が広がることは間違い無いです!

今回はその使い方をまとめていきます。

{purrr}とは

そもそも{purrr} とは、リストやベクトルを受け取って反復処理をするパッケージです。for文やapply族を使う場合が多いかと思いますが、{purrr}は色々な機能をそなえたモダンなパッケージとなっております。

全部は紹介しきれないので、よく使いそうな関数だけをまとめておきます!

map

まずは基本的な関数mapです。mapはリストの要素ごとに関数を適応する関数です。例えばこんな感じで、二つのベクトルをもつリストにmapを適応すると、それぞれのベクトルに対して関数を適応し、結果をリストで返します。

x <- list(c(1,2,3,4),c(10,20,30,40))
> x
[[1]]
[1] 1 2 3 4

[[2]]
[1] 10 20 30 40

> map(x,mean)
[[1]]
[1] 2.5

[[2]]
[1] 25

イメージとしてはこんな感じ

f:id:h-wadsworth02:20190131214045j:plain

for文で実装するとこうなります。

> #for文
> anlis <-list()
> for(lis in x){anlis <- append(anlis,mean(lis))}
[[1]]
[1] 2.5

[[2]]
[1] 25

厳密には少し違う中身になっているのですが、原理としては上記のようになってます。

何で結果がリストなんだよ!と思った方ご安心を。map_xxxと語尾(?)にreturn結果の型を指定をすることができます。指定できる型は

  • map_chr : 文字ベクトル
  • map_dbl : 実数ベクトル
  • map_dfc : データフレーム(列結合)
  • map_dfr : データフレーム(行結合)

がよく使う型かと思います。他にもあるので、興味のある方は調べてみてください。

実行してみます。

 > map_chr(x,mean)
[1] "2.500000"  "25.000000"

>map_dbl(x,mean)
[1]  2.5 25.0

> map_dfc(x,mean)
# A tibble: 1 x 2
     V1    V2
  <dbl> <dbl>
1   2.5    25

だだし、値を二つ以上返すような関数を適応した場合に、返り値をベクトル型で指定するとエラーになるので、注意してください。例えば、sqrt()を適応してみると

#これは大丈夫
> map(x,sqrt)
[[1]]
[1] 1.000000 1.414214 1.732051 2.000000

[[2]]
[1] 3.162278 4.472136 5.477226 6.324555

#これも大丈夫(ただし、ベクトルが同じ長さの場合のみ)
> map_dfc(x,sqrt)
# A tibble: 4 x 2
     V1    V2
  <dbl> <dbl>
1  1     3.16
2  1.41  4.47
3  1.73  5.48
4  2     6.32

#これはダメ
> map_dbl(x,sqrt)
 エラー: Result 1 is not a length 1 atomic vector
> map_chr(x,sqrt)
 エラー: Result 1 is not a length 1 atomic vector

また、リストではなくデータフレームにmapを適応すると列ごとに処理されます。

f:id:h-wadsworth02:20190131214230j:plain

> df <- data_frame(a=c(1,2,3,4),b=c(10,20,30,40))
> map(df,mean)
$a
[1] 2.5

$b
[1] 25

データフレームの列ごとの処理は、apply関数に似ていますね。map_dblとすると同じ結果になります。

> apply(df,2,mean)
   a    b 
 2.5 25.0 

> map_dbl(df,mean)
   a    b 
 2.5 25.0 

さらにmap_atを使うことで処理する列を指定できます。

#b列だけにmeanを適応
> map_at(df,"b",mean)
$a
[1] 1 2 3 4

$b
[1] 25

すごい!

引数が二つ以上ある関数を適応したい場合は、関数の後に続けて書きます。例えばrnormの平均値をリストで渡して、sdとnを指定すれば、平均値の異なる乱数を生成することができます。

> mu <- list(5, 10, -3)
> map(mu,rnorm,sd=1,n=5)
[[1]]
[1] 4.048647 4.867946 4.968174 5.450112 4.788637

[[2]]
[1]  9.375818  8.055653 10.459579 10.130679  9.351773

[[3]]
[1] -3.194008 -2.963746 -4.238364 -2.164801 -1.374328

#標準偏差を変えたい場合
> sigma <- list(2,3,4)
> map(sigma,rnorm,mean=1,n=5)
[[1]]
[1]  0.5443468 -0.0776026 -1.0920037 -1.1450414  2.2325834

[[2]]
[1]  3.505510  7.038860 -2.502432  0.174228  1.500462

[[3]]
[1]  0.8005832 -8.6206833  7.4006408  0.6503117 -3.7953110

map2,pmap

受け取るリストが二つある時に使えるのがmap2、それ以上の場合にpmapを使います。標準偏差のリスト、生成数nのリストも作って、map2,pmapに渡してみます。

f:id:h-wadsworth02:20190131214600j:plain

>#map2
> sigma <- list(2,3,4)
> map2(mu,sigma,rnorm,n=5)
[[1]]
[1] 4.546525 5.236469 7.253060 6.752078 7.117388

[[2]]
[1]  9.63385 11.28458 16.77749 12.86226 11.75143

[[3]]
[1] 1.7185900 0.3821178 7.2428390 7.5300247 6.5106281

pmapはリストにして渡す必要があることに注意して下さい。また、わかりやすいように引数名をつけておくと良いです。(引数名が無い場合は順番に渡される)

f:id:h-wadsworth02:20190131214627j:plain

>#pmap
> n <- list(1,2,3)
>#引数名をつけておくとよい
> pmap(list(n=n,mean=mu,sd=sigma),rnorm)
[[1]]
[1] 2.415151

[[2]]
[1] 11.95633 13.78740

[[3]]
[1] 4.768135 7.872961 6.459873

pmapはデータフレームとも相性がよいです。 先ほどの平均、標準偏差、生成数のデータフレームを作って、pmapに渡します。

> #データフレームにすれば長さも同じになり読みやすいので便利
> params <- data.frame( n=c(1,2,3),
+                       mean=c(5,10,-3),
+                       sd=c(2,3,4))
> pmap(params,rnorm)
[[1]]
[1] 2.910451

[[2]]
[1] 12.50149 11.69717

[[3]]
[1]   0.6610997   3.4350804 -12.6419800

よく使うpurrrの活用法

{purrr}では、データフレームの要因ごとに関数を適応するという使い方をよくします。

例えば、車のスピードと停車距離のR標準データセットcarsに天気の変数を適当に加えてcars2というデータセットを作成します。

medi-data.hatenablog.com

library(dplyr)
cars2<-cars %>% 
  mutate(wether=rep(c("rainy","sunny"),25))

#グラフ化
cars2 %>% 
  ggplot()+
  geom_point(aes(x=speed,y=dist,col=wether))+
  scale_color_manual(values=c("blue","red"))

f:id:h-wadsworth02:20190131215114j:plain

こんな感じのデータセットができました。 ここでやりたいのは、晴れの日と雨の日ごとに線形回帰モデルを適応することです。

通常であれば、2回lm関数を適応する必要がありますが、mapを使えば一度で終わらせることができます。

どうやるかというと、まずは{base}のsplit関数を使って、データセットを天気の要因で二つに分けます。splitを使うと二つのデータセットがリストで返ってくるので、あとはmap関数の中にlm関数を定義すれば完了です。

cars2 %>% 
  split(.$wether) %>% 
  map(function(df){lm(speed~dist,data=df)})

$rainy

Call:
lm(formula = speed ~ dist, data = df)

Coefficients:
(Intercept)         dist  
     8.7595       0.1473  


$sunny

Call:
lm(formula = speed ~ dist, data = df)

Coefficients:
(Intercept)         dist  
     7.5361       0.1906  

splitの中にあるドットはパイプを使った記述法なので、慣れていない方はこちらをご覧ください。

medi-data.hatenablog.com

さらに{purrr}ではショートカットとして、関数の定義をチルダを使って書くことで記述の手間を省くことができます。

#通常の書き方
cars2 %>% 
  split(.$kind) %>% 
  map(function(df){lm(speed~dist,data=df)})

#チルダを使ったショートカット
cars2 %>% 
  split(.$kind) %>% 
  map(~lm(speed~dist,data=.))

いちいちfunction()と書かなくてもいいので楽ですね。

また、少し高度なテクニックですが、{tidyr}のnestと{dplyr}のgroup_byを使うのがモダンなやり方らしいです。nestとは、tibble型データフレームを階層構造にする 関数なのですが、正直私も使いこなせていません...^^;

一応やってみます。

cars2 %>% 
  group_by(wether) %>% 
  nest()

# A tibble: 2 x 2
  wether data             
  <chr>  <list>           
1 rainy  <tibble [25 × 2]>
2 sunny  <tibble [25 × 2]>

このdataという列の中に天気ごとのデータフレームが格納されています。

f:id:h-wadsworth02:20190131215409j:plain

ここにlm関数を適応させるには、{dplyr}のmutate関数を使って、その中でmap関数を使います。

cars2 %>% 
  group_by(wether) %>% 
  nest() %>% 
  mutate(model=map(data,~lm(speed~dist,data=.)))

# A tibble: 2 x 3
  wether data              model   
  <chr>  <list>            <list>  
1 rainy  <tibble [25 × 2]> <S3: lm>
2 sunny  <tibble [25 × 2]> <S3: lm>

今度はmodelという列の中にlm関数の結果が格納されました。

f:id:h-wadsworth02:20190131215459j:plain

これをlm_modelsとして保存しておけば、結果にアクセスしやすくなります。 (unnestを使うやり方もありますが、私は使いこなせません...)

lm_models <- cars2 %>% 
  group_by(wether) %>% 
  nest() %>% 
  mutate(model=map(data,~lm(speed~dist,data=.))) 

> lm_models$model[[1]]

Call:
lm(formula = speed ~ dist, data = .)

Coefficients:
(Intercept)         dist  
     8.7595       0.1473  

nest()を使うメリットとしては、モデルやデータフレームを一括で管理できるので、解析の見通しが立てやすいことです。summaryの結果を格納しておけば、95%信頼区間やその他の統計量も一つのオブジェクトで管理できますね。

#summaryも格納
lm_models %>% 
  mutate(summ = map(model,summary))

 A tibble: 2 x 4
  wether data              model    summ            
  <chr>  <list>            <list>   <list>          
1 rainy  <tibble [25 × 2]> <S3: lm> <S3: summary.lm>
2 sunny  <tibble [25 × 2]> <S3: lm> <S3: summary.lm>

まとめ

今回はモダンな繰り返し処理を行うことができる{purrr}を紹介しました。使い方は少し複雑ですが、{purrr}を使いこなせば、かなーーーりスマートに作業を進めることができるのかなと思っています。

まだまだ紹介しきれていない機能もたくさんあるのでぜひ使ってみてください。

参考

そろそろ手を出すpurrr / nekosky - Speaker Deck

https://github.com/rstudio/cheatsheets/raw/master/purrr.pdf :pdf注意

{tidyr} nestしていこう。 - Qiita

21 Iteration | R for Data Science

上記サイトの和書版です。dplyrやpurrr、tidyrなどモダンなパッケージをまとめたtidyverseを中心に、データの解析手法そのものよりも、いかに効率よくデータを解析するかが書かれています。Rに慣れ始めた頃に読むのがおすすめです!

Rではじめるデータサイエンス

Rではじめるデータサイエンス