18  文字列の処理

この章で使うパッケージを読み込む。

pacman::p_load(tidyverse)

18.1 Rにおける文字列

 Rは数値だけでなく文字列 (string) も扱うことができる。近年、文字や文書をデータとして扱う機会が増えており、文字列を効率的に処理する方法を身につけることで、データとして扱う対象を拡大することができる。

 まず、最も基本的な文字列の扱い方を確認(復習)しよう(第9章も参照されたい)。Rで文字列を使う際は、文字列を引用符 (quotation marks) で囲む(囲むというのは、文字列が始まる前とそれが終わった後にそれぞれ引用符をつけるという意味である)。例えば、「SONG Jaehyun」という文字列をauthor1というオブジェクトに格納するには、次のようにする。

author1 <- "SONG Jaehyun"
author1
[1] "SONG Jaehyun"

文字列の中にスペースが含まれる場合、そのスペースもそのまま保存される。この例では、半角スペースを1つ含む文字列が保存される。

 Rにおける文字列は、character クラス・型として扱われる。

class(author1)
[1] "character"
is.character(author1)
[1] TRUE
typeof(author1)
[1] "character"
mode(author1)
[1] "character"

 引用符は、二重引用符"" でも 単一引用符'' でも良い。ただし、同じ記号をペアで使う必要がある。

author2 <- '矢内 勇生'
print(author2)
[1] "矢内 勇生"

この例では、全角スペースを1つ含む文字列が保存される。また、''を使って文字列を作ったのに、表示結果で文字列を囲む引用符は""に変わっていることがわかる。これは、文字列を作る際には二重引用符を使うことが想定されているためである。よって、特にこだわりがなければ、二重引用符""を使ったほうがよいだろう。

 引用符自体を文字列の一部として保存したいときは、次のようにする。

quote1 <- "Mura'Kamisama'"
quote2 <- 'We we"R"e born to use "R."'
quote1
[1] "Mura'Kamisama'"
quote2
[1] "We we\"R\"e born to use \"R.\""

quote2 の表示結果を見ると、文字列に含まれる二重引用符の前にバックスラッシュ \ が付いている。これは、文字列のエスケープと呼ばれる方法である。これについては後で詳しく説明する。この例においては、二重引用符を文字列を作るための命令として使う代わりに、単なる1つの文字として扱うために必要な処理である。cat()を使うと、保存した内容をそのまま表示することができる。

cat(quote2)
We we"R"e born to use "R."

 上の例では文字列内で二重引用符を使うので、文字列の外側ではは単一引用符を使った。 二重引用符のみで上と同じ文字列を作りたい場合は、エスケープ処理を利用すればよい。

quote3 <- "We we\"R\"e born to use \"R.\""
quote3
[1] "We we\"R\"e born to use \"R.\""
cat(quote3)
We we"R"e born to use "R."

エスケープ処理をしないとエラーになる。

quote3 <- "We we"R"e born to use "R.""
Error: malformed raw string literal (<text>:1:20)

これは、「“We we”」で二重引用符のペアが完成し、そのすぐ後の「R」が文字列として認識されていないために起こるエラーである。

 文字列を要素とするベクトルやリストなども、数値を要素とする場合と同じように作ることができる。

authors <- c(author1, author2, "Super Cat")
authors
[1] "SONG Jaehyun" "矢内 勇生"   "Super Cat"   

 数値と文字列の両者を要素としてもつベクトルを作ると、すべての要素が文字として保存される。

numchar <- c(1, 2, "three", 4, "five")
numchar
[1] "1"     "2"     "three" "4"     "five" 
class(numchar)
[1] "character"
sapply(numchar, class)
          1           2       three           4        five 
"character" "character" "character" "character" "character" 

Rでは、ベクトルのすべての要素は同じクラスになる。数値は文字として扱える一方で、一般的に文字を数値として扱うことはできないので、型が文字 (character) に統一される。

 リストは異なる型を保存することできる。

numchar_list <- list(1, 2, "three", 4, "five")
numchar_list
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] "three"

[[4]]
[1] 4

[[5]]
[1] "five"
class(numchar_list)
[1] "list"
lapply(numchar_list, class)
[[1]]
[1] "numeric"

[[2]]
[1] "numeric"

[[3]]
[1] "character"

[[4]]
[1] "numeric"

[[5]]
[1] "character"

数値を文字として扱いたいときは、as.character() を使う。

one <- as.character(1)
one
[1] "1"
one2ten <- 1:10 |> 
  as.character()
