Análisis de chats en WhatsApp: Parte 2 — Análisis de sentimientos y visualización de Datos con R

Si has llegado hasta este artículo directamente, quizás te recomendaría primero dar un vistazo a la Parte 1, en donde parto de presentarte a rwhatsapp, un package pequeño pero bastante útil a partir del cual realizamos un primer análisis y visualización de interesantes datos, extraídos de una conversación en WhatsApp entre dos personas, de 2018 al día de hoy, explicando el paso a paso:
Una vez comprendido el contexto y punto de partida, ahora iremos un poco más allá con la interacción entre nuestros dos individuos y su relación abierta (manteniendo aún su anonimato, claro, como “Él” y “Ella”), analizando la diversidad de léxico y realizando análisis de sentimientos a partir de lo emojis expresados. Bien, entonces retomando, usando las mismas librerías, mismas variables definidas y el mismo txt hasta ahora, continuemos.
¿Quién tiene un léxico más diverso?
Recordarás que en la primer parte, haciendo uso de la función stopwords() discriminamos las palabras cuyo significado es poco o nada relevante. Con base en esto y buscando palabras que no se repiten más que únicamente por un mismo usuario, podemos medir una diversidad de léxico.
# DIVERSIDAD DE LÉXICO
miChat %>%
unnest_tokens(input = text,
output = word) %>%
filter(!word %in% remover_palabras) %>%
group_by(author) %>%
summarise(lex_diversity = n_distinct(word)) %>%
arrange(desc(lex_diversity)) %>%
ggplot(aes(x = reorder(author, lex_diversity),
y = lex_diversity,
fill = author)) +
geom_col(show.legend = FALSE) +
scale_y_continuous(expand = (mult = c(0, 0, 0, 500))) +
geom_text(aes(label = scales::comma(lex_diversity)), hjust = -0.1) +
ylab("Diversidad léxica") +
xlab("Usuario") +
ggtitle("Diversidad de léxico en la conversación") +
coord_flip()
Por lo que obtendremos como resultado el siguiente plot donde podemos ver claramente que Ella es quien tiene una mayor diversidad de léxico.

Palabras únicas por usuario
Cuando pensamos en diversidad de léxico, muchas veces creemos que probablemente esto es porque una persona es más letrada que otra, usando términos sorprendentes y rimbombantes. En ocasiones esto es correcto, en algunas otras, no tanto. De cualquier modo, no está por demás averiguarlo a detalle.
# PALABRAS ÚNICAS POR ELLA
palabras_unicas_ella <- miChat %>%
unnest_tokens(input = text,
output = word) %>%
filter(author != "Ella") %>%
count(word, sort = TRUE)miChat %>%
unnest_tokens(input = text,
output = word) %>%
filter(author == "Ella") %>%
count(word, sort = TRUE) %>%
filter(!word %in% palabras_unicas_ella$word) %>% # SELECCIONAR SÓLO PALABRAS QUE NADIE MÁS USA
top_n(n = 15, n) %>%
ggplot(aes(x = reorder(word, n), y = n)) +
geom_col(show.legend = FALSE) +
ylab("Número de veces que se usó la palabra") + xlab("Palabras") +
coord_flip() +
ggtitle("Top de palabras únicas usadas por Ella")# PALABRAS ÚNICAS POR ÉL
palabras_unicas_el <- miChat %>%
unnest_tokens(input = text,
output = word) %>%
filter(author != "Él") %>%
count(word, sort = TRUE)miChat %>%
unnest_tokens(input = text,
output = word) %>%
filter(author == "Él") %>%
count(word, sort = TRUE) %>%
filter(!word %in% palabras_unicas_el$word) %>% # SELECCIONAR SÓLO PALABRAS QUE NADIE MÁS USA
top_n(n = 15, n) %>%
ggplot(aes(x = reorder(word, n), y = n)) +
geom_col(show.legend = FALSE) +
ylab("Número de veces que se usó la palabra") + xlab("Palabras") +
coord_flip() +
ggtitle("Top de palabras únicas usadas por Él")
Puedes ir haciendo tus propias deducciones, por mientras yo puedo hacer dos observando el siguiente plot generado: Al parecer, Ella es muy positiva, y a Él vaya que le encanta el Cloruro de Sodio.


