備忘ログ

チラシの裏的備忘録&メモ

Rで多肢多重回答のものをダミーコディングするメモ

Rで多肢多重回答のものをダミーコーディングするときのメモ。

調査回答で複数の選択肢から当てはまるものを選んでもらう回答方式を、多肢選択法(multiple-choice method)といい、一つだけしか選べないものを単一回答(single answer)とか、複数選べるものを多重回答(multiple answers)とかいって、それぞれSAとかMAとか略すこともある。

単一回答の場合は、一つの質問に対して一つの回答となるが、多重回答を許す場合には最大で選択肢分だけ答えとなることがある。

多重回答例としては「訪れたことのある地方をすべて選んでください。a.北海道 b.東北地方 c.関東地方 ……」など。

このとき、紙ベースの調査の場合は集計時に一つの値のところに複数の回答をまとめて載せてしまっている集計表をもらうことがある。

例えば次のようなデータである。

id answer
1 a,b,c
2 b,c
3 d
4 c,b,d
5 a,d
6 a,c

あるいは次のようなデータである。

id ans_1 ans_2 ans_3
1 a b c
2 b c NA
3 d NA NA
4 c b d
5 a d NA
6 a c NA

または次のようなパターンもあり得る。

id answer
1 abc
2 bc
3 d
4 cbd
5 ad
6 ac

いずれの場合も、このままでは処理しにくいので次のように、各選択肢を列名とし選択肢が選ばれている場合には1とし非選択の場合には0となるようにデータを整形したい(ダミーコーディング)。

id answer a b c d
1 a,b,c 1 1 1 0
2 b,c 0 1 1 0
3 d 0 0 0 1
4 c,b,d 0 1 1 1
5 a,d 1 0 0 1
6 a,c 1 0 1 0

今どきWEB調査などをすると最初から上記のようなデータとなっていることがあるが、紙ベースからの集計だと一つの値に”a,b”としたほうが入力しやすいかもしれない。あるいはコンマなしだったり、もともと最大で3つか4つくらいが選択されるものと想定されれば、その分の列を用意していたほうがExcelでデータ入力するときにしやすいかもしれない。

手始めに、選択肢がちゃんと任意の一文字で区切られている場合(今回の場合は”,“)か、選択肢が1文字(aとかbとか)の場合には比較的カンタン。そうでなければ、区切り文字を任意の一文字等になるように前処理する。

手始めに、データがコンマで分割されているデータの場合次のようなデータとなる。

df <- data.frame(id = 1:6,
                 answer = c("a,b,c",
                            "b,c",
                            "d",
                            "c,b,d",
                            "a,d",
                            "a,c"))

まずは値(文字列)を”,“で分割しリスト形式で持っておく。

value_list <- strsplit(df$answer, split = ",")

ここからユニークな値を取り出して、選択肢の項目とする。もし自前で選択肢のベクトルを持っていればそれでもいい。

colname_list <- unique(unlist(value_list))

これでリスト(value_list)の中身ごとにlapply()、選択肢のベクトル(colname_list)と%in%演算子を用いて比較参照し、TRUEが1、FALSEが0となるようにas.numeric()で数値化して、そのリストをdo.call()rbindしてリスト一つずつ要素を行とした行列にする。

そして扱いやすいように、それをデータフレーム化する。

res <- as.data.frame(do.call(rbind, lapply(value_list, function(x){
  as.numeric(colname_list %in% x)
})))

あとは比較したcolname_listをデータフレームの列名にすると

colnames(res) <- colname_list
res
##   a b c d
## 1 1 1 1 0
## 2 0 1 1 0
## 3 0 0 0 1
## 4 0 1 1 1
## 5 1 0 0 1
## 6 1 0 1 0

選択されたものが1、そうでないものが0のデータフレームが得られる。

あとはもとのデータフレームとくっつけると、最初にほしかったものができる。

cbind(df, res)
##   id answer a b c d
## 1  1  a,b,c 1 1 1 0
## 2  2    b,c 0 1 1 0
## 3  3      d 0 0 0 1
## 4  4  c,b,d 0 1 1 1
## 5  5    a,d 1 0 0 1
## 6  6    a,c 1 0 1 0

ちなみに、この方法だとNAの値はすべてが0となる。

もしNAという選択肢が選択された(選択しなかったという選択)という形にしたかったら、NAを文字列の"NA"にするといいと思う。

自分で使うようにもう少し汎用性をもたせた形でダミーコーディングする挙動をする関数をdummy_code()として自作関数のパッケージの{infun}に突っ込んでおいたので

remotes::install_github("indekun/infun")
library("infun")

source("https://raw.githubusercontent.com/indenkun/infun/main/R/dummy_code.R")

で読み込めるので

dummy_code(df$answer, split = ",")
##   a b c d
## 1 1 1 1 0
## 2 0 1 1 0
## 3 0 0 0 1
## 4 0 1 1 1
## 5 1 0 0 1
## 6 1 0 1 0

で簡単にダミーコーディングしたもののデータフレームが得られるようにした。あとはもとのデータにくっつけるかどうかはお任せ。

自分で使いやすいように、列名に統一した文字列を追加できるようにしている。

dummy_code(df$answer, split = ",", prefix = "q1_")
##   q1_a q1_b q1_c q1_d
## 1    1    1    1    0
## 2    0    1    1    0
## 3    0    0    0    1
## 4    0    1    1    1
## 5    1    0    0    1
## 6    1    0    1    0

さて、冒頭に上げた一つの値が一つのデータにはなっているが、選択肢分バラバラで選ばれていない場合はNAとなっている場合は次のように前処理すると今回のようにできる。

