Trabajo final de Diplomatura en Ciencias Sociales Computacionales y Humanidades Digitales (UNSAM)

Preprocesamiento

La data está en formato corpus: contiene cadenas de texto sin procesar, anotadas con metadatos y detalles adicionales. En este caso, se trata de un corpus con noticias entre enero de 2015 y diciembre de 2016, y contiene las siguientes variables: id, url, fecha, año, mes, día, medio, título y texto.

El primer paso es cargar los datos, preprocesarlos y generar una matriz token-por-fila para cada documento 1.

Cargo algunos paquetes que usaré.

library(readr)
library(tidyverse)
library(tidytext)

Cargo los datos del corpus.

corpus<-read_csv(file = "corpus.csv")

Paso el encoding de las columnas medio y texto a ASCII, a fines de eliminar tildes y ñ.

corpus <- corpus %>%
                mutate(texto = stringi::stri_trans_general(texto, "Latin-ASCII"),
                       medio = stringi::stri_trans_general(medio, "Latin-ASCII"))

También elimino los dígitos contenidos en el texto.

corpus<-corpus %>% mutate(texto = str_replace_all(texto, '[[:digit:]]+', ''))

Los datos “ordenados” o tidy data tienen la siguiente estructura (Wickham, 20142): cada variable es una columna; cada unidad u observación, una fila; y cada tipo de unidad observacional, una tabla.

En el caso de text mining, la estructura de tidy text implica un token por fila. Un token es una unidad conceptual y/o analíticamente significativa, y puede definirse según criterios particulares: palabras, n-grams, expresiones, etc. En este caso, definiré al token como una palabra. Entonces, uno de los primeros pasos en el preprocesamiento de corpus es su división en tokens, o tokenización.

Con la función unnest_tokens() de tidytext, tokenizo el texto y lo transformo en una estructura de datos ordenada: un token por fila.

corpus_tidy <- corpus %>%
        unnest_tokens(output = word, 
                      input = texto) 

En el archivo inicial, cada fila era una noticia. Ahora, cada fila es una palabra (un token), y cada palabra está asociada a las diferentes variables incluídas en la metadata del corpus (fecha, medio, título, etc). Además, la función unnest_tokens() realiza ciertas modificaciones por defecto, como pasar el texto a minúsculas y eliminar la puntuación. Estos criterios pueden modificarse según preferencias particulares, pero en este caso estas tareas de preprocesamiento resultarán útiles, por lo que no las modifico.

En el paso siguiente, elimino las stopwords. Éstas son palabras muy frecuentes que no aportan información relevante, y que resulta conveniente dejar de lado antes de avanzar en el análisis del texto. Se puede notar el peso que este tipo de palabras tiene en el corpus mediante una primera exploración de las palabras más usadas. Encabezan esta lista palabras como “de”, “la”, “que”, “el”, etc. Artículos, pronombres, preposiciones, adverbios u otras palabras que no aportan información relevante por sí solas: palabras vacías.

corpus_tidy %>%
        group_by(word) %>%
        summarise(n=n()) %>%
        arrange(desc(n))
## # A tibble: 97,241 × 2
##    word       n
##    <chr>  <int>
##  1 de    224713
##  2 la    146509
##  3 que   116653
##  4 el    113839
##  5 en     99257
##  6 y      88625
##  7 a      75451
##  8 los    55654
##  9 del    45124
## 10 un     39220
## # ℹ 97,231 more rows

Para eliminar las stopwords, utilizo un lexicon que contiene un conjunto de este tipo de palabras 3

Cabe resaltar que, en este caso, añadí algunas palabras al listado de stopwords luego de notar, algunos pasos más adelante, que se repetían y no aportaban información valiosa al análisis.

stop_words <- read_csv('https://raw.githubusercontent.com/Alir3z4/stop-words/master/spanish.txt', col_names=FALSE) %>%
        rename(word = X1) %>% 
                mutate(word = stringi::stri_trans_general(word, "Latin-ASCII"))

stop_words <- stop_words %>%
                bind_rows( tibble(word=c('ano', 'anos', 'dia','gusta', 'comentar', 'compartir','guardar','mira','lea','enterate','newsletter', 'newsletters', 'minutouno','minutounocom', 'jpg', 'https','ratingcero', 'embed','infobae', 'infobaetv', 'protected', 'ftp','twitter', 'facebook','email', 'whatsapp','the','pic.twitter.com','ratingcero.com','cablera.telam.com.ar','perfil.com','ambito.com','minutouno.com','quey','t.co','clarin.com','the', 'levantes','organices','quedarte'))) 

