Investigación y fuentes

Comprueba los recuentos de respuestas, las respuestas vacías y los duplicados antes de contabilizar una encuesta CSV

Separa los ID duplicados de las respuestas vacías en 10 respuestas sintéticas. Indica el denominador de respuestas válidas y guarda en un archivo nuevo los recuentos y porcentajes por opción, además de los motivos de exclusión.

Ver el índice

La traducción se ha realizado con IA. Comprueba el código, las unidades y los valores junto con el original. La revisión por hablantes nativos de cada idioma aún no se ha completado. English

Para quién esPrincipiantes que quieren comprobar por sí mismos los recuentos y porcentajes de respuestas después de exportar una encuesta a CSV.

Preparación
  • Instala Python 3.12 o posterior y comprueba el intérprete con python --version. No se necesitan paquetes adicionales.
  • Extrae el ZIP del ejemplo en una carpeta nueva y comprueba que example.py, responses.csv y README.txt estén juntos.
  • Abre una terminal en la carpeta que contiene example.py. Según tu entorno, puedes usar el comando py en Windows o python3 en macOS y Linux.
  • Los datos de práctica son sintéticos y no contienen encuestados reales. Los ID como R001 se crearon para este ejemplo.

01Lee primero las diez respuestas por tu cuenta

Si informas el número de filas de un archivo de encuesta como el número de encuestados, pueden incluirse envíos duplicados. El porcentaje de una misma opción también cambia según si las respuestas vacías se incluyen en el denominador. Este ejemplo utiliza una sola pregunta, “Would you use it again?”, para practicar las reglas que deben definirse antes de contabilizar. Antes de ejecutar nada, busca duplicados y respuestas vacías en el archivo original.

  1. Abre responses.csv en un editor de texto o en un programa que lea CSV. La primera línea tiene dos columnas: respondent_id,answer.
  2. Sin contar la cabecera, confirma que hay 10 filas de datos. Comprueba que R005 aparece dos veces y que las respuestas de R004 y R009 están vacías.
  3. Ejecuta el comando siguiente en una terminal situada en la carpeta que contiene example.py.
  4. Abre summary.json, choices.csv y audit_rows.csv creados en outputs, en ese orden.
bash
python example.py

El código localiza responses.csv en relación con la ubicación de example.py. La salida se escribe en una carpeta outputs separada y la entrada no se modifica. Aunque ejecutes el script mediante su ruta desde otra carpeta de trabajo, leerá el mismo archivo de entrada del ejemplo.

02Distingue la regla de duplicados y los dos denominadores

R005 envió tanto ‘예’ (Yes) como ‘아니오’ (No). Este ejemplo no elige arbitrariamente el primer ni el último valor; excluye todas las filas de cualquier ID que aparezca más de una vez, porque no existe información para determinar qué respuesta es válida. Esta política se indica en el código y en duplicate_policy dentro del archivo de resumen.

IndicadorCálculoResultado del ejemplo
Filas de datos originalesFilas leídas, sin contar la cabecera10 rows
ID únicosCada ID se cuenta una vez9
Filas excluidas por duplicadoTodas las filas de R0052 rows
Encuestados únicos restantes10 − 28
Respuestas válidas8 restantes − 2 respuestas vacías6

La tasa de finalización es 75.00%, es decir, la proporción de 6 respuestas válidas entre los 8 encuestados únicos restantes. El porcentaje de cada opción utiliza las 6 respuestas válidas como denominador. Los 9 ID únicos no se utilizan como denominador porque todas las respuestas de R005 fueron excluidas del análisis.

03Código completo con comprobaciones y recuento

read_rows comprueba las columnas, los ID y las opciones. summarize cuenta primero cuántas veces aparece cada ID y después asigna a cada fila uno de estos estados: duplicate_id, blank_answer o valid. Como las filas duplicadas se evalúan primero, una misma fila no se cuenta dos veces como exclusión por duplicado y por respuesta vacía.

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)

Cuenta las apariciones por ID y por opción con Counter, y redondea los porcentajes a dos decimales con Decimal. Se eliminan los espacios iniciales y finales de los ID y las respuestas. Las opciones desconocidas no se descartan silenciosamente; el script se detiene. La carpeta de salida solo se crea después de completar todas las comprobaciones de entrada.

04Comprueba los recuentos y porcentajes por opción

OpciónRecuentoDenominadorPorcentaje
예 (Yes)3650.00%
아니오 (No)2633.33%
잘 모르겠음 (Not sure)1616.67%

‘예’ (Yes) corresponde a las tres respuestas de R001, R003 y R007. El ‘예’ de R005 se excluyó, por lo que no son cuatro. ‘아니오’ (No) corresponde a dos respuestas, R002 y R008, y ‘잘 모르겠음’ (Not sure) a una respuesta, R006. La suma de los tres recuentos de opciones, 6, debe ser igual a valid_answer_rows.

choices.csv contiene cuatro columnas: answer, count, denominator y percent. percent guarda una cadena numérica como 50.00 sin el signo %. Al trasladarla a una tabla, recuerda que ya es un porcentaje multiplicado por 100. Evita aplicar directamente el formato de porcentaje de una hoja de cálculo y convertirlo en 5000%.

05Rastrea los motivos de exclusión hasta las filas originales

ArchivoQué comprobar
summary.jsonFilas originales, ID únicos, filas excluidas, ambos denominadores y tasas
choices.csvRecuento por opción y denominador de respuestas válidas
audit_rows.csvOrden de las filas de datos, ID, respuesta y estado asignado
  1. En audit_rows.csv, comprueba que data_row 5 y 6 sean duplicate_id. Ambos deben corresponder a R005.
  2. Comprueba que data_row 4 y 10 sean blank_answer. Los ID son R004 y R009.
  3. Comprueba que las seis filas restantes sean valid y que su recuento coincida con summary.json.
  4. Comprueba que el archivo responses.csv original no haya cambiado. Ejecutar el mismo comando otra vez debe detenerse conservando el outputs existente.

