Referencia rápida: análisis de texto en R

Esta página es una referencia rápida de las funciones de análisis computacional de texto que usamos en la Sesión 3.2 y en el Laboratorio 6. Todos los ejemplos son ejecutables con un corpus simulado pequeño. Pueden copiar y pegar el código en RStudio para practicar.

El flujo típico de análisis de texto es:

  1. Cargar el corpus
  2. Tokenizar (una fila por palabra)
  3. Limpiar: stopwords, números, palabras cortas
  4. Calcular frecuencias
  5. Aplicar una representación numérica (bag-of-words, TF-IDF)
  6. Ajustar un modelo (LDA, sentimiento, clasificación)
  7. Validar e interpretar

1 Paquetes y corpus simulado

1.1 Cargar paquetes

library(tidyverse)    # manipulación de datos y visualización
library(tidytext)     # tokenización tidy, TF-IDF, cast_dtm
library(topicmodels)  # LDA
library(tm)           # stopwords en español
library(ggwordcloud)  # nubes de palabras

set.seed(2026)        # fijar semilla para reproducibilidad

1.2 Crear un corpus simulado

Simulamos 20 textos cortos sobre 4 temas (economía, educación, salud, seguridad). Cada texto imita el vocabulario típico del tema. Usaremos este corpus en toda la página.

corpus <- tibble(
  id = 1:20,
  tema = rep(c("economia", "educacion", "salud", "seguridad"), each = 5),
  texto = c(
    # Economía
    "La inflación subió y el empleo formal sigue estancado en la región",
    "El crecimiento económico depende de la inversión privada y el empleo",
    "La deuda pública aumenta y los precios siguen altos en el país",
    "Políticas fiscales y monetarias deberían estabilizar la inflación",
    "El desempleo juvenil es un problema grave para la economía",
    # Educación
    "Las escuelas públicas necesitan más docentes y mejor infraestructura",
    "La educación universitaria debería ser gratuita y de calidad",
    "Los estudiantes enfrentan dificultades de aprendizaje tras la pandemia",
    "Las universidades deben invertir en investigación y docencia",
    "La deserción escolar aumenta en las escuelas rurales del país",
    # Salud
    "El sistema de salud pública requiere más hospitales y médicos",
    "La vacunación masiva protegió a millones de personas en la pandemia",
    "Los hospitales públicos no tienen suficientes recursos ni médicos",
    "La salud mental es un problema creciente tras la pandemia",
    "Las políticas de salud deberían priorizar la atención primaria",
    # Seguridad
    "La violencia urbana y los robos aumentaron en las ciudades",
    "El crimen organizado y el narcotráfico amenazan la seguridad pública",
    "La policía necesita más recursos para combatir la delincuencia",
    "La inseguridad afecta la calidad de vida en los barrios pobres",
    "El gobierno debe reducir la violencia con políticas integrales"
  )
)

glimpse(corpus)
Rows: 20
Columns: 3
$ id    <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 1…
$ tema  <chr> "economia", "economia", "economia", "economia", "economia", "edu…
$ texto <chr> "La inflación subió y el empleo formal sigue estancado en la reg…

2 Tokenización

2.1 Tokenizar con unnest_tokens()

unnest_tokens() divide cada texto en palabras, genera una fila por token y convierte todo a minúsculas. Es la función central de tidytext.

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

head(tokens, 10)
# A tibble: 10 × 3
      id tema     palabra  
   <int> <chr>    <chr>    
 1     1 economia la       
 2     1 economia inflación
 3     1 economia subió    
 4     1 economia y        
 5     1 economia el       
 6     1 economia empleo   
 7     1 economia formal   
 8     1 economia sigue    
 9     1 economia estancado
10     1 economia en       

2.2 Otras unidades de tokenización

Cambiando el argumento token, podemos trabajar con bigramas, oraciones o caracteres.

# Bigramas (pares de palabras consecutivas)
corpus |>
  slice(1:2) |>
  unnest_tokens(bigrama, texto, token = "ngrams", n = 2) |>
  head(6)
# A tibble: 6 × 3
     id tema     bigrama        
  <int> <chr>    <chr>          
1     1 economia la inflación   
2     1 economia inflación subió
3     1 economia subió y        
4     1 economia y el           
5     1 economia el empleo      
6     1 economia empleo formal  
# Oraciones
corpus |>
  slice(1:2) |>
  unnest_tokens(oracion, texto, token = "sentences") |>
  select(id, oracion)
