反復処理
Text Update: 12/27, 2018 (JST)

注:lm関数の回帰式指定が説明により異なっていたのを統一しました。
 
purrrパッケージはapply関数群が行うような反復処理をモダンなコーディングで実現するためのパッケージですが、比較的新しいパッケージであることや考え方が独特に見えることからあまり広く使われていないパッケージだと思います。しかし、dplyr::do関数が将来的にディスコンになりそうなこと、broomパッケージなどとの親和性が高いこと、tidyverseパッケージを読み込むと自動的に読み込まれることなどを考えると使わない手はありません。
 

> library(tidyverse)
-- Attaching packages --------------------------------------- tidyverse 1.2.1 --
√ ggplot2 3.0.0     √ purrr   0.2.5
√ tibble  1.4.2     √ dplyr   0.7.6
√ tidyr   0.8.1     √ stringr 1.3.1
√ readr   1.1.1     √ forcats 0.3.0
-- Conflicts ------------------------------------------ tidyverse_conflicts() --
x .GlobalEnv::%||%() masks purrr::%||%()
x dplyr::filter()    masks stats::filter()
x dplyr::lag()       masks stats::lag()

 

Packages and Datasets

本ページではR version 3.4.4 (2018-03-15)の標準パッケージ以外に以下の追加パッケージを用いています。
 

Package Version Description
tidyverse 1.2.1 Easily Install and Load the ‘Tidyverse’

 

また、本ページでは以下のデータセットを用いています。
 

Dataset Package Version Description
iris datasets 3.4.4 Edgar Anderson’s Iris Data

 

purrr

purrrパッケージは R for Data Science の「21. Iteration」に分類されているように反復処理(ループ処理)をモダンなコーディングで実現するためのパッケージです。例えばirisデータセットを品種(Species)毎に要約するような処理はdplyrパッケージを用いることで以下のようにで記述することが可能です。
 

iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarise_all(mean)
## # A tibble: 3 x 5
##   Species    Sepal.Length Sepal.Width Petal.Length Petal.Width
##   <fct>             <dbl>       <dbl>        <dbl>       <dbl>
## 1 setosa             5.01        3.43         1.46       0.246
## 2 versicolor         5.94        2.77         4.26       1.33 
## 3 virginica          6.59        2.97         5.55       2.03

 
しかし、dplyrパッケージの場合、返り値がデータフレーム型になることから返り値の長さが1を超える場合にはデータフレーム型の制約で処理ができません。
 

iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::summarise_all(quantile)    # 四分位数を求める(返り値の長さは5)
## Error in summarise_impl(.data, dots): Column `Sepal.Length` must be length 1 (a summary value), not 5

 
このような計算を行うにはdplyr::do関数を用いて以下のように処理する必要があります。
 

iris %>% 
  tidyr::gather(part, value, -Species) %>% 
  dplyr::group_by(Species, part) %>% 
  dplyr::do(qt = quantile(.$value)) %>%
  cbind(do.call(rbind, .$qt)) %>%
  dplyr::select(-qt)
## # A tibble: 12 x 7
##    Species    part          `0%` `25%` `50%` `75%` `100%`
##  * <fct>      <chr>        <dbl> <dbl> <dbl> <dbl>  <dbl>
##  1 setosa     Petal.Length   1    1.4   1.5   1.58    1.9
##  2 setosa     Petal.Width    0.1  0.2   0.2   0.3     0.6
##  3 setosa     Sepal.Length   4.3  4.8   5     5.2     5.8
##  4 setosa     Sepal.Width    2.3  3.2   3.4   3.68    4.4
##  5 versicolor Petal.Length   3    4     4.35  4.6     5.1
##  6 versicolor Petal.Width    1    1.2   1.3   1.5     1.8
##  7 versicolor Sepal.Length   4.9  5.6   5.9   6.3     7  
##  8 versicolor Sepal.Width    2    2.52  2.8   3       3.4
##  9 virginica  Petal.Length   4.5  5.1   5.55  5.88    6.9
## 10 virginica  Petal.Width    1.4  1.8   2     2.3     2.5
## 11 virginica  Sepal.Length   4.9  6.22  6.5   6.9     7.9
## 12 virginica  Sepal.Width    2.2  2.8   3     3.18    3.8

 
しかし、dplyr::do関数の後処理は慣れていないと記述が難しく可読性や保守性が高いとは言えないところがあります。このような場合、purrrパッケージを用いると同様の処理は以下のように記述できdplyr::doに比べると少し冗長なコードにはなりますが可読性や保守性が上がります。
 

