# Mirror CRAN
options(repos = c(CRAN = "https://cloud.r-project.org"))

packages <- c(
  "tidyverse","readr","stringr","syuzhet","ggplot2","tidyr",
  "knitr","kableExtra","tidytext","stopwords","wordcloud2"
)
to_install <- packages[!packages %in% installed.packages()[,1]]
if(length(to_install)) install.packages(to_install, dependencies = TRUE)
invisible(lapply(packages, library, character.only = TRUE))

options(stringsAsFactors = FALSE)

# Helpers
rename_es <- function(df) {
  mapa <- c(
    positive = "positivo",
    negative = "negativo",
    anger = "ira",
    anticipation = "anticipación",
    disgust = "asco",
    fear = "miedo",
    joy = "alegría",
    sadness = "tristeza",
    surprise = "sorpresa",
    trust = "confianza"
  )
  comunes <- intersect(names(mapa), names(df))
  dplyr::rename(df, !!!setNames(comunes, mapa[comunes]))
}

detect_cols <- function(df) {
  cols <- names(df)
  pos_col <- if ("positivo" %in% cols) "positivo" else "positive"
  neg_col <- if ("negativo" %in% cols) "negativo" else "negative"
  emo_candidates <- c("anger","anticipation","disgust","fear","joy","sadness","surprise","trust",
                      "ira","anticipación","asco","miedo","alegría","tristeza","sorpresa","confianza")
  emo_cols <- intersect(emo_candidates, cols)
  list(pos = pos_col, neg = neg_col, emos = emo_cols)
}

1) Lectura de fuente (diálogo humano)

Este documento usa por defecto focus_group_conversacion_dialogo.txt.
> El análisis considera solo las respuestas de participantes (P1:P10:).

txt_path <- params$txt_path
lines <- readLines(txt_path, encoding = "UTF-8")
length(lines)
## [1] 244
head(lines, 10)
##  [1] "I. Introducción y Calentamiento"                                                                        
##  [2] "MOD: Chicos, gracias por venir. Rapidito: nombre, distrito, con quién viven y qué hacen."               
##  [3] "P1: Hola, soy de SJL, vivo con mis papás y estoy estudiando administración... estoy en cuarto ciclo."   
##  [4] "P2: Qué tal, de Comas, vivo con mis tíos, trabajo en call center, turno tarde."                         
##  [5] "P3: Yo soy de VES, vivo con mi hermano; hago delivery por horas, cuando sale."                          
##  [6] "P4: Buenas, Independencia; alquilo con mi pareja y soy maestro de obra, ahorita en obra por Los Olivos."
##  [7] "P5: Ate, con mis abuelos, estudio contabilidad... y hago cachuelos de Excel a veces."                   
##  [8] "P6: SMP, con mi mamá; manejo mototaxi en el barrio."                                                    
##  [9] "P7: Cercado... vivo con un amigo."                                                                      
## [10] "P8: Chorrillos, con mis viejos; vendo por Instagram, ropa y cositas."

2) Extracción de respuestas

raw_tbl <- tibble(raw = lines)

respuestas <- raw_tbl |>
  filter(str_detect(raw, "^P(10|[1-9]):")) |>
  mutate(
    participante = str_extract(raw, "^P(10|[1-9])"),
    texto = raw |>
      # quita el prefijo "P#: " y espacios
      str_remove("^P(10|[1-9]):[[:space:]]*") |>
      # elimina marcas escénicas entre corchetes y paréntesis
      str_replace_all("\\[(?:[^\\]]+)\\]", " ") |>
      str_replace_all("\\((?:[^\\)]+)\\)", " ") |>
      # compacta espacios
      str_squish()
  ) |>
  filter(!is.na(texto) & texto != "")

knitr::kable(head(respuestas, 12)) |>
  kableExtra::kable_styling(full_width = FALSE)