one2ten
 [1] "1"  "2"  "3"  "4"  "5"  "6"  "7"  "8"  "9"  "10"
class(one2ten)
[1] "character"

保存されているのが数字であっても、それが文字列として保存されているなら、そのまま計算に利用することはできない。

sum(one2ten)
Error in sum(one2ten): invalid 'type' (character) of argument

しかし,as.numeric()as.integer() を使って数値に変換すれば、計算に利用することができる。

as.numeric(one2ten)
 [1]  1  2  3  4  5  6  7  8  9 10
one2ten |> 
  as.integer() |> 
  sum()
[1] 55
as.numeric("3.35") + as.numeric("4.24") * 2
[1] 11.83

数字以外の文字を数値に変換することはできない(NAになる)。

c("1", "2", "three", "4") |> 
  as.integer()
Warning: NAs introduced by coercion
[1]  1  2 NA  4

 Rの文字列に対して、そのままスライシング操作を行うことはできない。たとえば、author1の1文字目を取り出したいときに、次のようにしても1文字目の”S”は抽出できない。

author1[1]
[1] "SONG Jaehyun"

このように、文字列全体が取り出されてしまう。これは、この文字列が長さ1のベクトル(つまり、冗長に書けばc("SONG Jaehyun") である)なので、スライス操作で1番目の要素を取り出すと、ベクトルの第1要素が取り出されるためである。

したがって、文字列自体の長さをlength()で測ることはできない。

length(author1)
[1] 1
length(author2)
[1] 1

いずれの結果も1になるのは、どちらも長さが1のベクトルだからである。

 文字列に対して様々な処理を施す方法については、次節で説明する。

18.2 文字列の操作

Rでは、文字列に対して様々な処理を行うこができる。 文字列操作をする際に欠かせないのが、stringrパッケージである。 このパッケージは私たちが住むtidyverseに含まれているので、あらためて読み込む必要はない。

stringrパッケージで文字列操作を行う関数は、str_xxx()のような形式の関数である(xxxの部分を、操作に応じて変える)。まず、文字列の長さ(つまり、文字数)を数えるために、str_length() を使う。

str_length(authors)
[1] 12  5  9

スペースを含む文字数が返される。

 上で試みたように文字列の一部 (substring) を取り出すときは、str_sub()を使う。

str_sub(author1, start = 1, end = 1)
[1] "S"

このように始点 (start) と終点 (end) を指定することで、文字列の一部を取り出すことができる。 5文字目から8文字目までを取り出してみよう。

str_sub(author1, start = 6, end = 8)
[1] "Jae"

特定の位置より後をすべて取り出したいときは、startのみを指定する。

str_sub(author1, start = 6)
[1] "Jaehyun"

同様に、特定の位置までを取り出したいときは、endのみを指定する。

str_sub(author1, end = 4)
[1] "SONG"

end に指定した位置も含めて抽出されることに注意されたい。

文字列を分割 (split) したいときは、str_split()を使う。 patternで指定した文字の前後で文字列を分割する。 たとえば、次のようにする。

author1_s <- str_split(author1, pattern = " ") |> 
  print()
[[1]]
[1] "SONG"    "Jaehyun"

結果として返されるのは、リストである。 2つ以上の文字列に対して同様の操作を行うと、リストで返される理由がわかるだろう。

authors_s1 <- str_split(authors, pattern = " ") |> 
  print()
[[1]]
[1] "SONG"    "Jaehyun"

[[2]]
[1] "矢内 勇生"

[[3]]
[1] "Super" "Cat"  

authors は3つの文字列を要素としてもつベクトルである。 このベクトルに対して文字列分割の操作を行うと、それぞれの文字列の分割結果を要素とするリストが返される。そしてリストの各要素の中に、分割したそれぞれの文字列を要素をしてもつベクトルが保存される。

 ここで、リストの2つ目の要素が分割されていないことに注意してほしい。これは、この文字列には半角スペースがないからだ。代わりに全角スペースがあるので、これを分割したいならpatternに全角スペースを指定する必要がある。

authors_s2 <- str_split(authors, pattern = " ") |> 
  print()
[[1]]
[1] "SONG Jaehyun"

[[2]]
[1] "矢内" "勇生"

[[3]]
[1] "Super Cat"

今度は、2番目の文字列のみ分割された。すべて分割するには、| (or) を使って次のようにする。

authors_s3 <- str_split(authors, pattern = " | ") |> 
  print()
