FrontPage  Index  Search  Changes  Login

T. Yoshiizumi - tjs_b_guide Diff

  • Added parts are displayed like this.
  • Deleted parts are displayed like this.

単純集計に関する覚え書き/複数回答の集計

〜 rubyとRを用いた処理 〜

最終更新日: 2013/06/16

    

以下に掲げるドキュメントは、
[[tjs_b.zip|http://cup.sakura.ne.jp/tjs_b.zip]]
に同梱されている tjs_b.txt と同じ内容です。

    

<はじめに>

 「単純集計に関する覚え書き」の第2段として、複数回答の集計について記します。第1段は[[「単一回答の集計」|http://cup.sakura.ne.jp/hiki/hiki.cgi?tjs_a_guide]]でした。

 複数回答は、「主な情報源として用いているものを次の4つからお選びください(複数選択可)。」のような質問で、選択肢として「新聞、雑誌、テレビ、ラジオ」が示されているような場合です。多重回答ともいうようです。

    

 ソフトウェアとしては R と ruby を用います。拙作 rrxwin(rubyからRを利用するためのライブラリ) も使います。

 サンプルのスクリプトを試した環境は次のとおり。

* Windows XP | VISTA
* ruby ver 1.8.7 | 1.9.3
* R ver 2.15.2

    

--------

{{toc_here}}

--------

    


!1. 単一回答と複数回答のクロス集計

!!(1) Rのma()関数の利用

○ 要点:factor()でNAを1つの値として扱う時は exclude=NULL を指定。

    

 [[「R -- マルチアンサーのクロス集計」|http://aoki2.si.gunma-u.ac.jp/R/multianswer.html]]というサイトにマルチアンサーのクロス集計を行うための関数 ma() の解説があります。

 まず、この ma() の利用を考えます。

 素材データとして data04.csv を用います。これは、ID(整数値), 年齢層(20代・30代・40代のどれか1つ), 主な情報源(新聞・雑誌・テレビ・ラジオの複数回答)から構成されています。100人分のデータです。具体的には次のようになっています。

    ID,年齢層,新聞,雑誌,テレビ,ラジオ
    1,30代,1,,,
    2,40代,1,1,,
    3,40代,,1,,

 IDと年齢層には空欄がありません。すべて記入されています。

 主な情報源については、選択された項目のところに数字の1を入力し、選択されなかった時は空欄にしてあります。

 なお、data04.csv は、乱数発生によって作成したデータです。実態とは関係ありません。

 これを ma() で集計すると、次のような表が得られます。

||年齢層||新聞||雑誌||テレビ||ラジオ||該当数
||20代||13||18||14||14||32
||%||40.6||56.2||43.8||43.8
||30代||24||17||20||16||34
||%||70.6||50.0||58.8||47.1
||40代||15||22||18||17||34
||%||44.1||64.7||52.9||50.0
||合計||52||57||52||47||100
||%||52.0||57.0||52.0||47.0

 ma()に関する解説サイトでは、複数回答に関して、選択されていれば数字の1、選択されていなければ数字の0を入力した素材データが示され、それを扱う例が掲げられています。

 仮に、data04.csv の空欄のところに数字の0が入力されているとすれば、ma関数の利用例は次のようになります。

    −−−− Rプログラムここから
    source("http://aoki2.si.gunma-u.ac.jp/R/src/multianswer.R", encoding="euc-jp")
    dtf <- read.csv("data04.csv", header=T)
    for (i in 3:6) dtf[[i]] <- factor(dtf[[i]], 1:0, c("該当","非該当"))
    ma(c("年齢層"), c("新聞","雑誌","テレビ","ラジオ"), dtf, F, "output.txt")
    −−−− Rプログラムここまで

 上のRプログラムを実行すると、output.txt というファイルが作成されます。これはタブ区切りテキストのファイルです。

 最初の source(……) は、ma() の定義ファイルを取り込むものです。インターネット経由でWebから取り込みます。予め multianswer.R をダウンロードして、カレントディレクトリに置いてある場合は、次のように書きます。

      source("multianswer.R", encoding="euc-jp")

    

 「for (i in 3:6) dtf[[i]] dtf[[i]] <- factor(……)」は、データフレーム dtf の第3〜6列目それぞれについて、数字の1を「該当」、数字の0を「非該当」に置換・調整するものです。3〜6列というのは複数回答の「新聞・雑誌・テレビ・ラジオ」のことです。

 そのように置換・調整した上で ma() を呼び出します。

 ちなみに、dtf[[i]] dtf[[i]] は dtf[,i] と書くこともできます。

    

 ma() の第1引数と第2引数では、データフレーム内の注目する項目(列)を指定します。どちらか一方が単一回答の項目、他方が複数回答の項目です。どちらを先に持ってきてもかまいませんが、必ずどちらかが単一回答で、他方は複数回答でなければなりません。

 上のサンプルでは項目の名前で指定していますが、数字で指定することもできます。単一回答の「年齢層」が2列目、「新聞」などの複数回答が3〜6列目であることから、次のように書けます。

      ma(2, 3:6, dtf, F, "output.txt")

 ma() の第3引数ではデータフレームを指定します。サンプルでは dtf(data04.csvの読み込み結果)です。

 第4引数は LaTeX の出力をするか否かのフラグです。T にすれば LaTeX のソースを出力します。F だとタブ区切りテキストになるようです。

 第5引数 "output.txt" は、出力ファイル名です。これを省略するとコンソールに出力されます。

    

 さて、ここで本来の data04.csv に話を戻します。複数回答の項目で、選択されなかったもののところに数字の0を入れるのではなく、空欄にしている場合です。その場合、Rプログラムで次の2点を修正します。

 1つ目は、read.csv() の引数に「na.strings=""」を加えます。空欄を NA として取り込むための指定です。これがなくても空欄は NA になると思いますが、念のため書いておきます。

 2つ目は、factor() の引数に「exclude=NULL」を追加します。NAを除外せずに一つの値として扱うための指定です。

 こうすると、空欄の値である NA をうまく取り扱うことができます。具体的には次のようなスクリプトになります。ma()の出力結果をrubyの方で文字列(タブ区切りテキスト)として受け取る例を示します。

    −−−− tjs10.rb ここから
    #! ruby -Ks
      # 複数回答のクロス集計(ma関数の利用) (coding: Windows-31J)
    require "rrxwin"
        ##
    rpro = <<EOS
    source("http://aoki2.si.gunma-u.ac.jp/R/src/multianswer.R", encoding="euc-jp")
    dtf <- read.csv("data04.csv", header=T, na.strings="")
    for (i in 3:6) {
        dtf[[i]] <- factor(dtf[[i]], levels=c(1,NA),
          labels=c("該当","非該当"), exclude=NULL)}
    ma(c("年齢層"), c("新聞","雑誌","テレビ","ラジオ"), dtf, F, "@@ma@@")
    EOS
        ##
    hs = Rrx.rexec(rpro)
    ma = hs["ma"]  # タブ区切りテキストを文字列として得る
    print ma
    −−−− tjs10.rb ここまで

 Rプログラム中の ma() の最後の引数のところに "@@ma@@" があります。これは、テンポラリファイルの指定の仕方の1つです。rrxwinをrequireしていると、このような記述が可能になります。rubyの側で該当テンポラリファイルの内容を簡単に受け取ることができます。

 この ma() の出力結果は、複数回答に関する全体的な傾向や特徴をみるのに適しています。どれくらいの人が該当の選択肢を選んでいるか、その人数とパーセントが一覧で把握できます。

--------

!!(2) 複数回答を分解して集計してみる

○ 要点1:テーブルxxへの項目タイトル付加は{{br}}
    「names(dimnames(xx)) <- c("title1","title2")」

○ 要点2:Rプログラム中でsprintf()を使う。

    

 複数回答の全体像把握という観点からすると横道にそれますが、ここでは「新聞・雑誌・テレビ・ラジオ」の4項目を「主な情報源」という1つの枠組みで考えるのではなく、4つの項目に分解して集計してみることにします。

 複数回答というのは、例えば「新聞」を主な情報源としているか否かに対して、○か×かで答えてもらったものと考えることができます。「雑誌」や「テレビ」 「ラジオ」についても同様です。

 単一回答である年齢層と「新聞」とのクロス集計は、次のようになります。

|| ||新聞
||年齢層||○||×
||20代||13||19
||30代||24||10
||40代||15||19

 上は、複数回答に関する集計というよりも、単一回答と単一回答の組合せです。なので、カイ2乗検定にかけて、年齢層と新聞の関係を分析することができます。

 先述の ma() は、「×」には言及せず、「○」の方に着目した結果を4項目について取りまとめたものとみることができます。

 参考まで、上の集計を出力するためのRプログラムを掲げてみます。

    −−−− Rプログラムここから
    dtf <- read.csv("data04.csv", header=T, na.strings="")
    for (i in 3:6)  dtf[[i]] <- factor(dtf[[i]], c(1,NA), c("○","×"), NULL)
    for (i in 3:6) {
        xx <- table(dtf[[2]], dtf[[i]])
        names(dimnames(xx)) <- c(colnames(dtf)[2], colnames(dtf)[i])
        print(xx)
        cat("\n")
    }
    −−−− Rプログラムここまで

 「年齢層」と「新聞」のクロス集計だけでよければ、その要は次の1行で済みます。

      table(年齢層=dtf$年齢層, 新聞=dtf$新聞)

 しかし、「新聞」だけでなく「雑誌」などについても出力しようとすると、変数を用いるため上のサンプルのように少しごちゃごちゃします。

 先の table(……) の1行(年齢層と新聞のクロス集計)をサンプルに即した形で書くと、次のように2行になります。

      xx <- table(dtf[["年齢層"]], dtf[["新聞"]])  # 集計結果をxxに代入
      names(dimnames(xx)) <- c("年齢層", "新聞")  # 項目タイトルを付加

    

 ここで、複数回答分解型の4つのクロス集計表をrubyの側で受け取るスクリプトを掲げておきます。

 rubyの側では、4つの表をタブ区切りテキストの形で標準出力に出力します。

 項目タイトルを付けるのではなく、項目名の中に「新聞」とか「雑誌」などの名前を盛り込んで、「新聞○」 「新聞×」などにします。Rプログラムの中で sprintf() を用いていますが、これは、rubyのsprintf()と同じように使えます。

    −−−− tjs11.rb ここから
    #! ruby -Ks
      # 複数回答を分解して集計 (coding: Windows-31J)
    require "rrxwin"
        ##
    rpro = <<EOS
    dtf <- read.csv("data04.csv", header=T, na.strings="")
    ss <- 2  # single answer の番号
    mm <- 3:6  # multi answer の番号群
    cn <- colnames(dtf)  # 列名群
    for (i in mm)  dtf[[i]] <- factor(dtf[[i]], c(1,NA),
        c(sprintf("%s○",cn[i]), sprintf("%s×",cn[i])), NULL)
    for (i in mm)  robj(table(dtf[[ss]], dtf[[i]]))
    EOS
        ##
    hs = Rrx.rexec(rpro)
    n = 1
    while (xx = hs["robj#{n}"]) != nil
        tbl = xx["self"]
        printf("table #%d\n%s\n", n, Rrx.ary2str(tbl, "\t"))
        n += 1
    end
    −−−− tjs11.rb ここまで

--------

!!(3) 複数回答の選択個数に着目する

○ 要点:xx中のNAでない要素の個数は「length(xx[!is.na(xx)])」

    

 data04.csvは、「新聞・雑誌・テレビ・ラジオ」の4つの選択肢について、「主な情報源」として利用しているものにまるを付けてもらったものですが、各人が、4つの選択肢のうち何個にまるを付けたかに着目してみます。

 選択個数が0というのは無回答とみることができますが、単に選びたいものがなかったので選ばなかったのか(例えば「インターネット」があれば選んでいた)、そもそも質問に意義を見いだせずに答えなかったのか、なかなか判断できません。無回答を含めて集計すべきか否か、迷うところです。

 また、無回答が総数100人のうち何人かによってもその扱いを工夫しなければなりません。極端な話、半数くらいが無回答だとすれば、それも含めて集計するかどうか……。少なくとも、無回答が半数近くあることを注釈として添えなければ、フェアな解説にならないだろうと思います。

    

 あるいは、選択個数が2以上の人が1人もいなかったとすれば、複数回答とはいえ、実質的には単一回答ということになります。

 この場合、選択肢相互の関係を考察することができません。例えば、「新聞を選択する人は、雑誌も選択する傾向がみられる」といった分析を行うことができません。

 選択個数を把握しておけば、このように、分析・考察をどのように進めたらいいか、その判断の手がかりを得られます。

 ということで、ここでは選択個数をチェックしてみます。

    

 Rプログラムで、「xx <- dtf[2,3:6]」と書けば、dtfの2行目の3〜6列をxxに代入することになります。

 そして、「yy <- xx[!is.na(xx)]」と書くと、xxのうち NAでないものが抜き取られて yyに代入されます。

 更に、yyの個数 length(yy) は、NAでない要素の個数を返すことになります。

 以上をまとめると、「nn <- length(dtf[2,3:6][!is.na(dtf[2,3:6])])」によって、dtfの第2行目の3〜6列の中で、NAでない要素の個数をnnに代入することになります。これを第2行目だけでなく、すべての行に対して行えば、複数回答の選択個数を一通り把握することになります。

    

 これから掲げるスクリプトの要点は、このような選択個数の検出です。

 まず、ファイルを読み込んだ結果をデータフレーム dtf_org にセットします。

 次に、その中から集計に用いる列だけを抽出します。抽出結果は dtf にセットします。dtf_orgには「ID・年齢層・新聞・雑誌・テレビ・ラジオ」の6列あるわけですが、dtfにはIDを除く5列がセットされます。

 また、dtf の最後の列として、各人の複数回答の選択個数を追加することにします。列名は count です。

 そうしておけば、countを参照することによって、無回答の人の数を確認したり、選択個数が2つ以上の人がどれくらいいるかなどの確認を容易に行えるようになります。

 以下、スクリプトです。

    −−−− tjs12.rb ここから
    #! ruby -Ks
      # 選択個数の検出 (coding: Windows-31J)
    require "rrxwin"
        ##
    rpro = <<EOS
    dtf_org <- read.csv("data04.csv", header=T, na.strings="")
    ss <- 2  # single answer の番号
    mm <- 3:6  # multi answer の番号群
    cn <- colnames(dtf_org)  # dtf_orgの列名群
    nr <- nrow(dtf_org)  # dtf_orgの行数
    count <- rep(0, nr)  # 選択個数記録用変数(0がnr個からなる変数)
    for (i in 1:nr)  count[i] <- length(dtf_org[i,mm][!is.na(dtf_org[i,mm])])
    dtf <- cbind(dtf_org[,ss], dtf_org[,mm], count)  # 新たなデータフレーム設定
    colnames(dtf) <- c(cn[ss], cn[mm], "count")  # dtfの列名を設定
    xx <- table(dtf$count)  # 選択個数別の回答者数を集計
    xx
    EOS
        ##
    hs = Rrx.rexec(rpro)
    print hs[:sink]
    −−−− tjs12.rb ここまで

 上のスクリプトで最も分かりにくいのは、選択個数をかぞえあげるところだと思いますが、それについては先に述べたので繰り返しません。

 新たなデータフレーム dtf は、cbind() を用いて設定しています。

      dtf <- cbind(dtf_org[,ss], dtf_org[,mm], count)

 上のようにすると、「年齢層」の列名が「年齢層」にならずに、「dtf_org[,ss]」という意味不明のものになってしまいます。そこで、次の行で列名を設定し直しています。

      colnames(dtf) <- c(cn[ss], cn[mm], "count")

と書くことによって、「年齢層」の列名がちゃんと「年齢層」になります。

    

 このスクリプトの目的は、複数回答の選択個数別に、該当の回答者が何人いるかを確認することです。それを行っているのが table() による集計です。

 集計といってもクロス集計ではなく、選択個数の count だけに着目した集計です。そのため出力結果が次の2行のみです。

      0  1  2  3  4
      4 25 35 31  5

 1行目が選択個数、2行目が該当の回答者数です。

 無回答が4人、4項目すべてにまるを付けた人が5人いることが分かります。

 両方の行とも数字だけなので分かりにくいですが、1行目を「個数0, 個数1, …… 個数4」として表示するためには次のようにします。Rプログラムの該当箇所だけ示します。

    −−−− Rプログラムの該当箇所ここから
    xx <- table(dtf$count)  # 選択個数別の回答者数を集計
    nn <- dimnames(xx)[[1]]  # 項目名を取り出す
    for (i in 1:length(nn))  nn[i] <- sprintf("個数%s",nn[i])
    dimnames(xx)[[1]] <- nn  # 項目名を設定し直す
    xx
    −−−− Rプログラムの該当箇所ここまで

 項目名を得るところで「dimnames(xx)[[1]]dimnames(xx)[[1]]」としていますが、選択個数のcountだけに着目した集計であるため(つまり項目が1つだけなので)、このような書き方になります。詳しくは dimnames() の仕様を参照して下さい。

 tjs12.rb で新たに設けた dtf というデータフレームを使えば、無回答の人を除いた集計を行うといったことが簡単になります。

      xx <- subset(dtf, count>=1)

とすれば、無回答の人を除いた96人分のデータが xx にセットされます。

 参考まで、無回答の人を除いて、ma() を呼び出すスクリプトを掲げておきます。

    −−−− tjs13.rb ここから
    #! ruby -Ks
      # 複数回答のクロス集計(無回答を除く) (coding: Windows-31J)
    require "rrxwin"
        ##
    rpro = <<EOS
    source("http://aoki2.si.gunma-u.ac.jp/R/src/multianswer.R", encoding="euc-jp")
    dtf_org <- read.csv("data04.csv", header=T, na.strings="")
    ss <- 2  # single answer の番号
    mm <- 3:6  # multi answer の番号群
    cn <- colnames(dtf_org)  # dtf_orgの列名群
    nr <- nrow(dtf_org)  # dtf_orgの行数
    count <- rep(0, nr)  # 選択個数記録用変数(0がnr個からなる変数)
    for (i in 1:nr)  count[i] <- length(dtf_org[i,mm][!is.na(dtf_org[i,mm])])
    dtf <- cbind(dtf_org[,ss], dtf_org[,mm], count)  # 新たなデータフレーム設定
    colnames(dtf) <- c(cn[ss], cn[mm], "count")  # dtfの列名を設定
    xx <- subset(dtf, count >= 1)  # 選択個数1以上を抽出
    for (i in cn[mm]) {
        xx[[i]] <- factor(xx[[i]], levels=c(1,NA),
          labels=c("○","×"), exclude=NULL)}
    ma(cn[ss], cn[mm], xx, F, "@@ma@@")
    EOS
        ##
    hs = Rrx.rexec(rpro)
    ma = hs["ma"]  # タブ区切りテキストを文字列として得る
    print ma
    −−−− tjs13.rb ここまで

 上のスクリプトで少し気をつけなければならないのは、単一回答欄の番号 ss(数値の2)と、複数回答欄の番号 mm(3:6)です。

 これら番号は、あくまで dtf_org における番号です。新たに設けた dtf では、ID欄がないので「年齢層」の欄が2ではなく1です。複数回答欄の番号は 3:6 ではなく 2:5 です。

 そのためdtfから「年齢層」のデータを取り出す場合、dtf[,2] とか dtf[[2]] dtf[[2]] とすることはできません。dtf[,"年齢層"] とか dtf[["dtf[["年齢層"]] "]] とすれば大丈夫です。

 変数cnには dtf_org の列名群が代入されているので、cn[ss] は c("年齢層") と同値です。cn[mm] は c("新聞","雑誌","テレビ","ラジオ") と同値です。

 なので、dtf[,cn[ss]] で「年齢層」、dtf[,cn[mm]] で「新聞・雑誌・テレビ・ラジオ」のデータを取り出せます。

 ちなみに、dtfにおいて「年齢層」などが何番に当たるかを得たい場合は次のようにします。

      cn <- colnames(dtf)
      ss <- seq_along(cn)[cn %in% "年齢層"]
      mm <- seq_along(cn)[cn %in% c("新聞","雑誌","テレビ","ラジオ")]

 上のようにすると、ssには1、mmには2:5がセットされるはずです。

--------

!![補足] NAの特殊性

 唐突ですが、条件式の値をみることにします。

 「(1 == (1==1)」が TRUE になり、「(1 == (1==0)」が FALSE になることは容易に予想できます。これらは「if …… else ……」の条件式として使えます。

 では、「(1 == (1==NA)」とか「(1 != (1!=NA)」あるいは「(NA == (NA==NA)」はどうかというと、これらは TRUE | FALSE にならず、いずれも NA になります。そのため if の条件式として使おうとすると、エラーになります。

 xxがNAかどうかを確認したい時は、is.na(xx) あるいは !is.na(xx) を用います。

    

 少し話が替わりますが、仮に「xx <- c(1,0,0)」だとすると、xxの中に1がいくつ含まれているかを調べるには次のようにします。

      nn <- length(xx[xx==1])

 xxの中には1が1個だけ含まれているので、nnは1になります。蛇足ですが、

      nn <- length(xx[xx==0])

とすれば、nnは2になります。

 では、「xx <- c(1,NA,NA)」の場合はどうでしょうか。

      nn <- length(xx[xx==1])

とすると、nnは、1ではなく3になります。xxにNAが含まれていると、「xx==1xx==1」の部分が意図したように機能しないことになります。

 data04.csvの複数回答の列は、選択されていれば数字の1、選択されていなければ空欄です。しかし、だからといって、選択個数をチェックしようとして数字の1を数え上げると、うまくいきません。その理由は上に書いたことに由来します。

 「素直に処理できる」ようにしたければ、data04.csvも空欄ではなく数字の0を入力しておく方がいいのかもしれません。

    

--------

!2. 複数回答相互のクロス集計

!!(1) 複数回答の選択肢相互のクロス集計(分割表示)

○ 要点:ベクトルxx中の部分ベクトルppの要素番号を得るには{{br}}
   「nn <- seq_along(xx)[xx %in% pp]」

    

 新聞を主な情報源にしている人は、雑誌も情報源にしている傾向があるかどうかなどをみるため、複数回答の選択肢相互の関係をクロス集計の表にしてみます。4つの選択肢を個別に(ばらばらに)取り扱います。

 いわばtjs11.rb(複数回答を分解して集計)の応用版です。例えば次のような表を作ります。

|| ||雑誌○||雑誌×
||新聞○||29||23
||新聞×||28||20

 選択肢4つの相互の組合せなので、上のような表が6つできることになります。

 最終的に6つの表を比較しなければならないことから、分析・考察の仕方として効率的とはいえません。

 ただ、必ず2×2のクロス集計表になるので、それぞれの表は非常に単純です。また、カイ2乗検定やフィッシャーの検定を適用できます。クラメール係数によって相互の連関の強さを比較することも可能です。

 参考まで下にスクリプトを掲げます。

    −−−− tjs14.rb ここから
    #! ruby -Ks
      # 選択肢相互の関係を分割表示 (coding: Windows-31J)
    require "rrxwin"
        ##
    rpro = <<EOS
    dtf_org <- read.csv("data04.csv", header=T, na.strings="")
    mm <- 3:6  # multi answer の番号群
    nr <- nrow(dtf_org)  # dtf_orgの行数
    count <- rep(0, nr)  # 選択個数記録用変数(0がnr個からなる変数)
    for (i in 1:nr)  count[i] <- length(dtf_org[i,mm][!is.na(dtf_org[i,mm])])
    dtf <- cbind(dtf_org[,mm], count)
    colnames(dtf) <- c(colnames(dtf_org)[mm], "count")
    cn <- colnames(dtf)
    mm <- seq_along(cn)[cn %in% colnames(dtf_org)[mm]]
    for (i in cn[mm])  dtf[[i]] <- factor(dtf[[i]], c(1,NA),
        c(sprintf("%s○",i), sprintf("%s×",i)), NULL)
    cat("回答者総数 = ", nrow(dtf), "\n", sep="")
    l <- length(mm)
    for (i in 1:(l-1))
      for (j in (i+1):l)
        print(table(dtf[,mm[i]], dtf[,mm[j]]))
    EOS
        ##
    hs = Rrx.rexec(rpro)
    print hs[:sink]
    −−−− tjs14.rb ここまで

 上のスクリプトではcount(選択個数)を算出してはいますが、それを活用していません。

 もし「選択個数が2以上の人を対象にし、なおかつ、4つ全部にまるを付けた人は対象から除く」というような条件をつけて集計するなら、

 「mm <- seq_along(……)」の後に、次の1行を置きます。

      dtf <- subset(dtf, count>1 & count<4)

 このような条件をつけることが妥当かどうかは別問題として、無回答の人や全部にまるを付けた人など、特徴を検出しにくい人を除いて集計すれば、特徴・傾向がより鮮明に表面化するということはいえると思います。

    

 スクリプトについて少し補足します。

 「mm <- seq_along(……)」は、新たに作ったデータフレームdtfにおける複数回答の項目番号を取得するものです。「新聞……ラジオ」の番号を得るもので、具体的には 2:5 がmmにセットされます。

 この辺は2重の for ループになっていますが、要するに、iが1:3の範囲で1つづつ進み、jが2:4の範囲で1つづつ進みます。ただし、jの開始番号は、いつも2からということではなく、i+1からです。

 このスクリプトは、おそらく、もっと洗練された(そして凝縮された)書き方に変更できると思います。ただ、初心者の私には洗練されたプログラムほど理解が難しく、後で読み返しても訳が分からなくなるのでこの程度にしておきます。

--------

!!(2) 複数回答の選択肢相互のクロス集計(一括表示)

○ 要点1:「○と×」の2行・2列の表の左上端(両方とも○)のみに着目。

○ 要点2:4行・5列の行列の生成は「xx <- matrix(0, 4, 5)」

    

 先に、2行・2列のクロス集計表を6つ表示するサンプルを示しましたが、今度は、選択肢の「×」にふれず、「○」の方にだけ着目して、4項目を一括表示することを考えます。

 具体的には次のような表を作ります。

|| ||新聞||雑誌||テレビ||ラジオ
||新聞||52||29||23||28
||雑誌||29||57||30||25
||テレビ||23||30||52||23
||ラジオ||28||25||23||47

 新聞と新聞が交差する欄の数は、新聞にまるを付けた人の総数です。

 新聞と雑誌が交差するところは、新聞と雑誌の両方にまるを付けた人の数を示します。

 上は、無回答の人も含めた総数100人分の集計なので、度数がそのまま総数に対するパーセントになっています。

 この表は、縦軸と横軸を入れ換えても同じものになります。

 以下にスクリプトを掲げます。

    −−−− tjs15.rb ここから
    #! ruby -Ks
      # 選択肢相互の関係を一括表示 (coding: Windows-31J)
    require "rrxwin"
        ##
    rpro = <<EOS
    dtf_org <- read.csv("data04.csv", header=T, na.strings="")
    mm <- 3:6  # multi answer の番号群
    nr <- nrow(dtf_org)  # dtf_orgの行数
    count <- rep(0, nr)  # 選択個数記録用変数(0がnr個からなる変数)
    for (i in 1:nr)  count[i] <- length(dtf_org[i,mm][!is.na(dtf_org[i,mm])])
    dtf <- cbind(dtf_org[,mm], count)
    colnames(dtf) <- c(colnames(dtf_org)[mm], "count")
    cn <- colnames(dtf)
    mm <- seq_along(cn)[cn %in% colnames(dtf_org)[mm]]
    for (i in cn[mm])  dtf[[i]] <- factor(dtf[[i]], c(1,NA), c("○","×"), NULL)
    l <- length(mm)
    ans <- matrix(FALSE, l, l)  # l行・l列の行列を用意
    colnames(ans) <- cn[mm]
    rownames(ans) <- cn[mm]
    for (i in 1:l) {
      for (j in i:l) {
        xx <- table(dtf[,mm[i]], dtf[,mm[j]])
        ans[i,j] <- xx[1,1]
        if (!ans[j,i])  ans[j,i] <- xx[1,1]
      }
    }
    cat("回答者総数 = ", nrow(dtf), "\n", sep="")
    ans
    EOS
        ##
    hs = Rrx.rexec(rpro)
    print hs[:sink]
    −−−− tjs15.rb ここまで

 上のスクリプトで「xx <- table(……)」は、各選択肢の組合せ集計を行うものです。新聞と新聞、新聞と雑誌、新聞とテレビといった組合せで、2行・2列の集計表を作成します。「新聞と新聞」は奇妙に感じるかもしれませんが、ちゃんと集計されて、新聞にまるを付けた人の総数を検出できます。

 xxには2行・2列のtableが代入されるわけですが、今回は、その中の両方とも「○」のもの、つまり左上端の xx[1,1] だけに着目します。「ans[i,j] <- xx[1,1]」としているのはそのためです。

    

 変数ansは4行・4列の行列です。全部で16セルありますが、それらのデフォルト値は FALSE にしました。

 仮に「ans <- matrix(0, 4, 5)」とすれば、4行・5列の行列が生成され、行列の各セルのデフォルト値は 0 になります。

 今回の行列の場合、ans[i,j] と ans[j,i] は同じ値になるはずなので、ans[i,j] の値をセットする時に、まだ ans[j,i] が FALSE のままであれば、それもセットします。

    

 この表をパーセント表示する場合、回答者総数100人を分母とする方法の他に、新聞の項では新聞にまるを付けた人の総数(52人)を100%とし、雑誌・テレビ・ラジオについてもそれぞれ同じようにパーセントを算出して表示する、そんなやり方もあると思います。

 いずれにしても、Rではパーセントの算出をごく簡単に行えるので、そのためのスクリプトは省略します。

--------

!3. その他

!!(1) 別の形式で入力された素材データへの対応

 data04.csvでは、例えば「新聞」の列に関して、それにまるを付けた人のところは数字の1を入力し、まるを付けなかった人のところは空欄にしてあります。「雑誌」など他の列についても同様です。

 しかし、そういう形式ではなく、新聞:1, 雑誌:2, テレビ:3, ラジオ:4 という対応を想定し、新聞だけにまるを付けた人のところは「1」、新聞とテレビにまるは「1,3」、雑誌とテレビとラジオは「2,3,4」などのように入力した素材データもあると思います。

 data05.csvは、そうした形式のデータです。具体的には次のような中身です。csv形式で示します。

    ID,年齢層,情報源1,情報源2,情報源3,情報源4
    1,30代,1,,,
    2,40代,1,2,,
    3,40代,2,,,
    4,20代,3,4,,

 このような素材データをダイレクトに扱うのは骨がおれるので、data04.csvの形式に変換した上で処理することを考えます。

 ここでは、rubyで変換します。Rの変換関数を用意した方がいいのだと思いますが、私には少々荷が重いのでrubyの方でやります。

 to_zo() というメソッドを作って tjsf.rb に盛り込んだので、それを用います。zoは「zero / one」のつもりです。このメソッドは次のように呼び出します。

      to_zo(ary, pi, ni, nil)

 第1引数は、素材データである2次元配列です。csvかタブ区切りのデータ(文字列)でもかまいません。あるいは、csvかタブ区切りデータのファイル名でもOKです。

 第2引数のpiは、previous item のつもりで、旧項目名を示す配列です。

 第3引数のniは、新項目名を示す配列です。

 第4引数は「選択なし」の時の値で、nilだと空欄になります。数字の0だと、空欄でなく0が入ります。指定しない時は0とみなされます。

 data05.csvを処理する場合は次のように呼び出します。

      to_zo(ary, %w(情報源1 情報源2 情報源3 情報源4),
          %w(新聞 雑誌 テレビ ラジオ), nil)

 このメソッドの戻り値は、変換結果である2次元配列です。変換に失敗した時は nil を返します。

 以下、data05.csvを扱うスクリプトを掲げます。tjs11.rb(複数回答を分解して集計)と同じ結果を得るものです。

    −−−− tjs16.rb ここから
    #! ruby -Ks
      # 別形式の素材データを処理 (coding: Windows-31J)
    require "rrxwin"
    require "./tjsf"
        ##
    rpro = <<EOS
    dtf <- read.csv("@@last@@", header=T, na.strings="")
    ss <- 2  # single answer の番号
    mm <- 3:6  # multi answer の番号群
    cn <- colnames(dtf)  # 列名群
    for (i in mm)  dtf[[i]] <- factor(dtf[[i]], c(1,NA),
        c(sprintf("%s○",cn[i]), sprintf("%s×",cn[i])), NULL)
    for (i in mm)  robj(table(dtf[[ss]], dtf[[i]]))
    EOS
        ##
    filename = "data05.csv"
    pi = %w(情報源1 情報源2 情報源3 情報源4)
    ni = %w(新聞 雑誌 テレビ ラジオ)
    ary = to_zo(filename, pi, ni, nil)
    unless ary
      exit
    end
    Rrx.temp_make(ary, ".csv")  # aryをテンポラリファイルに書き出す
    hs = Rrx.rexec(rpro)
    n = 1
    while (xx = hs["robj#{n}"]) != nil
        tbl = xx["self"]
        printf("table #%d\n%s\n", n, Rrx.ary2str(tbl, "\t"))
        n += 1
    end
    −−−− tjs16.rb ここまで

 ruby経由でRを利用するためのライブラリ rrxwin の機能をあれこれ使っているので分かりにくいかもしれません。

 Rプログラム部分は、基本的に tjs11.rb と同じです。

 Rプログラム中に出てくる「@@last@@」は、テンポラリファイルの名前に置き換えられます。テンポラリファイルは、data05.csvを変換した上で書き出したものです。その中身はdata04.csvと同じです。

 rubyでの処理部分については説明を省略します。

--------

!!(2) 数量化IIIによる複数回答の分析

 単純集計から話が大きくそれますが、複数回答も分析可能な数量化IIIの利用例に簡単にふれておきます。

 数量化IIIは、質問の選択肢に対する反応(選択の有無)が似ている回答者の並び替えを行い、相関が最大になるよう試みるという方法のようです。

 カイ2乗検定のように集計結果を基にして分析するのではなく、選択の有無(0/1形式)の素材データに基づいて分析を行います。

 統計解析ソフトRでこの手法を用いることについては[[「数量化 III 類」|http://aoki2.si.gunma-u.ac.jp/lecture/Qt/qt3.html]]というサイトが参考になります。

 上のサイトでは、数量化IIIと関数qt3()の使い方が詳しく紹介されています。

 qt3() に引きわたすデータは、今回の新聞・雑誌・テレビ・ラジオのようなデータで、選択されたら1、選択されなかった時は0を入力したものです。

 data04.csvは、選択なしが0ではなく空欄になっているので、qt3() を利用する場合は、空欄を0に置換してやる必要があります。

 また、qt3() に引きわたすデータは、無回答(つまりどの選択肢にもまるを付けていないケース)が含まれていると、警告が出て分析が行われないので、予めそれを除いておかなければなりません。もし無回答も見のがせない1つの反応であると考えるのであれば、「新聞・……ラジオ」の次に「無回答」の列を設けて、無回答の人はそこに1をセットするということになるでしょうか。

 以下に data04.csv の複数回答欄を数量化IIIにかける例を示します。

    −−−− tjs17.rb ここから
    #! ruby -Ks
      # 数量化IIIの利用例 (coding: Windows-31J)
    require "rrxwin"
        ##
    rpro = <<EOS
    source("http://aoki2.si.gunma-u.ac.jp/R/src/make.dummy.R", encoding="euc-jp")
    source("http://aoki2.si.gunma-u.ac.jp/R/src/qt3.R", encoding="euc-jp")
    dtf_org <- read.csv("data04.csv", header=T, na.strings="")
    mm <- 3:6  # multi answer の番号群
    nr <- nrow(dtf_org)  # dtf_orgの行数
    count <- rep(0, nr)  # 選択個数記録用変数(0がnr個からなる変数)
    for (i in 1:nr)  count[i] <- length(dtf_org[i,mm][!is.na(dtf_org[i,mm])])
    dtf <- cbind(dtf_org[,mm], count)  # 新たなデータフレーム設定
    colnames(dtf) <- c(colnames(dtf_org)[mm], "count")  # dtfの列名を設定
    cn <- colnames(dtf)
    mm <- seq_along(cn)[cn %in% colnames(dtf_org)[mm]]
    for (i in mm)  dtf[,i][is.na(dtf[,i])] <- 0  # NAを0に置換
    dtf <- subset(dtf, count>0)  # 無回答を除く
    dat <- dtf[,mm]
    dat[,mm] <- lapply(dat, factor) # factor変数に変換
    qq <- qt3(dat)
    summary.qt3(qq)
    EOS
        ##
    hs = Rrx.rexec(rpro)
    print hs[:sink]
    −−−− tjs17.rb ここまで

 Rプログラムの最初にある「source("……make.dummy.R", ……)」の1行は、今回に限ると必要ありません。ただ、qt3() の中で make.dummy() を呼び出している箇所があるので入れておきました。

 make.dummy() は、「0/1形式」でないデータ、例えば単一回答のデータを「0/1形式」に変換するものです。

 上のtjs17.rbは、基本的にこれまで述べてきたノウハウを使っているだけですが、lapply() は初登場だと思います。これはapply関数群の1つで、他にsapply() などいくつか同類の関数があります。apply関数群については説明がややこしくなるので省略します。

 手抜きついでに、数量化IIIの出力結果の見方についても省略します。

--------

!![補足] to_zoメソッドの仕様

 rubyのメソッド to_zo() は、実は qt3() の利用を意識して作りました。

 「情報源1・情報源2・情報源3・情報源4」を「新聞・雑誌・テレビ・ラジオ」に変換するケースについては tjs16.rb のところでふれましたが、その他に、単一回答の列を「0/1形式」に変換することもできます。

 data04.csvにおいて「年齢層」の列は、「20代・30代・40代」の3種類の値が書き込まれています。これを「0/1形式」にする場合は、to_zo() を次のように呼び出します。

      ary = to_zo("data04.csv", "年齢層", %w(20代 30代 40代))

 こうすると、次のような配列が返されます。csv形式で記します。

    ID,20代,30代,40代,新聞,雑誌,テレビ,ラジオ
    1,0,1,0,1,,,
    2,0,0,1,1,1,,
    3,0,0,1,,1,,
    4,1,0,0,,,1,1

 「年齢層」という1つの列が「20代・30代・40代」という3つの列になって、各人の該当欄に0か1が入ります。20代の人であれば、「20代」の列のところに1が入り、「30代・40代」の列のところには0が入ります。

 仮に data04.csv の「年齢層」の列のところに、「20代:1, 30代:2, 40代:3」の対応を想定した値 1, 2, 3 の数字が書き込まれていた場合も、同じ変換結果になります。

    

 もし「40代・30代・20代」の陣盤にしたければ、次のように to_zo() を呼び出します。

      ary = to_zo("data04.csv", "年齢層", %w(40代 30代 20代))

 上のように呼び出したとき、もし data04.csv の「年齢層」の列のところに 1, 2, 3 の数字が書き込まれているとすれば、「40代:1, 30代:2, 20代:3」の対応が想定されて変換されます。

    

 話がややこしくなって恐縮ですが、to_zo() の第3引数を章略するケースがあります。例えば次のように呼び出します。

      ary = to_zo("data04.csv", "年齢層")

 こうすると、「年齢層」の列が全部チェックされ(つまり100人分チェックされて)、そこに書き込まれているデータが「20代・30代・40代」の3種類であることを把握します。これを機械的にsortして、第3引数の代用とします。

 機械的にsortされてしまうと困るという場合は、第3引数を指定するようにして下さい。

 以上が to_zo() の仕様です。

--------

 複数回答の単純集計に関する覚え書きは、この辺でおわりにします。

 単純集計から脱線した部分もありますが、私自身が記憶にとどめておきたかったことを記しました。

 apply関数群や数量化については著しく不完全燃焼で申し訳ありませんが、まだ勉強付則で解説を書けるほどにはなっていません。幸い、Webにいろいろな情報があるので、そちらを参照して下さい。

Copyright (C) T. Yoshiizumi, 2013 All rights reserved.