iris %>% 
  tidyr::gather(part, value, -Species) %>% 
  tidyr::unite("group", c(Species, part)) %>% 
  split(.$group) %>% 
  purrr::map(~ quantile(.$value)) %>% 
  purrr::map_dfr(broom::tidy, .id = "id") %>% 
  dplyr::mutate(names = forcats::as_factor(names), x = round(x, 2)) %>% 
  tidyr::spread(key = names, value = x) %>% 
  tidyr::separate(id, into = c("Species", "part"), sep = "_")
## # A tibble: 12 x 7
##    Species    part          `0%` `25%` `50%` `75%` `100%`
##    <chr>      <chr>        <dbl> <dbl> <dbl> <dbl>  <dbl>
##  1 setosa     Petal.Length   1    1.4   1.5   1.58    1.9
##  2 setosa     Petal.Width    0.1  0.2   0.2   0.3     0.6
##  3 setosa     Sepal.Length   4.3  4.8   5     5.2     5.8
##  4 setosa     Sepal.Width    2.3  3.2   3.4   3.68    4.4
##  5 versicolor Petal.Length   3    4     4.35  4.6     5.1
##  6 versicolor Petal.Width    1    1.2   1.3   1.5     1.8
##  7 versicolor Sepal.Length   4.9  5.6   5.9   6.3     7  
##  8 versicolor Sepal.Width    2    2.52  2.8   3       3.4
##  9 virginica  Petal.Length   4.5  5.1   5.55  5.88    6.9
## 10 virginica  Petal.Width    1.4  1.8   2     2.3     2.5
## 11 virginica  Sepal.Length   4.9  6.23  6.5   6.9     7.9
## 12 virginica  Sepal.Width    2.2  2.8   3     3.18    3.8
iris %>% 
  tidyr::gather(part, value, -Species) %>% 
  tidyr::unite("group", c(Species, part)) %>% 
  dplyr::group_by(group) %>% 
  tidyr::nest() %>%
  dplyr::mutate(out = purrr::map(data, ~ quantile(.$value)),
                out = purrr::map(out, broom::tidy)) %>% 
  tidyr::unnest(out) %>% 
  dplyr::mutate(names = forcats::as_factor(names), x = round(x, 2)) %>% 
  tidyr::spread(key = names, value = x) %>% 
  tidyr::separate(group, into = c("Species", "part"), sep = "_")
## # A tibble: 12 x 7
##    Species    part          `0%` `25%` `50%` `75%` `100%`
##    <chr>      <chr>        <dbl> <dbl> <dbl> <dbl>  <dbl>
##  1 setosa     Petal.Length   1    1.4   1.5   1.58    1.9
##  2 setosa     Petal.Width    0.1  0.2   0.2   0.3     0.6
##  3 setosa     Sepal.Length   4.3  4.8   5     5.2     5.8
##  4 setosa     Sepal.Width    2.3  3.2   3.4   3.68    4.4
##  5 versicolor Petal.Length   3    4     4.35  4.6     5.1
##  6 versicolor Petal.Width    1    1.2   1.3   1.5     1.8
##  7 versicolor Sepal.Length   4.9  5.6   5.9   6.3     7  
##  8 versicolor Sepal.Width    2    2.52  2.8   3       3.4
##  9 virginica  Petal.Length   4.5  5.1   5.55  5.88    6.9
## 10 virginica  Petal.Width    1.4  1.8   2     2.3     2.5
## 11 virginica  Sepal.Length   4.9  6.23  6.5   6.9     7.9
## 12 virginica  Sepal.Width    2.2  2.8   3     3.18    3.8

 
このように複数の返り値がある反復処理に力を発揮するのがpurrrパッケージです。
 

purrr::map関数

purrr::map関数はpurrrパッケージの中心となる関数です。purrr::map関数の考え方自体はシンプルで、第一引数で指定するベクトルまたはリストの各要素に対して、第二引数で指定する関数を適用するものです。
例えば、irisデータセットの品種(Species)ごとに回帰直線を計算する場合は以下のように記述します。
 

