Análise de texto no R - pacote tidytext

Uma abordagem “tidy” para texto

Corpora são os objetos clássicos para processamento de linguagem natural. No R, porém, há uma tendência a deixar tudo “tidy”. Vamos ver uma abordagem “tidy”, ou seja, com data frames no padrão do tidyverse, para texto.

Vamos fazer uma rápida introdução, mas recomendo fortemente a leitura do livro Text Mininig with R, disponível o formato “bookdown”.

Comecemos carregando os seguintes pacotes e abramos de novo os nossos textos do Congresso:

library(tidytext)
library(tidyverse)

url <- "https://github.com/JonnyPhillips/FLS6397_2019/raw/master/data/discursos.txt"
download.file(url, destfile="discursos.txt")

discursos <- readLines("discursos.txt")

Vamos recriar o data frame com os discursos:

discursos_df <- tibble(doc_id = 1:length(discursos), 
                          text = discursos)
discursos_df

Tokens

A primeira função interessante do pacote tidytext é justamente a tokenização de um texto:

discursos_token <- discursos_df %>%
  unnest_tokens(word, text)
discursos_token

Note que a variável doc_id, criada por nós, é mantida. “text”, porém, se torna “words”, na exata sequência do texto. Veja que o formato de um “tidytext” é completamnte diferente de um Corpus.

Como excluir stopwords nessa abordagem? Precisamos de um data frame com stopwords. Vamos recriar um vetor stopwords_pt, que é a versão ampliada das stopwords disponíveis no R, e criar um data frame com tal vetor:

stopwords_pt <- c(stopwords("pt"), "presidente", "é", "sr", "sra", "luiza", 
                  "erundina", "oradora", "revisão", "sp", "v.exa")
stopwords_pt_df <- tibble(word = stopwords_pt)

Com anti_join (lembra dessa função?) mantemos em “discursos_token” apenas as palavras que não estao em “stopwords_pt_df”

discursos_token <- discursos_token %>%
  anti_join(stopwords_pt_df, by = "word")

Para observarmos a frequência de palavras nos discursos, usamos count, do pacote dplyr:

discursos_token %>%
  count(word, sort = TRUE)

Com ggplot, podemos construir um gráfico de barras dos temos mais frequêntes, por exemplo, com frequência maior do que 500. Neste ponto do curso, nada do que estamos fazendo abaixo deve ser novo a você:

discursos_token %>%
  count(word, sort = TRUE) %>%
  filter(n > 500) %>%
  mutate(word = reorder(word, n)) %>%
  ggplot(aes(word, n)) +
    geom_col() +
    xlab(NULL) +
    coord_flip()

Incorporando a função wordcloud a nossa análise:

discursos_token %>%
  count(word, sort = TRUE) %>%
  with(wordcloud(word, n, max.words = 50))

A abordagem “tidy” para texto nos mantém no território confortável da manipulação de data frames e, particularmente, me parece mais atrativa do que a abordagem via Corpus para um conjunto grande de casos.

Bigrams

Já produzimos duas vezes a tokenização do texto, sem, no entanto, refletir sobre esse procedimento. Tokens são precisam ser formados por palavras únicas. Se o objetivo for, por exemplo, observar a ocorrência conjunta de termos, convém trabalharmos com bigrams (tokens de 2 palavras) ou ngrams (tokens de n palavras). Vejamos como:

discurso_bigrams <- discursos_df %>%
  unnest_tokens(bigram, text, token = "ngrams", n = 2)

Note que, ao tokenizar o texto, automaticamente foram excluídas as pontuações e as palavras foram alteradas para minúscula (use o argumento “to_lower = FALSE” caso não queira a conversão). Vamos contar os bigrams e veja os mais frequentes:

discurso_bigrams %>%
  count(bigram, sort = TRUE)

Como, porém, excluir as stopwords quando elas ocorrem em bigrams? Em primeiro, temos que separar os bigrams e duas palavras, uma em cada coluna:

