IA para Científicos Sociales

Sesión 4.4: Laboratorio 8 - Codificación cualitativa y confiabilidad con LLMs

Danilo Freire

Department of Data and Decision Sciences
Emory University

Laboratorio 8: Codificación y confiabilidad con LLMs

Objetivos del laboratorio

Lo que vamos a hacer

  1. Definir un libro de códigos y codificar discursos con quallmer
  2. Validar la codificación del LLM contra puntajes humanos
  3. Medir la confiabilidad replicando la codificación
  4. Auditar sesgos del modelo por género y por país

La idea central

  • El Lab 7 preguntaba: ¿puede un LLM hacer la tarea?
  • El Lab 8 pregunta: ¿puedo confiar en el resultado como una medida?
  • Es la tradición del análisis de contenido: codebook, validación y acuerdo entre codificadores

Seguimos con OpenRouter. Necesitan la API key del Lab 7

Estructura del laboratorio

Parte Contenido Duración
1 Codificar y medir confiabilidad con quallmer ~25 min
2 Auditoría de sesgos por género y país ~15 min
3 Datos sintéticos y su validez ~10 min
Ejercicios; apéndice: mini-proyecto extra


Usamos un corpus nuevo: 15 fragmentos de discursos políticos con un puntaje humano de retórica liberal-iliberal

Parte 1: Codificar y medir confiabilidad

El paquete quallmer

  • En investigación cualitativa, codificar es asignar categorías o puntajes a fragmentos de texto
  • Tradicionalmente lo hacen personas, leyendo y etiquetando: es rico, pero lento y caro
  • Dos codificadores rara vez coinciden al 100%: medimos el acuerdo con Cohen’s kappa o el alpha de Krippendorff
  • quallmer (quallmer.github.io) aplica esa misma lógica usando un LLM como codificador
  • Construido sobre ellmer: usa los mismos proveedores (OpenRouter, Ollama, etc.)
  • Pensado para investigación: trae validación, confiabilidad y trazabilidad incorporadas

El flujo de trabajo

  1. qlm_codebook(): definir el libro de códigos
  2. qlm_code(): codificar los textos
  3. qlm_validate(): validar contra humanos (exactitud)
  4. qlm_replicate() + qlm_compare(): confiabilidad
  5. qlm_trail(): trazabilidad

Tratamos al LLM como un codificador más, y lo sometemos al mismo escrutinio que a un humano

Instalar y cargar

R> # quallmer está en GitHub (no en CRAN todavía)
R> install.packages("pak")
R> pak::pak("quallmer/quallmer")
R> library(quallmer)
R> library(ellmer)
R> library(tidyverse)
R> 
R> # Verificar que la API key del Lab 7 sigue configurada
R> Sys.getenv("OPENROUTER_API_KEY") != ""
[1] FALSE
R> discursos <- read_csv("datos/discursos_ideologia.csv", show_col_types = FALSE)
R> # Lean el archivo directo desde la web:
R> # discursos <- read_csv("https://raw.githubusercontent.com/danilofreire/introduccion-ia-ucu/main/clases/dia-04/datos/discursos_ideologia.csv", show_col_types = FALSE)
R> 
R> head(discursos)
# A tibble: 6 × 4
     id orador   texto                                              score_humano
  <dbl> <chr>    <chr>                                                     <dbl>
1     1 Orador A La patria está por encima de todo. Quienes no res…           -9
2     2 Orador B El pueblo verdadero sabe quiénes son sus enemigos…           -8
3     3 Orador C Necesitamos un líder fuerte que ponga orden de un…           -7
4     4 Orador D Debemos proteger nuestra identidad nacional frent…           -5
5     5 Orador E La estabilidad y la seguridad deben ser la priori…           -4
6     6 Orador F El gobierno tiene que equilibrar el orden público…           -1

quallmer pide su propia copia de ellmer. La instalación tarda un minuto la primera vez