Una vez cargado el lexicon con stopwords, aplico la función anti_join() entre éste y el corpus, para obtener todas las palabras que están presentes en el segundo pero no en el primero. Con este paso, el corpus pasa de 3.397.810 a 1.489.837 filas (es decir, palabras).

corpus_tidy <- corpus_tidy %>%
        anti_join(stop_words)

Al volver a consultar las palabras más mencionadas, confirmo que se eliminaron las stopwords correctamente.

corpus_tidy %>%
        group_by(word) %>%
        summarise(n=n()) %>%
        arrange(desc(n))
## # A tibble: 96,678 × 2
##    word           n
##    <chr>      <int>
##  1 gobierno    5408
##  2 pais        4138
##  3 argentina   3811
##  4 presidente  3573
##  5 nacional    2936
##  6 millones    2850
##  7 macri       2716
##  8 mundo       2516
##  9 ciudad      2420
## 10 personas    2378
## # ℹ 96,668 more rows


Identificación de palabras características de cada medio

Retomo el primer objetivo de este trabajo: identificar las palabras más utilizadas en cada uno de los medios.

Métricas: TF, IDF, TF-IDF

Una de las ideas que surgen intuitivamente al plantearse este objetivo es realizar un conteo crudo de las veces que aparece cada palabra en cada medio. Sin embargo, este método presenta deficiencias significativas en la práctica. Entre éstas, se destacan dos: el largo de los documentos varía, y la información sobre el sentido no crece de forma proporcional a la ocurrencia de una palabra en un documento. El resultado del conteo crudo puede verse en el siguiente gráfico.

medio_palabras<-corpus_tidy %>%
        group_by(medio, word) %>%
        summarise(n = n()) %>%
        arrange(desc(n)) %>%
        ungroup()

medio_palabras %>%
        group_by(medio) %>%
        slice_max(n, n = 10) %>%
        ungroup() %>%
        mutate(word = reorder_within(word, n, medio)) %>%
        ggplot(aes(n, word, fill = medio)) +
        geom_col(show.legend = FALSE) +
        facet_wrap(~medio, ncol = 2, scales = "free") +
        scale_y_reordered()+
        labs(x = "n", y = NULL) +
        theme_minimal()+
        theme(text=element_text(size=16,family="mono",face = "bold"))

Frente a las deficiencias del conteo crudo, considero algunas métricas alternativas.

El objetivo es identificar una métrica que balancee dos dimensiones centrales de las palabras: su importancia (dada por su frecuencia en un documento) y su informatividad con respecto al contenido de dicho documento en particular (dada por su presencia en pocos documentos y no en todos los del corpus).

En este caso, lo que busco es identificar las palabras importantes e informativas de cada medio. Para lograrlo, introduzco al análisis las siguientes métricas:

  • El TF (Term frequency) refleja la importancia. Mide la frecuencia relativa de una palabra en cierto documento: la cantidad de veces que aparece la palabra en el documento sobre el total de palabras en dicho documento. En este sentido, la TF sortea una de las deficiencias del conteo crudo (documentos de diferentes tamaños) mediante su normalización a fines de obtener resultados que resulten comparables.

  • El IDF (Inverse document frequency) refleja la informatividad. Se calcula como —el logaritmo de— el tamaño del corpus (el total de documentos) dividido por la cantidad de documentos que contienen la palabra en cuestión. Esta métrica incrementa el peso de las palabras que no se usan mucho (en el corpus) en detrimento de las palabras de uso común.

  • El TF-IDF combina ambas métricas y, de esta forma, balancea importancia e informatividad. Se calcula multiplicando TF por IDF. Esta métrica busca identificar las palabras que son importantes (comunes) e informativas (no demasiado comunes). En síntesis, la métrica de TF-IDF permite identificar palabras características o distintivas de un documento dentro de una colección de documentos (el corpus).

Las tres métricas pueden obtenerse fácilmente mediante la función bind_tfi_idf() de tidytext.

library(forcats)

corpus_tf_idf <- medio_palabras %>%
  mutate(medio=recode(medio, "Clarin" = "Clarín", "Pagina 12" = "Página 12", "La Nacion" = "La Nación")) %>% 
  bind_tf_idf(word, medio, n)

