注: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