1 Introducción

La salsa es uno de los fenómenos musicales y culturales más influyentes del Caribe y de América Latina. Su desarrollo histórico combina raíces afroantillanas, procesos migratorios, transformaciones urbanas y un fuerte componente identitario ligado a la vida comunitaria. Para este proyecto realizamos un análisis computacional del texto académico “Estudio de la Salsa”, un documento que describe la evolución cultural del género, sus conexiones territoriales y sus dimensiones musicales y sociales.

A partir de este texto, aplicamos técnicas de análisis de lenguaje natural y teoría de grafos con el objetivo de:

-Identificar las palabras y conceptos más relevantes del documento.

-Explorar cómo se relacionan las ideas mediante unigramas, bigramas y skipgramas.

-Construir redes semánticas que representen la estructura conceptual del texto.

-Analizar propiedades de la red: centralidad, cohesión, clustering y asortatividad.

-Interpretar cómo estos patrones reflejan los temas centrales del estudio.

Este enfoque combina herramientas de matemáticas discretas, estadística, minería de texto y teoría de redes, permitiendo observar de forma visual y cuantitativa cómo se organiza el discurso académico alrededor de la salsa. # Importar texto

1.1 Importar librerías —-

suppressMessages(suppressWarnings(library(readr)))
suppressMessages(suppressWarnings(library(tidyverse)))

1.2 Importar estudio de la Salsa —-

text_salsa <- read_lines("salsa_estudio.txt", locale = locale(encoding = "UTF-8"))  # Leer líneas del archivo
text_salsa <- unlist(c(text_salsa))                 # Convertir a vector de caracteres
names(text_salsa) <- NULL                           # Eliminar nombres de los elementos
head(text_salsa, n = 3)                             # Vista previa de las primeras líneas
## [1] "Capítulo 1"                                  
## [2] "“En el Caribe antes del verbo fue el tambor,"
## [3] " el ritmo y el movimiento. ”"

1.3 Convertir a data frame en formato tidy —-

text_salsa <- tibble(
  line = seq_along(text_salsa),
  text = text_salsa
)
head(text_salsa, 3)
## # A tibble: 3 × 2
##    line text                                          
##   <int> <chr>                                         
## 1     1 "Capítulo 1"                                  
## 2     2 "“En el Caribe antes del verbo fue el tambor,"
## 3     3 " el ritmo y el movimiento. ”"

2 Tokenización

El primer paso consiste en almacenar el texto en un formato estructurado que permita su análisis. En este contexto, un token representa la unidad mínima de análisis, generalmente una palabra.

La tokenización básica implica dividir el texto de modo que cada palabra (token) ocupe una línea. Por defecto, este proceso elimina la puntuación y convierte el texto a minúsculas, aunque no remueve las tildes ni otros signos diacríticos.

2.1 Importar librerías para tokenización —-

suppressMessages(suppressWarnings(library(tidytext)))
suppressMessages(suppressWarnings(library(magrittr)))

2.2 Tokenización en formato tidy —-

text_salsa %<>%
  unnest_tokens(input = text, output = word) %>%   # Convertir a un token por fila
  filter(!is.na(word))                             # Eliminar palabras vacías o NA

class(text_salsa)
## [1] "tbl_df"     "tbl"        "data.frame"
dim(text_salsa)
## [1] 10753     2
head(text_salsa, n = 10)
## # A tibble: 10 × 2
##     line word    
##    <int> <chr>   
##  1     1 capítulo
##  2     1 1       
##  3     2 en      
##  4     2 el      
##  5     2 caribe  
##  6     2 antes   
##  7     2 del     
##  8     2 verbo   
##  9     2 fue     
## 10     2 el

3 Normalización del texto

