Tarea 6: Análisis de texto — Respuestas

IA para Científicos Sociales - UCU

Autor/a

Danilo Freire

Fecha de publicación

24 de abril de 2026

1 Instrucciones

Esta es la clave de respuestas de la Tarea 6. Cada pregunta incluye el código R completo y una respuesta escrita.

1.1 Configuración

library(tidyverse)
library(tidytext)
library(tm)

set.seed(2026)

textos <- read_csv("datos/textos_politicos.csv", show_col_types = FALSE)

stopwords_es <- tibble(palabra = tm::stopwords("spanish"))

tokens <- textos |>
  unnest_tokens(palabra, texto)

tokens_limpios <- tokens |>
  anti_join(stopwords_es, by = "palabra") |>
  filter(!str_detect(palabra, "^[0-9]+$"),
         nchar(palabra) > 2)

2 Exploración

2.1 Pregunta 1: Distribución por año

textos |>
  count(anio) |>
  ggplot(aes(x = factor(anio), y = n, fill = factor(anio))) +
  geom_col() +
  labs(title = "Textos por año",
       x = "Año", y = "Cantidad") +
  theme_minimal() +
  theme(legend.position = "none")

Respuesta: La distribución es desigual: 2019 es el año con más textos (14), seguido de 2024 (13) y 2018 (12), mientras que 2020 tiene solo 2 textos. Esto es relevante porque cualquier análisis comparativo entre años debe tener en cuenta que algunos están subrepresentados: las conclusiones sobre 2020 serán mucho menos robustas que sobre 2019 o 2024.

2.2 Pregunta 2: Longitud de los textos

longitudes <- tokens |>
  count(id, name = "n_tokens")

# Texto más corto
mas_corto <- longitudes |> slice_min(n_tokens, n = 1, with_ties = FALSE)
mas_corto
# A tibble: 1 × 2
     id n_tokens
  <dbl>    <int>
1    45       10
# Texto más largo
mas_largo <- longitudes |> slice_max(n_tokens, n = 1)
mas_largo
# A tibble: 1 × 2
     id n_tokens
  <dbl>    <int>
1     1       20
# Contenido de ambos
textos |> filter(id %in% c(mas_corto$id, mas_largo$id)) |> select(id, tema, texto)
# A tibble: 2 × 3
     id tema     texto                                                          
  <dbl> <chr>    <chr>                                                          
1     1 economia El crecimiento económico del país ha sido sólido este año grac…
2    45 salud    Los trabajadores de salud enfrentan condiciones precarias y sa…

Respuesta: El texto más corto tiene 10 palabras (tokens) y el más largo 20; la variación es moderada (hay un empate en el mínimo, con dos textos de 10 palabras; with_ties = FALSE se queda con uno solo para la comparación). Para análisis con corpora reales (artículos, discursos completos), la diferencia sería de órdenes de magnitud y habría que normalizar (TF-IDF ya lo hace por su construcción).

2.3 Pregunta 3: Bigramas más frecuentes

bigramas <- textos |>
  unnest_tokens(par, texto, token = "ngrams", n = 2) |>
  separate(par, c("palabra1", "palabra2"), sep = " ") |>
  filter(!palabra1 %in% stopwords_es$palabra,
         !palabra2 %in% stopwords_es$palabra,
         !str_detect(palabra1, "^[0-9]+$"),
         !str_detect(palabra2, "^[0-9]+$")) |>
  unite("bigrama", palabra1, palabra2, sep = " ") |>
  count(bigrama, sort = TRUE) |>
  head(10)

bigramas
# A tibble: 10 × 2
   bigrama                           n
   <chr>                         <int>
 1 alcanzado niveles                 2
 2 comunidades indígenas             2
 3 crecimiento económico             2
 4 implementado programas            2
 5 necesita mayor                    2
 6 acuerdos comerciales              1
 7 afectan desproporcionadamente     1
 8 agricultura sostenible            1
 9 agua afecta                       1
10 alcanzado coberturas              1

Respuesta: En este corpus pequeño casi ningún bigrama se repite mucho: los más frecuentes aparecen solo 2 veces (“crecimiento económico”, “comunidades indígenas”, “implementado programas”, “alcanzado niveles”, “necesita mayor”). Aun así ilustran la ventaja de los bigramas: recuperan colocaciones (palabras que van juntas con significado conjunto, como “crecimiento económico”) que las palabras sueltas pierden. Con un corpus más grande, estas colocaciones se repetirían lo suficiente como para ser muy informativas.

3 TF-IDF

3.1 Pregunta 4: Frecuencia bruta vs. TF-IDF

# Top 10 por frecuencia bruta (todo el corpus)
tokens_limpios |>
  count(palabra, sort = TRUE) |>
  head(10)
# A tibble: 10 × 2
   palabra         n
   <chr>       <int>
 1 región          9
 2 salud           8
 3 programas       7
 4 sigue           7
 5 comunidades     5
 6 educación       4
 7 representan     4
 8 rurales         4
 9 seguridad       4
10 afecta          3
# Top 3 por tema según TF-IDF
tokens_limpios |>
  count(tema, palabra) |>
  bind_tf_idf(palabra, tema, n) |>
  group_by(tema) |>
  slice_max(tf_idf, n = 3) |>
  arrange(tema, desc(tf_idf)) |>
  select(tema, palabra, tf_idf)
# A tibble: 82 × 3
# Groups:   tema [5]
   tema          palabra       tf_idf
   <chr>         <chr>          <dbl>
 1 economia      económico     0.0460
 2 economia      país          0.0460
 3 economia      crecimiento   0.0307
 4 economia      exportaciones 0.0307
 5 economia      fiscal        0.0307
 6 economia      laboral       0.0307
 7 educacion     educación     0.0664
 8 educacion     estudiantes   0.0498
 9 educacion     escolar       0.0332
10 medioambiente biodiversidad 0.0346
# ℹ 72 more rows

Respuesta: La lista por frecuencia bruta está dominada por palabras genéricas que aparecen en muchos temas: región, programas, sigue, comunidades, representan. Son frecuentes pero no distinguen un tema de otro. En la lista de TF-IDF esas palabras genéricas desaparecen y emergen términos específicos de cada tema (económico, estudiantes, biodiversidad, etc.). La razón es el factor IDF: una palabra que aparece en muchos grupos tiene IDF bajo (el logaritmo se acerca a cero), así que su TF-IDF se hunde por más frecuente que sea. Esta es justamente la razón de ser de TF-IDF: la frecuencia no es lo mismo que la capacidad de distinguir.

3.2 Pregunta 5: ¿Cuándo informa el TF-IDF por grupo?

tokens_limpios |>
  count(anio, palabra) |>
  bind_tf_idf(palabra, anio, n) |>
  group_by(anio) |>
  slice_max(tf_idf, n = 3) |>
  arrange(anio, desc(tf_idf)) |>
  select(anio, palabra, tf_idf)
# A tibble: 116 × 3
# Groups:   anio [7]
    anio palabra      tf_idf
   <dbl> <chr>         <dbl>
 1  2018 esenciales   0.0418
 2  2018 indígenas    0.0418
 3  2018 insuficiente 0.0418
 4  2018 permitido    0.0418
 5  2019 necesita     0.0351
 6  2019 mayor        0.0226
 7  2019 nuevo        0.0226
 8  2020 agrícolas    0.0973
 9  2020 condicionada 0.0973
10  2020 demostrado   0.0973
# ℹ 106 more rows

Respuesta: Las palabras distintivas por año son idiosincráticas (agrícolas, fiscal, año, agua, esenciales…) y no cuentan una historia temporal: no aparece un “año de la pandemia” con vocabulario sanitario, aunque el corpus sí contenga textos de salud que mencionan “pandemia”. El motivo es que los 5 temas están repartidos de forma pareja entre los años, así que el vocabulario casi no correlaciona con el tiempo. TF-IDF solo revela una señal interpretable cuando la variable de agrupamiento influye de verdad en las palabras que se usan, como sí ocurre con tema. La lección práctica: antes de confiar en un TF-IDF por grupo, pregúntense si ese grupo debería cambiar el vocabulario; agrupar por una variable arbitraria produce ruido que puede parecer señal.

3.3 Pregunta 6: TF-IDF de bigramas

bigramas_tema <- textos |>
  unnest_tokens(par, texto, token = "ngrams", n = 2) |>
  separate(par, c("palabra1", "palabra2"), sep = " ") |>
  filter(!palabra1 %in% stopwords_es$palabra,
         !palabra2 %in% stopwords_es$palabra,
         !str_detect(palabra1, "^[0-9]+$"),
         !str_detect(palabra2, "^[0-9]+$")) |>
  unite("bigrama", palabra1, palabra2, sep = " ") |>
  count(tema, bigrama) |>
  bind_tf_idf(bigrama, tema, n)

bigramas_tema |>
  group_by(tema) |>
  slice_max(tf_idf, n = 3, with_ties = FALSE) |>
  arrange(tema, desc(tf_idf)) |>
  select(tema, bigrama, n, tf_idf)
# A tibble: 15 × 4
# Groups:   tema [5]
   tema          bigrama                           n tf_idf
   <chr>         <chr>                         <int>  <dbl>
 1 economia      crecimiento económico             2 0.0766
 2 economia      acuerdos comerciales              1 0.0383
 3 economia      asia representan                  1 0.0383
 4 educacion     alimentación escolar              1 0.0374
 5 educacion     analfabetismo funcional           1 0.0374
 6 educacion     bajos recursos                    1 0.0374
 7 medioambiente afectan desproporcionadamente     1 0.0424
 8 medioambiente agricultura sostenible            1 0.0424
 9 medioambiente agua afecta                       1 0.0424
10 salud         alcanzado coberturas              1 0.0424
11 salud         atención primaria                 1 0.0424
12 salud         cada año                          1 0.0424
13 seguridad     américa central                   1 0.0473
14 seguridad     año consecutivo                   1 0.0473
15 seguridad     ciudades principales              1 0.0473

Respuesta: Algunos bigramas son más interpretables que las palabras sueltas porque recuperan colocaciones con significado conjunto: “crecimiento económico” (economía), “alimentación escolar” (educación), “atención primaria” (salud), “agricultura sostenible” (medioambiente). Pero la limitación del corpus chico salta a la vista en la columna n: casi todos los bigramas aparecen una sola vez, así que casi cualquier bigrama resulta “distintivo” de su tema y el ranking dentro de cada tema es en la práctica un empate roto de forma arbitraria (por eso hizo falta with_ties = FALSE, y por eso se cuelan bigramas poco informativos como “cada año” o “asia representan”). El único bigrama que se repite es “crecimiento económico” (n = 2). La conclusión práctica: los n-gramas ganan interpretabilidad pero fragmentan los conteos; necesitan corpus bastante más grandes que las palabras sueltas para que el TF-IDF discrimine de verdad.

4 Sentimiento

4.1 Pregunta 7: Ampliar diccionarios

positivas <- c("crecimiento", "mejora", "mejorado", "oportunidad", "fortalecido",
               "innovadores", "renovables", "sólido", "beneficio", "avance",
               "estabilizar", "reducir", "éxito", "progreso", "esperanza",
               "desarrollo", "inversión", "expansión", "estable", "positivo",
               "consolidación", "fortalecimiento", "incremento", "modernización",
               "inclusión", "sustentable", "eficiente", "transparencia",
               "participación", "cooperación", "innovación", "competitividad")

negativas <- c("crisis", "inflación", "pobreza", "violencia", "amenaza",
               "problema", "deuda", "riesgo", "deficiencias", "grave",
               "crimen", "narcotráfico", "deforestación", "informalidad",
               "inseguridad", "contaminación", "desempleo", "vulnerables",
               "corrupción", "recesión", "desigualdad", "exclusión",
               "abandono", "fracaso", "estancamiento", "deterioro",
               "marginación", "represión", "polarización", "fragmentación",
               "atraso", "conflicto", "tensión", "deficiente", "precario")

cat("Positivas:", length(positivas), "  Negativas:", length(negativas), "\n")
Positivas: 32   Negativas: 35 

Respuesta: Los diccionarios deben adaptarse al dominio. Para texto político en español agregué palabras sobre desarrollo institucional (transparencia, participación, modernización) en positivas y sobre conflicto político (corrupción, polarización, fragmentación) en negativas. La calidad del análisis depende fuertemente de qué palabras incluyamos.

4.2 Pregunta 8: Sentimiento por país

sentimiento_doc <- tokens_limpios |>
  mutate(valencia = case_when(
    palabra %in% positivas ~  1L,
    palabra %in% negativas ~ -1L,
    TRUE ~ 0L
  )) |>
  group_by(id, pais) |>
  summarise(score = sum(valencia), .groups = "drop")

sentimiento_pais <- sentimiento_doc |>
  group_by(pais) |>
  summarise(
    n_textos = n(),
    score_promedio = round(mean(score), 2),
    .groups = "drop"
  ) |>
  arrange(score_promedio)

sentimiento_pais
# A tibble: 18 × 3
   pais                 n_textos score_promedio
   <chr>                   <int>          <dbl>
 1 El Salvador                 2          -1.5 
 2 Panamá                      3          -1.33
 3 Colombia                    3          -0.67
 4 Argentina                   5          -0.6 
 5 Costa Rica                  4          -0.25
 6 Guatemala                   6          -0.17
 7 Chile                       2           0   
 8 Ecuador                     3           0   
 9 Honduras                    1           0   