iris %>% 
  split(.$Species) %>% 
  purrr::map(.x = .,
             .f = function(df) {
                    lm(Sepal.Length ~ Sepal.Width, data = df)
                  })
## $setosa
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = df)
## 
## Coefficients:
## (Intercept)  Sepal.Width  
##      2.6390       0.6905  
## 
## 
## $versicolor
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = df)
## 
## Coefficients:
## (Intercept)  Sepal.Width  
##      3.5397       0.8651  
## 
## 
## $virginica
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = df)
## 
## Coefficients:
## (Intercept)  Sepal.Width  
##      3.9068       0.9015

 
ただ、これではp値などが分からないので、summary関数にlm関数の計算結果を渡します。
 

iris %>% 
  split(.$Species) %>% 
  purrr::map(.x = .,
             .f = function(df) {
               lm(Sepal.Length ~ Sepal.Width, data = df) %>% summary()
             })
## $setosa
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.52476 -0.16286  0.02166  0.13833  0.44428 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   2.6390     0.3100   8.513 3.74e-11 ***
## Sepal.Width   0.6905     0.0899   7.681 6.71e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2385 on 48 degrees of freedom
## Multiple R-squared:  0.5514, Adjusted R-squared:  0.542 
## F-statistic: 58.99 on 1 and 48 DF,  p-value: 6.71e-10
## 
## 
## $versicolor
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.73497 -0.28556 -0.07544  0.43666  0.83805 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   3.5397     0.5629   6.289 9.07e-08 ***
## Sepal.Width   0.8651     0.2019   4.284 8.77e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.4436 on 48 degrees of freedom
## Multiple R-squared:  0.2766, Adjusted R-squared:  0.2615 
## F-statistic: 18.35 on 1 and 48 DF,  p-value: 8.772e-05
## 
## 
## $virginica
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.26067 -0.36921 -0.03606  0.19841  1.44917 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   3.9068     0.7571   5.161 4.66e-06 ***
## Sepal.Width   0.9015     0.2531   3.562 0.000843 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.5714 on 48 degrees of freedom
## Multiple R-squared:  0.2091, Adjusted R-squared:  0.1926 
## F-statistic: 12.69 on 1 and 48 DF,  p-value: 0.0008435

 

lapply関数でも同様の処理が可能ですが、purrr::map関数を使うメリットは別途説明します。
 

iris %>% 
  split(.$Species) %>% 
  lapply(X = .,
         FUN = function(df){
           lm(Sepal.Length ~ Sepal.Width, data = df) %>% broom::tidy() #summary()
         })
## $setosa
## # A tibble: 2 x 5
##   term        estimate std.error statistic  p.value
##   <chr>          <dbl>     <dbl>     <dbl>    <dbl>
## 1 (Intercept)    2.64     0.310       8.51 3.74e-11
## 2 Sepal.Width    0.690    0.0899      7.68 6.71e-10
## 
## $versicolor
## # A tibble: 2 x 5
##   term        estimate std.error statistic      p.value
##   <chr>          <dbl>     <dbl>     <dbl>        <dbl>
## 1 (Intercept)    3.54      0.563      6.29 0.0000000907
## 2 Sepal.Width    0.865     0.202      4.28 0.0000877   
## 
## $virginica
## # A tibble: 2 x 5
##   term        estimate std.error statistic    p.value
##   <chr>          <dbl>     <dbl>     <dbl>      <dbl>
## 1 (Intercept)    3.91      0.757      5.16 0.00000466
## 2 Sepal.Width    0.902     0.253      3.56 0.000843

 

簡略表記

このようにpurrr::map関数は各要素に対する計算を行ってくれますが、コーディングが少し難解です。無名関数を用いるとコーディングを簡略化できます。
 

iris %>% 
  split(.$Species) %>% 
  purrr::map(.x = .,
             .f = ~ lm(Sepal.Length ~ Sepal.Width, data = .x) %>% summary())