bigrams_separated <- discurso_bigrams %>%
  separate(bigram, c("word1", "word2"), sep = " ")

E, a seguir, filter o data frame excluindo as stopwords (note que aproveitamos o vetor “stopwords_pt”):

bigrams_filtered <- bigrams_separated %>%
  filter(!word1 %in% stopwords_pt) %>%
  filter(!word2 %in% stopwords_pt)

ou, usando anti_join, como anteriormente:

bigrams_filtered <- bigrams_separated %>%
  anti_join(stopwords_pt_df, by = c("word1" = "word")) %>%
  anti_join(stopwords_pt_df, by = c("word2" = "word"))

Produzindo a frequência de bigrams:

bigram_counts <- bigrams_filtered %>% 
  count(word1, word2, sort = TRUE)

Reunindo as palavras do bigram que foram separadas para excluirmos as stopwords:

bigrams_united <- bigrams_filtered %>%
  unite(bigram, word1, word2, sep = " ")

A abordagem “tidy” traz uma tremenda flexibilidade. Se, por exemplo, quisermos ver com quais palavras a palavra “poder” é antecedida:

bigrams_filtered %>%
  filter(word2 == "poder") %>%
  count(word1, sort = TRUE)

Ou precedida:

bigrams_filtered %>%
  filter(word1 == "poder") %>%
  count(word2, sort = TRUE)

Ou ambos:

bf1 <- bigrams_filtered %>%
  filter(word2 == "poder") %>%
  count(word1, sort = TRUE) %>%
  rename(word = word1)

bf2 <- bigrams_filtered %>%
  filter(word1 == "poder") %>%
  count(word2, sort = TRUE) %>%
  rename(word = word2)

bind_rows(bf1, bf2) %>%
  arrange(-n)

Super simples e legal, não?

Ngrams

Repetindo o procedimento para “trigrams”:

discursos_df %>%
  unnest_tokens(trigram, text, token = "ngrams", n = 3) %>%
  separate(trigram, c("word1", "word2", "word3"), sep = " ") %>%
  anti_join(stopwords_pt_df, by = c("word1" = "word")) %>%
  anti_join(stopwords_pt_df, by = c("word2" = "word")) %>%
  anti_join(stopwords_pt_df, by = c("word3" = "word")) %>%
  count(word1, word2, word3, sort = TRUE)

“colegas parlamentares telespectadores” and “sociedade civil organizada” são os “trigrams” mais frequente no discurso da deputada.

Redes de palavras

Para encerrar, vamos a um dos usos mais interessantes do ngrams: a construção de redes de palavras. Precisaremos de dois novos pacotes, igraph e ggraph. Instale-os se precisar:

library(igraph)
library(ggraph)

Em primeiro lugar, transformaremos nosso data frame em um objeto da classe igraph, do pacote de mesmo nome, usado para a presentação de redes no R:

bigram_graph <- bigram_counts %>%
  filter(n > 20) %>%
  graph_from_data_frame()

A seguir, com o pacote ggraph, faremos o grafo a partir dos bigrams dos discursos da deputada:

ggraph(bigram_graph, layout = "fr") +
  geom_edge_link() +
  geom_node_point() +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1)

Note que são formadas pequenas associações entre termos que, par a par, caminham juntos. Novamente, não vamos explorar aspectos analíticos da mineração de texto, mas estas associações são informações de grande interessa a depender dos objetivos da análise.

Para além do tutorial

No tutorial, vimos o básico da preparação de textos para mineração, como organizar um Corpus e criar tokens. Além disso, vimos várias utilidades do pacote stringr, que serve para além da mineração de texto e pode ser útil na organização de bases de dados que contém variáveis “character”.

Se houver tempo em sala de aula e você quiser se aprofundar no assunto, leia alguns dos capítulos de Text Mininig with R: