Rで重複している箇所を取り出す方法

概要
R言語において、1つのIDに対して2つ以上のレコードが存在している場合、その重複している箇所をすべて取り出す方法を2つ説明する。

やりたいこと

1つのIDに対して1つのレコードしか存在しない設定であるにもかかわらず、何らかの理由で1つのIDに対して2つ以上のレコードが存在してしまっている場合がある。つまり、何らかの理由で重複が発生しているということだ。そういったときに原因を究明するには、重複しているところだけを取り出すことが有用である。

例えば、次の表では、ID が 2435 のレコードが2つ存在してしまっている(夏目漱石の坊っちゃんと、樋口一葉のたけくらべ)。

ID 作家名 作品名
2435 夏目漱石 坊ちゃん
5342 芥川龍之介 羅生門
4813 森鴎外 舞姫
2435 樋口一葉 たけくらべ
8791 尾崎紅葉 金色夜叉
9318 中島敦 山月記
6534 宮沢賢治 風の又三郎

この表から重複が発生しているレコードを取り出して、以下のようなものができると、ID重複の原因究明の時に便利である。

ID 作家名 作品名
2435 夏目漱石 坊ちゃん
2435 樋口一葉 たけくらべ

以下、R言語を用いて、表型のデータにおいて重複している箇所を取り出す方法を紹介していきたいと思う。

解法1:dplyr パッケージを使う

dplyr パッケージを使うことで、簡単に重複しているデータを抽出することができる。具体的には、group_by()を使って ID となっている列を指定し、filter(n() > 1) でIDに重複がある行(レコード)を抽出することにある。

具体的なコードの例を見てみよう。ここでは、先に挙げた作家名・作品名が載った表を literature というデータフレームに入れている。

# 必要なパッケージの読み込み
library("dplyr")

# 操作対象となるデータフレームの準備
literature <- data.frame(
  ID = c(2435, 5342, 4813, 2435, 8791, 9318, 6534),
  作家名 = c("夏目漱石", "芥川龍之介", "森鴎外",
          "樋口一葉", "尾崎紅葉", "中島敦", "宮沢賢治"),
  作品名 = c("坊ちゃん", "羅生門", "舞姫",
          "たけくらべ", "金色夜叉", "山月記", "風の又三郎")
)

literature %>%
  group_by(ID) %>%
  filter(n()>1)

これを実行すると、以下のように出力される。

# A tibble: 2 x 3
# Groups:   ID [1]
     ID   作家名     作品名
  <dbl>   <fctr>     <fctr>
1  2435 夏目漱石   坊ちゃん
2  2435 樋口一葉 たけくらべ

どういう仕組みで動いているのかについて、簡単に説明しよう。まず、group_by(ID) をかけることで、ID 列の内容によって集計のグループが設定される。次に、n()はグループごとのレコード数を返す函数である。ID に重複がなければ、1個のIDに対するレコード数は1になる。しかし、ID に重複があれば、1個のIDに対するレコード数は2以上になる。よって、n() が2以上となる(=1より大きくなる)行を抽出すれば、重複している箇所を取り出すことができる。それが、最後の filter(n() > 1) が果たしている役割である。

group_by() を使うと、ID の役割を果たす列を簡単に複数個指定できる。例えば、以下のような小学校のテストの結果の表を考えてみよう。この表では、個人を識別するIDが1列になっていない。個人を識別するには、クラスと出席番号を組み合わせなくてはならない。

クラス 出席番号 国語 算数
1 7 84 45
1 12 65 24
1 14 86 38
1 21 78 68
2 3 56 18
2 3 43 15
2 7 89 53
2 15 45 78
3 8 15 27
3 8 89 68
3 12 24 28
3 21 78 81

そして、個々人のデータが重複している箇所を見るには、クラスと出席番号がまったく同じものが重複している箇所を取り出すことになる。この場合、group_by(クラス, 出席番号) と、2つの列を指定すれば良い。

# 操作対象となるデータフレームの準備
exams <- data.frame(
  クラス = c(1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3),
  出席番号 = c(7, 12, 14, 21, 3, 3, 7, 15, 8, 8, 12, 21),
  国語 = c(84, 65, 86, 78, 56, 43, 89, 45, 15, 89, 24, 78),
  算数 = c(45, 24, 38, 68, 18, 15, 53, 78, 27, 68, 28, 81)
)

exams %>%
  group_by(クラス, 出席番号) %>%
  filter(n()>1)
出力結果は以下の通りである。
# A tibble: 4 x 4
# Groups:   クラス, 出席番号 [2]
  クラス 出席番号  国語  算数
   <dbl>    <dbl> <dbl> <dbl>
1      2        3    56    18
2      2        3    43    15
3      3        8    15    27
4      3        8    89    68

解法2:duplicated() 函数を使う

Rには duplicated() という函数がある。この函数は、今までに出てきたものと重複していれば TRUE を返し、そうでなければ FALSE を返す。これを活用することで重複している箇所を取り出すことが可能である。ただし、dplyr パッケージを使うやり方よりややこしいのであまりお勧めはしない

さて、先ほどの作家名・作品名が載った表であれば、以下のようにすれば重複している部分を取り出すことができる。

literature[duplicated(literature$ID) |
        duplicated(literature$ID, fromLast = TRUE),]

ちなみに、上記のコードで literature[duplicated(literature$ID)] とするだけだと、「2435 樋口一葉 たけくらべ」のみ出力され、「2435 夏目漱石 坊ちゃん」は出力されない。なぜかと言えば、duplicated() は今までに出てきたものと重複がある場合に TRUE となるからだ。夏目漱石の方は2435というIDが初めて出現する場所なので、重複しているとは判断されないのである。

これを防ぐために、duplicated(literature$ID, fromLast = TRUE) というのを加えている。fromLast = TRUE というオプションにより末尾から順々に見ていくようになるので、樋口一葉の方で2435というIDが初めて出現し、夏目漱石の方で2435が重複して出てくると判定される。このため、fromLast = TRUE がある場合とない場合の論理和を取ることで、重複している箇所をすべて取り出すことができる。