-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathch5.qmd
298 lines (223 loc) · 15.3 KB
/
ch5.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# 効率的なデータ操作 {#functional-programming}
この章では、効率的にデータを処理・操作する方法を学ぶ。ここでの効率的、とは、コード内での繰り返しの記述を減らしたり、見通しの良い記述での実行を意味している。また対象として単なる繰り返しだけでなくグループごとに同じ処理を適用することも含んでいる。これらの技法を学ぶことは、データの前処理だけでなく統計解析、モデリングの関数や可視化を行うために役立ち、十分に投資する価値のあるものである。
## 繰り返しの処理はプログラムの得意分野
一つの状況を考えてみよう。データフレームの変数が、数値を格納するか、文字列であるのかは、Rの処理を行う上で意識しておきたい事項である。今、手持ちのデータフレームについて、各変数のデータ型が不明であるとき、どのように確認すれば良いだろう。ここではオブジェクトのクラスを確認するため、`names()`でデータフレームに含まれる変数の名前を確認した後、各変数への参照を`class()`で表示する方法を示す。
```{r}
names(trees) # データフレームの変数名 (列名)を取得する
class(trees$Girth) # 変数Girthの値をベクトル形式で参照する
class(trees$Height)
class(trees$Volume)
```
もう一つ、データフレームの列数を確認した後に`[`で値を参照する方法は以下のようになる。
```{r}
class(trees[, 1])
class(trees[, 2])
class(trees[, 3])
```
目的は達成できたが、一方でデータフレームの中身が同一であっても変数名が異なっていたり、同じ変数が大量にあるデータフレームで同じことを行う場合はどうだろうか。これらの方法は確実ではあるが、変化やコードへ記述する内容が増えてしまうという欠点がある。変化がある度に内容を書き換えたり、同じ内容の記述を行うと、どこかで入力間違いが発生するリスクも高まる。これから示す**purrr**による処理は、これらの欠点を解消するのに有効な手法となる。
<!-- for() や関数化には触れない? -->
### 引数に関数をもつ関数
Rの標準関数には`apply()`や`sapply()`など、関数を引数にもつ**高階関数** (汎関数)がある。これらの`apply`族と呼ばれる関数は、繰り返し行われる処理を効率的に行うために利用される。例えばこれをデータフレームの各変数に実行する場合、次のコードを実行すれば良い。
```{r}
# apply族関数を用いてtreesの変数のデータ型を確認
apply(X = trees, MARGIN = 2, FUN = class)
lapply(X = trees, FUN = class)
```
いずれの関数も対象のデータを*X*で指定し、データに適用する関数を*FUN*で与えている。また`apply()`の中で利用している**MARGIN*は関数の適用する方向を示しており、2は列の指定を示している。*FUN*には関数の名前が使われるが、文字列と区別するために引用符は用いられない。一方で`+`や`%*%`などの演算子を与える場合には演算子の名前を引用符あるいはバッククオート("\`")で囲む。これは`$`や`[`演算子を使った参照をデータフレームの各変数に適用するよりも効率的である。特に変数の多いデータでは`apply`族関数が効果的である。
tidyverseに含まれる**purrrr**パッケージは、こうした`apply`族関数と同機能をもつ関数`map()`をはじめとして、効率的にRオブジェクトを扱うための関数を提供する。また**purrrr**で扱う`map()`をはじめとした関数は、`apply`族関数よりも実行の仕方や返り値のデータ型についてわかりやすくなっている。加えてパイプ処理や**dplyr**、**ggplot2**といった他のtidyverseで利用されるパッケージとも相性が良い。**purrr**の関数`map`で先の処理を実行すると次のようになる。
```{r}
library(purrr) # purrrはtidyverseに含まれる
map(.x = trees, .f = class)
# purrrはtidyverseに含まれており、
# 第一引数に対象のオブジェクトを受け取るため
# パイプ処理が適用しやすい
trees %>%
map(class)
```
これは`lapply(trees, class)`の結果と同じで、返り値は3つの要素からなるリストとなっている。`map()`は**purrr**の基礎となる関数である。`map()`は引数*.x*に与えられた各要素に対して、*.f*で指定された関数を適用し、入力と同じ長さのベクトルを返却する関数である。
`map()`の第二引数の*.f*\footnote{functionを示す}には、次のように、無名関数も指定可能であるが、`~`を用いることで関数定義を短縮して記述することも可能である。すなわち以下のコードは全て同様の結果を導く。
```{r, eval = FALSE, echo = TRUE}
1:3 %>%
map(rnorm, n = 3)
# 無名関数を定義して与えても良い
1:3 %>%
map(function(x) rnorm(3, x))
# ~ は function(x)を省略した記述となる
1:3 %>%
map(~ rnorm(3, .))
```
無名関数を定義する場合、その第一引数にデータを指定する。`~`は`function(x)`の省略型であり、ラムダ式として無名関数を表現できる。その際はデータを"."または".x"、"..1"で示す。数字を使う場合、ドットが2回続くのに注意である。".x"や"..1"といった表現は、与えられる引数の位置を把握するのに優れ、次に述べる、複数の入力を元に処理を適用する場合にさらに便利である。
また`map()`では、添字を使った位置や名前による要素の指定も行える。ここでは引数に対象の要素の位置を指定するか、名前を与えて実行すれば良い。これは特にリストを扱う上で効果的である。
```{r}
(x <- list(list(id = 1,
name = "hoxom"),
list(id = 2,
name = "uribo",
age = 28)))
```
ここで**x**は*id*、*name*、*age*の3つの名前をもつリストである。リストにはさらに2つのリストが含まれている。ここから**x**の各要素を参照するための処理は次のようになる。
```{r}
# 各リスト中の第一の要素を参照する
x %>%
map(1)
# リスト中のnameの値を参照する
x %>%
map("name")
# 返却する要素が無い場合は NULLが与えられる
# NULLは引数.defaultの指定により変更可能
x %>%
map("age")
x %>%
map("age", .default = NA)
```
`map()`の返り値がリストになることは上記のコードで示した通りだが、`map_*()`を使ってベクトルとして得ることもできる(表XX)。`map_*()`には、返り値のベクトルのデータ型を定義するもので、それは、`map_*()`の接尾辞によって決まる。例えば文字列として出力する場合には`map_chr()`、整数値であるならば`map_int()`という具合である。もちろん、ここでもベクトル内部でのデータ型の混在は許されず、不可逆的なデータ型への変換は行えない。
`map_*()`の例として、長さ(文字数)の異なる3つの文字列を用意し、その長さを数えてみよう。ここでは文字列の長さを数える関数として`nchar()`を適用する。`nchar()`の返り値は整数であり、ベクトルとして出力するために`map_int()`が利用できる。また
```{r}
x <- c("こんにちは", "Hello", "您好")
x %>%
map(nchar)
x %>%
map_int(nchar)
```
次に関数`grepl`を用いて、Hで始まる要素の有無を確認してみる。`map()`は適用する関数の引数を関数内部で指定可能な高階関数であるので、次のように`grepl()`の引数*pattern*を`map()`の引数として指定する。これは`grepl(x[1], pattern = "^H"")`、`grepl(x[2], pattern = "^H"")`、そして`grepl(x[3], pattern = "^H"")`の処理を実行して得る結果に等しい。
```{r}
x %>%
map_lgl(grepl, pattern = "^H")
```
| 関数 | 返り値 |
|------|--------|
| `map_lgl()` | 論理値 |
| `map_chr()` | 文字列 |
| `map_int()` | 整数 |
| `map_dbl()` | 実数 |
ベクトルの他、データフレームを返却する`map_dfc()`、`map_dfr()`も利用できる。これらは処理結果をデータフレームの列および行方向に結合した結果を返す。
以下の例は**mtcars**の*cly*ごとにデータを分割し、その後、分割された各データにdispの平均値を算出し、平均値を`map_dfc()`によりデータフレームの横方向に結合する、というものになる。
```{r}
mtcars %>%
# splitはRで標準的に利用可能な関数
# 与えられた要素ごとにデータを分割する
split(.$cyl) %>%
map_dfc(~ mean(.$disp))
```
![](images/fig-purrr_map.png)
### mapの派生
`map()`には`map_int()`をはじめとして多数の派生した関数がある。これらは処理を適用する要素を制限したり、複数の入力を関数に与えることを可能にする。また、出力を伴わない処理を実行する関数も備わっている。
#### 位置や条件で適用する: map_at / map_if
`map()`では与えられた全ての要素に関数を適用をするが、`map_if()`や`map_at()`を使うことで条件や位置を元に適用箇所を制限することができる。2つの関数はそれぞれ*.p*\footnote{pはpredictを示す}、*.at*をもち、そこに適用する要素の条件およびその位置を指定する。なお、これらの関数の返り値は、共通のデータ型である場合でも常にリストとなる。
3つの要素からなる文字列ベクトルに`nchar()`を実行し、各文字数を数えるが、ここで条件を定義して適用する要素を制限するというコードは次のように記述する。
```{r}
x
# xの各要素でアルファベットを含むものを対象に
# nchar()を適用する
x %>%
map_if(.p = grepl("[a-z]", .), nchar)
# パイプ処理を用いない場合、 データの指定を次のようにする必要がある
# map_if(x, .p = ~ grepl("[a-z]", .x), nchar)
```
ここでは引数*.p*が条件として与えられる処理になる。条件には、関数`grepl`により、対象がアルファベッドに含まれる文字列であるものを指定した。`grepl()`の中で"."で示されるのはパイプ処理によって代名詞として利用可能な"x"を示している。条件に当てはまらない要素では、関数は適用されず、元の値が返却される。
条件の代わりに要素の位置を基準として処理を施すには、関数`map_at`の引数*.at*でその要素の位置を指定する。下記の例では2、3番目の要素に`nchar()`が実行された結果が出力される。
```{r}
# xの要素から、2、3番目のものに対して
# nchar()を実行
x %>%
map_at(.at = c(2, 3), nchar)
```
#### 複数の入力値を利用する: map2 / pmap
`map()`では、指定した関数に与えることができるデータは一つであるが、複数のデータや異なる引数の値を関数に引き渡したい時には`map()`の派生である`map2`関数群を利用する。例えば`map2()`は、`map()`の*.x*、*.f*引数に加えて*.y*をもち、ここでデータを指定できる。長さが等しい2つの文字列を入力に用いて`paste()`を実行する例を示す。長さの異なるデータを与えた場合にはエラーとなる。
```{r}
map2(
.x = c("Uryu", "Ishida"),
.y = c("Shinya", "Motohiro"),
# paste()は引数に与えられた値を文字列として結合する
.f = paste
)
```
```{r, eval = FALSE, echo = TRUE, purl = FALSE}
# .xには3つの要素が与えれるが.yは要素が2つしかない。
# 要素の数が.xと.yで一致しない場合、処理は停止される
map2(
.x = c("Uryu", "Ishida", "Makiyama"),
.y = c("Shinya", "Motohiro"),
.f = paste
)
# Error: `.x` (3) and `.y` (2) are different lengths
```
`map2()`で指定した引数は既定では、*.f*の関数の第一引数(*.x*)と第二引数(*.y*)に与えられる。ラムダ式を用いる場合、*.y*のデータは".y"あるいは"..2"を関数内で明示的に示す必要がある。ここで".y"が使われているのは、第一引数の".x"、"..1"と区分するためである。
```{r, eval = FALSE, echo = TRUE, purl = TRUE}
# 以下の結果は等しい
map2(
.x = c("Uryu", "Ishida"),
.y = c("Shinya", "Motohiro"),
# 無名関数の引数には.x, .yで与えられたデータが引き渡される
.f = function(x, y) paste(x, y)
)
map2(
.x = c("Uryu", "Ishida"),
.y = c("Shinya", "Motohiro"),
.f = ~ paste(.x, .y)
)
map2(
.x = c("Uryu", "Ishida"),
.y = c("Shinya", "Motohiro"),
.f = ~ paste(..1, ..2)
)
```
`map2()`の利用方法として、次の例のようにデータ以外に引数の値を変更するのに使うこともできる。この例では、`round()`の引数*digits*に異なる値を与えている。
```{r}
map2(c(1, 2.1, 0.332),
c(1, 0, 2),
~ round(.x, digits = .y))
```
2つ以上のデータを入力に用いる際、`map3()`、`map4()`といった具合に関数名の数値の部分を増やせば良い、という話ではない。2つ以上のデータを指定するには`pmap()`を利用することになる。ラムダ式では"..3"、"..4"と添字を増やしていくことで多数の引数の値を受け取ることができる。
```{r}
x <- list(a = list(1:3),
b = list(4:6),
c = list(7:9))
# .lで指定した要素は、順次.fの引数に渡される
pmap(.l = x,
.f = sum)
x %>% pmap(
~ ..1 + ..2 - ..3
)
```
`pmap()`に与えるデータの数よりも関数の引数が少ない場合はエラーになるが、引数*...*を含んでいる場合や*...*を引数にもった無名関数として定義することで処理を実行することができる。
```{r}
sum_xy <- function(a, b) {
a + b
}
```
```{r, eval = FALSE, echo = TRUE, purl = FALSE}
x %>% pmap(sum_xy)
# Error in .f(.l[[c(1L, 1L)]], .l[[c(2L, 1L)]], .l[[c(3L, 1L)]], ...) :
# unused argument (.l[[c(3, 1)]])
```
```{r}
# 引数に ... を含んだ関数では、
# .lで指定される余分な要素に対しても処理は実行される
sum_xy2 <- function(a, b, ...) {
a + b
}
# x[[1]]とx[[2]] の値が使われ、x[[3]]については利用されない
x %>%
pmap(sum_xy2)
```
```{r}
x %>%
pmap(function(...) sum_xy(..1, ..2))
x %>%
pmap(function(a, b, ...) a + b)
```
Rの関数は`...`引数をもつものが多いが、`...`引数を定義しない関数を実行する場合には、関数をラップし、`...`引数をもった自作の関数を定義すると良いだろう。
`map2()`、`pmap()`はいずれも返り値をベクトル化する`map2_int()`のような関数が用意されている。ここで指定可能なデータ型と関数名は、先に説明した`map_*()`と対応する。
<!--
#### walk
出力を隠すために
`walk()`
```{r}
x %>% map_lgl(~ grepl("[a-z]", .x)) %>%
keep(. == FALSE) %>%
map_int(nchar)
```
-->
## まとめ
- R言語徹底解説 関数型プログラミング云々の話はR言語徹底解説を参照するのが良い