Análise de texto no R - pacote tm

Corpus e o pacote tm

O pacote mais popular para trabalharmos com texto no R se chama tm (“Text Mining”). Vamos carregá-lo e passar por algumas funções do pacote para, então, trabalharmos com uma nova classe de objeto: Corpus.

Carregue o pacote e abre o mesmo arquivo que usamos em Tutorial 11:

library(tm)

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

discursos <- readLines("discursos.txt")

Uma boa prática ao trabalharmos com texto é transformarmos todas as palavras em minúsculas (exceto, obviamente, quando a diferenciação importar). tolower, função da biblioteca básica do R, cumpre a tarefa e vamos criar um objeto “discursos2”, que será nossa versão modificada dos discursos.

discursos2 <- tolower(discursos)
discursos2[1]

Pontuação também costuma ser um problema ao trabalharmos com texto. A não ser que nos interesse recortar o texto usando os pontos como marcas, convém aplicarmos a função removePunctuation do pacote tm para retirar a pontuação:

discursos2 <- removePunctuation(discursos2)
discursos2[1]

O mesmo ocorre com números. Se não forem de interesse específico, melhor extraí-los. A função removeNumbers resolve o problema:

discursos2 <- removeNumbers(discursos2)
discursos2[1]

Vamos olhar novamente para a nuvem de palavras, usando agora o nosso objeto de texto transformado:

wordcloud(discursos2, max.words = 50)

Note que as palavras com mais frequência são aquelas de maior ocorrência na língua portuguesa. Qual é a utilidade de incluí-las na análise se sabemos que são frequentes?

O pacote tm oferece a função stopwords. Essa função gera um vetor com as palavras mais frequentes da língua indicada:

stopwords("pt")

Com a função removeWords podemos excluir as “stopwords” da língua portuguesa de nosso conjunto de textos:

discursos2 <- removeWords(discursos2, stopwords("pt"))
discursos2[1]

Vamos aproveitar que já fizemos inúmeras remoções – pontuação, números e stopwords – e retirar os espaços excedentes que sobraram no texto:

discursos2 <- stripWhitespace(discursos2)
discursos2[1]

E vamos repetir nossa nuvem de palavras:

wordcloud(discursos2, max.words = 50)

Muito mais interessante, não?

Note, porém, o destaque a “presidente”. A deputada faz referências ao presidente da Câmara em praticamente todos os seus discursos e isso aumenta demais a frequência desta palavra. O mesmo ocorre com “luiza” e “erundina”, já que todas as vezes em que inicia uma fala, seu nome é transcrito.

Podemos, então, incrementar a lista de stopwords com padrões que conhecemos:

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

E gerar um novo objeto removendo as novas stopwords:

discursos3 <- removeWords(discursos2, stopwords_pt)
wordcloud(discursos3, max.words = 50)

Com uma imagem, podemos ter alguma ideia dos temas e termos recorrentes da deputada.

Uma funcionalidade do pacote tm não muito bem implementada em português é a “stemização de palavras”. “Word Stem”, em linguística, significa extrair de um conjunto de palavras apenas a raiz da palavra ou o denominador comum de várias palavras. Por exemplo, “discurso”, “discursivo”, “discursar” e “discussão”, “stemizadas”, deveriam se tornar “discus”, e poderíamos agrupá-las para fins analíticos. Vamos ver um exemplo em inglês:

stemDocument(c("politics", "political", "politically"), language = "english")

Vamos ver o resultado da função stemDocument no primeiro discurso:

discursos4 <- stemDocument(discursos2, language = "portuguese")
discursos4[1]

Hummmm… meio estranho, não? Mas você pegou o espírito. Vamos seguir em frente.

Tokenização

Tokenização de um texto significa a separação em pequenos “tokens”, que podem ser palavras ou n-grams, que são pequenos conjuntos de palavras. Bigrams, por exemplo, são pares de palavras. Voltaremos a esse tópico adiante e com mais cuidado. Mas vamos aproveitar o objeto tal como está para apresentarmos uma função do pacote stringr que deixamos propositalmente para trás: str_split. Como as palavras estão separadas por espaço, no resultado final será uma lista contendo um vetor de tokens para cada discurso:

tokens <- str_split(discursos2, " ")

unlist transforma a lista em um vetor único:

unlist(tokens)

Corpus

Corpus, em linguística, é um conjunto de textos, normalmente grande e em formato digital. Um corpus é composto pelo conteúdo dos textos e pelos metadados de cada texto. Na linguagem R, Corpus é também uma classe de objetos do pacote tm e à qual podemos aplicar uma série de funções e transformações.