library(kableExtra)
kbltfidf1<-kable(head(corpus_tf_idf,20),caption="Primeras 20 filas de tabla bind_tf_idf, a modo ilustrativo") %>%
  row_spec(0,bold=TRUE) %>% 
  kableExtra::kable_classic_2(full_width=TRUE) %>% 
  column_spec(1, bold = T)

kbltfidf1
Primeras 20 filas de tabla bind_tf_idf, a modo ilustrativo
medio word n tf idf tf_idf
Clarín recibir 1263 0.0040151 0 0
La Nación mail 1239 0.0038125 0 0
Clarín clarin 1226 0.0038975 0 0
La Nación gobierno 1178 0.0036248 0 0
Clarín viernes 1124 0.0035732 0 0
Clarín lunes 1118 0.0035542 0 0
Clarín gobierno 1041 0.0033094 0 0
Clarín manana 962 0.0030582 0 0
La Nación pais 906 0.0027878 0 0
Página 12 gobierno 902 0.0032526 0 0
La Nación argentina 885 0.0027232 0 0
Clarín paso 851 0.0027054 0 0
La Nación presidente 848 0.0026094 0 0
Página 12 pais 821 0.0029605 0 0
Clarín tarde 767 0.0024383 0 0
La Nación millones 751 0.0023109 0 0
Infobae gobierno 739 0.0041818 0 0
Clarín presidente 736 0.0023398 0 0
Clarín argentina 725 0.0023048 0 0
Telam gobierno 712 0.0041457 0 0

Al ver la tabla resultante, se puede distinguir una serie de palabras con un TF alto y un IDF igual a cero. Éstas suelen ser palabras muy comunes pero poco informativas. En el caso de no haberlas eliminado previamente, este sería el caso de las stopwords.

Las deficiencias de limitar el análisis de las palabras a su frecuencia relativa se refleja claramente en el siguiente gráfico, con las 10 palabras de mayor TF para cada medio. Resulta prácticamente imposible detectar diferencias relevantes entre cada medio, dado que las palabras de mayor TF suelen ser comunes y repetirse en los diferentes diarios: “gobierno”, “país”, “Argentina”, “nacional”, “presidente”.

corpus_tf_idf %>%
  group_by(medio) %>%
        slice_max(tf, n = 10) %>%
        ungroup() %>%
        mutate(word = reorder_within(word, tf, medio)) %>%
        ggplot(aes(tf, word, fill = medio)) +
        geom_col(show.legend = FALSE) +
        facet_wrap(~medio, ncol = 2, scales = "free") +
        labs(x = "tf", y = NULL)+
        scale_y_reordered()+
        theme_minimal()+
        theme(text=element_text(size=16,family="mono",face = "bold"))

Tal como fue mencionado, este problema puede evitarse con el uso de la métrica TF-IDF, que balancea importancia e informatividad. Al aplicarlo, estas palabras rápidamente desaparecen del ranking; la razón es simple: por más alto que sea el valor del TF, al multiplicarse por un IDF igual a cero resultará en un TF_IDF igual a cero también.

Gráfico final a partir de TF-IDF

Grafico las 10 palabras de mayor TF-IDF para cada medio.

corpus_tf_idf %>%
  group_by(medio) %>%
        slice_max(tf_idf, n = 15) %>%
        ungroup() %>%
        ggplot(aes(tf_idf, fct_reorder(word, tf_idf), fill = medio)) +
        geom_col(show.legend = FALSE) +
        facet_wrap(~medio, ncol = 2, scales = "free") +
        labs(x = "tf_idf", y = NULL)+
        theme_minimal()+
  theme(text=element_text(size=16,family="mono",face = "bold"))

Para el gráfico final, realizo algunos pasos como: modificar la columna “medio” para recuperar las tildes de los nombres de los diarios; descargar la paleta de colores que utilizaré; indicar ciertos criterios como el tamaño y la fuente del texto, el tamaño del gráfico, etc.

En el caso de Perfil y Minuto Uno, ciertas palabras encabezan la lista (tres palabras en Perfil, y seis en Minuto Uno), y muchas otras compiten por los siguientes lugares, por lo que decidí recortar la cantidad de palabras que aparecen en dichos gráficos a fines de poder leer claramente las etiquetas (con el argumento with_ties = FALSE). Es importante destacar que, al aplicar este corte sin un criterio en particular, también podría influir en cuán esclarecedor resultan los gráficos en sí.

