公開データを収集するときに出典・基準日・単位を記録する
title と URL だけを集めるのではなく、reference date、publication date、access date、units、terms of use を1行に記録します。コード不要で使える CSV template と checklist を含みます。
10件の合成回答から、重複IDと未回答を分けて確認します。有効回答の分母を明示し、選択肢ごとの件数・割合と除外理由を新しいファイルに保存します。
この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English
対象読者アンケートをCSVへエクスポートした後、回答数や割合を自分で確認したい初心者向けです。
アンケートファイルの行数をそのまま回答者数として報告すると、重複回答が含まれる場合があります。また、未回答を分母に含めるかどうかによって、同じ選択肢の割合も変わります。この例では「Would you use it again?」という1つの質問を使い、集計前に決めるべきルールを練習します。何かを実行する前に、元データで重複と未回答を探してください。
python example.pyコードはresponses.csvをexample.pyの場所を基準に探します。出力は別のoutputsフォルダーへ書き込まれ、入力は変更されません。別の作業フォルダーからスクリプトのパスを指定して実行しても、同じ例の入力を読み取ります。
R005は「예」(Yes)と「아니오」(No)の両方を送信しています。この例では、どちらの回答が有効か判断する情報がないため、最初または最後の値を任意に選ばず、複数回出現するIDのすべての行を除外します。この方針はコードとsummaryファイルのduplicate_policyに記載されています。
| 指標 | 計算 | 例の結果 |
|---|---|---|
| 元のデータ行 | ヘッダーを除いて読み取った行 | 10 rows |
| 一意のID | 各IDを1回だけ数える | 9 |
| 重複として除外した行 | R005のすべての行 | 2 rows |
| 残った一意の回答者 | 10 − 2 | 8 |
| 有効回答 | 残った8件 − 未回答2件 | 6 |
完了率は75.00%で、残った8人の一意の回答者のうち6件が有効回答である割合です。各選択肢の割合では、この6件の有効回答を分母にします。9個の一意のIDは分母に使用しません。R005の回答はすべて分析から除外されているためです。
read_rowsは列、ID、選択肢を確認します。summarizeはまず各IDの出現回数を数え、その後、各行にduplicate_id、blank_answer、validのいずれか1つのステータスを付けます。重複行を先に判定するため、同じ行が重複除外と未回答除外の両方として二重に数えられることはありません。
"""설문 합성 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と回答の前後の空白は削除されます。未知の選択肢は黙って除外されず、スクリプトが停止します。出力フォルダーはすべての入力チェックが完了してから作成されます。
| 選択肢 | 件数 | 分母 | 割合 |
|---|---|---|---|
| 예 (Yes) | 3 | 6 | 50.00% |
| 아니오 (No) | 2 | 6 | 33.33% |
| 잘 모르겠음 (Not sure) | 1 | 6 | 16.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%にしないようにしてください。
| ファイル | 確認すること |
|---|---|
| summary.json | 元の行数、一意のID数、除外行、2種類の分母、割合 |
| choices.csv | 選択肢ごとの件数と有効回答の分母 |
| audit_rows.csv | データ行の順序、ID、回答、判定ステータス |
data_rowはヘッダーを除いて読み取ったデータの順序です。CSV読み取り時には物理的な空行をスキップするため、テキストエディターの行番号とは常に一致するとは限りません。未回答を表すには、完全な空行ではなく、R004のようにIDがありanswerだけが空欄の行を使用してください。
練習を繰り返すときは、最初の結果を別名へ移すか、ZIPを新しいフォルダーへ展開してください。R004の空欄回答を「예」(Yes)へ変更したコピーを実行してみます。重複を除外した後も回答者は8人で、有効回答は7件になります。「예」は4/7 = 57.14%、完了率は7/8 = 87.50%になるはずです。
一意の回答者全員の回答を空欄にする場合も考えられます。回答者が残っていても有効回答が0件なら、完了率は0.00%です。一方、選択肢割合の分母は0なので計算しません。0%と区別するため、JSONにはnull、CSVには空欄が記録されます。
| 停止理由 | 確認方法 | 次の手順 |
|---|---|---|
| 列名または順序が違う | 1行目がrespondent_id,answerであることを確認します | エクスポートしたコピーの列をルールに合わせます。 |
| IDが空欄または形式が違う | IDがRに続く3桁の数字であることを確認します | 合成例のルールに合うようにIDを修正します。 |
| 未知の回答 | 예、아니오、잘 모르겠음以外の値がないか確認します | 元データを確認し、入力ミスか新しい選択肢かを判断します。 |
| 列数が一致しない | 余分なカンマや不足しているカンマを確認します | CSV構造を修復したコピーで実行します。 |
| outputsがすでに存在する | 以前の結果フォルダーが存在しないか確認します | 結果を別名で保持し、その後もう一度実行します。 |
入力エラーがある場合、スクリプトは出力フォルダーを作成する前に停止します。エラーを避けるために先に行を削除すると、除外の根拠が失われます。元データを保持し、変更内容とルールをコピー側で記録してから再実行してください。未知の回答が重複行に含まれていても、入力チェックが先に停止します。
実際のアンケートでは、まず重複回答が何を意味するかを決めてください。後の回答が修正版なのか、同じ端末を複数人が使ったのか、IDの割り当てが間違っていたのかによって、処理方針は変わります。この例の「重複IDをすべて除外する」をすべてのアンケートへ自動適用せず、調査設計に合った根拠を設定してください。同じIDであることだけでは、実際に同一人物であることも証明できません。
このコードは、1つの質問に対する単一選択回答だけを扱います。複数選択、条件分岐でスキップされた質問、重み付け、自由記述回答の分類、標本の代表性、統計的有意性は含まれていません。CSV構造を確認する段階と、実際の研究結論を導く段階を区別し、レポートでは分析した件数と除外ルールを合わせて明示してください。
すべてのデータをメモリへ読み込むため、大規模アンケートの性能テストは別の課題です。結果の書き込み中にディスクエラーが発生すると部分的な出力が残る場合があるため、完了メッセージと3つのファイルを確認してください。この例の検証には、個人情報や実際のアンケートサービスとの接続は使用していません。
2026-09-19 · Windows 11 · CPython 3.12.14 · 標準ライブラリのみ · 一時コピーで実行
コード、入力データ、実行手順が含まれています。展開して、まずREADME.txtを読んでください。
サンプルZIPをダウンロードサンプルコード、ファイル名、入力キーは原文のままです。翻訳本文のコマンドと確認手順も併せて参照してください。
独自に作成した練習用資料 · 元のファイルを別に保管してから実行してください。
説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。