raw participante texto
P1: Hola, soy de SJL, vivo con mis papás y estoy estudiando administración… estoy en cuarto ciclo. P1 Hola, soy de SJL, vivo con mis papás y estoy estudiando administración… estoy en cuarto ciclo.
P2: Qué tal, de Comas, vivo con mis tíos, trabajo en call center, turno tarde. P2 Qué tal, de Comas, vivo con mis tíos, trabajo en call center, turno tarde.
P3: Yo soy de VES, vivo con mi hermano; hago delivery por horas, cuando sale. P3 Yo soy de VES, vivo con mi hermano; hago delivery por horas, cuando sale.
P4: Buenas, Independencia; alquilo con mi pareja y soy maestro de obra, ahorita en obra por Los Olivos. P4 Buenas, Independencia; alquilo con mi pareja y soy maestro de obra, ahorita en obra por Los Olivos.
P5: Ate, con mis abuelos, estudio contabilidad… y hago cachuelos de Excel a veces. P5 Ate, con mis abuelos, estudio contabilidad… y hago cachuelos de Excel a veces.
P6: SMP, con mi mamá; manejo mototaxi en el barrio. P6 SMP, con mi mamá; manejo mototaxi en el barrio.
P7: Cercado… vivo con un amigo. P7 Cercado… vivo con un amigo.
P8: Chorrillos, con mis viejos; vendo por Instagram, ropa y cositas. P8 Chorrillos, con mis viejos; vendo por Instagram, ropa y cositas.
P9: Los Olivos; con mis papás; estudio cosmetología, peinados más que todo. P9 Los Olivos; con mis papás; estudio cosmetología, peinados más que todo.
P10: SJM; con mis abuelos; recién salí del cole, viendo qué estudiar. P10 SJM; con mis abuelos; recién salí del cole, viendo qué estudiar.
P1: Abarrotes básicos… arroz, fideos. Y siempre cae una gaseosa y galletitas para la noche. P1 Abarrotes básicos… arroz, fideos. Y siempre cae una gaseosa y galletitas para la noche.
P2: Igual, abarrotes y snack para el trabajo. Es lo que más se va. P2 Igual, abarrotes y snack para el trabajo. Es lo que más se va.

3) Análisis de sentimientos (NRC, español)

nrc <- syuzhet::get_nrc_sentiment(char_v = respuestas$texto, language = "spanish")
datos_sent <- dplyr::bind_cols(respuestas, nrc) |>
  dplyr::mutate(polaridad = positive - negative) |>
  rename_es()
det <- detect_cols(datos_sent)

3.1) Resumen por participante

sent_por_participante <- datos_sent |>
  group_by(participante) |>
  summarise(across(all_of(c(det$pos, det$neg, det$emos)), sum), .groups = "drop") |>
  mutate(polaridad = .data[[det$pos]] - .data[[det$neg]]) |>
  arrange(participante)

knitr::kable(sent_por_participante) |>
  kableExtra::kable_styling(full_width = FALSE)
participante positivo negativo ira anticipación asco miedo alegría tristeza sorpresa confianza polaridad
P1 14 4 3 3 3 5 3 2 1 6 10
P10 9 5 1 2 0 1 1 3 0 7 4
P2 15 6 1 6 3 0 3 3 3 6 9
P3 22 3 0 8 5 0 2 0 6 10 19
P4 4 1 0 4 1 2 1 0 2 2 3
P5 11 2 2 2 1 1 4 2 0 9 9
P6 4 3 0 0 0 1 0 2 0 2 1
P7 5 1 1 2 0 0 3 1 0 3 4
P8 7 4 0 1 1 0 0 2 2 2 3
P9 13 6 1 1 3 4 4 5 3 7 7

Gráfico: Polaridad por participante

ggplot(sent_por_participante, aes(x = participante, y = polaridad)) +
  geom_col() +
  geom_hline(yintercept = 0, linetype = "dashed") +
  labs(title = "Polaridad por participante (positivo - negativo)",
       x = "Participante", y = "Polaridad neta")

3.2) Resumen global

resumen_global <- datos_sent |>
  summarise(across(all_of(c(det$pos, det$neg, det$emos)), sum)) |>
  mutate(polaridad_total = .data[[det$pos]] - .data[[det$neg]])