[[1]]
[1] "SONG"    "Jaehyun"

[[2]]
[1] "矢内" "勇生"

[[3]]
[1] "Super" "Cat"  

上のコードでは、| の左側に半角スペース、右側に全角スペースが指定されている。

 あるいは、次のように「空白文字」を\\sで指定すれば、半角・全角をまとめて扱える(これについては後で正規表現を説明する際にもう少し説明する)。

authors_s3 <- str_split(authors, pattern = "\\s") |> 
  print()
[[1]]
[1] "SONG"    "Jaehyun"

[[2]]
[1] "矢内" "勇生"

[[3]]
[1] "Super" "Cat"  

 文字列操作をもう少し練習するために、以下の例文を使おう。

eg_Acton <- "Power tends to corrupt. Absolute power corrupts absolutely."
eg_Wilde <- "A little sincerity is a dangerous thing, and a great deal of it is absolutely fatal."

 str_split()は、同じパタンが複数回出てくる場合には、すべてを分割してくれる。

str_split(eg_Acton, pattern = "\\s")
[[1]]
[1] "Power"       "tends"       "to"          "corrupt."    "Absolute"   
[6] "power"       "corrupts"    "absolutely."

この例からわかるとおり、このような操作は英文をはじめとする単語間にスペースがある言語データをに対し、文字列を単語に分割する際に有用である。

 特定の文字(単語)を抽出したいときは、str_extract() を使う。

str_extract(eg_Acton, pattern = "corrupt")
[1] "corrupt"

eg_Actonオブジェクトには指定した文字列である “corrupt” に一致する部分があるので、関数は指定した文字列そのものを返す。 このオブジェクトには “corrupt” という文字列が2回登場するが、この関数は最初の “corrupt” を発見した時点でそれを返し、2つ目の”corrupt” には到達しない。

 オブジェクトの中に指定した文字列が存在しない場合、NAが返される。

str_extract(eg_Acton, pattern = "cats")
[1] NA

 str_extract() は指定した文字列を1度しか返さないが、str_extract_all() は一致したものをすべてを返す。

str_extract_all(eg_Acton, pattern = "corrupt")
[[1]]
[1] "corrupt" "corrupt"

この例が示す通り、 str_extract_all() はリストを返す。 リストの長さは文字列が一致した回数ではなく、パタンを探す対象として渡したオブジェクトの数である。 上の例では、1つのオブジェクトで2回マッチしているので、返されたリストの長さは1であり、リストの第1要素の長さは2である。

list_corrupt <- str_extract_all(eg_Acton, pattern = "corrupt") 
length(list_corrupt)
[1] 1
length(list_corrupt[[1]])
[1] 2

 次の例では、返されるリストの長さが2になる。

list_absolute <- str_extract_all(c(eg_Acton, eg_Wilde), 
                                 pattern = "absolute") |> 
  print()
[[1]]
[1] "absolute"

[[2]]
[1] "absolute"

 これらの関数は、いずれも大文字と小文字を区別する (case-sensitive)。大文字、小文字の別を無視したいときはregex()を使ってignore_case = TRUE を指定する。

str_extract_all(c(eg_Acton, eg_Wilde),
                pattern = regex("absolute", ignore_case = TRUE))
[[1]]
[1] "Absolute" "absolute"

[[2]]
[1] "absolute"

 オブジェクトの「先頭で」特定の文字列に一致するものを見つけたいときは、^ (the caret symbol) を次のように使う。

str_extract(eg_Acton, pattern = "^corrupt")
[1] NA
str_extract(eg_Acton, pattern = "^Power")
[1] "Power"

同様に、文字列の「末尾で」一致を探すときは、$ を使う。

str_extract_all(c(eg_Acton, eg_Wilde), pattern = ".$")
[[1]]
[1] "."

[[2]]
[1] "."

18.3 正規表現

 ウェブ上に存在するデータのほとんどはテキスト(文字)である。また、その大部分はデータセットとして利用可能な状態に構造化されていない。構造化されていない文字の集合そのものをデータとして利用する場合もあるが、たいていの場合、特定のパタンに合致する文字情報だけを取り出して利用したい。 よって、文字列の中から特定のパタンに当てはまるものを見つけてそれを取り出す必要がある。そのために使われるのが、正規表現 (regular expressions) である。

 ここでは、基本的な正規表現を紹介する。

