Codificador de preguntas abiertas

En investigación cuantitativa, las preguntas abiertas son el ingrediente incómodo. Todo el resto del cuestionario produce números directamente: escalas de 1 a 10, opciones múltiples, net promoter scores. Pero las preguntas abiertas producen texto libre, y el texto libre no entra en una regresión ni en un crosstab.

Para convertirlas en datos, existe un proceso que se llama codificación: alguien lee todas las respuestas, agrupa las que dicen lo mismo, asigna un número a cada grupo, y reemplaza el texto por ese número en la base de datos. “Hellmanns”, “la de tapa amarilla” y “hellmans (sic)” se convierten todas en el código 5, que corresponde a Hellmann’s.

Es un trabajo tedioso, repetitivo y propenso a errores de consistencia. Y cuando se trata de miles de respuestas a múltiples preguntas para varios proyectos corriendo en paralelo, es también caro. Decidí automatizarlo.


El problema en toda su complejidad

Antes de escribir una sola línea de código, tuve que entender bien el problema. Hay dos situaciones fundamentalmente distintas:

Cuando ya existe un libro de códigos. En estudios longitudinales, paneles o proyectos con múltiples olas, alguien ya definió las categorías en una vuelta anterior. Hay un archivo Excel —el libro de códigos, o LDC— con una lista de claves numéricas y los valores canónicos que les corresponden. La tarea aquí es imputar: tomar cada respuesta nueva y mapearla a la clave correcta. El desafío principal es la variabilidad ortográfica. “Cocca-cola”, “coca cola”, “CocaCola” y “la negra” son todas la clave 7.

Cuando no existe ningún libro de códigos. En estudios nuevos o preguntas que nunca se habían hecho, hay que inventar las categorías desde cero. El analista no sabe de antemano qué va a encontrar. Primero hay que descubrir qué categorías emergen de los datos, luego asignar cada respuesta a alguna, y finalmente generar un LDC reutilizable para futuras olas.

Ambos flujos comparten infraestructura pero tienen lógicas completamente distintas.


Un problema que nadie menciona: la respuesta multi-valor

Hay un tercer problema que no está en los manuales pero aparece todo el tiempo.

Imaginá una pregunta: “¿Qué cualidades debería tener un buen jugador de fútbol?”. Un encuestado responde: “Velocidad, técnica y garra”. Otro: “La actitud y el compromiso con el equipo”.

El primero mencionó tres cosas. El segundo mencionó dos. Pero en el archivo de datos, ambas ocupan una sola celda. Si codificamos esa celda como una unidad, perdemos menciones. En un estudio de imagen de marca o de atributos de producto, esas menciones perdidas pueden cambiar los resultados.

La solución es la expansión ad-hoc: detectar cuándo una respuesta contiene múltiples menciones reales, dividirlas, y distribuirlas en columnas adicionales. La columna original (P23) se queda con la primera mención (top of mind), y se crean columnas nuevas (P23adhoc2, P23adhoc3, etc.) para las siguientes. La detección no puede ser solo por regex —una coma puede separar menciones o puede ser parte de una frase narrativa— por eso la decisión final la toma el LLM.


La arquitectura

El orquestador (main.py) coordina todo: carga el archivo, detecta las preguntas, presenta el menú y llama a los flujos. Los módulos de procesamiento implementan cada modo. La capa de I/O maneja todo lo que toca disco. Hay módulos de soporte para normalización de texto, comunicación con la API y la interfaz de terminal.

main.py Seleccionar proyecto → Cargar → Menú preguntas IMPUTACIÓN LDC existe → asignar claves CODIFICACIÓN Sin LDC → crear categorías

Multi-proyecto y configuración por proyecto

La herramienta no fue diseñada para un único estudio. El menú inicial lista todos los proyectos disponibles en una carpeta central. Cada proyecto tiene su propia estructura:

proyectos/
  PROYECTO_A/
    data/          ← archivos Excel de entrada
    LDC/           ← libro(s) de códigos
    REFUERZOS/     ← variaciones aprendidas
    outputs/       ← resultados + checkpoints
  PROYECTO_B/
    APP/           ← subproyecto
    SC/            ← otro subproyecto