corpus_tf_idf<-corpus_tf_idf %>% mutate(medio=recode(medio, "Clarin" = "Clarín", "Pagina 12" = "Página 12", "La Nacion" = "La Nación"))
library(tarantino)
p<-tarantino_palette('ReservoirDogs')
library(forcats)

plotpalabras<-corpus_tf_idf %>%
  group_by(medio) %>%
        slice_max(tf_idf, n = 10,with_ties = FALSE) %>%
        ungroup() %>%
        ggplot(aes(tf_idf, fct_reorder(word, tf_idf), fill = medio)) +
        geom_col(show.legend = FALSE) +
        facet_wrap(~medio, ncol = 2, scales = "free") +
        labs(title="Palabras más importantes e informativas de cada medio
(de mayor TF_IDF)",x = "tf-idf", y = NULL) +
        theme_minimal()+
  scale_fill_manual(values = p)+
  theme(plot.title = element_text(family="mono",face = "bold",size = 18,hjust = 0.5),text=element_text(size=15,  family="mono",face = "bold"),strip.text = element_text(size=17,face = "bold",family="mono"))
png("plotpalabras.png",
    units="in", width=10, height=8,res=300)
print(plotpalabras)
plotpalabras

Tópicos: Latent Dirichlet Allocation (LDA)

Modelado de tópicos

El próximo paso es identificar los principales tópicos presentes en el corpus.

El método Latent Dirichlet Allocation (LDA) 4 es un algoritmo para el modelado de tópicos. Presenta dos supuestos centrales:

  1. Cada documento es una mezcla de tópicos (es decir, puede contener palabras de diversos tópicos en diferentes proporciones).

  2. Cada tópico es una mezcla de palabras (y éstas pueden formar parte de diferentes tópicos).

Mediante el método LDA, buscaré identificar la combinación de palabras dentro de cada tópico y la combinacion de tópicos que compone cada documento.

Cargo el paquete que usaré.

library(topicmodels)

Para realizar el modelado de tópicos, creo una DocumentTermMatrix: una matriz donde las filas corresponden a documentos; las columnas, a palabras; y los valores, a recuentos de palabras.

Para llegar a esta matriz, primero genero la tabla con los recuentos.

word_counts <- corpus_tidy %>%
        group_by(id, word) %>%
        summarise(n=n()) %>%
        ungroup()

Luego la transformo en una DocumentTermMatrix (porque así lo requiere el paquete topicmodels, que realizará la estimación de tópicos).

disc_dtm <- word_counts %>%
                cast_dtm(id, word, n)

disc_dtm
## <<DocumentTermMatrix (documents: 7000, terms: 96678)>>
## Non-/sparse entries: 1146136/675599864
## Sparsity           : 100%
## Maximal term length: 38
## Weighting          : term frequency (tf)

Al explorarla, me indica que la matriz resultante tiene 675.599.864 cantidad de entradas y que sólo 1.146.136 entradas no son cero, por lo que la matriz es casi 100% dispersa (básicamente, la matriz está repleta de ceros).

Esta matriz Document-Term Matrix será el insumo del paquete topicmodels, que estimará un modelo LDA. En este caso, le indico que identifique 14 tópicos.

lda_14 <- LDA(disc_dtm, k=14, control = list(seed = 1234))

Exploro los primeros 20 términos de cada tópico.

terms(lda_14,20)
##       Topic 1      Topic 2      Topic 3        Topic 4         Topic 5    
##  [1,] "papa"       "ciudad"     "cristina"     "juez"          "musica"   
##  [2,] "francisco"  "san"        "gobierno"     "causa"         "disco"    
##  [3,] "minutos"    "aires"      "pais"         "kirchner"      "teatro"   
##  [4,] "carrera"    "agua"       "sociales"     "federal"       "banda"    
##  [5,] "final"      "zona"       "clase"        "dinero"        "canciones"
##  [6,] "vaticano"   "metros"     "modelo"       "baez"          "argentina"
##  [7,] "equipo"     "rio"        "politico"     "empresa"       "arte"     
##  [8,] "san"        "nacional"   "presidenta"   "millones"      "mundo"    
##  [9,] "argentina"  "centro"     "justicia"     "cristina"      "publico"  
## [10,] "iglesia"    "manana"     "mundo"        "fiscal"        "cantante" 
## [11,] "partido"    "personas"   "meses"        "justicia"      "cancion"  
## [12,] "manana"     "dias"       "militante"    "denuncia"      "rock"     
## [13,] "mundo"      "vecinos"    "kirchnerismo" "investigacion" "festival" 
## [14,] "argentino"  "semana"     "uber"         "nacional"      "grupo"    
## [15,] "copa"       "arte"       "deberian"     "informacion"   "aires"    
## [16,] "boca"       "provincia"  "relato"       "empresas"      "of"       
## [17,] "viernes"    "obras"      "argentina"    "caso"          "vida"     
## [18,] "encuentro"  "villa"      "opositores"   "pesos"         "apple"    
## [19,] "lunes"      "barrio"     "izquierda"    "gobierno"      "artista"  
## [20,] "comentario" "kilometros" "problemas"    "lopez"         "musical"  
##       Topic 6       Topic 7      Topic 8       Topic 9        Topic 10       
##  [1,] "macri"       "gobierno"   "mundo"       "salud"        "policia"      
##  [2,] "gobierno"    "justicia"   "argentina"   "personas"     "fiscal"       
##  [3,] "presidente"  "presidente" "sistema"     "casos"        "seguridad"    
##  [4,] "frente"      "corte"      "politica"    "hospital"     "mujer"        
##  [5,] "scioli"      "pais"       "vida"        "argentina"    "casa"         
##  [6,] "nacional"    "ley"        "forma"       "ciento"       "victima"      
##  [7,] "provincia"   "derechos"   "sociedad"    "enfermedad"   "hombre"       
##  [8,] "candidato"   "camara"     "social"      "pais"         "causa"        
##  [9,] "mauricio"    "brasil"     "pais"        "riesgo"       "manana"       
## [10,] "elecciones"  "judicial"   "educacion"   "nacional"     "joven"        
## [11,] "jefe"        "caso"       "personas"    "pacientes"    "justicia"     
## [12,] "cristina"    "juicio"     "universidad" "desarrollo"   "federal"      
## [13,] "politica"    "argentina"  "informacion" "ministerio"   "investigacion"
## [14,] "gobernador"  "tribunal"   "sociales"    "enfermedades" "crimen"       
## [15,] "ministro"    "nacional"   "tecnologia"  "vida"         "caso"         
## [16,] "kirchner"    "politico"   "desarrollo"  "estudio"      "detenido"     
## [17,] "pro"         "juez"       "internet"    "caso"         "ciudad"       
## [18,] "pais"        "fiscal"     "paises"      "medico"       "tarde"        
## [19,] "electoral"   "jueces"     "gente"       "tratamiento"  "muerte"       
## [20,] "oficialismo" "proceso"    "cambio"      "cancer"       "zona"         
##       Topic 11     Topic 12      Topic 13   Topic 14       
##  [1,] "millones"   "trump"       "vida"     "pais"         
##  [2,] "argentina"  "mujeres"     "historia" "presidente"   
##  [3,] "gobierno"   "unidos"      "libro"    "gobierno"     
##  [4,] "ciento"     "clinton"     "mundo"    "unidos"       
##  [5,] "economia"   "violencia"   "casa"     "paises"       
##  [6,] "dolares"    "campana"     "mujer"    "personas"     
##  [7,] "pais"       "personas"    "hombre"   "acuerdo"      
##  [8,] "mercado"    "mujer"       "gente"    "seguridad"    
##  [9,] "precios"    "donald"      "amor"     "guerra"       
## [10,] "pesos"      "mundo"       "familia"  "siria"        
## [11,] "inflacion"  "genero"      "pelicula" "refugiados"   
## [12,] "sector"     "hillary"     "obra"     "paz"          
## [13,] "banco"      "sexual"      "libros"   "fuerzas"      
## [14,] "empresas"   "pais"        "cine"     "obama"        
## [15,] "aumento"    "hombres"     "paso"     "mundo"        
## [16,] "central"    "presidente"  "novela"   "cuba"         
## [17,] "meses"      "republicano" "madre"    "ministro"     
## [18,] "deuda"      "york"        "hijos"    "grupo"        
## [19,] "acuerdo"    "democrata"   "hijo"     "internacional"
## [20,] "produccion" "partido"     "mujeres"  "europa"

Para analizar la distribución de palabras para cada tópico, aplico la función tidy(), que genera un formato de un tópico-palabra por fila: para cada combinación posible, estima la probabilidad de que esa palabra se genere a partir de ese tópico (probabilidad denominada “beta”).

ap_topics <- tidy(lda_14, matrix = "beta")
ap_topics<-ap_topics %>%
  mutate(beta = round(beta, 5))
ap_topics
## # A tibble: 1,353,492 × 3
##    topic term       beta
##    <int> <chr>     <dbl>
##  1     1 acelero 0.00002
##  2     2 acelero 0      
##  3     3 acelero 0      
##  4     4 acelero 0.00002
##  5     5 acelero 0      
##  6     6 acelero 0.00001
##  7     7 acelero 0      
##  8     8 acelero 0.00003
##  9     9 acelero 0.00002
## 10    10 acelero 0.00006
## # ℹ 1,353,482 more rows

A partir de este proceso, identifico las 15 palabras más comunes dentro de cada tópico.

Visualizo el resultado.

ap_top_terms <- ap_topics %>%
  group_by(topic) %>%
  slice_max(beta, n = 15) %>% 
  ungroup() %>%
  arrange(topic, -beta)

plottopicos<-ap_top_terms %>%
  mutate(term = reorder_within(term, beta, topic)) %>%
  ggplot(aes(beta, term, fill = factor(topic))) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~ topic, scales='free') +
  scale_y_reordered() +
        labs(title="Tópicos revelados con LDA",y = "término") +
        theme_minimal()+
  scale_fill_manual(values = c(p,"#245C59","#707B5D","#696E68","#D7DA4C","#015c89","#B6523B","#8A7A23"))+
  theme(plot.title = element_text(family="mono",face = "bold",size = 18,hjust = 0.5),text=element_text(size=15,  family="mono",face = "bold"),strip.text = element_text(size=17,face = "bold",family="mono"))