## $setosa
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = .x)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.52476 -0.16286  0.02166  0.13833  0.44428 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   2.6390     0.3100   8.513 3.74e-11 ***
## Sepal.Width   0.6905     0.0899   7.681 6.71e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2385 on 48 degrees of freedom
## Multiple R-squared:  0.5514, Adjusted R-squared:  0.542 
## F-statistic: 58.99 on 1 and 48 DF,  p-value: 6.71e-10
## 
## 
## $versicolor
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = .x)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.73497 -0.28556 -0.07544  0.43666  0.83805 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   3.5397     0.5629   6.289 9.07e-08 ***
## Sepal.Width   0.8651     0.2019   4.284 8.77e-05 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.4436 on 48 degrees of freedom
## Multiple R-squared:  0.2766, Adjusted R-squared:  0.2615 
## F-statistic: 18.35 on 1 and 48 DF,  p-value: 8.772e-05
## 
## 
## $virginica
## 
## Call:
## lm(formula = Sepal.Length ~ Sepal.Width, data = .x)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.26067 -0.36921 -0.03606  0.19841  1.44917 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   3.9068     0.7571   5.161 4.66e-06 ***
## Sepal.Width   0.9015     0.2531   3.562 0.000843 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.5714 on 48 degrees of freedom
## Multiple R-squared:  0.2091, Adjusted R-squared:  0.1926 
## F-statistic: 12.69 on 1 and 48 DF,  p-value: 0.0008435

 

データフレーム化

ここまで見てきたようにpurrr::map関数の返り値はリスト型になり、その後の扱いが少々厄介です。purrr::map_dfr関数とbroomパッケージを用いると返り値をひとつのデータフレーム型にまとめることができます。
 

iris %>%
  base::split(.$Species) %>%
  purrr::map_dfr(~ lm(Sepal.Length ~ Sepal.Width, data = .x) %>% broom::tidy(),
                 .id = "Species")
## # A tibble: 6 x 6
##   Species    term        estimate std.error statistic  p.value
##   <chr>      <chr>          <dbl>     <dbl>     <dbl>    <dbl>
## 1 setosa     (Intercept)    2.64     0.310       8.51 3.74e-11
## 2 setosa     Sepal.Width    0.690    0.0899      7.68 6.71e-10
## 3 versicolor (Intercept)    3.54     0.563       6.29 9.07e- 8
## 4 versicolor Sepal.Width    0.865    0.202       4.28 8.77e- 5
## 5 virginica  (Intercept)    3.91     0.757       5.16 4.66e- 6
## 6 virginica  Sepal.Width    0.902    0.253       3.56 8.43e- 4

 
dplyr::do関数でも同様の処理は記述可能です。
 

# dplyr::do
iris %>% 
  dplyr::group_by(Species) %>% 
  dplyr::do(lm_res = lm(Sepal.Length ~ Sepal.Width, data = .)) %>% 
  broom::tidy(lm_res)
## # A tibble: 6 x 6
##   Species    term        estimate std.error statistic  p.value
##   <fct>      <chr>          <dbl>     <dbl>     <dbl>    <dbl>
## 1 setosa     (Intercept)    2.64     0.310       8.51 3.74e-11
## 2 setosa     Sepal.Width    0.690    0.0899      7.68 6.71e-10
## 3 versicolor (Intercept)    3.54     0.563       6.29 9.07e- 8
## 4 versicolor Sepal.Width    0.865    0.202       4.28 8.77e- 5
## 5 virginica  (Intercept)    3.91     0.757       5.16 4.66e- 6
## 6 virginica  Sepal.Width    0.902    0.253       3.56 8.43e- 4
# purrr::map_dfr
iris %>%
  base::split(.$Species) %>%
  purrr::map_dfr(~ lm(Sepal.Length ~ Sepal.Width, data = .x) %>% broom::tidy(),
                 .id = "Species")
## # A tibble: 6 x 6
##   Species    term        estimate std.error statistic  p.value
##   <chr>      <chr>          <dbl>     <dbl>     <dbl>    <dbl>
## 1 setosa     (Intercept)    2.64     0.310       8.51 3.74e-11
## 2 setosa     Sepal.Width    0.690    0.0899      7.68 6.71e-10
## 3 versicolor (Intercept)    3.54     0.563       6.29 9.07e- 8
## 4 versicolor Sepal.Width    0.865    0.202       4.28 8.77e- 5
## 5 virginica  (Intercept)    3.91     0.757       5.16 4.66e- 6
## 6 virginica  Sepal.Width    0.902    0.253       3.56 8.43e- 4
# purrr::map, map_dfr
iris %>% 
  base::split(.$Species) %>% 
  purrr::map(~ lm(Sepal.Length ~ Sepal.Width, data = .x)) %>%
  purrr::map_dfr(broom::tidy, .id = "Species") 