Los subproyectos se detectan automáticamente. Si los archivos siguen el patrón PROYECTO1.xlsx, PROYECTO2.xlsx, también pregunta el número de ola. Cada proyecto puede tener sus propios códigos especiales (en algunos estudios “OTRO” es 99, en otros es 88) configurados en un JSON central.


El flujo de imputación

Detección automática de especiales "no sé", "ninguno", vacíos → se asignan sin LLM Clasificación LLM en batches de 50 Prompt con LDC completo + refuerzos + ejemplos fonéticos ¿Quedó en OTRO? Segunda pasada (batches de 20) Re-clasifica con ejemplos reales de cada categoría Revisión interactiva + guardar Analista aprueba, edita o mueve · acumula refuerzos

Los refuerzos son el mecanismo de aprendizaje incremental. Cada vez que el LLM mapea una variación nueva (“cocca-cola” → Coca-Cola), esa relación se guarda en un JSON. La próxima vez, esas variaciones conocidas se inyectan en el prompt. Con el tiempo, el sistema necesita cada vez menos llamadas a la API.


El flujo de codificación (sin LDC)

FASE 1A — Definir categorías LLM ve todos los valores únicos + frecuencias → propone 10-25 nombres (sin asignar nada aún) Edición interactiva de categorías Analista agrega, elimina, renombra o fusiona FASE 2 — Clasificar valores (batches de 50) El LLM no puede inventar categorías nuevas Solo asigna a las ya definidas ¿Muchos en OTRO? (10+) Revisión final + guardar LDC Excel Distribución con frecuencias y % por categoría LDC reutilizable para próximas olas

La separación en dos fases —primero definir qué categorías crear, después asignar— es deliberada. Si se hace en un solo paso, el LLM tiende a crear demasiadas categorías específicas o a agrupar de forma inconsistente entre batches. Al fijar primero la lista, el segundo paso tiene un espacio de respuesta constante y produce clasificaciones coherentes.


Las frecuencias como señal de calidad

El LLM solo no siempre detecta bien qué merece categoría propia. Si hay 500 respuestas distintas, “ICBC” puede aparecer 5 veces con 5 grafías diferentes. Cada una parece un caso raro; juntas son una categoría relevante.

La solución fue pasarle las frecuencias normalizadas junto con los valores. Antes de armar el prompt, normalizo todos los textos (minúsculas, sin acentos, sin puntuación), cuento cuántas veces aparece cada término normalizado, y los más frecuentes van explícitamente al LLM. Esto también ayuda en la revisión interactiva, donde el analista ve el número de ocurrencias y el porcentaje que representa cada categoría. Saber que “OTRO” tiene el 23% de las respuestas es una señal para revisarlo con cuidado.


Resiliencia: checkpoints

Procesar mil respuestas implica docenas de llamadas a la API. Si la conexión falla a mitad, no querés empezar desde cero. Después de procesar cada pregunta, el estado completo del DataFrame se guarda en un archivo Parquet junto con un JSON de caché de los valores ya clasificados. Al iniciar, la herramienta busca checkpoints existentes y pregunta si continuar desde donde quedó.


La interactividad no es un defecto, es el producto

Una decisión de diseño que fui entendiendo con el tiempo: la automatización total no es el objetivo. El objetivo es que el analista confíe en el resultado para defenderlo ante el cliente.

Antes de clasificar, el analista puede editar las categorías propuestas. Después de clasificar, puede revisar cada categoría y mover los valores mal asignados. Antes de guardar, ve una distribución completa con frecuencias y porcentajes.

Lo que el LLM hace es eliminar el trabajo mecánico. Lo que el analista hace es tomar las decisiones que requieren criterio de dominio: si “Light” y “Light & Fit” deben ser la misma categoría o categorías separadas, qué hacer con las respuestas que critican el estudio en vez de responder la pregunta.


Resultado en producción

Una base con 1.500 respuestas a 4 preguntas abiertas que antes tomaba entre 4 y 6 horas de trabajo manual ahora toma entre 25 y 45 minutos: 15-25 de procesamiento automático y 10-20 de revisión interactiva. La detección de variantes ortográficas es más robusta que la humana en casos de errores de tipeo poco obvios, y no hay deriva de consistencia a lo largo de sesiones largas.

El código está en mi GitHub.