# A tibble: 2 × 2
     id oracion                                                             
  <int> <chr>                                                               
1     1 la inflación subió y el empleo formal sigue estancado en la región  
2     2 el crecimiento económico depende de la inversión privada y el empleo

3 Limpieza

3.1 Stopwords en español

Las stopwords son palabras muy frecuentes que aportan poco significado (“la”, “de”, “que”…). tm::stopwords("spanish") devuelve una lista estándar. Podemos ampliarla con términos específicos del dominio.

# Lista base en español
stop_es <- tibble(palabra = tm::stopwords("spanish"))

nrow(stop_es)
[1] 308
head(stop_es$palabra, 15)
 [1] "de"   "la"   "que"  "el"   "en"   "y"    "a"    "los"  "del"  "se"  
[11] "las"  "por"  "un"   "para" "con" 
# Ampliar con palabras frecuentes poco informativas para nuestro corpus
extra <- c("más", "país", "países", "región", "sigue", "siendo",
           "debe", "debería", "deberían", "ser", "siguen")

stop_es <- bind_rows(stop_es, tibble(palabra = extra))

3.2 Quitar stopwords, números y palabras cortas

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

# Comparar antes y después
tibble(
  etapa = c("con stopwords", "sin stopwords"),
  n = c(nrow(tokens), nrow(tokens_limpios))
)
# A tibble: 2 × 2
  etapa             n
  <chr>         <int>
1 con stopwords   196
2 sin stopwords   107

4 Frecuencias de palabras

4.1 Palabras más frecuentes del corpus

tokens_limpios |>
  count(palabra, sort = TRUE) |>
  head(10)
# A tibble: 10 × 2
   palabra        n
   <chr>      <int>
 1 pandemia       3
 2 políticas      3
 3 pública        3
 4 salud          3
 5 aumenta        2
 6 calidad        2
 7 empleo         2
 8 escuelas       2
 9 hospitales     2
10 inflación      2

4.2 Visualizar las más frecuentes

tokens_limpios |>
  count(palabra, sort = TRUE) |>
  slice_head(n = 15) |>
  mutate(palabra = fct_reorder(palabra, n)) |>
  ggplot(aes(x = n, y = palabra)) +
  geom_col(fill = "#2d4563") +
  labs(title = "15 palabras más frecuentes",
       x = "Frecuencia", y = NULL) +
  theme_minimal()

4.3 Nube de palabras con ggwordcloud

geom_text_wordcloud() crea nubes de palabras con la estética de ggplot2.

tokens_limpios |>
  count(palabra, sort = TRUE) |>
  slice_head(n = 50) |>
  ggplot(aes(label = palabra, size = n, colour = n)) +
  geom_text_wordcloud(rm_outside = TRUE) +
  scale_size_area(max_size = 14) +
  scale_colour_gradient(low = "#457b9d", high = "#e63946") +
  theme_minimal()

5 Bag-of-words

Bag-of-words es una representación donde cada documento es un vector de frecuencias. Ignora el orden pero funciona bien para muchos problemas.

bow <- tokens_limpios |>
  count(id, palabra) |>
  pivot_wider(names_from = palabra,
              values_from = n,
              values_fill = 0)

# Dimensiones: 20 documentos x N términos
dim(bow)
[1] 20 89
# Vista parcial
bow[1:5, 1:6]
# A tibble: 5 × 6
     id empleo estancado formal inflación subió
  <int>  <int>     <int>  <int>     <int> <int>
1     1      1         1      1         1     1
2     2      1         0      0         0     0
3     3      0         0      0         0     0
4     4      0         0      0         1     0
5     5      0         0      0         0     0

6 TF-IDF

6.1 Calcular TF-IDF con bind_tf_idf()

bind_tf_idf() añade tres columnas: tf (frecuencia en el documento), idf (rareza en el corpus) y tf_idf (producto de las dos).

tfidf <- tokens_limpios |>
  count(tema, palabra) |>
  bind_tf_idf(palabra, tema, n)

head(tfidf, 10)
# A tibble: 10 × 6
   tema     palabra         n     tf   idf tf_idf
   <chr>    <chr>       <int>  <dbl> <dbl>  <dbl>
 1 economia altos           1 0.0385 1.39  0.0533
 2 economia aumenta         1 0.0385 0.693 0.0267
 3 economia crecimiento     1 0.0385 1.39  0.0533
 4 economia depende         1 0.0385 1.39  0.0533
 5 economia desempleo       1 0.0385 1.39  0.0533
 6 economia deuda           1 0.0385 1.39  0.0533
 7 economia economía        1 0.0385 1.39  0.0533
 8 economia económico       1 0.0385 1.39  0.0533
 9 economia empleo          2 0.0769 1.39  0.107 