Vamos ver como criar um Corpus.

Em primeiro lugar, é preciso uma fonte. A fonte pode ser um vetor, um data frame ou um diretório. Vejamos os dois primeiros, começando com o vetor com o qual já estamos trabalhando:

discursos_source <- VectorSource(discursos)

“discursos_source” é um objeto que apenas indica uma fonte de textos para funções do pacote tm. Para criar um Corpus, utilizados a função VCorpus (volatile corpus, com o qual vamos trabalhar, e que armazena os dados na memória) ou PCorpus (permanent corpus, usada para quando os dados estão em uma base externa ao R).

discursos_corpus <- VCorpus(discursos_source)

Vamos observar o objeto “discursos_corpus” e sua classe:

discursos_corpus
class(discursos_corpus)

Veja que um VCorpus contém “Metadata” e “Content”. Neste caso, não temos nenhum metadado sobre os discursos, mas poderíamos criar. Vamos observar o que há na primeira posição de um VCorpus. (hey, note que um VCorpus é uma lista!)

str(discursos_corpus[[1]])

Em metadata temos diversas variáveis: author, description, id, language, etc. Veja que id está preenchido com a ordem dos discursos e a língua está em inglês, por “default”. Neste exercícios temos mais controle sobre os metadados, pois capturamos os textos de uma fonte específica, mas seria legal armazenar os metadados de um Corpus para compartilhá-lo ou trabalhar com Corpora (plural de Corpus) mais complexos.

Aliás, metadados são a única boa razão para trabalharmos com Corpus e não com vetores. Guardar informações sobre os textos é fundamental para selecionarmos subconjuntos e produzirmos análise.

Vamos reabrir os dados usando um data frame como fonte. Vamos criar um:

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

E repetir o processo, com a diferença que utilizamos DataframeSource para indicar a fonte dos dados:

discursos_df_source <- DataframeSource(discursos_df)
discursos_df_corpus <- VCorpus(discursos_df_source)

Mesma coisa, não?

Ao trabalharmos com Corpus, não aplicamos diretamente as funções do pacote tm. Em vez disso, utilizamos a função tm_map, que aplica uma outra função a todos os elementos do Corpus. Esse uso lembra as funções do pacote purrr e da família apply, caso você tenha lido sobre elas alhures. Observe a remoção de pontuação com removePunctuation:

discursos_df_corpus <- tm_map(discursos_df_corpus, removePunctuation)

A aplicação de qualquer função do pacote tm segue este procedimento. Quando a função não pertence ao pacote tm, porém, precisamos “passá-la” dentro da função content_transformer:

discursos_df_corpus <- tm_map(discursos_df_corpus, content_transformer(tolower))

Se você criar uma função para alteração de um texto, você deve utilizar content_transformer também.

Mais dois exemplos, com removeNumbers e removeWords:

discursos_df_corpus <- tm_map(discursos_df_corpus, removeNumbers)
discursos_df_corpus <- tm_map(discursos_df_corpus, removeWords, stopwords("pt"))

Porque tanto trabalho? Para trabalharmos com Corpus, que tem a vantagem de armazenar os metadados, em vez de um vetor.

Para poupar seu trabalho, você pode “envelopar” todas as transformações que quiser produzir em um Corpus em uma função:

limpa_corpus <- function(corpus){
  
  corpus <- tm_map(corpus, removePunctuation)
  corpus <- tm_map(corpus, content_transformer(tolower))
  corpus <- tm_map(corpus, removeNumbers)
  corpus <- tm_map(corpus, removeWords, stopwords("pt"))

  corpus
}

E aplicar a função aos Corpora com os quais estiver trabalhando:

discursos_corpus <- limpa_corpus(discursos_corpus)

Matriz Documento-Termo

O principal uso do pacote tm é gerar uma matriz de documentos-termos (“dtm”). Basicamente, essa matriz tem cada documento na linha e cada termo na coluna. O conteúdo da célula é a frequência do termo em cada documento.

dtm_discursos <- DocumentTermMatrix(discursos_corpus)

Veja um fragmento da “dtm” que criamos (documentos 101 a 105 e termos 996 a 1000):

as.matrix(dtm_discursos[401:410, 970:975])

Se quisermos rapidamente olhar os termos com frequência maior do que, digamos, 500:

findFreqTerms(dtm_discursos, 500)

Há uma série de usos para a classe Corpus do pacote tm para mineração de texto. Não vamos explorá-los e você pode buscar sozinh@. Vamos adotar agora uma abordagem que não envolve a criação de um Corpus.