---
title: "Tarea 8: Codificación y confiabilidad con LLMs -- Respuestas"
subtitle: "IA para Científicos Sociales - UCU"
author: "Danilo Freire"
date: "2026-04-15"
format:
  html:
    toc: true
    toc-depth: 2
    number-sections: true
    code-fold: false
lang: es
engine: knitr
---

## Instrucciones

Esta es la clave de respuestas de la Tarea 8. Los bloques que solo definen un codebook corren al renderizar; los que llaman a la API están marcados con `eval=FALSE` para no gastar peticiones. Para correrlos hace falta `OPENROUTER_API_KEY` en `.Renviron`, el paquete `quallmer` instalado y `max_active = 2` para que el modelo `:free` no se sature. Los puntajes hechos a mano son ejemplos: cada estudiante puede tener juicios distintos.

### Configuración

```{r setup, message=FALSE, warning=FALSE}
library(ellmer)
library(quallmer)
library(tidyverse)
library(tidytext)

set.seed(2026)

stopifnot(Sys.getenv("OPENROUTER_API_KEY") != "")

discursos <- read_csv("datos/discursos_ideologia.csv", show_col_types = FALSE)

MODELO <- "openrouter/nvidia/nemotron-3-super-120b-a12b:free"
```

## Codificación y validación

### Pregunta 1: Diseñar un codebook para populismo

```{r p1}
codebook_populismo <- qlm_codebook(
  name = "populismo",
  instructions = paste(
    "Medí el grado de RETÓRICA POPULISTA del fragmento, en una escala de 0 a 10.",
    "Es populista cuando apela a 'el pueblo', 'la gente común' o 'la mayoría'",
    "y los enfrenta a una 'élite', 'casta', 'los privilegiados' o 'los corruptos'.",
    "0 = sin rastro de populismo; 10 = populismo intenso (pueblo virtuoso contra élite corrupta).",
    sep = "\n"),
  schema = type_object(
    populismo = type_integer("Grado de populismo, de 0 a 10"),
    explicacion = type_string("Breve justificación del puntaje")
  ),
  role = "Sos un cientista político experto en retórica populista.",
  levels = list(populismo = "interval", explicacion = "nominal")
)
```

**Respuesta:** un buen codebook nombra la construcción, fija la escala y define los polos de forma concreta (qué cuenta como populismo y qué no). La parte difícil no es el código sino las `instructions`: cuanto más operacionales, más confiable y replicable es la codificación. Marcar `populismo` como `interval` le dice a `quallmer` que use correlación y error, no accuracy.

### Pregunta 2: Codificar a mano su propio gold

```{r p2, eval=FALSE}
# Seis discursos variados del corpus
muestra <- discursos |> slice(c(1, 3, 7, 10, 13, 15))

# Codificación a mano según tu propio juicio (estos valores son un ejemplo)
mis_puntajes <- c(8, 6, 1, 2, 4, 9)

gold <- qlm_humancoded(
  tibble(.id = seq_len(nrow(muestra)), populismo = mis_puntajes),
  name = "humano",
  codebook = codebook_populismo
)
gold
```

**Respuesta:** codificar a mano una submuestra es el paso que vuelve "validable" todo el resto: sin un gold humano no hay contra qué comparar al LLM. La columna `.id` alinea cada puntaje con su texto. Seis discursos es poco (sirve para la demostración); en una investigación real serían entre 50 y 200, idealmente codificados por más de una persona para medir también el acuerdo entre humanos.

### Pregunta 3: Codificar con el LLM y validar

```{r p3, eval=FALSE}
codificado <- qlm_code(muestra$texto, codebook_populismo,
                       model = MODELO, max_active = 2)

validacion <- qlm_validate(codificado, gold = gold,
                           by = "populismo", level = "interval")
as_tibble(validacion) |> select(measure, value)
```