plottopicos

png("plottopicos.png",
    units="in", width=10, height=10,res=300)
print(plottopicos)


Procedo a etiquetar —dentro de lo posible— cada tópico:

    1. Fútbol/deporte
    1. Servicios y espacios públicos/urbanismo?
    1. Política nacional
    1. Causas judiciales/corrupción
    1. Cultura: foco en música y teatro, espectáculos
    1. Elecciones
    1. Política de países latinoamericanos
    1. Desarrollo/educación/tecnología?
    1. Salud
    1. Inseguridad
    1. Economía
    1. Actualidad internacional
    1. Cultura: foco en cine y literatura (+ tópicos amplios estilo amor, vida, familia?)
    1. Política internacional con foco en conflictos/guerras/terrorismo


Me interesa confirmar si la diferencia que tracé entre el tópico 12 y el 14 es correcta. Para esto, identifico y visualizo las palabras que tengan la mayor diferencia en beta entre ambos tópicos. Para limitarlo a palabras significativas, filtro palabras con un beta de 2/1000 en al menos un tema (es decir, que su uso sea relativamente común). Hago esto con el siguiente código.

beta_wide <- ap_topics %>%
  mutate(topic = paste0("topic", topic)) %>%
  pivot_wider(names_from = topic, values_from = beta) %>% 
  filter(topic12 > .002 | topic14 > .002) %>%
  mutate(log_ratio12_14 = log2(topic12 / topic14))
