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
suppressMessages(suppressWarnings(library(readr)))
suppressMessages(suppressWarnings(library(tidyverse)))
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. ”"
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. ”"
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.
suppressMessages(suppressWarnings(library(tidytext)))
suppressMessages(suppressWarnings(library(magrittr)))
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
Limpieza del texto: se recomienda aplicar los siguientes pasos de normalización y depuración lingüística:
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
text_salsa %<>%
filter(!grepl(pattern = "[0-9]", x = word)) # Eliminar palabras que contienen dígitos
dim(text_salsa)
## [1] 10560 2
# 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
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.
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
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)")
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"
))
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.
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
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()
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)")
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.
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
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
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
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)"
)
# 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))
# 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
# 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
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
# 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
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
# 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))
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
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"
)
# 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
# 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
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
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)"
)
# 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.
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.
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:
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.
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:
Este análisis muestra conexiones latentes y una coherencia temática mayor que la observada con bigramas tradicionales.
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.
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í.
El método de Louvain permitió identificar comunidades semánticas claras, que corresponden a subtemas naturales del documento:
La visualización muestra un núcleo central robusto y grupos periféricos organizados, señal de un texto bien estructurado y coherente.
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.
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.
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.
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.