knitr::kable(resumen_global) |>
  kableExtra::kable_styling(full_width = FALSE)
positivo negativo ira anticipación asco miedo alegría tristeza sorpresa confianza polaridad_total
104 35 9 29 17 14 21 20 17 54 69

Gráfico: Emociones totales

# Paquetes
library(dplyr); library(tidyr); library(ggplot2); library(scales); library(stringr)
## Warning: package 'scales' was built under R version 4.4.3
## 
## Adjuntando el paquete: 'scales'
## The following object is masked from 'package:syuzhet':
## 
##     rescale
## The following object is masked from 'package:purrr':
## 
##     discard
## The following object is masked from 'package:readr':
## 
##     col_factor
emociones_df <- resumen_global |>
  select(all_of(det$emos)) |>
  pivot_longer(everything(), names_to = "emocion", values_to = "n") |>
  mutate(
    emocion = str_to_sentence(emocion)  # "alegria" -> "Alegria"
  ) |>
  arrange(n) |>
  mutate(emocion = factor(emocion, levels = unique(emocion)))  # ordenar por conteo

# Paleta (Okabe–Ito adaptada + positivos/negativos si existen)
pal <- c(
  "Ira"        = "#D55E00",
  "Anticipación" = "#F0E442",
  "Asco"       = "#009E73",
  "Miedo"      = "#56B4E9",
  "Alegría"    = "#E69F00",
  "Tristeza"   = "#0072B2",
  "Sorpresa"   = "#CC79A7",
  "Confianza"  = "#7F7F7F",
  "Positivo"   = "#1B9E77",
  "Negativo"   = "#D81B60"
)

ggplot(emociones_df, aes(x = emocion, y = n, fill = emocion)) +
  geom_col(width = 0.72) +
  geom_text(aes(label = comma(n)), hjust = -0.15, size = 3.4) +
  coord_flip(clip = "off") +
  scale_y_continuous(expand = expansion(mult = c(0, .08))) +
  scale_fill_manual(values = pal, guide = "none") +
  labs(title = "Emociones totales (NRC, español)",
       x = "Emoción", y = "Conteo") +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    axis.title.y = element_text(margin = margin(r = 8)),
    axis.title.x = element_text(margin = margin(t = 6)),
    panel.grid.minor = element_blank()
  )

4) Nube de palabras

# Paquetes necesarios
# install.packages(c("dplyr","stringr","tidytext","tibble","stopwords","wordcloud2"))
library(dplyr); library(stringr); library(tidytext); library(tibble)
library(stopwords)

# 1) Fuente de texto (respuestas$texto o lineas$texto)
if (exists("respuestas") && "texto" %in% names(respuestas)) {
  fuente <- respuestas %>% select(texto)
} else if (exists("lineas") && "texto" %in% names(lineas)) {
  fuente <- lineas %>% select(texto)
} else {
  stop("No encuentro un data frame con columna 'texto' (respuestas o lineas).")
}

# 2) Tokenización y limpieza
tokens <- fuente %>%
  transmute(texto = texto %>%
              str_to_lower() %>%
              str_remove_all("https?://[^[:space:]]+") %>%  # quitar URLs
              str_remove_all("[0-9]+") %>%
              str_replace_all("[[:punct:]]", " ")) %>%
  tidytext::unnest_tokens(word, texto) %>%
  filter(!is.na(word), nchar(word) > 2)

# 3) Stopwords ES
sw <- tibble(word = stopwords::stopwords("es"))

tokens_clean <- tokens %>%
  anti_join(sw, by = "word") %>%
  filter(!str_detect(word, "^[[:alpha:]]{1,2}$"))

# 4) Frecuencias
freq <- tokens_clean %>% count(word, sort = TRUE)

# 6) Top-N
topN <- 150
freq_top <- head(freq, topN)

# 7) Nube de palabras (wordcloud2::wordcloud2 es la función correcta)
wordcloud2::wordcloud2(data = freq_top,
                       size = 0.9,
                       minRotation = 0, maxRotation = 0)

Análisis por Pregunta del Guión