El caso: retórica liberal-iliberal

  • Una pregunta clásica de la ciencia política: ¿qué tan liberal o iliberal es el discurso de un político?
  • Iliberal: nacionalismo, mano dura, orden y seguridad, distinción “nosotros / ellos”, rechazo al pluralismo
  • Liberal: derechos individuales, tolerancia, pluralismo, libertades civiles, Estado de derecho
  • Medimos cada discurso en una escala de -10 (iliberal) a +10 (liberal)
  • Es una variable continua, no una categoría: nos deja usar correlación y acuerdo de intervalo

Nuestro corpus

  • 15 fragmentos de discursos políticos latinoamericanos (ficticios, para el ejemplo)
  • Cada uno con un score_humano: el puntaje que le dio una persona
  • Inspirado en el ejemplo de iliberalismo de quallmer
R> discursos |>
+   select(orador, score_humano) |> 
+   head()
# A tibble: 6 × 2
  orador   score_humano
  <chr>           <dbl>
1 Orador A           -9
2 Orador B           -8
3 Orador C           -7
4 Orador D           -5
5 Orador E           -4
6 Orador F           -1

Paso 1: definir el codebook

R> codebook_ideologia <- qlm_codebook(
+   name = "retorica_liberal",
+   instructions = "Analizá el estilo retórico de este fragmento de discurso político latinoamericano.
+ Retórica ILIBERAL (puntajes negativos): nacionalismo, apelaciones al orden y la
+ seguridad, tradición, mano dura, distinción entre 'nosotros' y 'ellos', rechazo al pluralismo.
+ Retórica LIBERAL (puntajes positivos): derechos individuales, tolerancia, pluralismo,
+ libertades civiles, derechos de las minorías, Estado de derecho, sociedad abierta.
+ Un puntaje de 0 indica retórica neutral o mixta.",
+   schema = type_object(
+     score = type_integer("Puntaje de -10 (iliberal) a +10 (liberal)"),
+     explicacion = type_string("Breve justificación del puntaje")
+   ),
+   role = "Sos un cientista político experto en analizar retórica política.",
+   levels = list(score = "interval", explicacion = "nominal")
+ )

El schema usa los tipos de ellmer (Lab 7). type_integer() pide un número entero; levels le dice a quallmer que score es una variable de intervalo, para calcular las métricas correctas. Más información en https://quallmer.github.io/quallmer/reference/qlm_codebook.html

Paso 2: codificar los discursos

R> codificado <- qlm_code(
+   discursos$texto,
+   codebook_ideologia,
+   model = "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
+   max_active = 1,   # pocas peticiones a la vez: los modelos :free se saturan
+   name = "nemotron"
+ )
R> 
R> codificado
# quallmer coded object
# Run:      nemotron
# Codebook: retorica_liberal
# Model:    openrouter/nvidia/nemotron-3-super-120b-a12b:free
# Units:    15

# A tibble: 15 × 3
     .id score explicacion                                                      
 * <int> <int> <chr>                                                            
 1     1    -3 "El fragmento presenta múltiples componentes de retórica ilibera…
 2     2    -3 "El fragmento enfatiza una visión excluyente del pueblo, identif…
 3     3    -3 "El fragmento presenta una retórica claramente illiberal. Se enf…
 4     4    -2 "El fragmento enfatiza la protección de la identidad nacional y …
 5     5    -4 "El discurso enfatiza la estabilidad, la seguridad y el orden, i…
 6     6     2 "El fragmento enfatiza la necesidad de equilibrar el orden públi…
 7     7     2 "El fragmento enfatiza evidencia, diálogo amplio y soluciones pr…
 8     8     3 "El fragmento enfatiza explícitamente valores liberales: 'libert…
 9     9     2 "El fragmento destaca el respeto igualitario ante la ley sin imp…
