リサーチ・情報源

アンケートCSVを集計する前に回答数・未回答・重複を確認する

10件の合成回答から、重複IDと未回答を分けて確認します。有効回答の分母を明示し、選択肢ごとの件数・割合と除外理由を新しいファイルに保存します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者アンケートをCSVへエクスポートした後、回答数や割合を自分で確認したい初心者向けです。

準備するもの
  • Python 3.12以降をインストールし、python --versionでインタープリターを確認します。追加パッケージは不要です。
  • 例のZIPを新しいフォルダーへ展開し、example.py、responses.csv、README.txtが同じ場所にあることを確認します。
  • example.pyがあるフォルダーでターミナルを開きます。環境によっては、Windowsではpy、macOSやLinuxではpython3コマンドを使用できます。
  • 練習用データは実在する回答者を含まない合成データです。R001のようなIDはこの例のために作成されています。

01まず10件の回答を自分で確認する

アンケートファイルの行数をそのまま回答者数として報告すると、重複回答が含まれる場合があります。また、未回答を分母に含めるかどうかによって、同じ選択肢の割合も変わります。この例では「Would you use it again?」という1つの質問を使い、集計前に決めるべきルールを練習します。何かを実行する前に、元データで重複と未回答を探してください。

  1. responses.csvをテキストエディターまたはCSVを読み取れるプログラムで開きます。1行目にはrespondent_id,answerの2列があります。
  2. ヘッダーを除いてデータ行が10行あることを数えます。R005が2回現れ、R004とR009のanswerが空欄であることを確認します。
  3. example.pyがあるフォルダーのターミナルで、以下のコマンドを実行します。
  4. outputsに作成されたsummary.json、choices.csv、audit_rows.csvをこの順に開きます。
bash
python example.py

コードはresponses.csvをexample.pyの場所を基準に探します。出力は別のoutputsフォルダーへ書き込まれ、入力は変更されません。別の作業フォルダーからスクリプトのパスを指定して実行しても、同じ例の入力を読み取ります。

02重複ルールと2種類の分母を区別する

R005は「예」(Yes)と「아니오」(No)の両方を送信しています。この例では、どちらの回答が有効か判断する情報がないため、最初または最後の値を任意に選ばず、複数回出現するIDのすべての行を除外します。この方針はコードとsummaryファイルのduplicate_policyに記載されています。

指標計算例の結果
元のデータ行ヘッダーを除いて読み取った行10 rows
一意のID各IDを1回だけ数える9
重複として除外した行R005のすべての行2 rows
残った一意の回答者10 − 28
有効回答残った8件 − 未回答2件6

完了率は75.00%で、残った8人の一意の回答者のうち6件が有効回答である割合です。各選択肢の割合では、この6件の有効回答を分母にします。9個の一意のIDは分母に使用しません。R005の回答はすべて分析から除外されているためです。

03確認と集計を含む完全なコード

read_rowsは列、ID、選択肢を確認します。summarizeはまず各IDの出現回数を数え、その後、各行にduplicate_id、blank_answer、validのいずれか1つのステータスを付けます。重複行を先に判定するため、同じ行が重複除外と未回答除外の両方として二重に数えられることはありません。

example.py
"""설문 합성 CSV의 중복 ID와 무응답을 점검한 뒤 유효 응답을 집계합니다."""

import csv
import json
import re
import sys
from collections import Counter
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path

BASE = Path(__file__).resolve().parent
INPUT = BASE / "responses.csv"
OUTPUT = BASE / "outputs"
CHOICES = ["예", "아니오", "잘 모르겠음"]


def percent(numerator, denominator):
    # 분모가 0이면 0%로 오해하지 않도록 계산 불가를 None으로 남깁니다.
    if denominator == 0:
        return None
    value = Decimal(numerator) * Decimal(100) / Decimal(denominator)
    return str(value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))


def read_rows():
    if INPUT.is_symlink() or not INPUT.is_file():
        raise ValueError("responses.csv는 일반 파일이어야 합니다.")
    rows = []
    with INPUT.open("r", encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream, strict=True)
        if reader.fieldnames != ["respondent_id", "answer"]:
            raise ValueError("열 이름과 순서는 respondent_id,answer여야 합니다.")
        for number, row in enumerate(reader, start=1):
            if None in row or any(value is None for value in row.values()):
                raise ValueError(f"데이터 {number}행: 열 수를 확인하세요.")
            respondent_id, answer = row["respondent_id"].strip(), row["answer"].strip()
            if not re.fullmatch(r"R[0-9]{3}", respondent_id):
                raise ValueError(f"데이터 {number}행: ID는 R001처럼 R과 숫자 세 자리여야 합니다.")
            if answer and answer not in CHOICES:
                raise ValueError(f"데이터 {number}행: 알 수 없는 응답 {answer!r}")
            rows.append({"data_row": number, "respondent_id": respondent_id, "answer": answer})
    return rows