En esta sección se calcula el sentimiento por cada pregunta del moderador (MOD) y se resumen métricas clave por bloque (pregunta). El flujo es robusto a tildes y conserva los códigos de participantes (P1–P10).

# Paquetes
# install.packages(c("dplyr","stringr","tidyr","tibble","purrr","ggplot2","tidytext","readr","knitr","syuzhet"))
library(dplyr); library(stringr); library(tidyr); library(tibble); library(purrr)
library(ggplot2); library(tidytext); library(readr); library(knitr); library(syuzhet)

# --- 1) Texto base y segmentación por MOD ---
if (exists("raw_tbl")) {
  base_txt <- raw_tbl$raw
} else if (exists("texto")) {
  base_txt <- if (is.character(texto)) texto else as.character(texto)
} else if (exists("params") && !is.null(params$txt_path)) {
  base_txt <- readLines(params$txt_path, encoding = "UTF-8")
} else {
  stop("No se encontró el objeto de texto base (raw_tbl$raw o texto) ni params$txt_path.")
}

full_txt <- paste(base_txt, collapse = "\n")

# Bloques que inician en 'MOD:'
bloques <- stringr::str_split(full_txt, "(?=\\bMOD:\\s*)")[[1]]
bloques <- bloques[stringr::str_detect(bloques, "^MOD:")]

preg_df <- tibble(
  bloque_id = seq_along(bloques),
  bloque = bloques
) %>%
  mutate(
    pregunta   = stringr::str_match(bloque, "(?m)^MOD:\\s*(.*)$")[,2] %>% stringr::str_squish(),
    respuestas = stringr::str_replace(bloque, "(?ms)^MOD:.*?(\\n|$)", "")
  )

# --- 2) Intervenciones por participante ---
lineas <- preg_df %>%
  mutate(linea = stringr::str_split(respuestas, "\\n")) %>%
  tidyr::unnest(cols = c(linea)) %>%
  mutate(linea = stringr::str_trim(linea)) %>%
  filter(linea != "") %>%
  filter(stringr::str_detect(linea, "^P(10|[1-9]):\\s")) %>%
  mutate(
    participante = stringr::str_match(linea, "^(P(10|[1-9])):")[,1],
    texto        = stringr::str_replace(linea, "^(P(10|[1-9])):\\s*", "")
  ) %>%
  select(bloque_id, participante, texto)

# --- 3) Sentimiento NRC (ES) por intervención y agregación por pregunta ---
nrc_lineas <- lineas %>%
  bind_cols(
    purrr::map_dfr(lineas$texto, ~ syuzhet::get_nrc_sentiment(char_v = ., language = "spanish"))
  )