10 Perú                        2           0   
11 Brasil                      7           0.14
12 Bolivia                     5           0.2 
13 Nicaragua                   4           0.25
14 Venezuela                   3           0.33
15 México                      4           0.5 
16 República Dominicana        2           0.5 
17 Paraguay                    3           0.67
18 Uruguay                     1           1   

Respuesta: Los países con tono más negativo en promedio son El Salvador (≈ -1.5), Panamá (≈ -1.3) y Colombia (≈ -0.7); Argentina también aparece negativa (≈ -0.6), consistente con sus textos sobre inflación. Pero conviene no leer demasiado en esto: con tan pocos textos por país (El Salvador y Panamá tienen 2 o 3), el promedio es muy sensible al azar. De hecho, Venezuela sale ligeramente positiva pese a la intuición de “país en crisis”, solo porque sus pocos textos no contienen muchas palabras del diccionario negativo. Es un recordatorio de que los promedios sobre muestras minúsculas son poco fiables.

4.3 Pregunta 9: El documento más positivo

doc_mas_positivo <- sentimiento_doc |>
  arrange(desc(score)) |>
  head(1)

doc_mas_positivo
# A tibble: 1 × 3
     id pais   score
  <dbl> <chr>  <int>
1     1 México     3
textos |>
  filter(id == doc_mas_positivo$id) |>
  select(id, tema, pais, anio, texto)
# A tibble: 1 × 5
     id tema     pais    anio texto                                             
  <dbl> <chr>    <chr>  <dbl> <chr>                                             
1     1 economia México  2020 El crecimiento económico del país ha sido sólido …

Respuesta: El texto más positivo es el id 1 (tema economía, México, 2020), con palabras como crecimiento, económico, sólido. El método capta bien el tono explícitamente positivo. Lo que NO capta: matices, condicionales (“podríamos crecer si…”), o palabras positivas específicas al dominio que no están en el diccionario (p. ej. “renacimiento”, “auge”).

4.4 Pregunta 10: Heatmap sentimiento por tema y año

sentimiento_tema_anio <- tokens_limpios |>
  mutate(valencia = case_when(
    palabra %in% positivas ~  1L,
    palabra %in% negativas ~ -1L,
    TRUE ~ 0L
  )) |>
  group_by(id, tema, anio) |>
  summarise(score = sum(valencia), .groups = "drop") |>
  group_by(tema, anio) |>
  summarise(score_promedio = mean(score), .groups = "drop")

ggplot(sentimiento_tema_anio,
       aes(x = factor(anio), y = tema, fill = score_promedio)) +
  geom_tile() +
  geom_text(aes(label = round(score_promedio, 1)), color = "white", size = 3) +
  scale_fill_gradient2(low = "#E74C3C", mid = "white", high = "#27AE60",
                      midpoint = 0) +
  labs(title = "Tono promedio por tema × año",
       x = "Año", y = "Tema", fill = "Score") +
  theme_minimal()

Respuesta: El heatmap se ve colorido, pero conviene leerlo con cautela: cada celda (tema × año) resume apenas 1 a 4 textos, así que los valores son sobre todo ruido. No emerge una tendencia temporal estable. Por ejemplo, seguridad es negativo en los años recientes (2022-2024) pero positivo en 2018 y 2021, y salud ni siquiera tiene textos en 2020. Las celdas más negativas se concentran en seguridad y economía de los últimos años, pero con tan pocos datos por celda no se puede afirmar una evolución. Es el mismo problema de muestras minúsculas que vimos en el sentimiento por país: cruzar dos dimensiones reparte 60 textos en demasiadas celdas como para que el resultado sea confiable.

5 Síntesis

5.1 Pregunta 11: Clasificador por palabras clave

ancla <- list(
  economia      = c("crecimiento", "inflación", "empleo", "pib", "mercado",
                    "inversión", "fiscal", "monetaria", "desempleo"),
  educacion     = c("estudiantes", "docentes", "escuelas", "universidad",
                    "educativa", "alumnos", "aprendizaje", "escolar"),
  salud         = c("hospitales", "vacunación", "pandemia", "médicos",
                    "sanitaria", "enfermedades", "covid"),
  seguridad     = c("violencia", "crimen", "policía", "delincuencia",
                    "narcotráfico", "robos", "inseguridad"),
  medioambiente = c("deforestación", "biodiversidad", "bosque", "clima",
                    "renovables", "contaminación", "sustentable")
)

