抽出による文字列分離(3)_書式をそろえる.R
要約
・str_match_all()では、ベクタとマトリックスが混在するリストを出力
・文字列の桁揃えはstr_pad()
・各文字列操作はまとめて関数として定義
・map()でリスト内の各要素に適用
前回記事
まずはFIをグループ化抽出(str_match_all())
というわけで、まずはグループ化しながらFIを抽出してみる。
> df$FI %>% head(2) %>%
+ str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?")
[[1]]
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] "G06Q20/06" "G06Q" "20" "06" NA NA
[[2]]
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] "E04F13/10@A" "E04F" "13" "10" NA "@A"
[2,] "E04F13/08,102@A" "E04F" "13" "08" ",102" "@A"
[3,] "E04F15/04@E" "E04F" "15" "04" NA "@E"
今回は含まれたり含まれなかったりする展開記号や分冊識別記号を入れてみたのだけれど、グループ化した順番に左から現れて、対応する記号がない場合にはNAが格納されるようだ。NAのところが左詰めになったりすると後々面倒かもなあとは思っていたので、ちょっと安心した。
さて、桁を揃えたいのは3列目のメイングループと、4列目のサブグループになる。考え方は色々だけれども、今回はメイングループは4桁、サブグループは5桁にしてみる。
また、5列目の展開記号や6列目の分冊識別記号については、存在しない場合でも文字列として全体の文字数が一致するように、NAを空白で置き換えるなどしてみたい。イメージとしては下のようになる。
"E04F13/10@A" >>> "E04F001310 A"
"E04F13/08,102@A" >>> "E04F001308 102 A"
各文字列操作
まず、桁を揃えるにはstrngr::str_pad()を使う。引数と出力は下の通り。
"E04F13/10@A"を例にとると、メイングループ"13"は4桁にして左側を0で埋めている。widthが桁数、padが埋める文字、sideが埋める側だ。
同様にサブグループ"10"を見ると、5桁で右側を空白で埋めるようにしている。
> str_pad("13", width = 4, pad = "0", side = "left")
[1] "0013"
> str_pad("10", width = 5, pad = " ", side = "right")
[1] "10 "
次に、展開記号だ。値があるならそのまま返しても良いのだけど、何しろコンマが邪魔なので(笑)、コンマは空白に置換しておきたい。すると、左側に空白1つが入った3桁数字がでてくるようになる。合計4桁分なので、NAのときには空白4文字が入るようにすればよい。
従って、下のようになる。
> df$FI %>% head(2) %>%
+ str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>% .[[2]] %>% .[1, 5] %>%
+ if_else(is.na(.), " ", .) %>% str_replace(",", " ")
[1] " "
> df$FI %>% head(2) %>%
+ str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+ .[[2]] %>% .[2, 5] %>%
+ if_else(is.na(.), " ", .) %>% str_replace(",", " ")
[1] " 102"
if_else()を使って条件分岐させている。is.na(.)がFALSE、すなわち値が入っているときにそのままstr_replace()をさせてみようと中に入れたのだが、どうにもそれではうまくいかないようだ(下記)。
そのため、TRUEでは空白4文字を、FALSEでは値をそのまま一旦返すようにし、その上で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])?") %>%
+ .[[2]] %>% .[1, 5] %>% if_else(is.na(.), " ", str_replace(., ",", " "))
エラー: `condition` must be a logical vector, not a character vector.
Run `rlang::last_error()` to see where the error occurred.
リストへ関数を適用して処理
リストへ適用するにはlapply()がある。また、tidyverseパッケージに入っているpurrr::map()もある。これらは2つともリストを返してくれるもので、下ではmap()を使っている。
map()はリストの各要素に対して関数を適用してくれる。関数はmap()の外で定義してもよいし、簡単なものなら中で定義してしまってもいい。
上記の文字列操作は各行のそれぞれの列の値に対して処理を行うものなので、文字列操作をまとめてひとつのmap()用の関数として定義してしまうことにする。
> subFI0_map <- function(x){
+ #サブクラスまでのIPC記号4桁
+ IPC4 <- x[,2]
+ #メイングループ_IPC記号; 空白で4桁に揃える
+ MGrp <- x[,3] %>% str_pad(width = 4, side = c("left"), pad = "0")
+ #サブグループ_IPC記号; 空白で5桁に揃える
+ SGrp <- x[,4] %>% str_pad(width = 5, side = c("right"), pad = " ")
+ #展開記号;コンマは空白で置き換えて、NAの場合は空白×4を返す
+ TEN <- x[,5] %>% if_else(is.na(.), " ", .) %>% str_replace(",", " ")
+ #分冊識別記号;アルファベットだけにして、NAの場合は空白×1を返す
+ BUN <- x[,6] %>% if_else(is.na(.), " ", .) %>% str_replace("@", " ")
+ #RETURN
+ #全て再結合
+ #展開記号と分冊識別記号はそれぞれ空白を入れておく
+ str_c(IPC4, MGrp, "/", SGrp, " ", TEN, " ", BUN)
+ }
これを使って実際にmap()で処理をするとこうなる。出力結果のリストには、各要素に先ほどの関数を適用した結果であるベクタが格納されている。与えたリストの[[2]]には元々FIが3つ入っているので、出力結果は変換後のFIが3つ格納された長さ3のベクタになっている。
ちなみに、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(subFI0_map)
[[1]]
[1] "G06Q0020/06 "
[[2]]
[1] "E04F0013/10 A" "E04F0013/08 102 A" "E04F0015/04 E"
次は、出力リストの中のベクタに対して、各FIを再び連結してマルチアンサーに戻してみる。セパレータをコンマに戻すのもしゃくなので、ここでは"|"を使ってみる。str_c()を使ったがstr_flatten()でもpaste0()でも同じだ。
> df$FI %>% head(2) %>%
+ str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+ map(subFI0_map) %>%
+ map(function(x){str_c(x, collapse = "|")})
[[1]]
[1] "G06Q0020/06 "
[[2]]
[1] "E04F0013/10 A|E04F0013/08 102 A|E04F0015/04 E"
同様に出力結果はリストなのだけれど、各要素に含まれるのがマルチアンサー化されたFIという単一の文字列になっている。
このままではちょっと使いづらいし、そもそもdf$FIとして最初に与えたのはベクタであって、そこへ戻してあげることも考えると、最終的にはベクタになっていた方がいいだろう。というか、そうでないと元のデータフレームと結合できない。
この場合は、flatten_chr()を使ってリストの階層を1段階落としてやれば、各要素内の文字列がそのまま文字列型のベクタになってくれる。あとはこれを、元のデータフレームに結合してやればよい。
flatten_*()は階層0になったリストを指定した型のベクタにしてくれる。従って、型によって関数が異なるので注意が必要で、この場合は文字列型なのでflatten_chr()になる。
また、リストの中に長さが2以上のベクタが入っていてもそのままつなげてしまうので、最終的なベクタの長さが一番最初に入力したベクタの長さと異なってしまい、元のデータフレームに結合できなくなる可能性がある。(もちろん今回のはそうならないのだけど。)
> df$FI %>% head(6) %>%
+ str_match_all("([A-H]\\d{2}[A-Z])(\\d{2,4})/(\\d{2,5})(,\\d{3})?(@[A-Z])?") %>%
+ map(subFI0_map) %>%
+ map(function(x){str_c(x, collapse = "|")}) %>%
+ flatten_chr()
[1] "G06Q0020/06 "
[2] "E04F0013/10 A|E04F0013/08 102 A|E04F0015/04 E"
[3] "A41D0013/00 "
[4] "G06Q0050/06 |G06Q0030/02 322 |G06Q0020/38 310 "
[5] "G06Q0050/00 "
[6] "B60L0053/80 |B62D0063/02 |B60L0050/60 "
今回は、スラッシュではなくコロンが使われていた古い書式については無視してやっている。実際に使うときにはそこは考慮してあげる必要があるだろう(参考下記記事)。
以上。