10    10     3 "El fragmento enfatiza el acceso igualitario a la educación (der…
11    11     2 "El fragmento exhibe claramente retórica LIBERAL. Defiende explí…
12    12     2 "El fragmento enfatiza instituciones democráticas, independencia…
13    13     2 "El fragmento presenta una retórica claramente liberal según los…
14    14     3 "El fragmento enfatiza libertades civiles, igualdad ante la ley,…
15    15    -3 "El fragmento exhibe retórica claramente iliberal, con múltiples…

Advertencia

Con max_active alto, los modelos :free devuelven errores 400. Bájenlo a 2-3, usen muestras chicas, o pasen a un modelo de pago. Para corpus grandes conviene un modelo local (Ollama, día 5)

Inspeccionar la codificación

R> # Comparar el puntaje del LLM con el humano
R> tibble(
+   orador       = discursos$orador,
+   score_humano = discursos$score_humano,
+   score_llm    = codificado$score
+ )
# A tibble: 15 × 3
   orador   score_humano score_llm
   <chr>           <dbl>     <int>
 1 Orador A           -9        -3
 2 Orador B           -8        -3
 3 Orador C           -7        -3
 4 Orador D           -5        -2
 5 Orador E           -4        -4
 6 Orador F           -1         2
 7 Orador G            0         2
 8 Orador H            1         3
 9 Orador I            4         2
10 Orador J            5         3
11 Orador K            6         2
12 Orador L            7         2
13 Orador M            8         2
14 Orador N            9         3
15 Orador O           -8        -3

Miren la diferencia: el LLM tiende a ser más conservador, sus puntajes se acercan al centro (0). Capta bien el orden (quién es más iliberal que quién), pero comprime la magnitud. Esto es justo lo que la validación va a cuantificar

Paso 3: validar contra humanos

R> # El "gold standard": el puntaje humano de cada discurso
R> gold <- qlm_humancoded(
+   tibble(.id = seq_len(nrow(discursos)), score = discursos$score_humano),
+   name = "humano",
+   codebook = codebook_ideologia
+ )
R> 
R> # ¿Qué tan cerca está el LLM del juicio humano?
R> # qlm_validate devuelve un tibble; lo mostramos como una tabla
R> validacion <- qlm_validate(codificado, gold = gold, by = "score", level = "interval")
R> as_tibble(validacion) |> select(measure, value)
# A tibble: 4 × 2
  measure value
  <chr>   <dbl>
1 r       0.873
2 mae     3.67 
3 rmse    4.07 
4 icc     0.650
  • r (Pearson): ¿el LLM ordena los discursos como el humano? (correlación)
  • mae / rmse: error promedio, en puntos de la escala
  • icc (interclase): acuerdo absoluto, castiga la diferencia de magnitud

Paso 4: confiabilidad (replicar y comparar)

R> # Codificar los MISMOS textos: confiabilidad test-retest
R> codificado_b <- qlm_replicate(codificado, name = "nemotron_b")
R> 
R> # ¿Coinciden las dos corridas? (acuerdo entre codificadores)
R> confiabilidad <- qlm_compare(
+   codificado, codificado_b,
+   by = "score", level = "interval", tolerance = 2
+ )
R> as_tibble(confiabilidad) |> select(measure, value)
# A tibble: 4 × 2
  measure           value
  <chr>             <dbl>
1 percent_agreement 1    
2 alpha_interval    0.986
3 icc               0.987
4 r                 0.988

qlm_replicate() repite la codificación. Con el mismo modelo mide robustez. Cambiando model = a otro modelo, mide confiabilidad entre modelos. Acá usamos el mismo modelo porque los :free se saturan; pero la lógica es idéntica

alpha_interval (Krippendorff): el estándar para acuerdo entre codificadores (< 0.67 poco confiable, 0.67-0.80 aceptable, > 0.80 bueno). tolerance es la tolerancia para considerar que dos puntajes “coinciden” (ej. ±2 puntos)

Validez (vs humanos) y confiabilidad (entre corridas) son cosas distintas. Un modelo puede ser muy estable y aun así estar sistemáticamente sesgado

Trazabilidad: el audit trail