10 economia estabilizar     1 0.0385 1.39  0.0533

6.2 Palabras más distintivas por tema

Las palabras con tf_idf alto son frecuentes en un tema pero raras en el corpus global: son el “vocabulario característico” del tema.

tfidf |>
  group_by(tema) |>
  slice_max(tf_idf, n = 3, with_ties = FALSE) |>
  arrange(tema, desc(tf_idf)) |>
  select(tema, palabra, tf_idf) |>
  mutate(tf_idf = round(tf_idf, 3))
# A tibble: 12 × 3
# Groups:   tema [4]
   tema      palabra     tf_idf
   <chr>     <chr>        <dbl>
 1 economia  empleo       0.107
 2 economia  inflación    0.107
 3 economia  altos        0.053
 4 educacion escuelas     0.107
 5 educacion aprendizaje  0.053
 6 educacion deben        0.053
 7 salud     salud        0.149
 8 salud     hospitales   0.099
 9 salud     médicos      0.099
10 seguridad violencia    0.103
11 seguridad afecta       0.051
12 seguridad amenazan     0.051

6.3 Visualizar TF-IDF por tema

tfidf |>
  group_by(tema) |>
  slice_max(tf_idf, n = 5, with_ties = FALSE) |>
  ungroup() |>
  mutate(palabra = reorder_within(palabra, tf_idf, tema)) |>
  ggplot(aes(x = tf_idf, y = palabra, fill = tema)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~ tema, scales = "free_y", ncol = 2) +
  scale_y_reordered() +
  labs(title = "Palabras más distintivas por tema (TF-IDF)",
       x = "TF-IDF", y = NULL) +
  theme_minimal()

7 Análisis de sentimiento con diccionarios

Un método simple: lista de palabras positivas y negativas, contar apariciones, sumar.

pos <- c("crecimiento", "inversión", "calidad", "gratuita",
         "protegió", "atención", "estabilizar")
neg <- c("inflación", "estancado", "deuda", "desempleo", "problema",
         "dificultades", "deserción", "violencia", "crimen",
         "narcotráfico", "delincuencia", "inseguridad", "pobres",
         "robos", "grave", "amenaza")

sentimiento <- tokens_limpios |>
  mutate(valencia = case_when(
    palabra %in% pos ~  1L,
    palabra %in% neg ~ -1L,
    TRUE ~ 0L
  )) |>
  group_by(id, tema) |>
  summarise(puntaje = sum(valencia), .groups = "drop")

# Puntaje promedio por tema
sentimiento |>
  group_by(tema) |>
  summarise(promedio = round(mean(puntaje), 2),
            n = n())
# A tibble: 4 × 3
  tema      promedio     n
  <chr>        <dbl> <int>
1 economia      -0.8     5
2 educacion      0       5
3 salud          0.2     5
4 seguridad     -1.4     5

8 Topic modeling con LDA

8.1 Crear la Document-Term Matrix

LDA necesita una matriz documento-término. cast_dtm() la construye a partir del formato tidy.

dtm <- tokens_limpios |>
  count(id, palabra) |>
  cast_dtm(id, palabra, n)

dtm
<<DocumentTermMatrix (documents: 20, terms: 88)>>
Non-/sparse entries: 107/1653
Sparsity           : 94%
Maximal term length: 15
Weighting          : term frequency (tf)

8.2 Ajustar LDA

Elegimos k = 4 porque sabemos que hay 4 temas. En aplicaciones reales, hay que probar varios K y evaluar coherencia.

modelo_lda <- LDA(dtm, k = 4,
                  control = list(seed = 2026))
modelo_lda
A LDA_VEM topic model with 4 topics.

8.3 Palabras más probables por tema (beta)

beta es la probabilidad de cada palabra dentro de cada tema. Los valores más altos muestran el “vocabulario característico” del tema según LDA.

lda_beta <- tidy(modelo_lda, matrix = "beta")

lda_beta |>
  group_by(topic) |>
  slice_max(beta, n = 5, with_ties = FALSE) |>
  summarise(palabras = paste(term, collapse = ", "))
# A tibble: 4 × 2
  topic palabras                                              
  <int> <chr>                                                 
1     1 calidad, políticas, educación, gratuita, universitaria
2     2 pandemia, tras, violencia, empleo, crecimiento        
3     3 inflación, empleo, estancado, formal, subió           
4     4 aumenta, pública, escuelas, altos, deuda              

