割合の合計は100%にそろえたい。でも、複数回答アンケートでそれをすると「何人が選んだか」が見えなくなります。今回の集計は、回答者5人で合計160%。このまま報告に使えます。
僕は、商品説明会のアンケートには「選んだ人数÷この設問の回答者数」を載せたいです。知りたいのは関心を持った人の割合。100%へそろえると、営業資料に書く意味まで変わります。
今日、6行のサンプルをPythonで集計したら、「価格」の割合が37.5%、50%、60%になりました。選んだのはどれも3人。割る数だけで、ここまで違います。
分母を「この設問に答えた人」に決める
「商品を選ぶときに重視する点」を複数選択で聞く練習用データです。
| 回答者ID | 選んだ項目 |
|---|---|
| A | 価格、使いやすさ |
| B | 価格、サポート |
| C | 使いやすさ、サポート |
| D | 価格 |
| E | 使いやすさ |
| F | 未回答 |
提出は6人分。この設問に答えたのは5人で、選択の延べ数は8件です。今回は1人1行を前提にします。
| 計算 | 結果 | 読めること |
|---|---|---|
| 3÷8 | 37.5% | 全選択に占める価格の割合 |
| 3÷6 | 50.0% | 提出者全体で価格を選んだ割合 |
| 3÷5 | 60.0% | この設問の回答者で価格を選んだ割合 |
37.5%を「回答者の37.5%が価格を重視」と説明すると誤りです。8は人数ではありません。
今回は最後の定義を採用し、未回答の1人も別に表示します。空欄と「重視する項目がない」は区別します。「特になし」は選択肢を別に用意する扱いです。
「その他」の長い自由記述も、集計表と別に読めるように残したいです。
Pythonで人数と選択率を一緒に出す
CPython 3.14.6で実行済み。次をsurvey.pyとして保存し、python3 survey.pyで動きます。追加パッケージや入力ファイルの準備は不要です。
入力はidとchoicesの2列。選択肢は半角の;区切りにしました。サービスの出力形式に合わせて調整します。
import csv
from collections import Counter
from io import StringIO
OPTIONS = ("価格", "使いやすさ", "サポート")
DATA = """id,choices
A,価格;使いやすさ
B,価格;サポート
C,使いやすさ;サポート
D,価格
E,使いやすさ
F,
"""
def summarize(rows):
seen = set()
counts = Counter()
answered = 0
for row in rows:
person = row["id"].strip()
if not person or person in seen:
raise ValueError("回答者IDが空欄または重複")
seen.add(person)
choices = {s.strip() for s in row["choices"].split(";")
if s.strip()}
unknown = choices - set(OPTIONS)
if unknown:
raise ValueError(f"未登録の選択肢: {sorted(unknown)}")
if choices:
answered += 1
counts.update(choices)
return len(seen), answered, counts
rows = list(csv.DictReader(StringIO(DATA)))
submitted, answered, counts = summarize(rows)
assert (submitted, answered) == (6, 5)
assert counts == Counter({"価格": 3, "使いやすさ": 3, "サポート": 2})
assert sum(counts.values()) == 8
print(f"提出{submitted}人/設問回答{answered}人/未回答{submitted-answered}人")
if answered:
for name in OPTIONS:
n = counts[name]
print(f"{name}: {n}人 / {answered}人 = {n / answered:.1%}")
print(f"選択率の合計: {sum(counts.values()) / answered:.1%}")
else:
print("設問回答0人: 選択率は計算しません")
print("assert: OK")
実行結果です。
提出6人/設問回答5人/未回答1人
価格: 3人 / 5人 = 60.0%
使いやすさ: 3人 / 5人 = 60.0%
サポート: 2人 / 5人 = 40.0%
選択率の合計: 160.0%
assert: OK
延べ8選択÷5人で160%。1人平均1.6項目です。同じ人が複数の項目に入るので、合計が100%を超えます。図にするなら項目ごとの棒グラフが合います。
「3人 / 5人」を割合の横に残す。
報告を受ける側にも効きます。60%だけだと、3人なのか300人なのか分かりません。この5人から市場全体の傾向までは読めません。
1人分の選択を先にまとめる理由
各行の選択肢をsetで重複のない集合にしてから、Counter.update()で件数を足します。手元の標準ライブラリの説明と実装も確認しました。
この順番に意味があります。
Aの欄を価格;使いやすさ;価格に変えると、そのまま数えた場合はAだけで価格が2件。setを通すと1件になり、全体の価格も3人のままでした。
全員の選択をまとめてからsetにすると、今度は別の人の「価格」まで1つになります。まとめる範囲は1行。ここを動かすと集計の単位が変わります。
同じIDが複数行あれば止めます。再回答を認めるなら、最新の提出だけ残す処理が必要です。この入力には提出日時がないので、新旧を選べません。
入力を変えた確認結果です。
| 入力の変更 | 確認した結果 |
|---|---|
| Aの価格を同じセル内で2回書く | 価格は3人のまま |
| Fの空欄を空白文字にする | 設問回答は5人のまま |
| 6人全員を未回答にする | 設問回答0人、選択数0件 |
| Aの行をもう1行足す | ValueError: 回答者IDが空欄または重複 |
| Aに「納期」を追加する | ValueError: 未登録の選択肢: ['納期'] |
前後の空白は除きますが、「価格」と「価格面」は統合しません。未登録の表記は止めます。選択肢を変えたらOPTIONSも更新します。
実ファイルはrowsを作る1行を次へ置き換えて読みます。UTF-8のCSVを想定し、BOM付きでも確認しました。
with open("survey.csv", encoding="utf-8-sig", newline="") as f:
rows = list(csv.DictReader(f))
直後の3つのassertはサンプル専用です。実データへ替える際は別のテストへ移します。列名はid,choicesにそろえ、列の過不足は事前に直す前提です。選択肢名に;が入る入力は扱いません。
報告書には「複数回答、設問回答5人、未回答1人」と添え、人数と割合を一緒に載せます。別の月と比べるときも、未回答を分母に入れるかをそろえます。今回持ち帰りたいのは、集計表に分母を残すこと。160%を100%へ直す作業は、これで不要です。