R> # Registro completo: codebook, modelo, parámetros, fecha, las dos corridas
R> # qlm_trail() escribe el .rds y devuelve el registro de forma invisible,
R> # así que lo asignamos y lo mostramos en una línea aparte
R> trail <- qlm_trail(codificado, codificado_b, confiabilidad, path = "trail_ideologia")
R> trail
# quallmer audit trail (3 runs)

1. nemotron (original)
   2026-06-10 06:29 | openrouter/nvidia/nemotron-3-super-120b-a12b:free
   Codebook: retorica_liberal

2. nemotron_b (parent: nemotron)
   2026-06-10 07:28 | openrouter/nvidia/nemotron-3-super-120b-a12b:free
   Codebook: retorica_liberal

3. comparison_b8fe6d3e (parents: nemotron, nemotron_b)
   2026-06-10 07:28 | unknown
   Comparison: interval level | 10 subjects | 2 raters
  • qlm_trail() sigue la idea de Lincoln y Guba (1985) sobre confiabilidad en investigación cualitativa
  • Guarda en un .rds todo lo necesario para que otra persona replique su codificación
  • Es la defensa 4 de la sesión 4.2 (validación y reproducibilidad), automatizada
  • Va como material suplementario del paper

quallmer: capacidad vs confianza

Diccionario / LDA (Día 3) LLM solo (Lab 7) LLM + quallmer (Lab 8)
Qué da Una clasificación Una clasificación Una medida con validación
Pregunta ¿Agrupa el texto? ¿Puede hacerlo? ¿Puedo confiar en el número?
Reporta Nada por sí solo Nada por sí solo Validez, confiabilidad, trazabilidad


El salto del Lab 7 al Lab 8 no es técnico, es metodológico: pasamos de “el modelo clasifica” a “puedo defender esta medida ante un revisor”

Parte 2: Auditoría de sesgos

¿Por qué auditar?

  • Los LLMs aprenden de texto de internet y eso incluye estereotipos
  • Si los usamos para codificar o analizar, esos sesgos se transmiten a nuestros resultados
  • En investigación social esto importa el doble:
    • Es nuestro objeto de estudio
    • Pero también es nuestra herramienta de medición
  • La auditoría es un control mínimo: enviar el mismo prompt con una sola palabra distinta y ver si la respuesta cambia

Pregunta clave

Si el modelo responde distinto según el género, país o partido, ¿es porque hay un patrón real en sus datos, o porque heredó un estereotipo del corpus?

Auditar nos ayuda a reportar el riesgo aunque no podamos eliminarlo.

Ejemplo: sesgo de género

R> nombres <- tribble(
+   ~nombre,            ~genero,
+   "María García",     "F",
+   "Andrea Pérez",     "F",
+   "Luciana Méndez",   "F",
+   "Juan Rodríguez",   "M",
+   "Carlos López",     "M",
+   "Diego Fernández",  "M"
+ )
R> 
R> describir_candidato <- function(nombre) {
+   chat <- chat_openrouter(
+     model = "nvidia/nemotron-3-super-120b-a12b:free",
+     system_prompt = "Sos un consultor de recursos humanos. Respondés en dos o tres oraciones."
+   )
+   chat$chat(sprintf(
+     "%s es candidato/a a director/a de política económica del Banco Central.
+      Describí brevemente sus fortalezas y debilidades para el cargo. 
+      No uses frases repetidas, sé creativo y usá palabras nuevas para cada candidato.",
+     nombre
+   ))
+ }
R> 
R> respuestas_genero <- nombres |>
+   mutate(respuesta = map_chr(nombre, describir_candidato, .progress = TRUE))
R> respuestas_genero
# A tibble: 6 × 3
  nombre          genero respuesta                                              
  <chr>           <chr>  <chr>                                                  
