Pesquisa e fontes

Verifique contagens de respostas, respostas em branco e duplicatas antes de contabilizar uma pesquisa CSV

Separe IDs duplicados de respostas em branco em 10 respostas sintéticas. Informe o denominador das respostas válidas e salve em um novo arquivo as contagens e porcentagens por opção, além dos motivos de exclusão.

Ver o sumário

A tradução foi feita com IA. Confira o código, as unidades e os valores junto com o original. A revisão por falantes nativos de cada idioma ainda não foi concluída. English

Para quem éIniciantes que querem verificar por conta própria as contagens e porcentagens de respostas depois de exportar uma pesquisa para CSV.

Preparação
  • Instale o Python 3.12 ou posterior e verifique o interpretador com python --version. Nenhum pacote adicional é necessário.
  • Extraia o ZIP do exemplo em uma nova pasta e confirme que example.py, responses.csv e README.txt estão juntos.
  • Abra um terminal na pasta que contém example.py. Dependendo do ambiente, você pode usar o comando py no Windows ou python3 no macOS e Linux.
  • Os dados de prática são sintéticos e não contêm respondentes reais. IDs como R001 foram criados para este exemplo.

01Leia primeiro as dez respostas por conta própria

Se você informar o número de linhas de um arquivo de pesquisa como o número de respondentes, envios duplicados podem ser incluídos. A porcentagem da mesma opção também muda dependendo de as respostas em branco estarem ou não no denominador. Este exemplo usa uma única pergunta, “Would you use it again?”, para praticar as regras que devem ser definidas antes da contabilização. Antes de executar qualquer coisa, procure duplicatas e respostas em branco no arquivo original.

  1. Abra responses.csv em um editor de texto ou programa que leia CSV. A primeira linha tem duas colunas: respondent_id,answer.
  2. Sem contar o cabeçalho, confirme que há 10 linhas de dados. Verifique que R005 aparece duas vezes e que as respostas de R004 e R009 estão em branco.
  3. Execute o comando abaixo em um terminal na pasta que contém example.py.
  4. Abra summary.json, choices.csv e audit_rows.csv criados em outputs, nessa ordem.
bash
python example.py

O código localiza responses.csv com base na localização de example.py. A saída é gravada em uma pasta outputs separada, e a entrada não é alterada. Mesmo que você execute o script pelo caminho a partir de outra pasta de trabalho, ele lê o mesmo arquivo de entrada do exemplo.

02Diferencie a regra de duplicatas e os dois denominadores

R005 enviou tanto ‘예’ (Yes) quanto ‘아니오’ (No). Este exemplo não escolhe arbitrariamente o primeiro ou o último valor; ele exclui todas as linhas de qualquer ID que apareça mais de uma vez, porque não há informação para decidir qual resposta é válida. Essa política é declarada no código e em duplicate_policy no arquivo de resumo.

IndicadorCálculoResultado do exemplo
Linhas de dados originaisLinhas lidas, sem o cabeçalho10 rows
IDs únicosCada ID contado uma vez9
Linhas excluídas como duplicadasTodas as linhas de R0052 rows
Respondentes únicos restantes10 − 28
Respostas válidas8 restantes − 2 respostas em branco6

A taxa de conclusão é 75.00%, a proporção de 6 respostas válidas entre os 8 respondentes únicos restantes. A porcentagem de cada opção usa as 6 respostas válidas como denominador. Os 9 IDs únicos não são usados como denominador porque todas as respostas de R005 foram excluídas da análise.

03Código completo com verificações e contabilização

read_rows verifica as colunas, os IDs e as opções. summarize primeiro conta quantas vezes cada ID aparece e depois atribui a cada linha um destes status: duplicate_id, blank_answer ou valid. Como as linhas duplicadas são avaliadas primeiro, a mesma linha não é contada duas vezes como exclusão por duplicata e por resposta em branco.

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)

As ocorrências por ID e por opção são contadas com Counter, e as porcentagens são arredondadas para duas casas decimais com Decimal. Espaços no início e no fim dos IDs e respostas são removidos. Opções desconhecidas não são descartadas silenciosamente; o script é interrompido. A pasta de saída só é criada depois que todas as verificações de entrada terminam.

04Verifique as contagens e porcentagens por opção

OpçãoContagemDenominadorPorcentagem
예 (Yes)3650.00%
아니오 (No)2633.33%
잘 모르겠음 (Not sure)1616.67%

‘예’ (Yes) corresponde às três respostas de R001, R003 e R007. O ‘예’ de R005 foi excluído, portanto não são quatro. ‘아니오’ (No) corresponde a duas respostas, R002 e R008, e ‘잘 모르겠음’ (Not sure) corresponde a uma resposta, R006. A soma das contagens das três opções, 6, deve ser igual a valid_answer_rows.

choices.csv fornece quatro colunas: answer, count, denominator e percent. percent registra uma string numérica como 50.00 sem o sinal %. Ao transferi-la para uma tabela, lembre-se de que ela já é uma porcentagem multiplicada por 100. Tenha cuidado para não aplicar diretamente a formatação de porcentagem de uma planilha e transformá-la em 5000%.

05Rastreie os motivos de exclusão até as linhas originais

ArquivoO que verificar
summary.jsonLinhas originais, IDs únicos, linhas excluídas, ambos os denominadores e taxas
choices.csvContagem por opção e denominador de respostas válidas
audit_rows.csvOrdem das linhas de dados, ID, resposta e status atribuído
  1. Em audit_rows.csv, verifique que data_row 5 e 6 são duplicate_id. Ambos devem ser R005.
  2. Verifique que data_row 4 e 10 são blank_answer. Os IDs são R004 e R009.
  3. Verifique que as seis linhas restantes são valid e que a contagem corresponde a summary.json.
  4. Verifique que o responses.csv original não foi alterado. Executar o mesmo comando novamente deve interromper o processo mantendo o outputs existente.