# Para cada documento, contar palabras de cada ancla
clasificacion <- tokens_limpios |>
  mutate(tema_predicho = case_when(
    palabra %in% ancla$economia      ~ "economia",
    palabra %in% ancla$educacion     ~ "educacion",
    palabra %in% ancla$salud         ~ "salud",
    palabra %in% ancla$seguridad     ~ "seguridad",
    palabra %in% ancla$medioambiente ~ "medioambiente",
    TRUE ~ NA_character_
  )) |>
  filter(!is.na(tema_predicho)) |>
  count(id, tema_predicho) |>
  group_by(id) |>
  slice_max(n, n = 1, with_ties = FALSE) |>
  ungroup() |>
  left_join(textos |> select(id, tema_real = tema), by = "id")

# Matriz de confusión
conf_mat <- table(Real = clasificacion$tema_real, Predicho = clasificacion$tema_predicho)
conf_mat
               Predicho
Real            economia educacion medioambiente salud seguridad
  economia             8         0             0     0         0
  educacion            1         6             0     0         0
  medioambiente        0         0             5     0         0
  salud                0         0             0     4         0
  seguridad            0         0             0     0         4
# Accuracy
accuracy <- sum(diag(conf_mat)) / sum(conf_mat)
cat("Accuracy:", round(accuracy, 3), "\n")
Accuracy: 0.964 

Respuesta: Entre los documentos que sí logra clasificar, el accuracy es muy alto (≈ 0.96): cuando un texto contiene una palabra-ancla, casi siempre apunta al tema correcto. Pero el verdadero límite no es la precisión sino la cobertura: solo unos 28 de los 60 textos contienen alguna palabra-ancla, y el resto queda sin clasificar. Es el clásico compromiso precisión-cobertura de los métodos por diccionario: son transparentes (sabemos exactamente por qué se clasifica cada texto), rápidos y fáciles de mejorar (agregar más anclas sube la cobertura), pero un diccionario corto deja muchos casos afuera. Para subir la cobertura sin perder transparencia, habría que ampliar las anclas o combinarlas con TF-IDF.

5.2 Pregunta 12: TF-IDF + sentimiento

Respuesta: A nivel de tema la relación es clara: los temas cuyo vocabulario distintivo (TF-IDF) es más negativo, como seguridad (“violencia”, “crimen”), son también los de sentimiento promedio más bajo. Las dos herramientas capturan la misma señal de fondo por caminos distintos, lo que da confianza en el resultado. A nivel de país la relación se diluye: con tan pocos textos por país, un solo texto muy negativo arrastra el promedio, así que el orden de países es más ruido que señal. La moraleja es metodológica: TF-IDF (qué palabras) y sentimiento (con qué tono) se refuerzan cuando el agrupamiento tiene suficientes datos y vocabulario propio (tema), y se vuelven poco fiables cuando no (país con 1-2 textos).

5.3 Pregunta 13: Reflexión

Respuesta:

  1. TF-IDF vs. sentimiento — cuándo usar cuál:
    • TF-IDF responde preguntas como: “¿Qué vocabulario distingue a [grupo A] de [grupo B]?” Ejemplo: “¿Qué palabras eran más comunes en los discursos del partido X frente al partido Y en 2023?”
    • Sentimiento responde preguntas como: “¿Cuál es el tono dominante de este corpus? ¿Cómo varía entre grupos?” Ejemplo: “¿Los medios de derecha cubren la migración con tono más negativo que los de izquierda?”
  2. Dos limitaciones serias del sentimiento por diccionario:
    • Negaciones e ironía: “no es un buen presidente” cuenta como positivo. Mitigación: usar bigramas como contexto, o un modelo entrenado con anotaciones humanas.
    • Dependencia del dominio: “viral” es positivo en marketing, negativo en epidemiología. Mitigación: validar diccionarios contra una muestra anotada manualmente para el dominio específico.
  3. Cuándo preferir un LLM:
    • Textos largos con tono mixto, donde un score promedio pierde matiz
    • Tareas que requieren entender contexto: detectar ironía, sarcasmo, alusiones culturales
    • Necesidad de extraer información estructurada (eventos, actores, fechas) sin reglas pre-definidas
    • Sin embargo, los métodos clásicos siguen siendo preferibles cuando se necesita transparencia y replicabilidad (un revisor puede inspeccionar exactamente qué palabra activó qué decisión, lo que un LLM no permite).
Volver arriba