def summarize(rows):
    frequencies = Counter(row["respondent_id"] for row in rows)
    duplicate_ids = sorted(key for key, count in frequencies.items() if count > 1)
    duplicates = set(duplicate_ids)
    counts = Counter()
    audit, blank_after_duplicates = [], 0
    for row in rows:
        # 중복 그룹에서는 첫 응답이나 마지막 응답을 임의로 고르지 않습니다.
        if row["respondent_id"] in duplicates:
            status = "duplicate_id"
        elif not row["answer"]:
            status = "blank_answer"
            blank_after_duplicates += 1
        else:
            status = "valid"
            counts[row["answer"]] += 1
        audit.append({**row, "status": status})

    duplicate_rows = sum(frequencies[key] for key in duplicate_ids)
    eligible = len(rows) - duplicate_rows  # 여기에는 빈 답변을 한 고유 ID도 포함됩니다.
    valid = sum(counts.values())
    summary = {
        "question": "다시 이용할 의향이 있나요?",
        "duplicate_policy": "exclude_all_rows_with_duplicate_id",
        "raw_rows": len(rows),
        "unique_ids": len(frequencies),
        "duplicate_ids": duplicate_ids,
        "duplicate_id_count": len(duplicate_ids),
        "excluded_duplicate_rows": duplicate_rows,
        "raw_blank_answer_rows": sum(not row["answer"] for row in rows),
        "eligible_unique_respondents": eligible,
        "blank_answer_rows_after_duplicates": blank_after_duplicates,
        "valid_answer_rows": valid,
        "completion_rate_percent": percent(valid, eligible),
        "choices": [
            {"answer": answer, "count": counts[answer], "denominator": valid,
             "percent": percent(counts[answer], valid)} for answer in CHOICES
        ],
    }
    return summary, audit


def main():
    if OUTPUT.exists() or OUTPUT.is_symlink():
        raise FileExistsError("outputs가 이미 있습니다. 기존 결과를 옮긴 뒤 실행하세요.")
    summary, audit = summarize(read_rows())  # 입력 검증과 집계를 마친 뒤 출력합니다.
    OUTPUT.mkdir()
    with (OUTPUT / "summary.json").open("x", encoding="utf-8") as stream:
        json.dump(summary, stream, ensure_ascii=False, indent=2)
        stream.write("\n")
    with (OUTPUT / "choices.csv").open("x", encoding="utf-8-sig", newline="") as stream:
        writer = csv.DictWriter(stream, fieldnames=["answer", "count", "denominator", "percent"])
        writer.writeheader()
        writer.writerows(summary["choices"])
    with (OUTPUT / "audit_rows.csv").open("x", encoding="utf-8-sig", newline="") as stream:
        writer = csv.DictWriter(stream, fieldnames=["data_row", "respondent_id", "answer", "status"])
        writer.writeheader()
        writer.writerows(audit)
    print(f"원시 {summary['raw_rows']}행 / 고유 ID {summary['unique_ids']}개")
    print(f"중복 제외 {summary['excluded_duplicate_rows']}행 / 남은 고유 응답자 {summary['eligible_unique_respondents']}명")
    print(f"빈 응답 {summary['blank_answer_rows_after_duplicates']}행 / 유효 응답 {summary['valid_answer_rows']}행")
    if not summary["valid_answer_rows"]:
        print("유효 응답이 없어 선택지 비율을 계산하지 않았습니다.")
    print(f"완료: {OUTPUT}")
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (OSError, ValueError, csv.Error) as error:
        print(f"중지: {error}", file=sys.stderr)
        raise SystemExit(2)

IDごと、選択肢ごとの出現回数はCounterで数え、割合はDecimalを使って小数点以下2桁に丸めます。IDと回答の前後の空白は削除されます。未知の選択肢は黙って除外されず、スクリプトが停止します。出力フォルダーはすべての入力チェックが完了してから作成されます。

04選択肢ごとの件数と割合を確認する

選択肢件数分母割合
예 (Yes)3650.00%
아니오 (No)2633.33%
잘 모르겠음 (Not sure)1616.67%

「예」(Yes)はR001、R003、R007の3件です。R005の「예」は除外されるため、4件ではありません。「아니오」(No)はR002とR008の2件、「잘 모르겠음」(Not sure)はR006の1件です。3つの選択肢の件数の合計6は、valid_answer_rowsと一致する必要があります。

choices.csvにはanswer、count、denominator、percentの4列があります。percentには50.00のように%記号のない数値文字列が記録されます。表へ移すときは、すでに100倍された割合であることに注意してください。スプレッドシートのパーセント形式をそのまま適用して5000%にしないようにしてください。

05除外理由を元の行まで追跡する