Análisis de sentimientos con Emoji Sentiment Ranking
Un grupo de desarrolladores rusos crearon un léxico de sentimientos emoji, llamado “Emoji Sentiment Ranking”, que mapea los sentimientos de los emojis más utilizados actualmente. El sentimiento de los emojis se calcula a partir del sentimiento de miles de tuits que ocurren en distintos idiomas. El proceso y análisis de la clasificación se detallan por sus mismos autores en este artículo (muy interesante, por cierto).
En fin, usaremos el paquete rvest para extraer y manipular los datos de la página.
# USAMOS EL PAQUETE RVEST
library(rvest)# FETCH DE PÁGINA HTML EMOJI SENTIMENT RANKING 1.0
url_base <- "http://kt.ijs.si/data/Emoji_sentiment_ranking/index.html"
doc <- read_html(url_base)# BUSCAR TABLA DE EMOJI Y PROCESO
tabla_emojis <- doc %>%
html_node("#myTable") %>%
html_table() %>%
as_tibble()
El rank tiene columnas que dan un puntaje a cada Emoji con respecto a representar un sentimiento positivo, neutral o negativo. Luego podemos agregarlo, clasificando indirectamente los sentimientos del mensaje.
# OBTENER PUNTAJE DE SENTIMIENTO Y LIMPIAR
sentimiento_emoji <- tabla_emojis %>%
select(1,6:9) %>%
set_names("char", "negativo","neutral","positivo","sent.score")# EXTRAER EMOJI Y UNIR CON SENTIMIENTO
emoji_chat <- miChat %>%
unnest(emoji, emoji_name) %>%
mutate( emoji = str_sub(emoji, end = 1)) %>%
inner_join(sentimiento_emoji, by=c("emoji"="char"))# VISUALIZACIÓN PREVIA
emoji_chat %>%
select(-source, -day, -estacion) %>%
slice(1207:1219) %>%
kable() %>%
kable_styling(font_size = 10)
Obtendremos una vista previa como esta, corroborando lo anteriormente dicho.

Ahora entonces estamos listos para poder dar cuenta del sentimiento promedio de cada usuario.
# OCURRENCIAS DE SENTIMIENTOS POR EMOJIS, POR USUARIO
emoji_sentimiento_usuarios <- emoji_chat %>%
group_by(author) %>%
summarise(
positivo=mean(positivo),
negativo=mean(negativo),
neutral=mean(neutral),
balance=mean(sent.score)
) %>%
arrange(desc(balance))# FORMATO DE DATOS PARA REALIZAR PLOT
emoji_sentimiento_usuarios %>%
mutate( negativo = -negativo,
neutral.positivo = neutral/2,
neutral.negativo = -neutral/2) %>%
select(-neutral) %>%
gather("sentiment","mean", -author, -balance) %>%
mutate(sentiment = factor(sentiment, levels = c("negativo", "neutral.negativo", "positivo", "neutral.positivo"), ordered = T)) %>%
ggplot(aes(x=reorder(author,balance), y=mean, fill=sentiment)) +
geom_bar(position="stack", stat="identity", show.legend = F, width = .5) +
scale_fill_manual(values = brewer.pal(4,"RdYlGn")[c(1,2,4,2)]) +
ylab(" - Negativo / Neutral / Positivo +") + xlab("Usuario") +
ggtitle("Análisis de sentimientos por usuario","Basado en el puntaje promedio de sentimientos por emojis") +
coord_flip() +
theme_minimal()
Tendremos el siguiente plot como resultado, en donde podríamos deducir que en su gran mayoría, la interacción entre ambos usuarios genera sentimientos positivos (ojo, basándonos solamente en la expresión de emojis).

Análisis de sentimientos con AFINN
Un segundo enfoque alternativo a “Emoji Sentiment Ranking” es utilizar un diccionario con la clasificación de los sentimientos de las palabras y asociar el sentimiento con el nombre de Emoji, que muy convenientemente está disponible después de importar la conversación a través del package rwhatsapp.
El léxico AFINN es uno de los diccionarios más simples y populares usados ampliamente para el análisis de sentimientos. La versión actual contiene más de 3300 palabras con un puntaje de polaridad asociado con cada palabra. Puedes encontrar este léxico en el repositorio en GitHub del autor.
Usaremos el paquete textdata, que ya descarga el léxico. Echemos un vistazo a su contenido para comprender cómo funciona.
library(textdata)# OBTENER LÉXICO POSITIVO/NEGATIVO DEL PACKAGE DE LEXICON
lexico_negpos <- get_sentiments("afinn") # INTENSIDAD DE VALOR# PREVIEW DEL FORMATO DE LEXICO
lexico_negpos %>%
head(10) %>%
kable() %>%
kable_styling(full_width = F, font_size = 11)# PREVIEW CUÁLES SON LOS VALORES POSIBLES
table(lexico_negpos$value) %>%
head(10) %>%
kable() %>%
kable_styling(full_width = F, font_size = 11)

