Guías para herramientas de encuestas
Cómo revisar respuestas abiertas
Populiva no lee las respuestas de las encuestas. En su lugar, este script se ejecuta en tu propia computadora con la exportación de tu encuesta (y, si quieres, tu CSV de Populiva) y marca las respuestas que conviene revisar. Solo usa la biblioteca estándar de Python, así que no hay que instalar nada más que Python 3.
-
1. Ejecuta la revisión
Guarda el script de abajo como open_text_check.py, exporta tu encuesta en CSV y ejecútalo con los nombres de tus columnas de texto abierto. Agrega la exportación de tu estudio de Populiva para usar las señales de la encuesta (pegados, velocidad de escritura, la instrucción oculta para IA), unidas por SESSION_ID:
python3 open_text_check.py survey.csv --text Q7 Q12 --populiva populiva-export.csv --id SESSION_ID -
2. Qué marca
Texto con estilo de chatbot (frases como “As an AI” o “Certainly! Here's”), respuestas casi idénticas a las de otro participante y, si agregas tu exportación de Populiva, pegados en la encuesta, escritura más rápida de lo humano y respuestas que siguieron nuestra instrucción oculta para IA.
Escribe una copia de tu CSV con una columna check_flags. Una marca es una razón para volver a leer una respuesta, no una prueba. Revisa tú mismo las respuestas marcadas antes de rechazar a alguien, y recuerda que los participantes pueden apelar.
-
3. El script
Puedes usarlo y modificarlo libremente. Ajusta la lista de frases y los umbrales al principio según tu estudio.
#!/usr/bin/env python3 """Flag open-text answers worth a closer look (Populiva guide, ADR 0003). Runs on your own computer: answer text never leaves it. Standard library only. python3 open_text_check.py survey.csv --text Q7 Q12 [--populiva populiva-export.csv] [--id SESSION_ID] Writes survey-checked.csv with a `check_flags` column. Flags are reasons to read an answer again, never proof: review before rejecting anyone. """ import argparse, csv, difflib, re, sys PHRASES = [ # wording that often appears when a chatbot wrote the answer "as an ai", "as a language model", "i don't have personal", "i do not have personal", "i'm just an ai", "certainly! here", "here's a", "in conclusion,", "overall, it is important to", "it's important to note", "i hope this helps", "delve into", ] MIN_LENGTH = 40 # characters; shorter answers are too generic to compare DUPLICATE_RATIO = 0.9 # near-identical answers from different people FAST_TYPING_CPM = 1200 # Populiva snippet: characters per minute while typing def load(path): with open(path, newline="", encoding="utf-8-sig") as handle: rows = list(csv.DictReader(handle)) return rows, (list(rows[0].keys()) if rows else []) def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("survey") parser.add_argument("--text", nargs="+", required=True, help="open-text column names") parser.add_argument("--populiva", help="Populiva study CSV export, to use in-survey signals") parser.add_argument("--id", default="SESSION_ID", help="column that matches Populiva's SESSION_ID") args = parser.parse_args() rows, headers = load(args.survey) missing = [c for c in args.text + [args.id] if c not in headers] if missing: sys.exit(f"Missing columns in {args.survey}: {', '.join(missing)}") signals = {} if args.populiva: export, _ = load(args.populiva) signals = {row["SESSION_ID"]: row for row in export} flags = {i: set() for i in range(len(rows))} for column in args.text: answers = [(i, re.sub(r"\s+", " ", (row.get(column) or "").strip().lower())) for i, row in enumerate(rows)] long_answers = [(i, text) for i, text in answers if len(text) >= MIN_LENGTH] for i, text in long_answers: if any(phrase in text for phrase in PHRASES): flags[i].add(f"{column}: chatbot-style wording") for a in range(len(long_answers)): for b in range(a + 1, len(long_answers)): (i, x), (j, y) = long_answers[a], long_answers[b] if difflib.SequenceMatcher(None, x, y).ratio() >= DUPLICATE_RATIO: flags[i].add(f"{column}: near-duplicate of another participant") flags[j].add(f"{column}: near-duplicate of another participant") for i, row in enumerate(rows): signal = signals.get(row.get(args.id, "")) if not signal: continue if signal.get("snippet_pastes") not in (None, "", "0"): flags[i].add("pasted text in the survey") cpm = signal.get("snippet_chars_per_minute") or "" if cpm.isdigit() and int(cpm) > FAST_TYPING_CPM: flags[i].add("typed faster than people can") if "ai_instruction_followed" in (signal.get("quality_flags") or ""): flags[i].add("followed Populiva's hidden AI instruction") out = args.survey.rsplit(".", 1)[0] + "-checked.csv" with open(out, "w", newline="", encoding="utf-8") as handle: writer = csv.DictWriter(handle, fieldnames=headers + ["check_flags"]) writer.writeheader() for i, row in enumerate(rows): writer.writerow({**row, "check_flags": "; ".join(sorted(flags[i]))}) print(f"Wrote {out}: {sum(1 for f in flags.values() if f)} of {len(rows)} rows flagged for a closer look.") if __name__ == "__main__": main()