18.3.1 一般的なパタン一致

 ここまでは、特定の文字列に一致する文字列を見つけてきた。 しかし、正規表現を使うと、より一般的なパタンに一致する文字列を見つけることができる。 たとえば、. (dot) は正規表現ではあらゆる文字 (character) と一致する。

str_extract_all(eg_Wilde, pattern = "i.")
[[1]]
[1] "it" "in" "it" "is" "in" "it" "is"

ドット自体を取り出したいときは、バックスラッシュ と一緒にして \. とする。

str_extract_all(eg_Wilde, pattern = '\\.')
[[1]]
[1] "."

ここで、バックスラッシュ (\) はRで定義された文字列の意味から私たちを逃してくれるので、 エスケープ文字 (escape character) と呼ばれる。R(やその他の多くのプログラミング言語における正規表現)において . は「あらゆる文字」を意味するが、\. はその意味から私たちを解放し、ドットそのものとの一致を探してくれる。 ただし、バックスラッシュ自体が正規表現のバックスラッシュであることをRに伝えるために、バックスラッシュを二重にする必要がある。

 “it” または “in”を見つけるには次のようにする。

str_extract_all(eg_Wilde, pattern = "i[tn]")
[[1]]
[1] "it" "in" "it" "in" "it"

つまり、[] で囲まれ文字のうちいずれかが入るパタンで一致を探す。

パタンではなく、単語としての “it” と “in” のみを抜き出す(たとえば、littleに含まれる “it” は除く)には、次のようにする。

str_extract_all(eg_Wilde, pattern = "\\bi[tn]\\b")
[[1]]
[1] "it"

\bは、そこが単語の境界 (boundary) であることを示す。

ある文字以外を指定したいとき、たとえば、i から始まりt以外で終わる2文字の単語は、[^]を使って次のようにして取り出せる。

str_extract_all(eg_Wilde, pattern = "\\bi[^t]\\b")
[[1]]
[1] "is" "is"

 これらの例からわかるように、正規表現を使えば様々な条件にあった文字列を探し出すことができる。 表 18.1 に、よく使う正規表現のパタンを示す。