Como la anterior descripción visual anticipa, las palabras reciben un puntaje, en una escala de -5 a 5. Ahora veamos cómo cruzarlo.
# EXTRAER EMOJIS
emoji_sentimiento_score <- miChat %>%
select( emoji, emoji_name) %>%
unnest( emoji, emoji_name) %>%
mutate( emoji = str_sub(emoji, end = 1)) %>%
mutate( emoji_name = str_remove(emoji_name, ":.*")) %>%
distinct() %>%
unnest_tokens(input=emoji_name, output=emoji_words) %>%
inner_join(lexico_negpos, by=c("emoji_words"="word"))# CREAR TABLA DE 3 COLUMNAS
bind_cols(
slice(emoji_sentimiento_score, 01:10),
slice(emoji_sentimiento_score, 11:20),
slice(emoji_sentimiento_score, 21:30)
) %>%
kable() %>%
kable_styling(full_width = F, font_size = 11)

Repitiendo el mismo procedimiento que hicimos antes, podemos proceder entonces a ver la intensidad promedio en la escala de sentimientos para cada usuario.
# EXTRAER EMOJIS
emoji_chat <- miChat %>%
unnest(emoji, emoji_name) %>%
mutate( emoji = str_sub(emoji, end = 1)) %>%
mutate( emoji_name = str_remove(emoji_name, ":.*"))# TOKENIZAR EL NOMBRE DE EMOJI
emoji_chat <- emoji_chat %>%
select(author, emoji_name) %>%
unnest_tokens(input=emoji_name, output=emoji_words)# JOIN CON LEXICON
usuario_summary <- emoji_chat %>%
inner_join(lexico_negpos, by=c("emoji_words"="word")) %>%
count(author, value) %>%
group_by(author) %>%
mutate(mean=n/sum(n)) %>%
ungroup()# COLORES Y GRÁFICA
reordenar_niveles <- c(-3,-2,-1,3,2,1)
colores <- c("#d7191c","#fdae61","#ffffbf","#1a9641","#a6d96a","#ffffbf")
mis_colores <- brewer.pal(5,"RdYlGn")[c(1,2,3,5,4,3)]# PLOT DE LA GRÁFICA
usuario_summary %>%
mutate( mean = ifelse(value<0, -mean, mean)) %>%
group_by(author) %>%
mutate( balance = sum(mean)) %>%
ungroup() %>%
mutate( value = factor(value, levels = reordenar_niveles, ordered=T)) %>%
ggplot(aes(x=reorder(author,balance), y=mean, fill=value)) +
geom_bar(stat="identity",position="stack", show.legend = F, width = .5) +
scale_fill_manual(values = mis_colores) +
xlab("Usuario") + ylab("Escala de netagivo a positivo") +
coord_flip() +
ggtitle("Análisis de sentimientos por uso de emojis", "Uso de package Lexicon") +
theme_minimal()
Obtendremos como resultado el siguiente plot, que como podrás ver, difiere un tanto del primer enfoque aplicado por “ Emoji Sentiment Ranking”. Se preguntarán algunos aún el por qué, pues bueno, toma en cuenta que no estamos clasificando por esta vía el sentimiento del Emoji en sí mismo, sino por el sentimiento de la palabra que le da su nombre.
Bajo este enfoque, al parecer ella prefiere mostrarse neutral, y él tiende un poco más hacia lo negativo, eh.

¿Cuál es la emoción más frecuente? (Con EmoLex)
Un tercer enfoque, que también explora el nombre de Emoji, es cruzarlo con una base de sentimientos por emociones, un léxico que vincula las palabras con las emociones. El NRC Emotion Lexicon es una lista de palabras en inglés y sus asociaciones con ocho emociones básicas ( anticipación, sorpresa, confianza, tristeza, alegría, asco, ira, y miedo,) y dos sentimientos (negativo y positivo). Demos un vistazo a cómo se ven algunas de las calificaciones de sentimientos.
# OBTENER OTRO LÉXICO CON NOMBRE DE SENTIMIENTOS
lexico_sentimientos <- get_sentiments("nrc") # EXTRAER EMOJIS
emoji_emocion <- miChat %>%
select( emoji, emoji_name) %>%
unnest( emoji, emoji_name) %>%
mutate( emoji = str_sub(emoji, end = 1)) %>%
mutate( emoji_name = str_remove(emoji_name, ":.*")) %>%
unnest_tokens(input=emoji_name, output=emoji_words) %>%
inner_join(lexico_sentimientos, by=c("emoji_words"="word")) %>%# REMOVER CLASIFICACIÓN NEGATIVA/POSITIVA
filter(!sentiment %in% c("negative","positive")) %>%
# MANTENER SÓLO LOS 4 EMOJI MÁS FRECUENTES PARA CADA SENTIMIENTO
count(emoji, emoji_words, sentiment) %>%
group_by(sentiment) %>%
top_n(4,n) %>%
slice(1:4) %>%
ungroup() %>%
select(-n)# PONER TABLAS JUNTAS
bind_cols(
slice(emoji_emocion, 01:16),
slice(emoji_emocion, 17:32)
) %>%
kable() %>%
kable_styling(full_width = F, font_size = 11)
Como podrán ver, podremos identificar en el preview la asociación que hace respecto a una emoción por Emoji.

