Rでアンケートの回答を集計してみよう(総合演習)
ここまでの R レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。ベクトル・data.frame・NA・ifelse・関数——すでに練習したものだけで組み立てます。
作るのはアンケートの集計です。平均と中央値を出し、満足度の分布を棒グラフにし、流入元ごとに比べて、レポートにまとめます。
この演習には無回答が混ざっています。——実際のアンケートがそうだからです。年齢を答えない人、満足度を飛ばす人は必ずいます。
RはそれをNAという特別な値で表します。そしてNAを含むデータの平均はNAになります。0でも無視でもなく、「分からない」が伝染するのです。
これはRの親切です。黙って除かれるより、「無回答をどう扱うか、あなたが決めてください」と突き返してくれるほうが安全です。この演習では、その決め方を実際に書きます。
このページではコードを実行していません。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際のRで走らせて得た出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== アンケート集計 ===== 回答数: 12件 --- 数値項目 --- 年齢 有効10件(無回答2件) 平均 33.60 中央値 32.5 満足度 有効11件(無回答1件) 平均 3.73 中央値 4.0 --- 満足度の分布 --- 高評価 7件 ####### ふつう 2件 ## 低評価 2件 ## 無回答 1件 # --- 流入元 --- 検索 5件 平均満足度 4.60 友人 3件 平均満足度 4.00 広告 4件 平均満足度 2.50 -------------------------------------------- 推奨する人の割合: 66.7% 高評価(4点以上): 7件 そのうち推奨: 7件 推奨の割合は良好です。
STEP 1回答をdata.frameで表す
🎯 このステップのゴール: 列ごとに型の違う表を作り、行数と列名を確かめる
まずデータの形を決めます。data.frameはRでいちばんよく使う入れ物で、表そのものだと思ってください。
特徴は「列ごとに型が違ってよい」ことです。ageは数値、recommendは真偽値、channelは文字列。行方向には揃っている必要がありません。
ただし列の長さは全部同じでなければなりません。1つでも足りないとエラーになります。表なので当然ですが、書き足すときに間違えやすい場所です。
id = 1:12の1:12は「1から12までの連番」です。Rではforを書かずに連番が作れます——この手軽さが、あとで効いてきます。
c(...)のcはcombine(つなげる)の頭文字です。Rでベクトルを作る、いちばん基本の関数で、これから何度も出てきます。
stringsAsFactors = FALSEは「文字列を factor に変換しないで」という指定です。
R 4.0より前は、文字列が勝手に factor になっていました。factor は「決まった選択肢のどれか」を表す型で、知らずに使うと文字列として扱えず混乱します。今は既定がFALSEなので不要ですが、古いコードでは必ず見かけるので覚えておいてください。
nrow(df)で行数、ncol(df)で列数、names(df)で列名が取れます。
回答数: 12件 項目数: 5 列名: id age satisfy recommend channel 1件目: 年齢23 満足度5 流入元検索
STEP 2NAを含む平均を正しく出す
🎯 このステップのゴール: NAが伝染することを確かめ、除く方法を選ぶ
年齢の平均を出します。mean(answers$age)——結果はNAです。
これはバグではありません。「分からない値が混ざっているのだから、平均も分からない」——Rの言い分は筋が通っています。
他の多くの言語なら、無回答は0か空文字になって、黙って平均に混ざります。年齢0歳の人が2人いることになり、平均が下がっているのに誰も気づきません。
除き方は2つあります。
1つ目はna.rm = TRUE。mean(x, na.rm = TRUE)と書けば、NAを除いて計算します。短くて、いちばんよく使う書き方です。
2つ目は先に取り除くことです。values[!is.na(values)]——is.na()が「NAかどうか」を要素ごとに判定し、!で反転。真の要素だけが残ります。
この演習では2つ目を選びました。理由は「何件除いたか」を数えられるからです。na.rm = TRUEは静かに除くので、「12件の平均」なのか「10件の平均」なのか、出力を見ても分かりません。
「有効10件(無回答2件)」と出せば、読む人が数字をどこまで信じてよいか判断できます。これは集計を書くときの基本の作法です。
x == NAと書いてはいけません。結果はNAになります——「分からない値が何かと等しいか」も分からないからです。必ずis.na()を使ってください。
そのまま: NA na.rm: 33.6 有効10件(無回答2件) 平均 33.60 NA == NA の結果: NA is.na(NA) の結果: TRUE
STEP 3ifelseでベクトルをまとめて分類する
🎯 このステップのゴール: for文を書かずに、全要素を一度に振り分ける
満足度を「高評価・ふつう・低評価・無回答」に振り分けます。
他の言語ならforで回してifで分けるところです。Rでは違います。
ifelse(scores >= HIGH_SCORE, "高評価", "それ以外")——これで12件ぜんぶが一度に処理されます。
ifとifelseは別物です。ifは1つの値にしか使えず、ベクトルを渡すとエラーか警告になります。ifelseはベクトル全体を扱います。
この違いはRを書き始めた人が必ず踏みます。「なぜか1件目しか判定されない」という症状で現れます。
入れ子にすれば3つ以上に分けられます。順番が結果を決めます——is.na(scores)をいちばん外に置いているのが要点です。
NA >= 4はNAになります。真でも偽でもないので、ifelseはその要素にNAを返してしまいます。先にNAを捕まえておかないと、無回答が「無回答」に分類されずに消えます。
数え方もRらしいです。sum(labels == name)——labels == nameが真偽値のベクトルになり、sumが真の個数を数えます。
RではTRUEが1、FALSEが0として足せます。だから「条件に合う件数」はsum(条件)で出ます。数え上げのループを書く必要がありません。
paste(rep("#", n), collapse = "")で棒グラフを作ります。repが繰り返し、paste(collapse=)がつなげる——2段階です。
分類結果: 高評価 高評価 ふつう 高評価 高評価 低評価 高評価 ふつう 無回答 高評価 高評価 低評価 高評価 7件 ####### ふつう 2件 ## 低評価 2件 ## 無回答 1件 #
STEP 4条件で行を絞り込む
🎯 このステップのゴール: 角括弧で行を選び、NAが混ざらないようにする
流入元ごとに満足度を比べます。まず「検索から来た人の行だけ」を取り出します。
df[df$channel == "検索", ]——角括弧の中がカンマで区切られているのに注目してください。
data.frameの角括弧は[行の条件, 列の条件]です。カンマの後ろを空けると「全部の列」という意味になります。
このカンマを忘れると、まったく違う結果になります。df[df$channel == "検索"]と書くと列を選ぼうとしておかしなことになります。Rでいちばん多い書き間違いの1つです。
df$channel == "検索"が返すのは12個の真偽値です。真の行だけが残ります——ステップ3のsum(条件)と同じ仕組みが、ここでは行の選択に効いています。
条件にNAが混ざる列では注意が要ります。df[df$satisfy >= 4, ]と書くと——NAの行が「NAだらけの幽霊行」として残ります。NA >= 4がNAになり、真でも偽でもないためです。
だからdf[!is.na(df$satisfy) & df$satisfy >= HIGH_SCORE, ]とNAを先に除いています。
&は&&ではありません。&が要素ごと、&&は1つの値だけ。行の絞り込みでは必ず&です。
平均にはna.rm = TRUEを付けています。ここでは件数を別に出しているので、静かに除いて構いません。
検索 5件 平均満足度 4.60 友人 3件 平均満足度 4.00 広告 4件 平均満足度 2.50 NAを除かない: 8行(うちNAの行 1) NAを除いた : 7行
STEP 5真偽値をそのまま数える
🎯 このステップのゴール: TRUEが1として足せることを使い、割合を出す
「推奨する」と答えた人の割合を出します。
sum(df$recommend) / nrow(df) * 100——これだけです。
recommendはTRUEとFALSEの列なのに、そのままsumできます。RではTRUEが1、FALSEが0として扱われるからです。
だからsumが「真の個数」になります。数え上げのループも、ifで数える変数も要りません。
この性質はいたるところで使えます。ステップ3のsum(labels == name)も、ステップ4の行の絞り込みも、すべて同じ仕組みです。「条件がベクトルで返り、それをそのまま使う」——Rらしさの核心がここにあります。
mean(df$recommend)と書けば、割合が直接出ます(1と0の平均は、1の割合そのものです)。知っていると短く書けますが、読む人には意図が伝わりにくいので、この演習ではsumとnrowで書いています。
高評価かつ推奨の件数はsum(high$recommend)——絞り込んだあとの表に対して、また同じ数え方をしています。
表示の%.1f%%に注目してください。%%でパーセント記号そのものを出します。%1つだと書式指定の始まりと解釈されるので、2つ重ねます。
recommendにNAが無いから、そのままsumできています。もし混ざっていたら結果はNAです——ステップ2で見たことが、ここでも同じように効きます。
推奨した人数: 8 推奨する人の割合: 66.7% mean で出しても: 66.7% 高評価(4点以上): 7件 そのうち推奨: 7件
STEP 6ひとつの関数にまとめて仕上げる
🎯 このステップのゴール: reportにまとめ、invisibleで戻り値を静かにする
最後の仕上げです。ここまでの部品をreportという1つの関数にまとめます。
summarize_columnの末尾にinvisible(valid)と書いています。
Rの関数は、最後に評価した式をそのまま返します。そしてトップレベルで返り値があると、勝手に画面に表示されます。
だから何も工夫しないと、catで出した行のあとに、返り値のベクトルまで表示されてしまいます。invisible()で包むと「返すけれど表示はしない」という意味になります。
この仕組みはRの標準関数でもよく使われています。print()やlibrary()が値を返しているのに何も出ないのは、invisibleのおかげです。
catとprintの使い分けも押さえておきましょう。catは書いたとおりに出します。printは[1]のような添字を付けます——人に見せる文章にはcatです。
sprintfで桁を揃えています。%-8sが左揃え、%2dが右揃えの2桁、%.2fが小数第2位まで。日本語は幅が2倍で表示されるので完全には揃いませんが、大きくは整います。
strrep("-", 44)で区切り線を引きます。ステップ3のpaste(rep(...))より短く書けます。
完成です。HIGH_SCOREを5に変えてみてください。高評価の件数が減り、分布も変わります。
answersのsatisfyをもう1つNAにしてみるのも良い確認になります。有効件数が減り、無回答の棒が伸びます——NAを除いて数えておいた効果が、そのまま表示に出ます。
===== アンケート集計 ===== 回答数: 12件 --- 数値項目 --- 年齢 有効10件(無回答2件) 平均 33.60 中央値 32.5 満足度 有効11件(無回答1件) 平均 3.73 中央値 4.0 --- 満足度の分布 --- 高評価 7件 ####### ふつう 2件 ## 低評価 2件 ## 無回答 1件 # --- 流入元 --- 検索 5件 平均満足度 4.60 友人 3件 平均満足度 4.00 広告 4件 平均満足度 2.50 -------------------------------------------- 推奨する人の割合: 66.7% 高評価(4点以上): 7件 そのうち推奨: 7件 推奨の割合は良好です。
よくある質問
このページではコードを実行しないのですか?
していません。Rはブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際のRで走らせた出力を、そのまま載せています。
平均が NA になってしまいます
データに NA が混ざっているからです。Rは「分からない値が入っているなら、平均も分からない」と考えます。
除き方は2つ。mean(x, na.rm = TRUE)と書くか、x[!is.na(x)]で先に取り除くかです。
後者を勧めます。na.rm = TRUEは静かに除くので、何件除いたかが出力から分かりません。「12件の平均」なのか「10件の平均」なのかは、読む人にとって大事な情報です。
if と ifelse は何が違いますか
扱える値の数が違います。
ifは1つの値にしか使えません。ベクトルを渡すと、Rのバージョンによって警告で1件目だけ見るか、エラーになります。
ifelseはベクトル全体を一度に処理し、同じ長さのベクトルを返します。データフレームの列を分類するなら、必ずこちらです。
「なぜか1件目しか判定されない」という症状が出たら、ifを使っていないか確かめてください。
df[条件, ] のカンマは必要ですか
必須です。data.frameの角括弧は[行の条件, 列の条件]という形だからです。
カンマの後ろを空けると「全部の列」という意味になります。カンマごと省くと、Rは列を選ぼうとしてまったく違う結果を返します。
もう1つの注意は条件にNAが混ざる場合です。NA >= 4はNAになるので、NAだらけの幽霊行が残ります。!is.na(x) & x >= 4のように、NAを先に除いてください。
🚩 次はこのカテゴリへ
Rと同じく数値計算に強い言語として、Juliaがあります。Rのベクトル化に当たるものが、Juliaではドット演算子です。考え方は似ていますが、Juliaは自分で書いた関数にも同じように使えます。Juliaカテゴリの最終演習では、平均・中央値・標準偏差をライブラリに頼らず自分で組み立てます。
Juliaのレッスンへ進む