data_row es el orden de los datos leídos, sin contar la cabecera. Las líneas físicamente vacías se omiten al leer el CSV, por lo que no siempre coincide con los números de línea de un editor de texto. Para representar una falta de respuesta, utiliza una fila con ID y solo la respuesta vacía, como R004, en lugar de una línea completamente vacía.

06Un pequeño experimento en el que cambia el denominador

Al repetir la práctica, mueve el primer resultado con otro nombre o extrae el ZIP en una carpeta nueva. Prueba una copia en la que la respuesta vacía de R004 se cambie a ‘예’ (Yes). Después de eliminar duplicados siguen quedando 8 encuestados y 7 respuestas válidas. ‘예’ debe ser 4/7 = 57.14% y la tasa de finalización 7/8 = 87.50%.

También puedes considerar dejar vacía la respuesta de todos los encuestados únicos. Si siguen existiendo encuestados pero hay 0 respuestas válidas, la tasa de finalización es 0.00%. En cambio, el denominador de los porcentajes por opción es 0, por lo que no se calculan. JSON registra null y el CSV deja el campo vacío para distinguirlos de 0%.

07Errores de entrada que debes corregir antes de contabilizar

Motivo de detenciónCómo comprobarloSiguiente paso
Nombres de columnas u orden incorrectosComprueba que la primera línea sea respondent_id,answerHaz que las columnas de la copia exportada sigan las reglas.
ID vacío o formato incorrectoComprueba que el ID sea R seguido de tres dígitosCorrige los ID para que sigan las reglas del ejemplo sintético.
Respuesta desconocidaComprueba si hay valores distintos de 예, 아니오 y 잘 모르겠음Revisa los datos originales para determinar si es un error tipográfico o una opción nueva.
El número de columnas no coincideComprueba si faltan o sobran comasEjecuta una copia en la que se haya reparado la estructura CSV.
outputs ya existeComprueba si existe una carpeta de resultados anteriorConserva los resultados con otro nombre y vuelve a ejecutar.

Si hay un error de entrada, el script se detiene antes de crear la carpeta de salida. Si primero eliminas una fila para evitar el error, se pierde la evidencia de la exclusión. Conserva el original, registra los cambios y las reglas en una copia y vuelve a ejecutar. Incluso si una respuesta desconocida se encuentra en una fila duplicada, la comprobación de entrada se detiene primero.

08Qué debes decidir al ampliar el ejemplo a una encuesta real

En una encuesta real, primero decide qué significa un envío duplicado. La política de tratamiento depende de si una respuesta posterior es una revisión, si varias personas usan el mismo dispositivo o si se asignó un ID incorrecto. No apliques automáticamente la regla de este ejemplo de “excluir todos los ID duplicados” a cualquier encuesta; establece un criterio adecuado al diseño de la encuesta. Un mismo ID tampoco demuestra por sí solo que se trate realmente de la misma persona.

Este código también maneja únicamente respuestas de selección única a una sola pregunta. No incluye selección múltiple, preguntas omitidas por condiciones, ponderaciones, clasificación de respuestas de texto libre, representatividad de la muestra ni significación estadística. Distingue el paso de extraer conclusiones reales de investigación del paso de comprobar la estructura del CSV, e indica conjuntamente en el informe el número analizado y las reglas de exclusión.

Todos los datos se cargan en memoria, por lo que las pruebas de rendimiento para encuestas grandes son una tarea aparte. Si ocurre un error de disco al escribir los resultados, puede quedar una salida parcial, así que comprueba el mensaje de finalización y los tres archivos. No se utilizó información personal ni conexión con un servicio real de encuestas para verificar el ejemplo.

Registro de ejecución y verificación

2026-09-19 · Windows 11 · CPython 3.12.14 · solo biblioteca estándar · ejecutado en una copia temporal

  • Superó 14 pruebas independientes: se confirmaron 10 filas originales, 9 ID únicos, 2 filas duplicadas excluidas, 8 restantes, 2 vacías y 6 válidas
  • Se confirmaron porcentajes por opción de 50.00%, 33.33% y 16.67%, y una tasa de finalización de 75.00%
  • Se confirmó el manejo de denominador cero para datos vacíos, todas las respuestas vacías y todos los ID duplicados
  • Se confirmó que las respuestas vacías dentro de un grupo duplicado no se excluyen dos veces, además de la normalización de espacios
  • Se confirmó el rechazo de errores de entrada, la conservación de resultados existentes y un SHA-256 idéntico del original antes y después de la ejecución
Límites de la verificación
  • No se utilizó ningún servicio real de encuestas ni información personal.
  • No se verificaron la representatividad de la muestra, la significación estadística, las ponderaciones ni la selección múltiple.
  • El entorno de verificación fue Windows y no se midió el rendimiento con grandes volúmenes de datos.

Criterios de redacción y verificación de todo el sitio

Archivos de ejemplo para ejecutar

Incluye código, datos de entrada e instrucciones de ejecución. Extrae el ZIP y lee primero README.txt.

Descargar ZIP de ejemplo

El código de ejemplo, los nombres de archivo y las claves de entrada se mantienen como en el original. Consulta también los comandos y los pasos de comprobación del texto traducido.

Material de práctica original · Guarda los archivos originales por separado antes de ejecutarlo.

Fuentes de referencia

Las explicaciones y los ejemplos son de elaboración propia. Puedes consultar los comportamientos y conceptos relacionados en las siguientes fuentes oficiales.