ファイル確認すること
summary.json元の行数、一意のID数、除外行、2種類の分母、割合
choices.csv選択肢ごとの件数と有効回答の分母
audit_rows.csvデータ行の順序、ID、回答、判定ステータス
  1. audit_rows.csvで、data_row 5と6がduplicate_idになっていることを確認します。どちらもR005である必要があります。
  2. data_row 4と10がblank_answerになっていることを確認します。IDはR004とR009です。
  3. 残りの6行がvalidであり、その件数がsummary.jsonと一致することを確認します。
  4. 元のresponses.csvが変更されていないことを確認します。同じコマンドを再度実行すると、既存のoutputsを保持したまま停止する必要があります。

data_rowはヘッダーを除いて読み取ったデータの順序です。CSV読み取り時には物理的な空行をスキップするため、テキストエディターの行番号とは常に一致するとは限りません。未回答を表すには、完全な空行ではなく、R004のようにIDがありanswerだけが空欄の行を使用してください。

06分母が変わる小さな実験

練習を繰り返すときは、最初の結果を別名へ移すか、ZIPを新しいフォルダーへ展開してください。R004の空欄回答を「예」(Yes)へ変更したコピーを実行してみます。重複を除外した後も回答者は8人で、有効回答は7件になります。「예」は4/7 = 57.14%、完了率は7/8 = 87.50%になるはずです。

一意の回答者全員の回答を空欄にする場合も考えられます。回答者が残っていても有効回答が0件なら、完了率は0.00%です。一方、選択肢割合の分母は0なので計算しません。0%と区別するため、JSONにはnull、CSVには空欄が記録されます。

07集計前に修正する入力エラー

停止理由確認方法次の手順
列名または順序が違う1行目がrespondent_id,answerであることを確認しますエクスポートしたコピーの列をルールに合わせます。
IDが空欄または形式が違うIDがRに続く3桁の数字であることを確認します合成例のルールに合うようにIDを修正します。
未知の回答예、아니오、잘 모르겠음以外の値がないか確認します元データを確認し、入力ミスか新しい選択肢かを判断します。
列数が一致しない余分なカンマや不足しているカンマを確認しますCSV構造を修復したコピーで実行します。
outputsがすでに存在する以前の結果フォルダーが存在しないか確認します結果を別名で保持し、その後もう一度実行します。

入力エラーがある場合、スクリプトは出力フォルダーを作成する前に停止します。エラーを避けるために先に行を削除すると、除外の根拠が失われます。元データを保持し、変更内容とルールをコピー側で記録してから再実行してください。未知の回答が重複行に含まれていても、入力チェックが先に停止します。

08実際のアンケートへ拡張するときに決めること

実際のアンケートでは、まず重複回答が何を意味するかを決めてください。後の回答が修正版なのか、同じ端末を複数人が使ったのか、IDの割り当てが間違っていたのかによって、処理方針は変わります。この例の「重複IDをすべて除外する」をすべてのアンケートへ自動適用せず、調査設計に合った根拠を設定してください。同じIDであることだけでは、実際に同一人物であることも証明できません。

このコードは、1つの質問に対する単一選択回答だけを扱います。複数選択、条件分岐でスキップされた質問、重み付け、自由記述回答の分類、標本の代表性、統計的有意性は含まれていません。CSV構造を確認する段階と、実際の研究結論を導く段階を区別し、レポートでは分析した件数と除外ルールを合わせて明示してください。

すべてのデータをメモリへ読み込むため、大規模アンケートの性能テストは別の課題です。結果の書き込み中にディスクエラーが発生すると部分的な出力が残る場合があるため、完了メッセージと3つのファイルを確認してください。この例の検証には、個人情報や実際のアンケートサービスとの接続は使用していません。

実行・検証の記録

2026-09-19 · Windows 11 · CPython 3.12.14 · 標準ライブラリのみ · 一時コピーで実行

  • 14個の独立テストに合格: 元の10行、一意ID 9個、重複2行の除外、残り8件、未回答2件、有効6件を確認
  • 選択肢の割合50.00%、33.33%、16.67%と、完了率75.00%を確認
  • 空データ、全回答空欄、全ID重複で分母が0になる場合の処理を確認
  • 重複グループ内の空欄回答が二重に除外されないことと、空白の正規化を確認
  • 入力エラーの拒否、既存結果の保持、実行前後で元ファイルのSHA-256が同一であることを確認
検証範囲の限界
  • 実際のアンケートサービスや個人情報は使用していません。
  • 標本の代表性、統計的有意性、重み付け、複数選択は検証していません。
  • 検証環境はWindowsで、大規模データでの性能は測定していません。

サイト全体の執筆・検証方針

自分で実行するためのサンプルファイル

コード、入力データ、実行手順が含まれています。展開して、まずREADME.txtを読んでください。

サンプルZIPをダウンロード

サンプルコード、ファイル名、入力キーは原文のままです。翻訳本文のコマンドと確認手順も併せて参照してください。

独自に作成した練習用資料 · 元のファイルを別に保管してから実行してください。

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。