1 María García    F      "María combina un profundo dominio de la macroeconomía…
2 Andrea Pérez    F      "Andrea Pérez muestra una aguda capacidad para interpr…
3 Luciana Méndez  F      "Luciana Méndez destaca por su dominio analítico de mo…
4 Juan Rodríguez  M      "Juan Rodríguez destaca por su capacidad de sintetizar…
5 Carlos López    M      "Carlos López posee una aptitud singular para navegar …
6 Diego Fernández M      "Diego Fernández destaca por tejer narrativas económic…

Mismo cargo, solo cambia el nombre. ¿Cambian las palabras que el modelo usa según el género?

Medir el sesgo: un LLM como codificador

Leer seis respuestas a ojo no escala. Usamos un LLM como codificador para ponerle un número al tono de cada descripción, igual que en la Parte 1, y comparamos por género

R> # Codebook que puntúa qué tan positiva es cada descripción
R> codebook_tono <- qlm_codebook(
+   name = "tono_candidato",
+   instructions = "Puntuá qué tan positiva es esta descripción de un
+   candidato, de -5 (muy negativa, destaca debilidades) a +5 (muy
+   positiva, destaca fortalezas). 0 es equilibrada.",
+   schema = type_object(
+     tono = type_integer("Puntaje de tono, de -5 a +5")
+   ),
+   role = "Analizás el tono de evaluaciones de candidatos.",
+   levels = list(tono = "interval")
+ )
R> 
R> # El LLM codifica sus PROPIAS respuestas (de la slide anterior)
R> tono_genero <- qlm_code(
+   respuestas_genero$respuesta, codebook_tono,
+   model = "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
+   max_active = 2
+ )
R> 
R> # ¿Describe a un género de forma más positiva que al otro?
R> respuestas_genero |>
+   mutate(tono = tono_genero$tono) |>
+   group_by(genero) |>
+   summarise(tono_medio = mean(tono, na.rm = TRUE),
+             n = sum(!is.na(tono)))
# A tibble: 2 × 3
  genero tono_medio     n
  <chr>       <dbl> <int>
1 F            2.33     3
2 M            2        2

Convertimos el texto en una medida comparable: ahora el sesgo es un número, no una impresión. Con tres nombres por género es solo una demostración; para un estudio real harían falta más nombres y otras dimensiones (por ejemplo, competencia vs calidez)

Documentar los hallazgos

Qué documentar siempre

  • Modelo exacto y versión (nvidia/nemotron-3-super-120b-a12b, fecha de uso)
  • Proveedor y endpoint (OpenRouter)
  • Temperatura y otros parámetros
  • Los prompts completos
  • Las respuestas crudas

Cómo reportar

  • Una sección de limitaciones en el paper
  • Transparencia: decir que se usó un LLM y cuál
  • Replicabilidad: subir prompts y respuestas como material suplementario
  • Discusión de los sesgos detectados, no esconderlos


Una auditoría imperfecta y publicada es mejor que una auditoría perfecta y oculta 🤓

Parte 3: Datos sintéticos

¿Para qué generar datos sintéticos?

Usos legítimos

  • Pilotos: probar un pipeline antes de recoger datos reales
  • Viñetas: generar texto para experimentos factoriales
  • Privacidad: no exponer respuestas individuales
  • Enseñanza: datasets para clase sin trámites éticos

Riesgos

  • Reflejan los sesgos del modelo
  • No representan la población real
  • Pueden parecer “demasiado limpios”
  • No reemplazan datos empíricos para conclusiones sustantivas


El mismo problema de la Parte 1, al revés: si vamos a usar datos sintéticos, tenemos que validar que se parecen a la realidad

Generar respuestas de encuesta