plotdif<-beta_wide %>%
  mutate(pos = log_ratio12_14 >= 0) %>% 
  ggplot(aes(x=reorder(term,log_ratio12_14) , y=log_ratio12_14, fill=pos)) +
    geom_col(show.legend = FALSE) +
    coord_flip() +
    labs(x='término',
         y='Log2 ratio topic14/topic12') +
        theme_minimal()+
  scale_fill_manual(values = c("#245C59","#B6523B"))+
  theme(plot.title = element_text(family="mono",face = "bold",size = 18,hjust = 0.5),text=element_text(size=15,  family="mono",face = "bold"))
png("plotdif.png",
    units="in", width=10, height=5,res=300)
print(plotdif)
plotdif

Puede verse que mientras que en el tópico 14 se destacan palabras vinculadas a conflictos internacionales (como “siria”, “francia” —cabe recordar los atentados en Francia durante este período—, “refugiados”, “fuerzas”, “seguridad”, “guerra” y “paz”), en el tópico 12 se destacan términos vinculados a la política interna de EEUU (“hillary”, “trump”, “clinton”, “republicano” —cabe recordar el triunfo de Trump en 2016—), a la iglesia (“vaticano”, “papa”, “iglesia”, “francisco”) y a cuestiones de género (“género”, “mujeres” ¿y —posiblemente— “sexual”? 5).