## # A tibble: 6 x 6
##   Species    term        estimate std.error statistic  p.value
##   <chr>      <chr>          <dbl>     <dbl>     <dbl>    <dbl>
## 1 setosa     (Intercept)    2.64     0.310       8.51 3.74e-11
## 2 setosa     Sepal.Width    0.690    0.0899      7.68 6.71e-10
## 3 versicolor (Intercept)    3.54     0.563       6.29 9.07e- 8
## 4 versicolor Sepal.Width    0.865    0.202       4.28 8.77e- 5
## 5 virginica  (Intercept)    3.91     0.757       5.16 4.66e- 6
## 6 virginica  Sepal.Width    0.902    0.253       3.56 8.43e- 4

 

dplyr::group_by関数と併用する

tidyverseなモダンなコーディングなのだからdplyr::group_byを使ってpurrr::map処理をしたい!という場合には、tidyr::nest関数を用いてグループごとリスト化しデータフレーム内にネストします。
 

層別に回帰モデルを求める場合

iris %>% 
  dplyr::group_by(Species) %>%
  tidyr::nest() %>% 
  dplyr::mutate(out = purrr::map(data,
                                 ~ lm(Sepal.Length ~ Sepal.Width, data = .x) %>% 
                                   broom::tidy())) %>%
  tidyr::unnest(out)
## # A tibble: 6 x 6
##   Species    term        estimate std.error statistic  p.value
##   <fct>      <chr>          <dbl>     <dbl>     <dbl>    <dbl>
## 1 setosa     (Intercept)    2.64     0.310       8.51 3.74e-11
## 2 setosa     Sepal.Width    0.690    0.0899      7.68 6.71e-10
## 3 versicolor (Intercept)    3.54     0.563       6.29 9.07e- 8
## 4 versicolor Sepal.Width    0.865    0.202       4.28 8.77e- 5
## 5 virginica  (Intercept)    3.91     0.757       5.16 4.66e- 6
## 6 virginica  Sepal.Width    0.902    0.253       3.56 8.43e- 4

 

層別に四分位数を求める場合

iris %>% 
  tidyr::gather(part, value, -Species) %>% 
  tidyr::unite("group", c(Species, part)) %>% 
  dplyr::group_by(group) %>% 
  tidyr::nest() %>%
  dplyr::mutate(out = purrr::map(data, ~ quantile(.$value)),
                out = purrr::map(out, broom::tidy)) %>% 
  tidyr::unnest(out) %>% 
  dplyr::mutate(names = forcats::as_factor(names), x = round(x, 2)) %>% 
  tidyr::spread(key = names, value = x) %>% 
  tidyr::separate(group, into = c("Species", "part"), sep = "_")
## # A tibble: 12 x 7
##    Species    part          `0%` `25%` `50%` `75%` `100%`
##    <chr>      <chr>        <dbl> <dbl> <dbl> <dbl>  <dbl>
##  1 setosa     Petal.Length   1    1.4   1.5   1.58    1.9
##  2 setosa     Petal.Width    0.1  0.2   0.2   0.3     0.6
##  3 setosa     Sepal.Length   4.3  4.8   5     5.2     5.8
##  4 setosa     Sepal.Width    2.3  3.2   3.4   3.68    4.4
##  5 versicolor Petal.Length   3    4     4.35  4.6     5.1
##  6 versicolor Petal.Width    1    1.2   1.3   1.5     1.8
##  7 versicolor Sepal.Length   4.9  5.6   5.9   6.3     7  
##  8 versicolor Sepal.Width    2    2.52  2.8   3       3.4
##  9 virginica  Petal.Length   4.5  5.1   5.55  5.88    6.9
## 10 virginica  Petal.Width    1.4  1.8   2     2.3     2.5
## 11 virginica  Sepal.Length   4.9  6.23  6.5   6.9     7.9
## 12 virginica  Sepal.Width    2.2  2.8   3     3.18    3.8