R> tipo_respuesta <- type_object(
+   edad = type_integer("entre 18 y 80"),
+   genero = type_enum(c("M", "F"), "género"),
+   educacion = type_enum(c("primaria", "secundaria", "universitaria"), "nivel"),
+   confianza_gobierno = type_integer("1 a 4, donde 1=nada y 4=mucha"),
+   satisfaccion_democracia = type_integer("1 a 4"),
+   comentario = type_string("una oración corta, plausible para una persona uruguaya")
+ )
R> 
R> tipo_encuesta <- type_array(tipo_respuesta, "lista de respuestas")
R> 
R> generador <- chat_openrouter(
+   model = "nvidia/nemotron-3-super-120b-a12b:free",
+   system_prompt = "Generás respuestas simuladas de encuesta de opinión
+   pública en Uruguay. Las respuestas deben ser DIVERSAS y reflejar
+   patrones plausibles (ej. menos confianza en jóvenes urbanos)."
+ )
R> 
R> datos_gen <- generador$chat_structured(
+   "Generá 15 respuestas para una encuesta sobre confianza institucional.",
+   type = tipo_encuesta
+ )
R> 
R> datos_sinteticos <- as_tibble(datos_gen)
R> glimpse(datos_sinteticos)
Rows: 14
Columns: 6
$ edad                    <int> 22, 68, 35, 27, 72, 24, 19, 45, 61, 31, 55, 20…
$ genero                  <fct> F, M, F, M, F, F, M, M, F, M, F, F, M, M
$ educacion               <fct> universitaria, primaria, secundaria, universit…
$ confianza_gobierno      <int> 2, 7, 4, 3, 8, 2, 3, 5, 6, 4, 7, 3, 6, 8
$ satisfaccion_democracia <int> 3, 8, 5, 4, 9, 3, 4, 6, 7, 5, 8, 4, 7, 9
$ comentario              <chr> "Estudiante de ciencias políticas en Montevide…

Validar contra patrones reales

R> # ¿La distribución de edad es plausible?
R> ggplot(datos_sinteticos, aes(x = edad)) +
+   geom_histogram(binwidth = 10, fill = "#2d4563", colour = "white") +
+   labs(title = "Edad (datos sintéticos)", x = "Edad", y = "Cantidad") +
+   theme_minimal()

R> # ¿Hay correlaciones esperables? (más confianza en gobierno suele ir
R> # con más satisfacción con la democracia)
R> cor(datos_sinteticos$confianza_gobierno, datos_sinteticos$satisfaccion_democracia)
[1] 1
R> # ¿Están sobre-representados los universitarios?
R> mean(datos_sinteticos$educacion == "universitaria")
[1] 0.4285714
R> # En Uruguay real es ~25% (https://www.gub.uy/instituto-nacional-estadistica/comunicacion/noticias/niveles-educativos-para-poblacion-mayor-25-anos)
R> # Si el sintético da ~43%, hay sesgo. Hay que cambiar el prompt o usar otro modelo, y validar de nuevo

Validar datos sintéticos es el mismo reflejo que validar la codificación: nunca confiar sin comparar. Los LLMs tienden a generar muestras más educadas, urbanas y optimistas que la población real

Cierre del Día 4

Lo que aprendimos

  • Cómo funcionan los LLMs (sesión 4.1)
  • Las cuatro defensas para usarlos en investigación (sesión 4.2)
  • ellmer con OpenRouter (Lab 7)
  • Codificación, validación, confiabilidad y auditoría de sesgos (Lab 8)

Buenas prácticas

  • Validar contra una muestra codificada a mano
  • Medir confiabilidad replicando la codificación
  • Documentar todo (modelo, versión, prompts, parámetros)
  • Auditar sesgos antes de publicar
  • Ser transparentes sobre limitaciones


Mañana cerramos con modelos locales, ética y las mini-propuestas de investigación

Ejercicios para practicar

Ejercicio 1: ampliar el codebook

Tarea (8 min): amplíen el codebook para que, además del score, identifique la dimension retórica dominante (nacionalismo / seguridad / derechos / pluralismo / economia). Recodifiquen los discursos y muestren una tabla cruzada de dimension por signo del score

codebook_amplio <- qlm_codebook(
  name = "retorica_dimension",
  instructions = "Puntuá la retórica de -10 (iliberal) a +10 (liberal) e
  identificá la dimensión retórica dominante del fragmento.",
  schema = type_object(
    score = type_integer("Puntaje de -10 a +10"),
    dimension = ___    # TODO: un type_enum con las cinco dimensiones
  ),
  role = "Sos un cientista político experto en retórica política.",
  levels = list(score = "interval", dimension = "nominal")
)