Composición de tópicos de cada medio

En la sección anterior, el análisis fue guiado por el primer supuesto de LDA: cada tópico es una mezcla de palabras. Esta sección se enfoca en el segundo: cada documento es una mezcla de tópicos.

Busco identificar la composición de tópicos de cada medio.

Calculo las probabilidades por documento por tópico (denominadas “gamma”) con el siguiente código. Estos valores reflejan la proporción estimada de las palabras de cierto documento que se generan a partir de cierto tópico.

doc_2_topics1 <- tidy(lda_14, matrix = "gamma")
doc_2_topics1<-doc_2_topics1 %>%
  mutate(gamma = round(gamma, 5))
doc_2_topics1
## # A tibble: 98,000 × 3
##    document topic   gamma
##    <chr>    <int>   <dbl>
##  1 92           1 0.00019
##  2 132          1 0.00009
##  3 250          1 0.00003
##  4 346          1 0.00008
##  5 455          1 0.00029
##  6 465          1 0.00012
##  7 489          1 0.00005
##  8 502          1 0.00008
##  9 508          1 0.00009
## 10 534          1 0.00023
## # ℹ 97,990 more rows

A partir de la matriz gamma generada, examino la composición de tópicos de cada medio.

Aplico un left_join con el corpus, donde aparece la variable medio 6.

Mediante el siguiente código calculo el promedio de gamma para cada tópico para cada medio.

doc_2_topics<-doc_2_topics1 %>%
  rename(id = document) %>%
  mutate(id = as.integer(id)) %>%
  left_join(corpus %>% select(id, medio) %>% unique()) %>%
  group_by(medio, topic) %>%
  summarise(mean = mean(gamma)*100)

doc_2_topics<-doc_2_topics %>% mutate(medio=recode(medio, "Clarin" = "Clarín", "Pagina 12" = "Página 12", "La Nacion" = "La Nación"))
etiq<-c("Fútbol/deporte", "Servicios y espacios públicos/urbanismo", "Política nacional", "Causas judiciales/corrupción", "Cultura: foco en música y teatro", "Elecciones", "Política de países latinoamericanos", "Desarrollo/educación/tecnología", "Salud", "Inseguridad", "Economía","Actualidad internacional", "Cultura: foco en cine y literatura", "Conflictos internacionales")

plottop<-doc_2_topics %>%
  ggplot(aes(factor(topic), mean,fill=factor(topic)))+  
  geom_col() +
  facet_wrap(~ medio,ncol = 4) +
  labs(x = "tópico", y = "mean")+
        labs(title="Composición de tópicos de cada medio") +
        theme_minimal()+
  scale_fill_manual(values = c(p,"#245C59","#707B5D","#696E68","#D7DA4C","#015c89","#B6523B","#8A7A23"),name = "Tópico", labels=etiq)+
  theme(plot.title = element_text(family="mono",face = "bold",size = 24,hjust = 0.5),text=element_text(size=13,  family="mono",face = "bold"),strip.text = element_text(size=13,face = "bold",family="mono"),panel.spacing = unit(2, "lines"),legend.text = element_text(size=13),legend.title = element_text(size=13))

plottop

png("plottop.png",
    units="in", width=18, height=11,res=300)
print(plottop)

Una forma alternativa para visualizar la composición de tópicos por medio es la siguiente:

plottop2<-doc_2_topics %>%
  ggplot(aes(factor(medio), mean,fill=factor(topic),label=round(mean)))+  
  geom_bar(position="stack", stat="identity") +
  geom_text(size = 5,color="white",face="bold", position = position_stack(vjust = 0.5)) +
  labs(x = "medio", y = "mean")+
        labs(title="Composición de tópicos de cada medio") +
        theme_minimal()+
  scale_fill_manual(values = c(p,"#245C59","#707B5D","#696E68","#D7DA4C","#015c89","#B6523B","#8A7A23"),name = "Tópico", labels=etiq)+
  theme(plot.title = element_text(family="mono",face = "bold",size = 24,hjust = 0.5),text=element_text(size=13,  family="mono",face = "bold"),axis.text.x = element_text(angle = 13, size=19,  family="mono",face = "bold",color="black"),strip.text = element_text(size=13,face = "bold",family="mono"),panel.spacing = unit(2, "lines"),legend.text = element_text(size=13),legend.title = element_text(size=19))