**Respuesta:** con seis puntos las métricas son ruidosas, pero la lectura es la de siempre: si la correlación de Pearson es alta, el LLM ordena los discursos como ustedes; si el ICC es más bajo, coincide en el orden pero no en el nivel exacto. Mostramos el resultado con `as_tibble() |> select()` porque el `print()` de `qlm_validate()` usa `cli` y no siempre se ve al renderizar. Lo importante es el método: nunca usar el LLM sin haberlo comparado contra una muestra codificada por uno mismo.

## Confiabilidad y métodos clásicos

### Pregunta 4: Confiabilidad de dos formulaciones

```{r p4}
codebook_breve <- qlm_codebook(
  name = "populismo_breve",
  instructions = "Puntuá qué tan populista es el discurso, de 0 a 10.",
  schema = type_object(populismo = type_integer("de 0 a 10")),
  role = "Sos un cientista político.",
  levels = list(populismo = "interval")
)
```

```{r p4-uso, eval=FALSE}
codificado_breve <- qlm_code(muestra$texto, codebook_breve,
                             model = MODELO, max_active = 2)

confiabilidad <- qlm_compare(codificado, codificado_breve,
                             by = "populismo", level = "interval", tolerance = 1)
as_tibble(confiabilidad) |> select(measure, value)
```

**Respuesta:** si las dos formulaciones dan puntajes parecidos (alpha de Krippendorff alto), la medida es robusta a cómo se escribe el codebook. Si difieren mucho, el resultado depende demasiado de la redacción: el codebook detallado y el breve estarían "midiendo" cosas distintas, una señal de alarma. Reformular el codebook y comparar es una forma barata de auditar la confiabilidad antes de codificar todo el corpus.

### Pregunta 5: LLM vs diccionario

```{r p5, eval=FALSE}
populistas <- c("pueblo", "gente", "élite", "elite", "casta", "corruptos",
                "privilegiados", "mayoría", "enemigos", "nosotros", "ellos")

conteo <- muestra |>
  mutate(.id = row_number()) |>
  unnest_tokens(palabra, texto) |>
  mutate(es_populista = palabra %in% populistas) |>
  group_by(.id) |>
  summarise(n_populista = sum(es_populista), .groups = "drop")

comparacion <- conteo |>
  mutate(score_llm = codificado$populismo)

cor(comparacion$n_populista, comparacion$score_llm)
```

**Respuesta:** el diccionario es un baseline barato y perfectamente reproducible: cuenta palabras de una lista. Suele correlacionar de forma moderada con el LLM, pero falla cuando el populismo es implícito (un discurso puede oponer pueblo y élite sin usar esas palabras exactas, o usar "pueblo" en un sentido no populista). El LLM capta ese matiz; el diccionario, no. La estrategia sensata: empezar por el diccionario como baseline y chequeo, y usar el LLM donde el matiz del contexto importa.

## Auditoría y datos sintéticos

### Pregunta 6: Sesgo por etiqueta ideológica

```{r p6}
codebook_tono <- qlm_codebook(
  name = "tono_descripcion",
  instructions = "Puntuá el tono de la descripción, de -5 (muy crítica,
  destaca defectos) a +5 (muy favorable, destaca virtudes). 0 es neutral.",
  schema = type_object(tono = type_integer("de -5 a +5")),
  role = "Analizás el tono de descripciones políticas.",
  levels = list(tono = "interval")
)
```

```{r p6-uso, eval=FALSE}
etiquetas <- c("un político de izquierda", "un político de derecha")

describir <- function(etiqueta) {
  chat <- chat_openrouter(
    model = "nvidia/nemotron-3-super-120b-a12b:free",
    system_prompt = "Sos un analista político. Respondés en dos o tres oraciones."
  )
  chat$chat(sprintf("Describí las propuestas económicas de %s.", etiqueta))
}

respuestas <- tibble(
  etiqueta = etiquetas,
  respuesta = map_chr(etiquetas, describir)
)

tono <- qlm_code(respuestas$respuesta, codebook_tono,
                 model = MODELO, max_active = 2)

respuestas |> mutate(tono = tono$tono) |> select(etiqueta, tono)
```