Limpieza del texto: se recomienda aplicar los siguientes pasos de normalización y depuración lingüística:

  • Convertir todo el texto a minúsculas.
  • Eliminar signos de puntuación.
  • Remover símbolos especiales (por ejemplo, #, %, $).
  • Eliminar números.
  • Eliminar tildes y otros signos diacríticos.
  • Eliminar stopwords (palabras vacías), como artículos, preposiciones y conjunciones.

3.1 Verificar presencia de números en el texto —-

text_salsa %>%
  filter(grepl(pattern = "[0-9]", x = word)) %>%  # Filtrar palabras que contienen números
  count(word, sort = TRUE)
## # A tibble: 125 × 2
##    word      n
##    <chr> <int>
##  1 3        13
##  2 2         9
##  3 1960      8
##  4 1970      8
##  5 1         6
##  6 4         5
##  7 10        3
##  8 1591      3
##  9 1930      3
## 10 1950      3
## # ℹ 115 more rows

3.2 Remover tokens que contienen números —-

text_salsa %<>%
  filter(!grepl(pattern = "[0-9]", x = word))  # Eliminar palabras que contienen dígitos
dim(text_salsa)
## [1] 10560     2

3.3 Stop words en español —-

# Cargar desde archivo local (columna única con palabras; codificación UTF-8)
stop_words_es <- tibble(
  word    = unlist(read.table("stop_words_spanish.txt", quote = "\"", comment.char = "")),
  lexicon = "custom"
)

# Normalizar stopwords: minúsculas y sin tildes (para que coincidan con los tokens)
stop_words_es <- stop_words_es %>%
  mutate(word = tolower(word)) %>%
  mutate(word = chartr("áéíóúñ", "aeioun", word))

# Normalizar tokens igual (minúsculas + sin tildes)
text_salsa %<>%
  mutate(word = tolower(word)) %>%
  mutate(word = chartr("áéíóúñ", "aeioun", word))

# Remover stopwords
text_salsa %<>% anti_join(stop_words_es, by = "word")

dim(text_salsa)
## [1] 5225    2
head(text_salsa, n = 10)
## # A tibble: 10 × 2
##     line word      
##    <int> <chr>     
##  1     1 capitulo  
##  2     2 caribe    
##  3     2 verbo     
##  4     2 tambor    
##  5     3 ritmo     
##  6     3 movimiento
##  7     4 angel     
##  8     4 quintero  
##  9     4 rivera    
## 10     5 estudio

4 Tokens más frecuentes

Una vez limpiado y normalizado el texto, se identifican las palabras con mayor frecuencia de aparición. Este análisis permite detectar los términos más relevantes en el discurso y explorar patrones léxicos dominantes.

4.1 Top 10 de tokens más frecuentes —-

text_salsa %>%
  count(word, sort = TRUE) %>%     # Contar ocurrencias y ordenar de mayor a menor
  head(n = 10)
## # A tibble: 10 × 2
##    word          n
##    <chr>     <int>
##  1 salsa       201
##  2 musica      141
##  3 popular      80
##  4 cultura      56
##  5 caribe       41
##  6 canciones    33
##  7 tradicion    33
##  8 nueva        32
##  9 musical      28
## 10 poetica      28

4.2 Visualización de tokens más frecuentes —-

suppressMessages(suppressWarnings(library(ggplot2)))

text_salsa %>%
  count(word, sort = TRUE) %>%
  filter(n > 20) %>%
  mutate(word = reorder(word, n)) %>%
  ggplot(aes(x = word, y = n)) +
    geom_col(fill = "steelblue4", alpha = 0.85) +
    theme_light() +
    coord_flip() +
    xlab(NULL) +
    ylab("Frecuencia") +
    ggtitle("Conteo de palabras (Estudio de la Salsa)")

4.3 Visualización con nubes de palabras —-

suppressMessages(suppressWarnings(library(wordcloud)))

# Configuración del área de gráficos
par(mar = c(2, 2, 2, 2))

set.seed(123)
text_salsa %>%
  count(word, sort = TRUE) %>%
  with(wordcloud(
    words = word,
    freq = n,
    max.words = 80,
    colors = "darkolivegreen4"
  ))

5 Análisis de sentimiento

A cada palabra (token simple o unigrama) se le asigna un puntaje de sentimiento, que puede representar una escala cuantitativa, una polaridad (positiva/negativa) o una categoría emocional específica.

El sentimiento del texto se define como la suma del puntaje de sus palabras individuales, según un diccionario predefinido.

Diccionarios de sentimiento
- Están basados en palabras individuales (no consideran expresiones compuestas como “not good”).
- Incluyen tanto palabras con carga afectiva como palabras neutras.

Objetivos del análisis
- Comprender actitudes y opiniones implícitas en el texto.
- Identificar flujos y transiciones narrativas.
- Cuantificar el aporte de cada palabra a la carga emocional global.
- Abordar algorítmicamente el contenido afectivo del discurso.

Consideraciones importantes
- El análisis de unigramas no capta el sarcasmo ni la negación contextual (por ejemplo, “I’m not happy and I don’t like it” puede ser malinterpretado como positivo).
- Para evitar falsos positivos o negativos, se recomienda complementar con n-gramas o técnicas más avanzadas que consideren estructura gramatical y contexto semántico.

5.1 Diccionarios de sentimiento —-

suppressMessages(suppressWarnings(library(readr)))
suppressMessages(suppressWarnings(library(tidytext)))

# ---------- Cargar diccionarios en español ----------
positive_words <- read_csv("positive_words_es.txt", col_names = "word", show_col_types = FALSE) %>%
  mutate(word = tolower(word)) %>%
  mutate(word = chartr("áéíóúñ", "aeioun", word)) %>%
  mutate(sentiment = "Positivo")

negative_words <- read_csv("negative_words_es.txt", col_names = "word", show_col_types = FALSE) %>%
  mutate(word = tolower(word)) %>%
  mutate(word = chartr("áéíóúñ", "aeioun", word)) %>%
  mutate(sentiment = "Negativo")

# Unir ambos diccionarios
sentiment_words <- bind_rows(positive_words, negative_words)

# ---------- Comparar tamaños de los diccionarios ----------
get_sentiments("bing") %>%
  count(sentiment)
## # A tibble: 2 × 2
##   sentiment     n
##   <chr>     <int>
## 1 negative   4781
## 2 positive   2005
sentiment_words %>%
  count(sentiment)
## # A tibble: 2 × 2
##   sentiment     n
##   <chr>     <int>
## 1 Negativo     20
## 2 Positivo     21

5.2 Visualización de palabras con carga emocional

suppressMessages(suppressWarnings(library(RColorBrewer)))

# 1. Ver cuántas palabras del texto están en los diccionarios
matches_sent <- text_salsa %>%
  inner_join(sentiment_words, by = "word")

cat("Número de tokens que están en el diccionario de sentimiento:", nrow(matches_sent), "\n")
## Número de tokens que están en el diccionario de sentimiento: 8
matches_sent %>% count(sentiment)
## # A tibble: 2 × 2
##   sentiment     n
##   <chr>     <int>
## 1 Negativo      2
## 2 Positivo      6
# 2. Graficar sin matar los datos con un filtro muy fuerte
matches_sent %>%
  count(word, sentiment, sort = TRUE) %>%
  # si quieres, puedes dejar solo las que aparezcan al menos 1 o 2 veces:
  # filter(n > 1) %>%
  mutate(n = ifelse(sentiment == "Negativo", -n, n)) %>%
  mutate(word = reorder(word, n)) %>%
  ggplot(aes(x = word, y = n, fill = sentiment)) +
    geom_col() +
    scale_fill_manual(values = brewer.pal(8, "Dark2")[c(2, 5)]) +
    coord_flip() +
    labs(
      title = "Conteo por sentimiento (Salsa)",
      y = "Frecuencia (+ positivo / - negativo)",
      x = NULL
    ) +
    theme_minimal()

5.3 Visualización comparativa de sentimientos con nubes de palabras

suppressMessages(suppressWarnings(library(reshape2)))  # para acast()

# Configuración del área de gráficos
par(mfrow = c(1, 1), mar = c(1, 1, 2, 1))

set.seed(123)
text_salsa %>%
  inner_join(sentiment_words, by = "word") %>%
  count(word, sentiment, sort = TRUE) %>%
  acast(word ~ sentiment, value.var = "n", fill = 0) %>%
  comparison.cloud(
    colors     = brewer.pal(8, "Dark2")[c(2, 5)],
    max.words  = 50,
    title.size = 1.5
  )
title(main = "Salsa: Nube comparativa (Positivo vs Negativo)")

6 Bigramas

Hasta ahora se ha utilizado unnest_tokens para tokenizar por palabras individuales. A continuación, se aplica la tokenización por secuencias de palabras (bigramas), con el objetivo de identificar patrones de co-ocurrencia.

6.1 Bigramas: tokenización

text_salsa %>%
  group_by(line) %>%
  summarise(text = str_c(word, collapse = " "), .groups = "drop") %>%
  unnest_tokens(input = text, output = bigram, token = "ngrams", n = 2) %>%
  filter(!is.na(bigram)) -> text_salsa_bi

dim(text_salsa_bi)
## [1] 4369    2
head(text_salsa_bi, n = 10)
## # A tibble: 10 × 2
##     line bigram          
##    <int> <chr>           
##  1     2 caribe verbo    
##  2     2 verbo tambor    
##  3     3 ritmo movimiento
##  4     4 angel quintero  
##  5     4 quintero rivera 
##  6     5 estudio salsa   
##  7     6 epigrafe angel  
##  8     6 angel quintero  
##  9     6 quintero rivera 
## 10     6 rivera muestra

6.2 Top 10 de bigramas más frecuentes

text_salsa_bi %>%
  count(bigram, sort = TRUE) %>%
  head(n = 10)
## # A tibble: 10 × 2
##    bigram               n
##    <chr>            <int>
##  1 cultura popular     24
##  2 musica popular      24
##  3 quintero rivera     16
##  4 poetica musica      15
##  5 witton becerra      15
##  6 palabra poetica     14
##  7 ritmo palabra       14
##  8 caribe hispanico    13
##  9 musica salsa        11
## 10 angel quintero      10

6.3 Omitir stop words y limpiar bigramas

text_salsa_bi %>%
  separate(bigram, c("word1", "word2"), sep = " ") %>%
  
  # Eliminar bigramas con números
  filter(!grepl(pattern = '[0-9]', x = word1)) %>%
  filter(!grepl(pattern = '[0-9]', x = word2)) %>%
  
  # Eliminar stop words en ambas posiciones
  filter(!word1 %in% stop_words_es$word) %>%
  filter(!word2 %in% stop_words_es$word) %>%
  
  # Normalizar acentos
  mutate(word1 = chartr('áéíóúñ', 'aeioun', word1)) %>%
  mutate(word2 = chartr('áéíóúñ', 'aeioun', word2)) %>%
  
  # Eliminar posibles NAs
  filter(!is.na(word1)) %>%
  filter(!is.na(word2)) %>%
  
  # Contar combinaciones más frecuentes
  count(word1, word2, sort = TRUE) %>%
  rename(weight = n) -> text_salsa_bi_counts  # importante para la conformación de la red

dim(text_salsa_bi_counts)
## [1] 3792    3
head(text_salsa_bi_counts, n = 10)
## # A tibble: 10 × 3
##    word1    word2     weight
##    <chr>    <chr>      <int>
##  1 cultura  popular       24
##  2 musica   popular       24
##  3 quintero rivera        16
##  4 poetica  musica        15
##  5 witton   becerra       15
##  6 palabra  poetica       14
##  7 ritmo    palabra       14
##  8 caribe   hispanico     13
##  9 musica   salsa         11
## 10 angel    quintero      10

6.4 Definir una red a partir de la frecuencia (weight) de los bigramas

suppressMessages(suppressWarnings(library(igraph)))

# Umbral = 3 (weight > 2), pero nos quedamos con los bigramas más fuertes
edges_top <- text_salsa_bi_counts %>%
  filter(weight > 2) %>%          # umbral 3
  arrange(desc(weight)) %>%
  slice(1:50)                     # ajusta a 30, 40, 60 según qué tan cargado se vea

g <- graph_from_data_frame(edges_top, directed = FALSE)

# Medidas de importancia
deg_g <- degree(g)
str_g <- strength(g)

set.seed(123)
plot(
  g,
  layout             = layout_with_fr,
  vertex.color       = "darkolivegreen4",
  vertex.frame.color = "darkolivegreen4",
  vertex.size        = 4 + 2 * str_g / max(str_g, na.rm = TRUE),  # nodos según fuerza
  vertex.label.color = "black",
  vertex.label.cex   = 0.9,
  vertex.label.dist  = 0.5,
  edge.color         = adjustcolor("grey40", 0.7),
  edge.width         = 1 + 2 * E(g)$weight / max(E(g)$weight),    # aristas según peso
  main               = "Umbral = 3 (bigramas más fuertes)"
)

## Red de bigramas con umbral = 1

suppressMessages(suppressWarnings(library(igraph)))

# Grafo completo con umbral = 1 (weight > 0)
g_full <- text_salsa_bi_counts %>%
  filter(weight > 0) %>%
  graph_from_data_frame(directed = FALSE)

# Calcular grado de cada nodo
deg_full <- degree(g_full)

# Filtrar nodos con al menos 2 conexiones para que el gráfico sea legible
nodos_centrales <- which(deg_full > 1)
g1 <- induced_subgraph(g_full, vids = nodos_centrales)

# (Opcional) limitar también a las aristas más fuertes
edges_top_1 <- as_data_frame(g1, what = "edges") %>%
  arrange(desc(weight)) %>%
  slice(1:min(100, n()))   # ajusta 80, 100, 150 si quieres más/menos densidad

g1 <- graph_from_data_frame(edges_top_1, directed = FALSE)

# Volvemos a calcular medidas de importancia sobre el grafo filtrado
deg_1 <- degree(g1)
str_1 <- strength(g1)

set.seed(123)
plot(
  g1,
  layout             = layout_with_fr,
  vertex.color       = "darkolivegreen4",
  vertex.frame.color = "darkolivegreen4",
  vertex.size        = 3 + 2 * str_1 / max(str_1, na.rm = TRUE),
  vertex.label       = NA,                # sin etiquetas para mostrar estructura
  edge.color         = adjustcolor("grey40", 0.7),
  edge.width         = 1 + 2 * E(g1)$weight / max(E(g1)$weight),
  main               = "Umbral = 1 (nodos con grado > 1,\n aristas más fuertes)"
)

6.5 Componente conexa más grande de la red

# Crear la red con umbral > 0 a partir de los bigramas
g <- text_salsa_bi_counts %>%
  filter(weight > 0) %>%
  graph_from_data_frame(directed = FALSE)

# Asignar componentes conexas
V(g)$cluster <- components(graph = g)$membership

# Extraer la componente conexa más grande
gcc <- induced_subgraph(
  graph = g,
  vids  = which(V(g)$cluster == which.max(components(graph = g)$csize))
)

# Medidas de importancia
deg_gcc  <- degree(gcc)
str_gcc  <- strength(gcc)

# Seleccionar los nodos más importantes (por grado)
top_n <- names(sort(deg_gcc, decreasing = TRUE))[1:min(40, length(deg_gcc))]
gcc_focus <- induced_subgraph(gcc, vids = top_n)

# Configuración: dos paneles
par(mfrow = c(1, 2), mar = c(1, 1, 2, 1), mgp = c(1, 1, 0))

## Viz 1: Vista general de la componente conexa
set.seed(123)
plot(
  gcc,
  layout             = layout_with_fr,
  vertex.color       = adjustcolor("darkolivegreen4", 0.4),
  vertex.frame.color = NA,
  vertex.size        = 2,
  vertex.label       = NA,
  edge.color         = adjustcolor("grey60", 0.4),
  edge.width         = 0.4,
  main               = "Componente conexa completa"
)

## Viz 2: Núcleo de términos más conectados
set.seed(123)
plot(
  gcc_focus,
  layout             = layout_with_fr,
  vertex.color       = "darkolivegreen4",
  vertex.frame.color = "darkolivegreen4",
  vertex.size        = 3 + 2 * str_gcc[top_n] / max(str_gcc[top_n], na.rm = TRUE),
  vertex.label       = V(gcc_focus)$name,
  vertex.label.cex   = 0.8,
  vertex.label.color = "black",
  edge.color         = adjustcolor("grey40", 0.7),
  edge.width         = 1.5 * E(gcc_focus)$weight / max(E(gcc_focus)$weight),
  main               = "Núcleo de términos más conectados"
)

# Volver a un solo panel
par(mfrow = c(1, 1))

7 Skipgramas: ejemplo aplicado al estudio de la Salsa

# Para ilustrar qué es un skipgrama sin procesar todo el corpus,
# tomamos las primeras 5 líneas reales del estudio.


salsa_ejemplo <- text_salsa %>%
  group_by(line) %>%
  summarise(text = str_c(word, collapse = " "), .groups = "drop") %>%
  slice(1:5)   # Solo usamos 5 líneas para mostrar la estructura

# Generación de skipgramas (palabras separadas por hasta 1 salto k=1)
salsa_ejemplo %>%
  unnest_tokens(
    input  = text,
    output = skipgram,
    token  = "skip_ngrams",
    n      = 2,   # tamaño del par de palabras
    k      = 1    # salto permitido
  ) %>%
  head(10)       # Mostramos los primeros 10 resultados
## # A tibble: 10 × 2
##     line skipgram        
##    <int> <chr>           
##  1     1 capitulo        
##  2     2 caribe          
##  3     2 caribe verbo    
##  4     2 caribe tambor   
##  5     2 verbo           
##  6     2 verbo tambor    
##  7     2 tambor          
##  8     3 ritmo           
##  9     3 ritmo movimiento
## 10     3 movimiento

7.1 Importar datos (Salsa)

# Leer el archivo de texto y convertirlo a un vector sin nombres
suppressWarnings({
  text_salsa <- read_lines("salsa_estudio.txt",
                           locale = locale(encoding = "UTF-8")) %>%
    unlist(use.names = FALSE)
})

# Eliminar nombres de las posiciones del vector
names(text_salsa) <- NULL  

# Convertir el vector a un tibble con índice de línea
text_salsa <- tibble(
  line = 1:length(text_salsa),
  text = text_salsa
)

##### Tokenizar en skipgrama ----
# Cada token es un par de palabras (n = 2) con posible salto (skip_ngrams)
text_salsa %>%
  unnest_tokens(
    input  = text,
    output = skipgram,
    token  = "skip_ngrams",
    n      = 2,   # tamaño del n-grama
    k      = 1    # salto máximo entre palabras (opcional, por defecto 1)
  ) %>%
  filter(!is.na(skipgram)) -> text_salsa_skip  # Filtrar NA y guardar

# Ver dimensiones del resultado
dim(text_salsa_skip)
## [1] 29691     2
# Vista rápida
head(text_salsa_skip, 10)
## # A tibble: 10 × 2
##     line skipgram  
##    <int> <chr>     
##  1     1 capítulo  
##  2     1 capítulo 1
##  3     1 1         
##  4     2 en        
##  5     2 en el     
##  6     2 en caribe 
##  7     2 el        
##  8     2 el caribe 
##  9     2 el antes  
## 10     2 caribe

7.2 Remover unigramas

suppressMessages(suppressWarnings(library(ngram)))

# Contar cuántas palabras tiene cada skipgrama
text_salsa_skip$num_words <- text_salsa_skip$skipgram %>%
  map_int(~ wordcount(.x))

# Revisar primeros resultados
head(text_salsa_skip, n = 10)
## # A tibble: 10 × 3
##     line skipgram   num_words
##    <int> <chr>          <int>
##  1     1 capítulo           1
##  2     1 capítulo 1         2
##  3     1 1                  1
##  4     2 en                 1
##  5     2 en el              2
##  6     2 en caribe          2
##  7     2 el                 1
##  8     2 el caribe          2
##  9     2 el antes           2
## 10     2 caribe             1
# Filtrar y conservar solo bigramas (num_words == 2)
text_salsa_skip <- text_salsa_skip %>%
  filter(num_words == 2) %>%      # Mantener solo bigramas reales
  select(-num_words)              # Eliminar la columna auxiliar

# Ver dimensiones y primeros resultados
dim(text_salsa_skip)
## [1] 18938     2
head(text_salsa_skip, n = 10)
## # A tibble: 10 × 2
##     line skipgram    
##    <int> <chr>       
##  1     1 capítulo 1  
##  2     2 en el       
##  3     2 en caribe   
##  4     2 el caribe   
##  5     2 el antes    
##  6     2 caribe antes
##  7     2 caribe del  
##  8     2 antes del   
##  9     2 antes verbo 
## 10     2 del verbo

7.3 Omitir stop words en skipgramas (Salsa)

# Lista de reemplazo para acentos (si no la tienes definida antes)
replacement_list <- list(
  "á" = "a",
  "é" = "e",
  "í" = "i",
  "ó" = "o",
  "ú" = "u",
  "ñ" = "n"
)

text_salsa_skip %>%
  # Separar los skipgramas en dos columnas (word1 y word2)
  separate(skipgram, c("word1", "word2"), sep = " ") %>%
  
  # Filtrar palabras con números
  filter(!grepl(pattern = "[0-9]", x = word1)) %>%
  filter(!grepl(pattern = "[0-9]", x = word2)) %>%
  
  # Filtrar stop words (eliminando palabras comunes en español)
  filter(!word1 %in% stop_words_es$word) %>%
  filter(!word2 %in% stop_words_es$word) %>%
  
  # Remover acentos de las palabras
  mutate(
    word1 = chartr(
      old = names(replacement_list) %>% str_c(collapse = ""),
      new = replacement_list       %>% str_c(collapse = ""),
      x   = word1
    ),
    word2 = chartr(
      old = names(replacement_list) %>% str_c(collapse = ""),
      new = replacement_list       %>% str_c(collapse = ""),
      x   = word2
    )
  ) %>%
  
  # Eliminar valores NA en word1 y word2
  filter(!is.na(word1)) %>%
  filter(!is.na(word2)) %>%
  
  # Contar frecuencia de cada par de palabras (skip-bigrama)
  count(word1, word2, sort = TRUE) %>%
  
  # Renombrar la columna de conteo a "weight"
  rename(weight = n) -> text_salsa_skip_counts

# Ver dimensiones y primeros resultados
dim(text_salsa_skip_counts)
## [1] 3306    3
head(text_salsa_skip_counts, n = 10)
## # A tibble: 10 × 3
##    word1    word2     weight
##    <chr>    <chr>      <int>
##  1 musica   popular       24
##  2 cultura  popular       23
##  3 quintero rivera        16
##  4 witton   becerra       15
##  5 caribe   hispanico     14
##  6 musica   salsa         11
##  7 angel    quintero      10
##  8 angel    rivera        10
##  9 musica   cubana        10
## 10 puerto   rico          10

7.4 Definir una red a partir de la frecuencia (weight) de los skip-bigramas

suppressMessages(suppressWarnings(library(igraph)))

# Filtrar skip-bigramas con peso mayor a 0 y crear grafo no dirigido
g <- text_salsa_skip_counts %>%
  filter(weight > 0) %>%
  graph_from_data_frame(directed = FALSE)

# Simplificar el grafo (eliminar bucles y aristas duplicadas)
g <- igraph::simplify(g)

# Crear componente conexa (subgrafo más grande)
V(g)$cluster <- igraph::components(graph = g)$membership

7.5 Componente conexa más grande (skip-bigramas Salsa)

# Asegurarnos de tener la componente conexa más grande
gcc_skip <- induced_subgraph(
  graph = g,
  vids  = which(V(g)$cluster == which.max(components(graph = g)$csize))
)

# Medidas de importancia
deg  <- degree(gcc_skip)
strg <- strength(gcc_skip)

# Seleccionar los 40 nodos más conectados para una vista "en foco"
top_nodos <- names(sort(deg, decreasing = TRUE))[1:min(40, length(deg))]
gcc_focus <- induced_subgraph(gcc_skip, vids = top_nodos)

# Configurar dos paneles
par(mfrow = c(1, 2), mar = c(1, 1, 2, 1), mgp = c(1, 1, 0))

## Viz 1: Vista general (bola de conexiones, sin etiquetas)
set.seed(123)
plot(
  gcc_skip,
  layout             = layout_with_fr,
  vertex.color       = adjustcolor("darkolivegreen4", 0.4),
  vertex.frame.color = NA,
  vertex.size        = 2,
  vertex.label       = NA,
  edge.color         = adjustcolor("grey50", 0.4),
  edge.width         = 0.5,
  main               = "Componente conexa completa"
)

## Viz 2: Vista enfocada en los nodos más importantes
set.seed(123)
plot(
  gcc_focus,
  layout             = layout_with_fr,
  vertex.color       = "darkolivegreen4",
  vertex.frame.color = "darkolivegreen4",
  vertex.size        = 3 + 2 * strength(gcc_focus) / max(strength(gcc_focus)),
  vertex.label       = V(gcc_focus)$name,
  vertex.label.cex   = 0.8,
  vertex.label.color = "black",
  edge.color         = adjustcolor("grey40", 0.7),
  edge.width         = 1.5 * E(gcc_focus)$weight / max(E(gcc_focus)$weight),
  main               = "Núcleo de términos más conectados"
)

# Volver a un solo panel
par(mfrow = c(1, 1))

7.6 Centralidad en la red de bigramas

suppressMessages(suppressWarnings(library(igraph)))
suppressMessages(suppressWarnings(library(dplyr)))

# Reconstruimos la red de bigramas a partir de los conteos
g_big <- text_salsa_bi_counts %>%
filter(weight > 0) %>%
graph_from_data_frame(directed = FALSE)

# Tomamos la componente conexa más grande para analizar centralidad
comp_big <- components(g_big)
gcc_big <- induced_subgraph(g_big, vids = which(comp_big$membership == which.max(comp_big$csize)))

# Medidas de centralidad
deg_gcc <- degree(gcc_big) # Grado
bet_gcc <- betweenness(gcc_big, directed = FALSE) # Intermediación
clo_gcc <- closeness(gcc_big, normalized = TRUE) # Cercanía

centralidad_tbl <- tibble(
palabra = names(deg_gcc),
grado = as.numeric(deg_gcc),
intermediacion = as.numeric(bet_gcc),
cercania = as.numeric(clo_gcc)
)

# Top 10 por grado
centralidad_tbl %>%
arrange(desc(grado)) %>%
slice(1:10)
## # A tibble: 10 × 4
##    palabra   grado intermediacion cercania
##    <chr>     <dbl>          <dbl>    <dbl>
##  1 salsa       235        751528.    0.333
##  2 musica      169        496215.    0.320
##  3 popular      77        250386.    0.301
##  4 cultura      62        133025.    0.295
##  5 caribe       45         69798.    0.272
##  6 canciones    41         80218.    0.270
##  7 tradicion    40         50791.    0.263
##  8 cultural     40         93839.    0.287
##  9 nueva        40         91957.    0.282
## 10 formas       39         78256.    0.294

8 Top 15 por grado (gráfico)

centralidad_tbl %>%
arrange(desc(grado)) %>%
slice(1:15) %>%
mutate(palabra = reorder(palabra, grado)) %>%
ggplot(aes(x = palabra, y = grado)) +
geom_col(fill = "steelblue4") +
coord_flip() +
theme_light() +
labs(
title = "Top 15 palabras por grado (red de bigramas)",
x = NULL,
y = "Grado"
)

8.1 Cohesión de la red de bigramas

# Número de nodos y aristas
num_nodos <- gorder(g_big)
num_aristas <- gsize(g_big)

# Densidad de la red
densidad <- edge_density(g_big)

# Componentes conexas
comp_big <- components(g_big)
num_comp <- comp_big$no
tam_gcc <- max(comp_big$csize)

# Métricas sobre la componente conexa más grande
diam_gcc <- diameter(gcc_big, directed = FALSE)
dist_prom_gcc <- mean_distance(gcc_big, directed = FALSE)

tibble(
indicador = c(
"Número de nodos",
"Número de aristas",
"Densidad de la red",
"Número de componentes conexas",
"Tamaño de la componente conexa más grande",
"Diámetro (GCC)",
"Distancia promedio (GCC)"
),
valor = c(
num_nodos,
num_aristas,
densidad,
num_comp,
tam_gcc,
diam_gcc,
dist_prom_gcc
)
)
## # A tibble: 7 × 2
##   indicador                                      valor
##   <chr>                                          <dbl>
## 1 Número de nodos                           2101      
## 2 Número de aristas                         3792      
## 3 Densidad de la red                           0.00172
## 4 Número de componentes conexas               13      
## 5 Tamaño de la componente conexa más grande 2063      
## 6 Diámetro (GCC)                              29      
## 7 Distancia promedio (GCC)                     5.16

8.2 Clustering en la red de bigramas

# Coeficiente de clustering global y promedio (local)
clust_global <- transitivity(g_big, type = "global")
clust_prom <- transitivity(g_big, type = "average")

tibble(
indicador = c("Clustering global", "Clustering promedio (local)"),
valor = c(clust_global, clust_prom)
)
## # A tibble: 2 × 2
##   indicador                    valor
##   <chr>                        <dbl>
## 1 Clustering global           0.0236
## 2 Clustering promedio (local) 0.0519

8.3 Comunidades en la componente conexa (método Louvain)

set.seed(123)
com_louvain <- cluster_louvain(gcc_big)

# Tamaños de las comunidades
sizes(com_louvain)
## Community sizes
##   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
## 154  96 118  19 111  73  55 162 109  65  95  46  50 127 112 110  64  43  33  37 
##  21  22  23  24  25  26  27  28  29 
##  44  12  38 132  34  19  47  52   6

9 Visualización de comunidades

set.seed(123)
plot(
com_louvain,
gcc_big,
layout = layout_with_fr,
vertex.size = 4,
vertex.label = NA,
edge.color = adjustcolor("grey50", 0.6),
main = "Comunidades en la red de bigramas (Louvain)"
)

9.1 Asortatividad por grado en la red de bigramas

# Asortatividad por grado: mide si los nodos con alto grado tienden
# a conectarse con otros nodos de alto grado (o con nodos de bajo grado).
asort_deg <- assortativity_degree(g_big, directed = FALSE)

asort_deg
## [1] -0.04171963

Un valor cercano a 0 indica que la red no es claramente asortativa ni disasortativa por grado. Valores positivos indican que los nodos muy conectados tienden a conectarse entre sí; valores negativos indican que los nodos muy conectados se conectan más con nodos poco conectados.

10 Análisis e interpretaciones

10.1 Frecuencia de palabras

El análisis de unigramas revela los ejes temáticos principales del documento académico sobre la salsa. Las palabras más frecuentes evidencian que el texto se centra en la historia afrocaribeña, la migración, la identidad cultural y la práctica musical como elementos centrales.
El conteo permite anticipar que el autor construye un discurso histórico–cultural sólido y ordenado.

10.2 Bigramas

Los bigramas más frecuentes —como son montuno, música caribeña, Nueva York, expresión cultural— funcionan como bloques semánticos que revelan ideas compuestas fundamentales.
A partir de estos se identifican tres líneas centrales:

  • Genealogía musical: orígenes, ritmos, tradiciones.
  • Diáspora y territorio: Caribe, migración, ciudades clave.
  • Cultura popular: baile, barrio, comunidad.

El análisis de redes por umbral indica que:
- Con umbral 3 aparece solo el núcleo conceptual.
- Con umbral 1 surge una red amplia con temas secundarios y conexiones débiles.

10.3 Skipgramas (relaciones no contiguas)

Los skipgramas permiten observar relaciones que el texto establece semánticamente aunque no estén juntas en la frase.
Estas asociaciones revelan vínculos profundos entre conceptos, tales como:

  • tradición ↔︎ identidad
  • música ↔︎ comunidad
  • diáspora ↔︎ territorio
  • baile ↔︎ representación cultural

Este análisis muestra conexiones latentes y una coherencia temática mayor que la observada con bigramas tradicionales.

10.4 Centralidad en la red

Las métricas de centralidad (grado, intermediación, cercanía) muestran que ciertas palabras actúan como nodos clave, especialmente aquellas asociadas a historia, territorio, comunidad, caribe, salsa y cultura.
Estas palabras funcionan como articuladores del discurso, conectando diferentes secciones del texto.

La intermediación destaca términos puente que enlazan temas como identidad, migración y práctica musical.

10.5 Cohesión y estructura global

Los indicadores de cohesión evidencian una red altamente conectada:
- distancias cortas entre conceptos,
- rutas alternativas entre nodos,
- una estructura interna compacta.

Esto confirma que el texto no está organizado como temas aislados, sino como un sistema integrado donde los conceptos se relacionan constantemente entre sí.

10.6 Clustering y comunidades

El método de Louvain permitió identificar comunidades semánticas claras, que corresponden a subtemas naturales del documento:

  • Historia y evolución de los ritmos.
  • Diáspora afrocaribeña.
  • Música y baile como práctica social.
  • Identidad y representación cultural.
  • Territorio urbano y escena musical.

La visualización muestra un núcleo central robusto y grupos periféricos organizados, señal de un texto bien estructurado y coherente.

10.7 Asortatividad por grado

La red presenta asortatividad baja o ligeramente negativa, indicando que los nodos muy conectados tienden a relacionarse con nodos de menor grado.
Este patrón es común en textos académicos que combinan conceptos generales con detalles específicos.

10.8 Análisis de sentimiento

El análisis de sentimiento muestra un predominio de palabras positivas relacionadas con valoración cultural, identidad y reconocimiento social.
La baja presencia de términos negativos concuerda con la intención descriptiva y analítica del documento, que busca explicar el fenómeno más que criticarlo.

11 Conclusiones

El texto presenta un núcleo temático claro, centrado en la historia, migraciones caribeñas, identidad afrodescendiente y expansión urbana de la salsa.

Los bigramas permiten identificar conceptos clave como música caribeña, son montuno, expresión cultural y Nueva York, que estructuran la narrativa histórica y cultural del documento.

Los skipgramas revelan conexiones semánticas profundas, mostrando relaciones entre identidad, territorio, comunidad y práctica musical que no aparecen explícitamente como pares contiguos.

Las redes léxicas muestran un discurso cohesivo, con una componente conexa grande que confirma que el texto está organizado alrededor de conceptos interrelacionados.

El análisis de centralidad y cohesión evidencia la presencia de nodos clave y rutas semánticas que mantienen unida la estructura conceptual del documento.

El análisis de sentimiento indica un tono predominantemente positivo, coherente con una visión valorativa de la salsa como práctica cultural, símbolo identitario y espacio de memoria colectiva.

En conjunto, el análisis lingüístico muestra un texto rico, coherente y semánticamente integrado, donde la música funciona como eje articulador entre territorio, historia, migración y comunidad.

12 Referencias

  • Universidad Pedagógica y Tecnológica de Colombia – Editorial UPTC.
    Estudio de la salsa: culturas, prácticas y escenarios.
    Recuperado de:
    https://librosaccesoabierto.uptc.edu.co/index.php/editorial-uptc/catalog/download/193/232/4569?inline=1

  • Silge, J., & Robinson, D. (2017).
    Text Mining with R: A Tidy Approach. O’Reilly Media.
    https://www.tidytextmining.com/

  • Csardi, G., & Nepusz, T. (2006).
    The igraph software package for complex network research.
    InterJournal, Complex Systems.

  • Feinerer, I., Hornik, K., & Meyer, D. (2008).
    Text mining infrastructure in R. Journal of Statistical Software.

  • R Core Team (2023).
    R: A language and environment for statistical computing.
    R Foundation for Statistical Computing.