data_row representa a ordem dos dados lidos, excluindo o cabeçalho. Linhas fisicamente vazias são ignoradas durante a leitura do CSV, portanto esse número nem sempre corresponde às linhas de um editor de texto. Para representar uma ausência de resposta, use uma linha com ID e somente a resposta em branco, como R004, em vez de uma linha completamente vazia.

06Um pequeno experimento em que o denominador muda

Ao repetir a prática, mova o primeiro resultado para outro nome ou extraia o ZIP em uma nova pasta. Tente executar uma cópia em que a resposta em branco de R004 seja alterada para ‘예’ (Yes). Depois de remover duplicatas, continuam existindo 8 respondentes e passam a existir 7 respostas válidas. ‘예’ deve ser 4/7 = 57.14%, e a taxa de conclusão 7/8 = 87.50%.

Você também pode considerar deixar em branco a resposta de todos os respondentes únicos. Se ainda houver respondentes, mas existirem 0 respostas válidas, a taxa de conclusão será 0.00%. Por outro lado, o denominador das porcentagens por opção será 0, então elas não serão calculadas. O JSON registra null e o CSV deixa o campo em branco para diferenciá-los de 0%.

07Erros de entrada que devem ser corrigidos antes da contabilização

Motivo da interrupçãoComo verificarPróximo passo
Nomes de colunas ou ordem incorretosVerifique que a primeira linha seja respondent_id,answerFaça as colunas da cópia exportada seguirem as regras.
ID em branco ou formato incorretoVerifique que o ID seja R seguido por três dígitosCorrija os IDs para seguirem as regras do exemplo sintético.
Resposta desconhecidaVerifique valores diferentes de 예, 아니오 e 잘 모르겠음Confira os dados de origem para determinar se é um erro de digitação ou uma nova opção.
Quantidade de colunas não correspondeVerifique vírgulas extras ou ausentesExecute com uma cópia em que a estrutura CSV tenha sido corrigida.
outputs já existeVerifique se já existe uma pasta de resultados anteriorMantenha os resultados com outro nome e execute novamente.

Se houver um erro de entrada, o script é interrompido antes de criar a pasta de saída. Se você excluir primeiro a linha para evitar o erro, perde a evidência da exclusão. Mantenha o original, registre as alterações e regras em uma cópia e execute novamente. Mesmo que uma resposta desconhecida esteja em uma linha duplicada, a verificação de entrada é interrompida primeiro.

08O que decidir ao expandir para uma pesquisa real

Em uma pesquisa real, primeiro decida o que significa um envio duplicado. A política de tratamento depende de uma resposta posterior ser uma revisão, de várias pessoas usarem o mesmo dispositivo ou de um ID ter sido atribuído incorretamente. Não aplique automaticamente a regra deste exemplo de “excluir todos os IDs duplicados” a qualquer pesquisa; defina uma base adequada ao desenho da pesquisa. O mesmo ID, por si só, também não prova que se trata realmente da mesma pessoa.

Este código também lida apenas com respostas de escolha única para uma pergunta. Ele não inclui múltipla escolha, perguntas puladas por condições, pesos, classificação de respostas de texto livre, representatividade da amostra ou significância estatística. Diferencie a etapa de tirar conclusões reais de pesquisa da etapa de verificar a estrutura do CSV e informe em conjunto, no relatório, a quantidade analisada e as regras de exclusão.

Todos os dados são carregados na memória, portanto testes de desempenho para pesquisas grandes são uma tarefa separada. Se ocorrer um erro de disco durante a gravação dos resultados, uma saída parcial pode permanecer, então verifique a mensagem de conclusão e os três arquivos. Nenhuma informação pessoal ou conexão com um serviço real de pesquisa foi usada para verificar o exemplo.

Registro de execução e verificação

2026-09-19 · Windows 11 · CPython 3.12.14 · somente biblioteca padrão · executado em uma cópia temporária

  • Passou em 14 testes independentes: foram confirmadas 10 linhas originais, 9 IDs únicos, 2 linhas duplicadas excluídas, 8 restantes, 2 em branco e 6 válidas
  • Foram confirmadas porcentagens por opção de 50.00%, 33.33% e 16.67%, além de taxa de conclusão de 75.00%
  • Foi confirmado o tratamento de denominador zero para dados vazios, todas as respostas em branco e todos os IDs duplicados
  • Foi confirmado que respostas em branco em um grupo duplicado não são excluídas duas vezes, além da normalização de espaços
  • Foram confirmados a rejeição de erros de entrada, a preservação de resultados existentes e SHA-256 idêntico do original antes e depois da execução
Limites da verificação
  • Nenhum serviço real de pesquisa ou informação pessoal foi utilizado.
  • Representatividade da amostra, significância estatística, pesos e múltipla escolha não foram verificados.
  • O ambiente de verificação foi Windows, e o desempenho com grandes volumes de dados não foi medido.

Princípios de redação e verificação de todo o site

Arquivos de exemplo para executar

Inclui código, dados de entrada e instruções de execução. Extraia o ZIP e leia primeiro o README.txt.

Baixar ZIP de exemplo

O código de exemplo, os nomes de arquivos e as chaves de entrada são mantidos como no original. Consulte também os comandos e os procedimentos de conferência do texto traduzido.

Material de prática original · Guarde os arquivos originais separadamente antes de executar.

Fontes de referência

As explicações e os exemplos são de elaboração própria. Os comportamentos e conceitos relacionados podem ser consultados nas fontes oficiais abaixo.