抽出による文字列分離(3.5)_map補遺.R

要約

・map()はあくまでもリストまたはベクタの各要素ごとに適用
・map()内のベクタまたはマトリクスの各行に適用するには一工夫要る
・原理の理解って大事よね

map()の挙動って本当にこれで良かったっけ?

 そういえば、map()ではリスト内の各要素に関数を適用できるのだけど、さらに各要素内のマトリクスの各行に対して適用するようなものだったんだっけ?
 前回のは各行ごとに処理できているように見えるけど、よくよく考えてみれば、applyを勝手に入れ子にしてくれているような気がする。
 そういうわけで、ちょっと試してみた。str_flatten()は文字列ベクタを与えるとベクタに含まれる文字列すべてを結合してくれる。

> df$FI %>% head(2) %>%
+     str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+     map(function(x){str_flatten(x, collapse = "|")})
[[1]]
[1] NA

[[2]]
[1] NA

 ……あれ? さすがにNAが返ってくるとは思っていなかった。でもまあ、str_flatten()が要素に対して適用されていそうだということは分かる。
 もう少し明示的に入れ子構造で与えてみる。つまり、リスト内の各要素にlapply()で適用する関数にapplyを入れてみるということだ。

> df$FI %>% head(2) %>%
+     str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+     lapply(function(x){apply(x, MARGIN = 1, function(y){str_flatten(y, collapse = "|")})})
[[1]]
[1] NA

[[2]]
[1] NA                                   "E04F13/08,102@A|E04F|13|08|,102|@A"
[3] NA                                  

 あー、なんとなく理解した。NAが入ってるからだ。とりあえずreplace_na()をlapply()でリストに対して適用して、NAを空白で置換してみる。

> df$FI %>% head(2) %>%
+    str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+     lapply(function(x){replace_na(x, " ")})
[[1]]
    [,1]        [,2]   [,3] [,4] [,5] [,6]
[1,] "G06Q20/06" "G06Q" "20" "06" " "  " " 

[[2]]
    [,1]              [,2]   [,3] [,4] [,5]   [,6]
[1,] "E04F13/10@A"     "E04F" "13" "10" " "    "@A"
[2,] "E04F13/08,102@A" "E04F" "13" "08" ",102" "@A"
[3,] "E04F15/04@E"     "E04F" "15" "04" " "    "@E"

 NAがなくなったので再チャレンジ。lapply()で要素内の行列にapplyで関数を適用するように入れ子で与えてやるとこうなる。

> df$FI %>% head(2) %>%
+     str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+     lapply(function(x){replace_na(x, " ")}) %>%
+     lapply(function(x){apply(x, MARGIN = 1, function(y){str_flatten(y, collapse = "|")})})
[[1]]
[1] "G06Q20/06|G06Q|20|06| | "

[[2]]
[1] "E04F13/10@A|E04F|13|10| |@A"        "E04F13/08,102@A|E04F|13|08|,102|@A"
[3] "E04F15/04@E|E04F|15|04| |@E"       

 とりあえず、applyを入れ子にしておけば、一番内側の関数(function(y))をリストの要素の行列の各行に対して適用できるということは分かった。

改めて、自動入れ子になるのかどうか

 じゃあ、翻ってmap()を使えば自動で入れ子にしてくれるのかどうか、もう一度試してみる。

> df$FI %>% head(2) %>%
+     str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+     lapply(function(x){replace_na(x, " ")}) %>%
+     map(function(y){str_flatten(y, collapse = "|")})
[[1]]
[1] "G06Q20/06|G06Q|20|06| | "

[[2]]
[1] "E04F13/10@A|E04F13/08,102@A|E04F15/04@E|E04F|E04F|E04F|13|13|15|10|08|04| |,102| |@A|@A|@E"

 なりませーん。やっぱりなりません。行列内のすべての要素を全部つなぎやがった。まあ、ある意味でstr_flatten()の挙動確認にはなった。

 さて、色々試してみると、どうやら関数を記述するときに列を指定してやると、自動入れ子のような挙動になるということが分かった。この場合のmap()はlapply()でも良い。

> df$FI %>% head(2) %>%
+     str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+     lapply(function(x){replace_na(x, " ")}) %>%
+     map(function(y){str_c(y[,2], y[,3], y[,4], y[,5], y[,6], collapse = "|")})
[[1]]
[1] "G06Q2006  "