resumen_bloque <- nrc_lineas %>%
  group_by(bloque_id) %>%
  summarise(
    pregunta  = first(preg_df$pregunta[preg_df$bloque_id == first(bloque_id)]),
    pos       = sum(positive, na.rm = TRUE),
    neg       = sum(negative, na.rm = TRUE),
    score     = pos - neg,
    n_palabras= sum(stringr::str_count(texto, "\\S+"), na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(bloque_id)

# --- 4) Visual resumen por pregunta ---
ggplot(resumen_bloque, aes(x = reorder(paste0("#", bloque_id), score), y = score)) +
  geom_col() +
  coord_flip() +
  labs(title = "Score de sentimiento por pregunta (NRC-ES, MOD)",
       x = "Pregunta (ID de bloque)",
       y = "Score (positive − negative)") +
  theme_minimal()

# Tabla resumen
resumen_bloque %>%
  mutate(pregunta = stringr::str_trunc(pregunta, 90)) %>%
  knitr::kable(caption = "Resumen NRC-ES por pregunta (pregunta truncada a 90 caracteres)")
Resumen NRC-ES por pregunta (pregunta truncada a 90 caracteres)
bloque_id pregunta pos neg score n_palabras
1 Chicos, gracias por venir. Rapidito: nombre, distrito, con quién viven y qué hacen. 13 1 12 120
2 En la semana, ¿qué compran más seguido, así del día a día? 7 5 2 95
3 Si te falta algo “al paso”, ¿adónde caes cerca de casa o chamba? 5 0 5 73
4 Última compra pequeña: ¿qué fue, dónde y por qué? 6 3 3 92
5 Bodegas del barrio: ¿cuál frecuentan y qué compran? 10 2 8 179
6 ¿Y tiendas de conveniencia (Tambo, MASS, Precio Uno) para qué las usan? 3 2 1 69
7 Escenario 1: sales tarde, cansado, se te antoja gaseosa helada y papitas. ¿Adónde y por… 5 1 4 65
8 Escenario 2: estás cocinando y te falta aceite. ¿Adónde y por qué? 0 2 -2 65
9 Escenario 3: reunión improvisada, necesitas hielo, piqueos, cervezas. ¿Primera opción? 1 0 1 56
10 Atributos: precios. ¿Dónde sienten mejor precio y en qué productos? 4 1 3 76
11 Atributos: cercanía. ¿Prefieres esquina de tu casa o una avenida por donde siempre pasas? 5 0 5 55
12 Atributos: horarios. ¿Les ha pasado que bodega cerrada? ¿Valoran horario extendido? 8 2 6 57
13 Servicio: trato en bodega vs Tambo/MASS. ¿Importa que te fíen? 6 1 5 97
14 Variedad/experiencia: variedad, limpieza, orden, rapidez. 8 0 8 74
15 ¿Cambió algo con tantas tiendas de conveniencia cerca? 3 1 2 70
16 Formas de pago: ¿Yape/Plin en bodega también? 8 3 5 48
17 ¿Piden delivery (Rappi, etc.) a bodega o conveniencia? 0 6 -6 46
18 ¿Algo que quisieran nuevo en la bodega (saludables, listos para comer, licores)? 7 3 4 64
19 Si fueran dueños de la bodega y compitieran con Tambo/MASS, ¿tres cosas clave? 5 0 5 54
20 ¿Algo más para cerrar? 0 2 -2 16

2) Top términos con polaridad por pregunta (usando NRC-ES)

syuzhet no expone el diccionario como tibble, pero podemos clasificar cada palabra pasando el vector de palabras al lexicón NRC-ES y luego contar por bloque.

# Tokenización de las intervenciones
tokens <- lineas %>%
  transmute(bloque_id, texto = texto %>%
              stringr::str_to_lower() %>%
              stringr::str_remove_all("https?://[^[:space:]]+") %>%
              stringr::str_remove_all("[0-9]+") %>%
              stringr::str_replace_all("[[:punct:]]", " ")) %>%
  tidytext::unnest_tokens(palabra, texto) %>%
  filter(stringr::str_detect(palabra, "^[[:alpha:]]+$"),
         nchar(palabra) > 2)

# Clasificar cada palabra con NRC-ES (positive/negative)
lex_words <- unique(tokens$palabra)
nrc_words <- syuzhet::get_nrc_sentiment(char_v = lex_words, language = "spanish")
nrc_words <- tibble(palabra = lex_words) %>% bind_cols(as_tibble(nrc_words)) %>%
  transmute(palabra,
            sentiment = case_when(positive > 0 & negative == 0 ~ "positive",
                                  negative > 0 & positive == 0 ~ "negative",
                                  TRUE ~ NA_character_)) %>%
  filter(!is.na(sentiment))

tokens_labeled <- tokens %>% inner_join(nrc_words, by = "palabra")

top_terms <- tokens_labeled %>%
  count(bloque_id, sentiment, palabra, sort = TRUE) %>%
  group_by(bloque_id, sentiment) %>%
  slice_max(n, n = 8, with_ties = FALSE) %>%
  ungroup()

ggplot(top_terms, aes(x = reorder(palabra, n), y = n)) +
  geom_col() +
  coord_flip() +
  facet_wrap(~ bloque_id + sentiment, scales = "free_y") +
  labs(title = "Top términos con polaridad por pregunta (NRC-ES)",
       x = NULL, y = "Frecuencia") +
  theme_minimal(base_size = 5)