はじめに
こんにちは、事業会社で働いているデータサイエンティストです。
最近は会社での業務と自分の研究で複雑な問題に直面して、ChatGPT先生に相談したら、purrr::reduceを使うといいですよと言われたので、詳細を説明したいと思います。
また、いきなり会社での業務と自分の研究に立ち入っても話が難しくなるだけなので、本記事では入門編として、問題を抽象化してわかりやすい事例で説明します。
本当に解決したかった問題は文字列処理の複雑な話になりますので、別の記事で解説します。
purrrパッケージの目的
purrrはtidyverseのパッケージ群の一つで、forループの代替として、複数の要素に対して処理を行う機能を持っています。
先に実例を挙げると、下のループは
> a <- c()
>
> for (i in 1:10){
a[i] <- i * 2
}
> a
[1] 2 4 6 8 10 12 14 16 18 20
purrrのmap系関数で代替できます。
> purrr::map_dbl(1:10, ~ .x * 2)
[1] 2 4 6 8 10 12 14 16 18 20
さて、早速タイトルの.xさんが登場しました。
では、詳細に入りましょう。
purrr::map関数群
まずR言語は足し算をこのように書けることに留意してください
> `+`(1, 2)
[1] 3
purrrのmap関数は、第一引数の要素を一個一個取り出して、第二引数の関数に代入します。
その時に、第二引数の関数の前に、回帰モデルの式のように"~"を入れれば、第二引数の関数の中で.xで第一引数の要素が入るところを指定できます。
例えば:
> purrr::map(1:5, ~ `+`(1, .x))
[[1]]
[1] 2
[[2]]
[1] 3
[[3]]
[1] 4
[[4]]
[1] 5
[[5]]
[1] 6
で、1、2、3、4、5にそれぞれ1を足しました。
次に、文字列で.xの位置を変える効果を確認しましょう:
> purrr::map(c("aさん", "bさん", "cさん"), ~ stringr::str_c(.x, "こんにちは"))
[[1]]
[1] "aさんこんにちは"
[[2]]
[1] "bさんこんにちは"
[[3]]
[1] "cさんこんにちは"
> purrr::map(c("aさん", "bさん", "cさん"), ~ stringr::str_c("こんにちは", .x))
[[1]]
[1] "こんにちはaさん"
[[2]]
[1] "こんにちはbさん"
[[3]]
[1] "こんにちはcさん"
一番目のコードは、.xを前の方に置くので、aさんbさんcさんがこんにちはより先に現れます。
二番目のコードは、.xを後の方に置くので、aさんbさんcさんがこんにちはより後に現れます。
出力の型の制御
これは少しおまけなんですが、ソフトウェア開発者にとって大変ありがたいことに、purrr::mapの関数群は出力の型を制御できます。
上で見たpurrr::map関数は何も指定していないので、リストに結果のデータが格納されます。
詳細はpurrrのサイトで確認してほしいですが
purrr::map_XXXでXXX型で出力するように指定できます。
まずはinteger型の場合:
> purrr::map_int(1:5, ~ `+`(1, .x))
[1] 2 3 4 5 6
character型の場合:
> purrr::map_chr(c("aさん", "bさん", "cさん"), ~ stringr::str_c("こんにちは", .x))
[1] "こんにちはaさん" "こんにちはbさん" "こんにちはcさん"
ここで、問題のある出力を受け取ったらエラーを吐いてくれます:
> purrr::map_int(c("aさん", "bさん", "cさん"), ~ stringr::str_c("こんにちは", .x))
Error in `purrr::map_int()`:
ℹ In index: 1.
Caused by error:
! Can't coerce from a string to an integer vector.
Run `rlang::last_trace()` to see where the error occurred.
"こんにちはaさん"は整数に変換できないからです。
ちなみに「ℹ In index: 1.」のところで最初に問題が起きた要素のインデックスを教えてくれるので、デバッグしやすいですね。
purrr:reduce関数
purrr::reduceとは、同じ関数にある変数を繰り返し代入する処理をやってくれる関数です。
具体的には:
> 1 |> sin() |> sin() |> sin() |> sin() |> sin() |> sin() |> sin() |> sin() |> sin() |> sin()
[1] 0.4629579
ここでは1をsin関数に代入して、その結果をさらにsin関数に代入して、その結果をさらにsin関数に代入して、その結果をさらにsin関数に代入して... で合計10回sin関数に代入しました。
この処理をpurrr::reduceでやると:
> purrr::reduce(1:10, ~ sin(.x), .init = 1)
[1] 0.4629579
で書けて、まだ詳細の説明に入っていないですが、簡潔でわかりやすいと視覚的にわかるはずです。
ちなみに結果は一致しています:
> long_sin <- 1 |> sin() |> sin() |> sin() |> sin() |> sin() |> sin() |> sin() |> sin() |> sin() |> sin()
> reduce_sin <- purrr::reduce(1:10, ~ sin(.x), .init = 1)
> identical(long_sin, reduce_sin)
[1] TRUE
動作確認用の関数
さて、purrr::reduceにおける.xと.yに入る前に、まずは便利な関数を定義します。
個人的にはpurrr::reduceの使用の理解に苦労していましたので、動作確認用の関数を作りました:
look_plus <- function(x, y){
stringr::str_c(x, "に", y, "を足しています") |>
print()
return(x + y)
}
これで裏で何がどう動いているのかを確認できます:
> look_plus(1,2)
[1] "1に2を足しています"
[1] 3
purrr::reduceの.xと.y
言葉で言うと、
.xは前の関数の出力を表す
.yは新しく追加される変数を表す
図で表してみるとこうなります:
「今回の処理」にとっての.xと.yを表しています。
と言われても多分わからない人が多いと思いますので、動作確認用関数の出番ですね:
> purrr::reduce(1:10, ~ look_plus(.x, .y), .init = 10)
[1] "10に1を足しています"
[1] "11に2を足しています"
[1] "13に3を足しています"
[1] "16に4を足しています"
[1] "20に5を足しています"
[1] "25に6を足しています"
[1] "31に7を足しています"
[1] "38に8を足しています"
[1] "46に9を足しています"
[1] "55に10を足しています"
[1] 65
ここでは、.init引数は最初の入力なので、「[1] "10に1を足しています"」の.xの部分に入り、10の数字が入ります。
10はまず1を足され、11となって出力されます。.xは前の関数の出力を表すと説明したので、二行目の文字列の.xに該当するところに11が代入され、1:10の二番目の要素の2が.yに代入されます。よって「[1] "11に2を足しています"」になります。
このように、最終的に10に1から10までの整数を足し、65が出力されます。
purrr::mapとpurrr::reduceの組み合わせ
最後に、mapとreduceを組み合わせる方法を紹介します。
意図としては、このように:
> purrr::reduce(1:10, ~ look_plus(.x, .y), .init = 2)
[1] "2に1を足しています"
[1] "3に2を足しています"
[1] "5に3を足しています"
[1] "8に4を足しています"
[1] "12に5を足しています"
[1] "17に6を足しています"
[1] "23に7を足しています"
[1] "30に8を足しています"
[1] "38に9を足しています"
[1] "47に10を足しています"
[1] 57
> purrr::reduce(1:10, ~ look_plus(.x, .y), .init = 4)
[1] "4に1を足しています"
[1] "5に2を足しています"
[1] "7に3を足しています"
[1] "10に4を足しています"
[1] "14に5を足しています"
[1] "19に6を足しています"
[1] "25に7を足しています"
[1] "32に8を足しています"
[1] "40に9を足しています"
[1] "49に10を足しています"
[1] 59
> purrr::reduce(1:10, ~ look_plus(.x, .y), .init = 6)
[1] "6に1を足しています"
[1] "7に2を足しています"
[1] "9に3を足しています"
[1] "12に4を足しています"
[1] "16に5を足しています"
[1] "21に6を足しています"
[1] "27に7を足しています"
[1] "34に8を足しています"
[1] "42に9を足しています"
[1] "51に10を足しています"
[1] 61
のfor i in c(2,4,6)で対処できそうな処理をpurrr::mapに任せるだけです。
ここでは特に.xと~はそれぞれ2回現れますが、mapの.xと~なのか、それともreduceの.xと~なのかをしっかり確認すれば難しくないはずです。
> purrr::map(c(2,4,6,8,10), ~ purrr::reduce(1:10, ~ look_plus(.x, .y), .init = .x))
[1] "2に1を足しています"
[1] "3に2を足しています"
[1] "5に3を足しています"
[1] "8に4を足しています"
[1] "12に5を足しています"
[1] "17に6を足しています"
[1] "23に7を足しています"
[1] "30に8を足しています"
[1] "38に9を足しています"
[1] "47に10を足しています"
[1] "4に1を足しています"
[1] "5に2を足しています"
[1] "7に3を足しています"
[1] "10に4を足しています"
[1] "14に5を足しています"
[1] "19に6を足しています"
[1] "25に7を足しています"
[1] "32に8を足しています"
[1] "40に9を足しています"
[1] "49に10を足しています"
[1] "6に1を足しています"
[1] "7に2を足しています"
[1] "9に3を足しています"
[1] "12に4を足しています"
[1] "16に5を足しています"
[1] "21に6を足しています"
[1] "27に7を足しています"
[1] "34に8を足しています"
[1] "42に9を足しています"
[1] "51に10を足しています"
[1] "8に1を足しています"
[1] "9に2を足しています"
[1] "11に3を足しています"
[1] "14に4を足しています"
[1] "18に5を足しています"
[1] "23に6を足しています"
[1] "29に7を足しています"
[1] "36に8を足しています"
[1] "44に9を足しています"
[1] "53に10を足しています"
[1] "10に1を足しています"
[1] "11に2を足しています"
[1] "13に3を足しています"
[1] "16に4を足しています"
[1] "20に5を足しています"
[1] "25に6を足しています"
[1] "31に7を足しています"
[1] "38に8を足しています"
[1] "46に9を足しています"
[1] "55に10を足しています"
[[1]]
[1] 57
[[2]]
[1] 59
[[3]]
[1] 61
[[4]]
[1] 63
[[5]]
[1] 65
まとめ
このように、purrrのmapとreduceを使えば、複雑なforループを解消し、やりたい処理を簡潔に記述できます。
この記事で紹介した技術を複雑な文字列処理に応用する記事も書きますのでお楽しみに!