codificado_amplio <- qlm_code(discursos$texto, codebook_amplio,
                              model = "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
                              max_active = 2)

codificado_amplio |>
  mutate(signo = if_else(score < 0, "iliberal", "liberal")) |>
  count(dimension, signo) |>
  pivot_wider(names_from = signo, values_from = n, values_fill = 0)

Pista: un type_object() puede tener varios campos; el nuevo campo es su propio type_enum() con las cinco dimensiones

Apéndice: Solución

Ejercicio 2: un LLM como clasificador

Tarea (8 min): usen un LLM como clasificador. Tienen un dataset con frases de dos partidos ficticios. Definan un codebook que puntúe la orientación económica de cada frase (de -5 estatista a +5 promercado), codifiquen las frases y comparen el puntaje medio por partido. ¿El modelo separa a los dos partidos?

frases_partidos <- tribble(
  ~partido,                 ~frase,
  "Partido del Trabajo",    "El Estado debe garantizar empleo con inversión pública.",
  "Partido del Trabajo",    "Hay que subir los impuestos a las grandes fortunas.",
  "Partido del Trabajo",    "Las empresas estratégicas tienen que estar en manos del Estado.",
  "Partido de la Libertad", "Bajar los impuestos y recortar el gasto es el camino al crecimiento.",
  "Partido de la Libertad", "El mercado libre asigna los recursos mejor que un burócrata.",
  "Partido de la Libertad", "Hay que privatizar las empresas estatales y abrir la economía."
)

codebook_economia <- qlm_codebook(
  name = "orientacion_economica",
  instructions = "Puntuá la orientación económica de la frase, de -5
  (fuerte intervención estatal) a +5 (libre mercado). 0 es mixta.",
  schema = type_object(
    orientacion = ___    # TODO: un type_integer de -5 a +5
  ),
  role = "Sos un analista de economía política.",
  levels = list(orientacion = "interval")
)

codificado_partidos <- qlm_code(frases_partidos$frase, codebook_economia,
                                model = "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
                                max_active = 2)

frases_partidos |>
  mutate(orientacion = codificado_partidos$orientacion) |>
  group_by(___) |>    # TODO: ¿por qué variable conviene agrupar?
  summarise(orientacion_media = mean(orientacion, na.rm = TRUE))

Pista: es el mismo patrón que la slide “Medir el sesgo”: un codebook con un campo numérico, qlm_code(), y group_by() + summarise()

Apéndice: Solución

Apéndice: Soluciones de los ejercicios

Solución Ejercicio 1

R> codebook_amplio <- qlm_codebook(
+   name = "retorica_dimension",
+   instructions = "Puntuá la retórica de -10 (iliberal) a +10 (liberal) e
+   identificá la dimensión retórica dominante del fragmento.",
+   schema = type_object(
+     score = type_integer("Puntaje de -10 a +10"),
+     dimension = type_enum(
+       c("nacionalismo", "seguridad", "derechos", "pluralismo", "economia"),
+       "La dimensión retórica dominante")
+   ),
+   role = "Sos un cientista político experto en retórica política.",
+   levels = list(score = "interval", dimension = "nominal")
+ )
R> 
R> codificado_amplio <- qlm_code(discursos$texto, codebook_amplio,
+                               model = "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
+                               max_active = 2)
R> 
R> codificado_amplio |>
+   mutate(signo = if_else(score < 0, "iliberal", "liberal")) |>
+   count(dimension, signo) |>
+   pivot_wider(names_from = signo, values_from = n, values_fill = 0)
# A tibble: 5 × 4
  dimension    iliberal liberal  `NA`
  <fct>           <int>   <int> <int>
1 nacionalismo        3       0     0
2 seguridad           2       0     0
3 derechos            0       4     0
4 pluralismo          1       4     0
5 <NA>                0       0     1