plottop2

png("plottop2.png",
    units="in", width=18, height=11,res=300)
print(plottop2)

Evolución temporal de tópicos

Por último, me interesa analizar la evolución de los tópicos en el tiempo.

Aplico un left_join con el corpus, donde aparece la variable fecha.

doc_2_topicstemp<-doc_2_topics1 %>%
  rename(id = document) %>%
  mutate(id = as.integer(id)) %>%
  left_join(corpus %>% select(id, fecha) %>% unique()) %>%
  group_by(fecha, topic) %>%
  summarise(mean = mean(gamma)*100)

Exploro y visualizo la evolución de los tópicos a lo largo del tiempo. Lo hago analizando la evolución en el tiempo del promedio de gamma para cada tópico para cada fecha, y aplicando geom_smooth() para descartar el ruido y revelar la tendencia.

plottemp<-ggplot(doc_2_topicstemp, aes(x = fecha, y = mean, color=factor(topic))) + 
  geom_smooth(size=2)+
  scale_color_manual(values = c(p,"#245C59","#707B5D","#696E68","#D7DA4C","#015c89","#B6523B","#8A7A23"),name = "Tópico", labels=etiq) +
  facet_wrap(~topic, ncol = 3, scales = "free_y")+
  labs(x = "fecha", y = "mean")+
  labs(title="Evolución de tópicos en el tiempo") + 
  scale_x_date(date_labels = "%b-%y",date_breaks  ="3 month")+
  theme_minimal()+
  theme(plot.title = element_text(family="mono",face = "bold",size = 25,hjust = 0.5),text=element_text(size=17,  family="mono",face = "bold"),strip.text = element_text(size=19,face = "bold",family="mono"),panel.spacing = unit(2, "lines"),legend.text = element_text(size=14),legend.title = element_text(size=15),axis.text.x = element_text(angle = 45, hjust = 1.1,size=10,family="mono",face = "bold",color="black"))

plottemp

d=data.frame(date=as.Date(c("2015-08-09", "2015-10-25", "2015-11-22")), evento=c("PASO", "Primera vuelta", "Segunda vuelta"))

plottempelecc<-ggplot(data=filter(doc_2_topicstemp,topic==6), aes(x = fecha, y = mean)) + 
  geom_smooth(size=2,color="#245C59")+
  labs(x = "fecha", y = "mean")+
  labs(title="Evolución del tópico elecciones en el tiempo",legend=FALSE) + 
  scale_x_date(date_labels = "%b-%y",date_breaks  ="3 month")+
  theme_minimal()+
  theme(plot.title = element_text(family="mono",face = "bold",size = 25,hjust = 0.5),text=element_text(size=17,  family="mono",face = "bold"),strip.text = element_text(size=19,face = "bold",family="mono"),panel.spacing = unit(2, "lines"),axis.text.x = element_text(angle = 45, hjust = 1.1,size=10,family="mono",face = "bold",color="black"))+
  geom_vline(data=d,mapping=aes(xintercept=date), linetype=2, colour='black')+
geom_text(data=d, mapping=aes(x=date, y=12, label=evento), size=6, color="black", angle=90, vjust=-0.4, hjust=0)

plottempelecc


  1. En esta ocasión los documentos son los artículos que forman parte del corpus.↩︎

  2. Wickham, Hadley. 2014. “Tidy Data.” Journal of Statistical Software, Articles 59 (10).↩︎

  3. Nota: en este proceso elimino las tildes del lexicón, y renombro el encabezado de la columna con la lista de stopwords de “X1” a “word” (esto resultará práctico para el anti_join en el próximo paso).↩︎

  4. La elección del modelo LDA por sobre el STM se debió principalmente a límites computacionales y el peso de aplicar el modelo STM.↩︎

  5. En este punto resulta clara la utilidad de los n-grams para obtener un análisis más detallado de los tópicos presentes en los medios durante esta época. Por ejemplo, si bien la palabra “sexual” puede referirse a un amplio espectro de temáticas, “abuso sexual” podría emerger (o no) como un término relevante, delimitando claramente un tema en particular.↩︎

  6. Nota: en el proceso, renombro previamente la columna “document” como “id” y recupero las tildes en los medios para el gráfico final. También añado las etiquetas que definí para que aparezcan como guía en el gráfico, de forma tal que resulte más fácil de interpretar.↩︎