Ahora entonces, podemos cruzar el léxico con los nombres de Emoji que contienen los mensajes.
# JOIN CON EMOJIS
sentimiento_chat <- emoji_chat %>%
inner_join(lexico_sentimientos, by=c("emoji_words"="word")) %>%# REMOVER CLASIFICACIÓN POITIVA/NEGATIVA
filter(!sentiment %in% c("negative","positive"))# PLOT DE EMOCIONES MAYORMENTE EXPRESADAS
sentimiento_chat %>%
count(sentiment) %>%
ggplot(aes(x=reorder(sentiment,n), y=n)) +
geom_col(aes(fill=n), show.legend = FALSE, width = .1) +
geom_point(aes(color=n), show.legend = FALSE, size = 3) +
coord_flip() +
ylab("Número de veces expresado") + xlab("Emoción") +
scale_fill_gradient(low="#2b83ba",high="#d7191c") +
scale_color_gradient(low="#2b83ba",high="#d7191c") +
ggtitle("Emoción expresada con mayor frecuencia","Expresado por uso de emojis") +
theme_minimal()
Claramente visualizamos ahora cuáles son las emociones predominantes en la conversación. Curioso eh, hay alegría o placer, miedo, pero muy poca confianza expresada de manera general entre Él y Ella.

¿Qué emociones expresan con mayor frecuencia cada usuario?
Siguiendo con el enfoque de NRC, por último, para hacer esto más interesante, veamos la expresión de emociones por usuario, y su asociación a qué tan negativos o positivos son los sentimientos generados que resultan de su interacción por Emojis.
# PLOT DE EMOCIONES POR USUARIO
sentimiento_chat %>%
count(author, sentiment) %>%
left_join(filter(lexico_sentimientos, sentiment %in% c("negative","positive")),by=c("sentiment"="word")) %>%
rename( sentimiento = sentiment.y) %>%
mutate( sentimiento = ifelse(is.na(sentimiento), "neutral", sentimiento)) %>%
mutate( sentimiento = factor(sentimiento, levels = c("negative", "neutral", "positive"), ordered=T) ) %>%
group_by(author) %>%
top_n(n = 8, n) %>%
slice(1:8) %>%
ggplot(aes(x = reorder(sentiment, n), y = n, fill = sentimiento)) +
geom_col() +
scale_fill_manual(values = c("#d7191c","#fdae61", "#1a9641")) +
ylab("Número de veces expresado") +
xlab("Emoción") +
coord_flip() +
facet_wrap(~author, ncol = 3, scales = "free_x") +
ggtitle("Emociones mayormente expresadas por usuario", "Expresado por uso de emojis") +
theme_minimal() + theme(legend.position = "bottom")
Como resultado obtendremos el siguiente plot donde podemos ver a detalle la intensidad de las ocho emociones expresadas. ¡Con que en comparación, Él se enoja y tiene más emociones de disgusto que Ella, y ella expresa más temor eh!

Reuniendo la evidencia de la Parte 1 y la Parte 2 (este artículo), para ti, ¿Alguno de nuestros usuarios es tan tóxico como Chernóbil?.
Del mismo modo que en la primer parte de este artículo, por aquí les dejo un poco más atractivos los plots generados con plotly en un flexdashboard que armé: https://rpubs.com/cosmoduende/whatsapp-sentiment-analysis
Y aquí pueden encontrar también el código completo de esta segunda entrega: https://github.com/cosmoduende/r-whatsapp-analysis-parte2
¡Les agradezco haber llegado hasta acá, les deseo que tengan felices análisis, que lo puedan poner en práctica y se sorprendan y diviertan tanto como yo con los resultados!
Otros artículos que he escrito
Si te ha gustado este post, agradeceré mucho tus claps. Además te invito a visitar también otros artículos que he escrito:
Gracias y hasta la próxima.