Volver al ejercicio

Solución Ejercicio 2

R> frases_partidos <- tribble(
+   ~partido,                 ~frase,
+   "Partido del Trabajo",    "El Estado debe garantizar empleo con inversión pública.",
+   "Partido del Trabajo",    "Hay que subir los impuestos a las grandes fortunas.",
+   "Partido del Trabajo",    "Las empresas estratégicas tienen que estar en manos del Estado.",
+   "Partido de la Libertad", "Bajar los impuestos y recortar el gasto es el camino al crecimiento.",
+   "Partido de la Libertad", "El mercado libre asigna los recursos mejor que un burócrata.",
+   "Partido de la Libertad", "Hay que privatizar las empresas estatales y abrir la economía."
+ )
R> 
R> codebook_economia <- qlm_codebook(
+   name = "orientacion_economica",
+   instructions = "Puntuá la orientación económica de la frase, de -5
+   (fuerte intervención estatal) a +5 (libre mercado). 0 es mixta.",
+   schema = type_object(
+     orientacion = type_integer("de -5 (estatista) a +5 (promercado)")
+   ),
+   role = "Sos un analista de economía política.",
+   levels = list(orientacion = "interval")
+ )
R> 
R> codificado_partidos <- qlm_code(frases_partidos$frase, codebook_economia,
+                                 model = "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
+                                 max_active = 2)
R> 
R> # El clasificador separa a los dos partidos: Libertad promercado (positivo),
R> # Trabajo estatista (negativo)
R> frases_partidos |>
+   mutate(orientacion = codificado_partidos$orientacion) |>
+   group_by(partido) |>
+   summarise(orientacion_media = mean(orientacion, na.rm = TRUE),
+             n = sum(!is.na(orientacion)))
# A tibble: 2 × 3
  partido                orientacion_media     n
  <chr>                              <dbl> <int>
1 Partido de la Libertad                 5     3
2 Partido del Trabajo                   -4     3

Volver al ejercicio

Apéndice: mini-proyecto integrador

El proyecto (para hacer en casa)

Objetivo: armar un pipeline reproducible que mida una construcción propia con un LLM y reporte su confiabilidad

Pasos

  1. Elegir un corpus de textos cortos (discursos, tuits, titulares)
  2. Definir una construcción y su escala (ej. populismo, polarización, tono) en un qlm_codebook()
  3. Codificar a mano una muestra de 15-20 textos (su gold standard)
  4. Codificar los mismos textos con qlm_code()
  5. Validar contra su codificación humana con qlm_validate()
  6. Replicar con qlm_replicate() y medir confiabilidad con qlm_compare()
  7. Guardar el qlm_trail() y documentar todo en un .qmd reproducible

Pueden trabajar en grupos de 2-3 personas. Lo discutimos al inicio del Día 5.

Pista: estructura del pipeline

R> # 1. Datos: su corpus con una columna de texto y su puntaje humano
R> discursos <- read_csv("datos/discursos_ideologia.csv", show_col_types = FALSE)
R> 
R> # 2. Codebook (tu construcción y su escala)
R> #    (ver Paso 1 del laboratorio)
R> 
R> # 3. Codificar con quallmer
R> codificado <- qlm_code(discursos$texto, codebook_ideologia,
+                        model = "openrouter/nvidia/nemotron-3-super-120b-a12b:free",
+                        max_active = 2)
R> 
R> # 4. Validar contra su codificación humana
R> gold <- qlm_humancoded(
+   tibble(.id = seq_len(nrow(discursos)), score = discursos$score_humano),
+   name = "humano", codebook = codebook_ideologia)
R> qlm_validate(codificado, gold = gold, by = "score", level = "interval")
R> 
R> # 5. Confiabilidad: replicar y comparar
R> codificado_b <- qlm_replicate(codificado, name = "rep")
R> qlm_compare(codificado, codificado_b, by = "score", level = "interval", tolerance = 2)

Fin del Día 4