df_2 <- data.frame(id = 1:6,
                   ans_1 = c("a", "b", "d", "c", "a", "a"),
                   ans_2 = c("b", "c", NA, "b", "d", "c"),
                   ans_3 = c("c", NA, NA, "d", NA, NA)) 
df_2
##   id ans_1 ans_2 ans_3
## 1  1     a     b     c
## 2  2     b     c  <NA>
## 3  3     d  <NA>  <NA>
## 4  4     c     b     d
## 5  5     a     d  <NA>
## 6  6     a     c  <NA>

これを全部くっつけて任意の文字で区切ったデータの列をつくる。

# tidyverse的に書くなら次のようになる?
library(dplyr)
library(stringr)
df_2 |> 
  mutate(ans_all = str_c(str_replace_na(ans_1), 
                         str_replace_na(ans_2),
                         str_replace_na(ans_3),
                         sep = ",") |> 
           str_remove_all(",NA"))
##   id ans_1 ans_2 ans_3 ans_all
## 1  1     a     b     c   a,b,c
## 2  2     b     c  <NA>     b,c
## 3  3     d  <NA>  <NA>       d
## 4  4     c     b     d   c,b,d
## 5  5     a     d  <NA>     a,d
## 6  6     a     c  <NA>     a,c
# baseで書くなら次のようになる
# str_cの使用の都合で、pasteのほうが書きやすい
transform(df_2, ans_all = paste(ans_1, ans_2, ans_3, sep = ",") |> 
            gsub(",NA", "", x = _))
##   id ans_1 ans_2 ans_3 ans_all
## 1  1     a     b     c   a,b,c
## 2  2     b     c  <NA>     b,c
## 3  3     d  <NA>  <NA>       d
## 4  4     c     b     d   c,b,d
## 5  5     a     d  <NA>     a,d
## 6  6     a     c  <NA>     a,c

みたいにデータ前処理して、コンマ区切りのデータを作ってこの記事の冒頭からの処理を行うか、dummy_code()かけるとダーミーコーディングできる。

df_2_new <- transform(df_2, ans_all = paste(ans_1, ans_2, ans_3, sep = ",") |> 
                        gsub(",NA", "", x = _))
dummy_code(df_2_new$ans_all, split = ",")
##   a b c d
## 1 1 1 1 0
## 2 0 1 1 0
## 3 0 0 0 1
## 4 0 1 1 1
## 5 1 0 0 1
## 6 1 0 1 0

あと区切り文字がない場合はもともと、strsplit()split = ""とすると、各文字切れるのでそのまま処理できる。

df_3 <- data.frame(id = 1:6,
                   answer = c("abc",
                              "bc",
                              "d",
                              "cbd",
                              "ad",
                              "ac"))
df_3
##   id answer
## 1  1    abc
## 2  2     bc
## 3  3      d
## 4  4    cbd
## 5  5     ad
## 6  6     ac

これをこうするとできる。

dummy_code(df_3$answer, split = "")
##   a b c d
## 1 1 1 1 0
## 2 0 1 1 0
## 3 0 0 0 1
## 4 0 1 1 1
## 5 1 0 0 1
## 6 1 0 1 0

単純なダミーコーディングなら{psych}dummy.code()を使う

ところでなんでこんなことを書いたかというと、One Hot Coding(ダミーコーディング)をやる方法を{tidyverse}{tidymodels}を使ってやる方法を見て、「せっかくだから俺は温故知新で{psych}dummy.code()を使うぜ」というのを書こうと思った。

socinuit.hatenablog.com

blog.atusy.net

例えば、

df <- data.frame(id = 1:5,
                 value = c("a", "b", "c", "b", "c"))
df
##   id value
## 1  1     a
## 2  2     b
## 3  3     c
## 4  4     b
## 5  5     c

のような場合、

df_dummy <- psych::dummy.code(df$value)
df_dummy
##      b c a
## [1,] 0 0 1
## [2,] 1 0 0
## [3,] 0 1 0
## [4,] 1 0 0
## [5,] 0 1 0

と書ける。

{psych}は結構返しが行列形式なので必要に応じてデータフレーム化して、また必要があればもとのデータフレームとくっつければいい。

df <- cbind(df, as.data.frame(df_dummy))
df
##   id value b c a
## 1  1     a 0 0 1
## 2  2     b 1 0 0
## 3  3     c 0 1 0
## 4  4     b 1 0 0
## 5  5     c 0 1 0

ただ、この方法では当たり前だが、一番最初に書いたような複数の回答を一つの値として任意の文字列で区切られた状態で保持されているような場合はうまくいかない。

df <- data.frame(id = 1:5,
                 value = c("a", "b", "c,a", "b", "c"))
df
##   id value
## 1  1     a
## 2  2     b
## 3  3   c,a
## 4  4     b
## 5  5     c
psych::dummy.code(df$value)
##      b a c c,a
## [1,] 0 1 0   0
## [2,] 1 0 0   0
## [3,] 0 0 0   1
## [4,] 1 0 0   0
## [5,] 0 0 1   0

そこで冒頭に書いたような処理となる。

余談

多重回答を許す場合はどう処理したらいいかなぁと思って書いてみた。

どういうコードを書いたのか忘れてしまうのでメモがてら書いて、githubにも汎用性を持たせて関数化したものをおいておいた。いつかの自分に役立つかもしれない。

これを書いている途中で、ループしてあたりをかける方法をみつけた。

【R言語】一つのセルに複数回答の選択肢が記録されているデータのRでの処理方法:Pythonのコードを参考にR言語に翻訳してみました:How to break out Multi-Answer responses into separate cells | One of my favorite things is ... - 楽天ブログ

別にどちらの方がいいとかないが、今回の手法のほうが文字列を分割したときにリストになったものをそのままlapply()で処理できているのでコードの行数少なめになっている。