表 18.1: よく使う正規表現
正規表現 意味
[:digit:] 数字(0, 1, 2, ..., 9)
[:lower:] 小文字(a, b, c, ..., z)
[:upper:] 大文字(A, B, C, ..., Z)
[:alpha:] アルファベット (a, b, ..., z と A, B, ..., Z
[:alnum:] アルファベットと数字(a, ..., z, A, ..., Z, 0, ..., 9)
[:punct:] パンクチュエーション文字(! " # $ % & ’ ( ) * + , - . /)
[:blank:] 空白文字(スペースとタブ)
[:space:] スペース文字(スペース、タブ、改行など)
[:print:] 印刷可能な文字 ([:alnum:], [:punct:], [:space:])
表 18.2: よく使う正規表現

また、特別な意味をもつ記号を 表 18.3 に示す。 これらを使うときには \\w のようにバックスラッシュを1つ増やす必要がある。

表 18.3: 特別な意味をもつ記号
正規表現 意味
\w 文字
\W 文字以外
\s スペース文字
\S スペース文字以外
\d 数字
\D 数字以外
\b 単語の境界
\B 単語の境界以外
\< 単語の先頭
\> 単語の末尾
^ 文字列の先頭
$ 文字列の末尾

 これらの正規表現を組み合わせて使うことで、様々なパタンに一致する文字列を探したり、抜き出したり、入れ替えたりすることができる。さらに、それぞれの正規表現に一致する回数も指定することができる。たとえば、5文字の単語を探すには、次のようにする。

str_extract_all(eg_Wilde, pattern = "\\b[:alpha:]{5}\\b")
[[1]]
[1] "thing" "great" "fatal"

数を指定するときは、 表 18.4 に示す限量詞 (quantifier) を登場回数を指定したい表現の直後につける。

表 18.4: 正規表現と一緒に使う限量詞
限量詞 意味
? 直前の表現と0回以上一致(あってもなくてもよい)で、最大で1回一致
* 直前の表現と0回以上一致
+ 直前の表現と1回以上一致
{m} 直前の表現とちょうどm回一致
{m,} 直前の表現とm回以上一致
{m,n} 直前の表現とm回以上n回以下の一致

限量詞をつけない場合はちょうど1回一致である。

 これらの限量詞を使うと、指定した条件に一致する範囲において貪欲マッチング (greedy matching) が実行される。貪欲マッチングとは、パタンに一致するもののうち、できるだけ長いものとマッチしようとする性質である。たとえば、以下の例を見てみよう。

cats <- c("猫猫猫猫猫猫虎猫猫虎")
str_extract(cats, pattern = ".+虎")
[1] "猫猫猫猫猫猫虎猫猫虎"

限量詞+で「何らかの文字」が1回以上出現した後に「虎」が1回(限量詞がないので、ちょうど1回)出現するパタンを抜き出したところ、先頭の猫から途中の虎を1回含んで最後の猫までを貪欲に抜き出した。

 同様に、次の例を見てみよう。

str_extract(cats, pattern = "猫+\\w+")
[1] "猫猫猫猫猫猫虎猫猫虎"

この例では、限量詞+で「猫」が1回以上出現した後に「何らかの文字」が1回以上出現するパタンを抜き出したところ、先頭の「猫」から6個の猫が貪欲に抜き出された後に、1回以上の何らかの文字として「虎猫猫虎」が抜き出された。結果として、文字列全体が抽出された。

 では、「猫虎」という2文字だけを抜き出したいときはどうすればいいのだろうか。そのような処理には、怠惰マッチング (lazy matching) を使う。怠惰マッチングを実行する際には、限量詞の後に? を付ける。    たとえば、次のようにする。

str_extract(cats, pattern = ".+?虎")
[1] "猫猫猫猫猫猫虎"

限量詞+で「何らかの文字」が1回以上出現した後に「虎」が1回というパタンを指定しているが、前半の「1回以上」の部分を+?として怠惰マッチングにした効果として、(2回目ではなく)1回目の「虎」まででマッチングが終了している。ただし、「虎」の前についている「猫」の個数は1個ではないので注意が必要である。

 同様に、次の例を見てみよう。

str_extract(cats, pattern = "猫+?\\w+?")
[1] "猫猫"

ここでは、「猫」を1回以上怠惰にマッチした後に、何らかの文字を1回以上怠惰にマッチするという指示をしている。結果として、最初の2文字「猫猫」の時点で条件を満たし、マッチングが完了している。

 +? だけでなく、*???{m,n}? なども怠惰マッチングとして利用できる。

 例を用いて、正規表現の使い方をもう少し確認してみよう。練習のために、似たような文字列をいくつか含むベクトル y_names を用意する。

y_names <- c("Yada", "Yano", "Yamada", "Yamaji", "Yamamoto", 
             "Yamashita", "Yamai", "Yanai", "Yanagi", "Yoshida")

この中から、“Ya.a.+” というパタンに一致する文字列のみ抜き出そう(結果を見やすくするために、unlist()を使ってベクトルにする)。

y_names |> 
  str_extract_all(pattern = "Ya.a.+") |> 
  unlist()
[1] "Yamada"    "Yamaji"    "Yamamoto"  "Yamashita" "Yamai"     "Yanai"    
[7] "Yanagi"   

これらの文字列のみが抜き出された理由はわかるだろうか?たとえば、“Yada” が抜き出されなかったのはなぜだろうか?それは、パタンで指定した”Ya.a.+” の”.+“が、”Ya.a”の後に何らかの文字が1回以上出現することを要求しているからである。 したがって、その部分を”.?“に変えると、結果が変わる。

y_names |> 
  str_extract_all(pattern = "Ya.a.?") |> 
  unlist()
[1] "Yada"  "Yamad" "Yamaj" "Yamam" "Yamas" "Yamai" "Yanai" "Yanag"

 他のパタンもいくつか試してみよう。

y_names |> 
  str_extract_all(pattern = "Ya.a.?i") |> 
  unlist()
[1] "Yamaji" "Yamai"  "Yanai"  "Yanagi"
y_names |> 
  str_extract_all(pattern = "Y.+da") |> 
  unlist()
[1] "Yada"    "Yamada"  "Yoshida"
y_names |> 
  str_extract_all(pattern = "Yama.+t.?") |> 
  unlist()
[1] "Yamamoto"  "Yamashita"
y_names |> 
  str_extract_all(pattern = "Ya[:alpha:]{4}$") |> 
  unlist()
[1] "Yamada" "Yamaji" "Yanagi"
y_names |> 
  str_extract_all(pattern = "^\\w{6,}$") |> 
  unlist()
[1] "Yamada"    "Yamaji"    "Yamamoto"  "Yamashita" "Yanagi"    "Yoshida"  
y_names |> 
  str_extract_all(pattern = "^[:alpha:]{6,8}$") |> 
  unlist()
[1] "Yamada"   "Yamaji"   "Yamamoto" "Yanagi"   "Yoshida" 
y_names |> 
  str_extract_all(pattern = "\\w+m\\w+") |> 
  unlist()
[1] "Yamada"    "Yamaji"    "Yamamoto"  "Yamashita" "Yamai"    

18.3.2 文字列操作の例

例として、全国の市区役所と町村役場のデータ Offices.csvを読み込んで利用する。

Offices <- read_csv("Data/Offices.csv")
Rows: 1864 Columns: 5
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (5): Code, Name, Zip, Address, Tel

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(Offices, n = 3)
# A tibble: 3 × 5
  Code  Name   Zip      Address                        Tel         
  <chr> <chr>  <chr>    <chr>                          <chr>       
1 01100 札幌市 060-8611 北海道札幌市中央区北1条西2丁目 011-211-2111
2 01202 函館市 040-8666 北海道函館市東雲町4-13         0138-21-3111
3 01203 小樽市 047-8660 北海道小樽市花園2-12-1         0134-32-4111

このデータフレームには5つの列がある。このうち、Addressに注目しよう。この列には、役所・役場の住所が記録されている。

Offices$Address[1:5]
[1] "北海道札幌市中央区北1条西2丁目" "北海道函館市東雲町4-13"        
[3] "北海道小樽市花園2-12-1"         "北海道旭川市6条通9丁目"        
[5] "北海道室蘭市幸町1-2"           

練習として、ここから都道府県名のみを抜き出してみよう。 当然ながら、都道府県名の末尾は「都」「道」「府」「県」のいずれかなので、「何らかの文字列を1回以上 (\\w+)」含み、その後に「都または道または府まはた県を1回 ([都道府県])」というパタンを試してみよう(実は、これはうまくいかない)。 各都道府県を1回だけ抜き出すために、最後にunique()を使う。

try1 <- Offices |> 
  pull(Address) |> 
  str_extract(pattern = "\\w+[都道府県]") |> 
  unique()
length(try1)
[1] 111

得られた都道府県の数が111になっており、「失敗」したことがわかる。結果を一部見てみよう。

try1[1:10]
 [1] "北海道"                              
 [2] "北海道寿都郡寿都"                    
 [3] "北海道寿都"                          
 [4] "北海道虻田郡留寿都村字留寿都"        
 [5] "北海道中川郡音威子府村字音威子府"    
 [6] "北海道常呂郡訓子府"                  
 [7] "青森県"                              
 [8] "青森県下北郡大間町大字大間字奥戸下道"
 [9] "青森県三戸郡三戸町大字在府"          
[10] "青森県三戸郡階上町大字道"            

抽出したすべての文字列の末尾が「都道府県」で終わっている。 つまり、私たちのRが(いつも通り!)私たちの指示に従ってくれたという意味において、この結果は決して失敗ではない。しかし、都道府県名のみを抽出するという私たちの希望通は叶えられなかった。 パタンの指示がまずかったのだ。

 \\w+[都道府県]というパタン指示では、\\w+の部分が貪欲マッチングになっている。 そのため、[都道府県]という文字を最後に含む文字列のうち、最も長い文字列が抽出されたのだ。

 貪欲マッチングを怠惰マッチングに変えて、もう1度試してみよう。

try2 <- Offices |> 
  pull(Address) |> 
  str_extract(pattern = "\\w+?[都道府県]") |> 
  unique()
length(try2)
[1] 47

47の都道府県が抽出されたようだ。果たして、うまくいったのだろうか。 確認してみよう。

try2
 [1] "北海道"   "青森県"   "岩手県"   "宮城県"   "秋田県"   "山形県"  
 [7] "福島県"   "茨城県"   "栃木県"   "群馬県"   "埼玉県"   "千葉県"  
[13] "東京都"   "神奈川県" "新潟県"   "富山県"   "石川県"   "福井県"  
[19] "山梨県"   "長野県"   "岐阜県"   "静岡県"   "愛知県"   "三重県"  
[25] "滋賀県"   "京都"     "大阪府"   "兵庫県"   "奈良県"   "和歌山県"
[31] "鳥取県"   "島根県"   "岡山県"   "広島県"   "山口県"   "徳島県"  
[37] "香川県"   "愛媛県"   "高知県"   "福岡県"   "佐賀県"   "長崎県"  
[43] "熊本県"   "大分県"   "宮崎県"   "鹿児島県" "沖縄県"  

この結果には1つ問題がある。基本的には都道府県名が「都道府県」までを含めて抽出されているが、京都府のみ「京都」になってしまった。これは、京都の「都」が「都道府県」にマッチしたためである。今回のパタン指示の問題はどこにあったのだろうか。問題は、「都」の前の「京」のみで、怠惰マッチングである"\\w+?に合致してしまった点にある。

 こういうときには、自分がもっている知識が役に立つことがある。私たちは、日本の都道府県名(末尾の[都道府県]を除く)が、2文字または3文字であることを知っている。この知識を使えば、「京」という1文字だけが取り出されるパタンを除外することができる。2文字以上3文字以下で怠惰マッチングを実行するために{2,3}?という限量詞を使う。

try3 <- Offices |> 
  pull(Address) |> 
  str_extract(pattern = "\\w{2,3}?[都道府県]") |> 
  unique()
length(try3)
[1] 47
try3
 [1] "北海道"   "青森県"   "岩手県"   "宮城県"   "秋田県"   "山形県"  
 [7] "福島県"   "茨城県"   "栃木県"   "群馬県"   "埼玉県"   "千葉県"  
[13] "東京都"   "神奈川県" "新潟県"   "富山県"   "石川県"   "福井県"  
[19] "山梨県"   "長野県"   "岐阜県"   "静岡県"   "愛知県"   "三重県"  
[25] "滋賀県"   "京都府"   "大阪府"   "兵庫県"   "奈良県"   "和歌山県"
[31] "鳥取県"   "島根県"   "岡山県"   "広島県"   "山口県"   "徳島県"  
[37] "香川県"   "愛媛県"   "高知県"   "福岡県"   "佐賀県"   "長崎県"  
[43] "熊本県"   "大分県"   "宮崎県"   "鹿児島県" "沖縄県"  

これで、市区町村役場(所)の住所から、47都道府県の名前を抽出することができた。

ここから、末尾の[道府県]を取り除いてみよう(北海道はそのまま残す)。str_replace() を使って、特定の文字列を""(つまり、何も含まない文字列)に置換 (replace) する1

try3 |> 
  str_replace(pattern = "[都府県]",
              replacement = "")
 [1] "北海道" "青森"   "岩手"   "宮城"   "秋田"   "山形"   "福島"   "茨城"  
 [9] "栃木"   "群馬"   "埼玉"   "千葉"   "東京"   "神奈川" "新潟"   "富山"  
[17] "石川"   "福井"   "山梨"   "長野"   "岐阜"   "静岡"   "愛知"   "三重"  
[25] "滋賀"   "京府"   "大阪"   "兵庫"   "奈良"   "和歌山" "鳥取"   "島根"  
[33] "岡山"   "広島"   "山口"   "徳島"   "香川"   "愛媛"   "高知"   "福岡"  
[41] "佐賀"   "長崎"   "熊本"   "大分"   "宮崎"   "鹿児島" "沖縄"  

ほとんどの都道府県については意図通りの結果が得られたが、「京都府」については「都」が取り除かれて「京府」になってしまった。これを防ぐためには、[道府県]が末尾にあることを$で指定すればよい。次のようにする。

try3 |> 
  str_replace(pattern = "[都府県]$",
              replacement = "")
 [1] "北海道" "青森"   "岩手"   "宮城"   "秋田"   "山形"   "福島"   "茨城"  
 [9] "栃木"   "群馬"   "埼玉"   "千葉"   "東京"   "神奈川" "新潟"   "富山"  
[17] "石川"   "福井"   "山梨"   "長野"   "岐阜"   "静岡"   "愛知"   "三重"  
[25] "滋賀"   "京都"   "大阪"   "兵庫"   "奈良"   "和歌山" "鳥取"   "島根"  
[33] "岡山"   "広島"   "山口"   "徳島"   "香川"   "愛媛"   "高知"   "福岡"  
[41] "佐賀"   "長崎"   "熊本"   "大分"   "宮崎"   "鹿児島" "沖縄"  

これで望み通りの結果が得られた。

次に、OfficesTelの列に注目しよう。 この列には、電話番号が記録されている。 ハイフンで番号が区切られているので、それを利用して市外局番を抜き出してみよう。 数字を1文字以上の怠惰マッチング (\\d+?) の後に、ハイフン1つとマッチング (-) するパタンを指定する。ただし、必要なのは数字だけなので、ハイフンまでを含む文字列を抽出した後、ハイフンを取り除く。

area_code <- Offices |> 
  pull(Tel) |> 
  str_extract(pattern = "\\d+?-") |> 
  str_replace(pattern = "-",
              replacement = "")
head(area_code)
[1] "011"  "0138" "0134" "0166" "0143" "0154"

0から始まる数字が抜き出されている。市外局番の桁数を調べてみよう。

area_code |> 
  str_length() |> 
  table()

   2    3    4    5 
  40  500 1273   51 

市外局番の桁数は、2桁から5桁までであることがわかる。ユニークな市外局番の個数は、

area_code |> 
  unique() |> 
  length()
[1] 379

である。

最後に、電話番号の下4桁を抜き出してみよう。 パタン一致の代わりに、str_sub() で最後から4文字目 (-4) から最後まで(end は指定しない)を抽出する。

last4 <- Offices |> 
  pull(Tel) |> 
  str_sub(start = -4)
head(last4)
[1] "2111" "3111" "4111" "1111" "1111" "5151"

念の為、すべての結果が4桁であることを確認する。まず、4文字であることを確認する。

last4 |> 
  str_length() |> 
  table()

   4 
1864 

すべて4文字である。次に、すべてが数字であることを確認する。そのためにstr_count() で、特定のパタンに一致する回数を数える。ここでは、pattern = "\\d" で数字を指定し、その回数が4になることを確かめる。

last4 |> 
  str_count(pattern = "\\d") |> 
  table()

   4 
1864 

すべの結果で数字が4回登場することが確認できた。文字の長さが4であることと合わせて、4桁の数字の抜き出しに成功したことがわかる。

ユニークな電話番号下4桁の個数は、

last4 |> 
  unique() |> 
  length()
[1] 365

であり、下4桁については同じ番号が多く利用されていることがわかる。

北海道を例として、電話番号についてもう少し詳しく検討しよう。 そのために、まず北海道のデータを抜き出そう。住所 (Address) に「北海道」という文字列が含まれている行を、dplyr::filter()stringr::str_detect() で抜き出す。

H <- Offices |> 
  filter(str_detect(Address, pattern = "北海道")) 
head(H, n = 3)
# A tibble: 3 × 5
  Code  Name   Zip      Address                        Tel         
  <chr> <chr>  <chr>    <chr>                          <chr>       
1 01100 札幌市 060-8611 北海道札幌市中央区北1条西2丁目 011-211-2111
2 01202 函館市 040-8666 北海道函館市東雲町4-13         0138-21-3111
3 01203 小樽市 047-8660 北海道小樽市花園2-12-1         0134-32-4111
tail(H, n =3)
# A tibble: 3 × 5
  Code  Name     Zip      Address                        Tel         
  <chr> <chr>    <chr>    <chr>                          <chr>       
1 01692 中標津町 086-1197 北海道標津郡中標津町丸山2-22   0153-73-3111
2 01693 標津町   086-1632 北海道標津郡標津町北2条西1-1-3 0153-82-2131
3 01694 羅臼町   086-1892 北海道目梨郡羅臼町栄町100-83   0153-87-2111
nrow(H)
[1] 179

北海道には179の市区町村がある。このデータに対し、先程と同じ方法で電話番号の下4桁を抽出する。

H_last4 <-  H |> 
  pull(Tel) |> 
  str_sub(start = -4)
H_last4 |> 
  unique() |> 
  length()
[1] 58

179の自治体に対し、電話番号下4桁のパタンは58しかない。 下2桁はどうだろうか?

H |> 
  pull(Tel) |> 
  str_sub(start = -2) |> 
  table()

 00  01  11  12  20  21  22  28  30  31  34  36  41  45  51  61  71  75  81  91 
  3   5 113   1   2  19   1   1   1  15   2   1   4   2   2   2   1   1   2   1 

下2桁は全部で20パタンあるが、ほとんどの自治体で下2桁が「11」になっていることがわかる。 入力しやすい電話番号にしようという配慮だろう。

18.4 文書データの分析

「吾輩は猫である」

 にゃーにゃーにゃにゃーにゃにゃにゃ!にゃーにゃー!


  1. 特定の文字列を削除するだけならstr_replace()でなく、str_remove()を使っても良い。これはreplacement = ""と指定されたstr_replace()と同じ機能をする。したがって、str_replace(pattern = "[都府県]", replacement = "")str_remove(pattern = "[都府県]")と簡略化することができる。↩︎