[[2]]
[1] "E04F1310 @A|E04F1308,102@A|E04F1504 @E"

 もしかするとstr_c()にベクタを与えた時の挙動を使って行列を行ごとに処理しているだけで、map()そのものはやはりリスト内の最初の階層の各要素に対して処理を行っているのかもしれない。何しろstr_c()にベクタを与えた場合の挙動は下のようになるから。

> str_c(c(1:3), c(4:6))
[1] "14" "25" "36"

 ……でも待てよ?パイプ処理してるということは、普通にやるとそうはならないよね?

> df$FI %>% head(2) %>%
+     str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+     lapply(function(x){replace_na(x, " ")}) %>%
+     .[[2]]
    [,1]              [,2]   [,3] [,4] [,5]   [,6]
[1,] "E04F13/10@A"     "E04F" "13" "10" " "    "@A"
[2,] "E04F13/08,102@A" "E04F" "13" "08" ",102" "@A"
[3,] "E04F15/04@E"     "E04F" "15" "04" " "    "@E"

#上記のマトリクスをstr_c()に%>%で渡す
> df$FI %>% head(2) %>%
+     str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+     lapply(function(x){replace_na(x, " ")}) %>%
+     .[[2]]
+     %>% str_c(.[,2], .[,3], .[,4], .[,5], .[,6], collapse = "|")
[1] "E04F13/10@AE04F1310 @A|E04F13/08,102@AE04F1308,102@A|E04F15/04@EE04F1504 @E|E04FE04F1310 @A|E04FE04F1308,102@A|E04FE04F1504 @E|13E04F1310 @A|13E04F1308,102@A|15E04F1504 @E|10E04F1310 @A|08E04F1308,102@A|04E04F1504 @E| E04F1310 @A|,102E04F1308,102@A| E04F1504 @E|@AE04F1310 @A|@AE04F1308,102@A|@EE04F1504 @E"

 ほらね。str_c()には行列そのものを渡しているので、渡した行列と、さらに与えた各列ベクタを結合している感じだ。

 なので、確かにmap()のおかげではあるものの、map内関数の引数xに行列が渡されているために、行列そのものは使わずに各列ベクタは使うことができているということのようだ。
 やはりmap()そのものはリスト内の最初の階層の各要素に対して処理を行っているのだと思われるし、前回はたまたま狙った挙動になっただけということである。
 まあ、原理が分かって何よりだ。

理解したところで再チャレンジ

 そういうわけで上記のことを意識しながら前回のコードをmap()の内部関数だけで定義して適用するとこうなる。

> df$FI %>% head(2) %>%
+  str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+  map(function(x){str_c(x[,2],
+  str_pad(x[,3], width = 4, side = "left", pad = "0"),
+  str_pad(x[,4], width = 5, side = c("right"), pad = " "),
+  str_replace(if_else(is.na(x[,5]), "    ", x[,5]), ",", " "),
+  str_replace(if_else(is.na(x[,6]), "  ", x[,6]), "@", " "))})
[[1]]
[1] "G06Q002006         "

[[2]]
[1] "E04F001310        A" "E04F001308    102 A" "E04F001504        E"

#str_replace()を省くために正規表現をいじった
> df$FI %>% head(2) %>%
+  str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,)?(\\d{3})?(@)?([A-Z])?") %>%
+  map(function(x){str_c(x[,2],
+  str_pad(x[,3], width = 4, side = "left", pad = "0"),
+  str_pad(x[,4], width = 5, side = c("right"), pad = " "),
+  " ", 
+  if_else(is.na(x[,6]), "   ", x[,6]),
+  " ",
+  if_else(is.na(x[,8]), " ", x[,8]))})
[[1]]
[1] "G06Q002006         "

[[2]]
[1] "E04F001310        A" "E04F001308    102 A" "E04F001504        E"

 やってみたらstr_replace()周りの入れ子が見にくかったので、正規表現を少し弄ってstr_replace()を省略した(下側)。","や"@"を明示的に空白で置換して、さらにNAを置換する空白の数も単純に本来の桁数と同じにしている。こっちの方が、狙った処理がより分かりやすいかもしれない。
 ここまでくれば、リスト内は単純な文字列ベクタだけになっている。あとは前回やったように、map()でstr_flatten()を適用して、さらにflatten_chr()とすれば、処理済FIがマルチアンサーになったものが最初に与えたdf$FIと同じ長さのベクタで返ってきて、元のデータフレームに再結合できるようになる。

 あーつかれた。

以上。

この記事が気に入ったらサポートをしてみませんか?