8.4 Visualizar palabras por tema

lda_beta |>
  group_by(topic) |>
  slice_max(beta, n = 6, with_ties = FALSE) |>
  ungroup() |>
  mutate(term = reorder_within(term, beta, topic)) |>
  ggplot(aes(x = beta, y = term, fill = factor(topic))) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~ topic, scales = "free_y", ncol = 2,
             labeller = labeller(topic = function(x) paste("Tema", x))) +
  scale_y_reordered() +
  labs(title = "Palabras más probables por tema (LDA)",
       x = "Probabilidad (beta)", y = NULL) +
  theme_minimal()

8.5 Proporción de temas por documento (gamma)

gamma es la proporción de cada tema en cada documento. Suma 1 por documento. Permite asignar cada documento a su tema dominante.

lda_gamma <- tidy(modelo_lda, matrix = "gamma") |>
  mutate(document = as.integer(document))

head(lda_gamma, 8)
# A tibble: 8 × 3
  document topic   gamma
     <int> <int>   <dbl>
1        1     1 0.00229
2        2     1 0.00191
3        3     1 0.00229
4        4     1 0.00229
5        5     1 0.00229
6        6     1 0.00191
7        7     1 0.991  
8        8     1 0.00191

8.6 Validar contra las etiquetas reales

Si LDA funciona, cada tema real debería concentrarse en un solo tema LDA. Una matriz de confusión muestra la correspondencia.

clasificacion <- lda_gamma |>
  group_by(document) |>
  slice_max(gamma, n = 1) |>
  ungroup() |>
  left_join(corpus |> select(id, tema),
            by = c("document" = "id"))

table(real = clasificacion$tema, lda = clasificacion$topic)
           lda
real        1 2 3 4
  economia  0 1 3 1
  educacion 1 1 0 3
  salud     2 1 1 1
  seguridad 2 2 1 0

8.7 Heatmap de correspondencia real ↔︎ LDA

lda_gamma |>
  left_join(corpus |> select(id, tema),
            by = c("document" = "id")) |>
  group_by(tema, topic) |>
  summarise(gamma = mean(gamma), .groups = "drop") |>
  ggplot(aes(x = factor(topic), y = tema, fill = gamma)) +
  geom_tile() +
  geom_text(aes(label = round(gamma, 2)), colour = "white") +
  scale_fill_gradient(low = "#457b9d", high = "#e63946") +
  labs(x = "Tema LDA", y = "Tema real", fill = "Gamma") +
  theme_minimal()

9 Recomendaciones prácticas

  • Empezar simple: frecuencias y TF-IDF revelan mucho antes de llegar a LDA.
  • Ampliar stopwords con vocabulario específico del corpus. Las listas estándar no cubren todo.
  • Palabras cortas: filtrar con nchar(palabra) > 2 elimina muchas siglas poco útiles.
  • LDA es inestable con corpus pequeños: probar varias semillas y distintos K.
  • Validar siempre contra lectura manual de una muestra. La clasificación automática puede parecer convincente sin serlo.
  • TF-IDF funciona por grupo: si calculan por documento vs. por tema vs. por país, obtendrán resultados distintos.
  • El orden importa: anti-join de stopwords antes de filtrar por longitud evita contar tokens vacíos.

10 Resumen de funciones

Función Paquete Para qué sirve
unnest_tokens() tidytext Tokeniza texto (palabras, bigramas, oraciones)
anti_join() dplyr Elimina filas que aparecen en otra tabla (útil para stopwords)
bind_tf_idf() tidytext Calcula TF, IDF y TF-IDF
cast_dtm() tidytext Convierte formato tidy en Document-Term Matrix
cast_sparse() tidytext Convierte en matriz dispersa (para SVD, embeddings)
LDA() topicmodels Ajusta Latent Dirichlet Allocation
tidy() broom / tidytext Extrae beta o gamma de un modelo LDA
terms() topicmodels Palabras más probables de cada tema
stopwords() tm Listas de stopwords en varios idiomas
geom_text_wordcloud() ggwordcloud Nubes de palabras con ggplot2
reorder_within() tidytext Reordena factores dentro de facets
scale_y_reordered() tidytext Acompaña reorder_within() en gráficos

Para la referencia de aprendizaje supervisado, ver Referencia: tidymodels. Para clustering y PCA, ver Referencia: aprendizaje no supervisado.

Volver arriba