🎯 最終演習

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.frameRでいちばんよく使う入れ物で、表そのものだと思ってください。

特徴は「列ごとに型が違ってよい」ことです。ageは数値、recommendは真偽値、channelは文字列。行方向には揃っている必要がありません

ただし列の長さは全部同じでなければなりません。1つでも足りないとエラーになります。表なので当然ですが、書き足すときに間違えやすい場所です。

id = 1:121: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 = TRUEmean(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件ぜんぶが一度に処理されます

ififelseは別物です。if1つの値にしか使えず、ベクトルを渡すとエラーか警告になります。ifelseベクトル全体を扱います。

この違いはRを書き始めた人が必ず踏みます。「なぜか1件目しか判定されない」という症状で現れます。

入れ子にすれば3つ以上に分けられます。順番が結果を決めます——is.na(scores)いちばん外に置いているのが要点です。

NA >= 4NAになります。真でも偽でもないので、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 >= 4NAになり、真でも偽でもないためです。

だから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——これだけです

recommendTRUEFALSEの列なのに、そのままsumできます。RではTRUEが1、FALSEが0として扱われるからです。

だからsumが「真の個数」になります。数え上げのループも、ifで数える変数も要りません。

この性質はいたるところで使えます。ステップ3のsum(labels == name)も、ステップ4の行の絞り込みも、すべて同じ仕組みです。「条件がベクトルで返り、それをそのまま使う」——Rらしさの核心がここにあります。

mean(df$recommend)と書けば、割合が直接出ます(1と0の平均は、1の割合そのものです)。知っていると短く書けますが、読む人には意図が伝わりにくいので、この演習ではsumnrowで書いています。

高評価かつ推奨の件数はsum(high$recommend)——絞り込んだあとの表に対して、また同じ数え方をしています。

表示の%.1f%%に注目してください。%%でパーセント記号そのものを出します。%1つだと書式指定の始まりと解釈されるので、2つ重ねます。

recommendNAが無いから、そのまま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のおかげです。

catprintの使い分けも押さえておきましょう。catは書いたとおりに出します。print[1]のような添字を付けます——人に見せる文章にはcatです。

sprintfで桁を揃えています。%-8sが左揃え、%2dが右揃えの2桁、%.2fが小数第2位まで。日本語は幅が2倍で表示されるので完全には揃いませんが、大きくは整います。

strrep("-", 44)で区切り線を引きます。ステップ3のpaste(rep(...))より短く書けます

完成です。HIGH_SCORE5に変えてみてください。高評価の件数が減り、分布も変わります。

answerssatisfyをもう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 は何が違いますか

扱える値の数が違います。

if1つの値にしか使えません。ベクトルを渡すと、Rのバージョンによって警告で1件目だけ見るか、エラーになります

ifelseベクトル全体を一度に処理し、同じ長さのベクトルを返します。データフレームの列を分類するなら、必ずこちらです。

「なぜか1件目しか判定されない」という症状が出たら、ifを使っていないか確かめてください。

df[条件, ] のカンマは必要ですか

必須です。data.frameの角括弧は[行の条件, 列の条件]という形だからです。

カンマの後ろを空けると「全部の列」という意味になります。カンマごと省くと、Rは列を選ぼうとしてまったく違う結果を返します。

もう1つの注意は条件にNAが混ざる場合です。NA >= 4NAになるので、NAだらけの幽霊行が残ります!is.na(x) & x >= 4のように、NAを先に除いてください

🚩 次はこのカテゴリへ

Rと同じく数値計算に強い言語として、Juliaがあります。Rのベクトル化に当たるものが、Juliaではドット演算子です。考え方は似ていますが、Juliaは自分で書いた関数にも同じように使えます。Juliaカテゴリの最終演習では、平均・中央値・標準偏差をライブラリに頼らず自分で組み立てます。

Juliaのレッスンへ進む
広告スペース(バナー)
広告スペース(記事内)