**Respuesta:** mismo pedido, solo cambia la etiqueta ideológica. Si una etiqueta recibe de forma sistemática un tono más favorable, el modelo está proyectando una asociación de su corpus de entrenamiento, no un hecho del mundo. Con solo dos descripciones es una demostración; un estudio real generaría muchas descripciones por etiqueta y compararía las medias con un test. La auditoría no elimina el sesgo, pero lo vuelve visible y reportable, que es lo mínimo exigible.

### Pregunta 7: Viñetas sintéticas para un experimento

```{r p7, eval=FALSE}
tipo_vineta <- type_object(
  postura = type_enum(c("restrictiva", "abierta"), "postura migratoria del candidato"),
  texto = type_string("viñeta de 2-3 oraciones que describe al candidato")
)
tipo_vinetas <- type_array(tipo_vineta, "lista de viñetas")

generador <- chat_openrouter(
  model = "nvidia/nemotron-3-super-120b-a12b:free",
  system_prompt = "Generás viñetas para un experimento de encuesta. Las viñetas
  deben ser realistas, comparables y diferir SOLO en la postura migratoria."
)

vinetas <- generador$chat_structured(
  "Generá 4 viñetas: 2 con postura restrictiva y 2 con postura abierta.",
  type = tipo_vinetas
)

as_tibble(vinetas)
```

**Respuesta:** generar viñetas con un LLM acelera el diseño de experimentos factoriales y de conjoint: el modelo produce texto realista para cada combinación de atributos, manteniendo constante todo lo demás. La advertencia clave es que las viñetas heredan los sesgos del modelo, así que hay que revisarlas a mano, equilibrarlas y, idealmente, pre-registrar el material del experimento. Nunca usar estímulos generados sin leerlos uno por uno.

## Reflexión

### Pregunta 8: Reporte para un paper

**Respuesta (ejemplo):** Para medir el populismo de los 3.000 discursos construimos un libro de códigos con `quallmer` que define una escala continua de 0 (sin retórica populista) a 10 (populismo intenso), con instrucciones explícitas sobre la oposición entre "el pueblo" y "la élite". La codificación se realizó con el modelo `nvidia/nemotron-3-super-120b` a través de OpenRouter (acceso del 15 de abril de 2026), con temperatura 0 para favorecer la reproducibilidad. Validamos la codificación contra una submuestra de 150 discursos codificados a mano por dos asistentes, reportando la correlación de Pearson y el ICC. Evaluamos la confiabilidad reformulando el codebook y comparando las dos codificaciones con el alpha de Krippendorff. Antes de aplicar el modelo al corpus completo auditamos posibles sesgos enviando descripciones que solo diferían en la etiqueta ideológica del actor. El codebook, los prompts y el registro de ejecución (`qlm_trail()`) se incluyen como material suplementario. (≈150 palabras.)

### Pregunta 9: Diseño de un mini-experimento

**Respuesta (ejemplo):**

- **Hipótesis nula:** el tono que el modelo usa para describir a políticos de izquierda no difiere del que usa para políticos de derecha
- **Variable independiente:** la etiqueta ideológica en el prompt (izquierda / derecha), manteniendo el cargo y el resto del texto idénticos
- **Variable dependiente:** el puntaje de tono de la descripción (-5 a +5), codificado con un codebook de `quallmer`
- **Tamaño de muestra:** 100 descripciones por etiqueta (200 prompts), generando varias por etiqueta para promediar la variabilidad del modelo
- **Métrica de comparación:** diferencia de medias en el tono, con una prueba t o un test de permutación
- **Amenaza de validez:** "izquierda" y "derecha" significan cosas distintas según el país; el modelo puede anclarse en un contexto (por ejemplo, el estadounidense). Conviene especificar el país en el prompt o repetir el experimento por región
