Códigos utilizados para as análises em Pesquisa Literária com R: Análise Quantitativa de Dados Textuais, Quanteda tomando como exemplo o Livro do Desassossego (Giménez e Gomide 2022).

1 Ferramentas e preparação dos dados

1.1 Instalação

Quanteda (Quantitative Analysis of Textual Data) é um pacote de R para a manipulação e análise de dados textuais.

A instalação do R varia de acordo com o sistema operacional (ex.: Windows, Mac, Linux) bem como suas diferentes versões. Há várias fontes onde se pode obter instruções atualizadas de como instalar o R (e.x.https://didatica.tech/como-instalar-a-linguagem-r-e-o-rstudio/). O Comprehensive R Archive Network (CRAN), a rede oficial de distribuição do R, oferece instruções confiáveis para tal, porém, talvez não tão detalhada como em outras fontes.

Uma outra sugestão é instalar uma interface gráfica do utilizador, do inglês Graphical User Interface (GUI). As GUIs facilitam consideravelmente a interação do usuário com o computador. O (RStudio) é a GUI mais utilizada para R, e, assim como o R, é gratuita e possui o código aberto.

1.2 Configuração: preparando o ambiente.

Ao reutilizar códigos, é uma boa prática estar atento à versão instalada tanto do R quanto das bibliotecas utilizadas. Não é necessário que as versões sejam as mesmas daquelas utilizadas durante a criação dos códigos, entretanto, em alguns casos, pode não haver compatibilidade entre versões diferentes e algumas funções ou pacotes podem ter sido descontinuados. Este artigo foi escrito utilizando a versão 4.2.0 do R. Para nossa análise, utilizaremos alguns pacotes já existentes. Estes pacotes nada mais são que extensões para o R que normalmente contém dados ou códigos. Para utilizá-los, precisamos instalá-los no computador, caso ainda não tenha sido feito, e carregá-lo ao R. Uma vantagem de carregar apenas os pacotes necessários (em vez de todos os pacotes instalados) é evitar processamento computacional desnecessário. O código abaixo cria uma lista dos pacotes utilizados na presente análise e os carrega, instalando os que ainda não estavam presentes.

# verificar a versão do R
R.version.string
## [1] "R version 4.2.0 (2022-04-22 ucrt)"

Para nossa análise, utilizaremos alguns pacotes já existentes. Estes pacotes nada mais são que extensões para o R que normalmente contém dados ou códigos. Para utilizá-los, precisamos instalá-los no computador, caso ainda não tenha sido feito, e carregá-lo ao R. Uma vantagem de carregar apenas os pacotes necessários (ao invés de todos os pacotes instalados) é evitar processamento computacional desnecessário. O código abaixo cria uma lista dos pacotes utilizados na presente análise e os carrega, instalando os que ainda não estavam presentes.

# Listamos os pacotes que precisamos
packages = c("quanteda", "quanteda.textmodels", "quanteda.textstats", "quanteda.textplots",
             "newsmap", # para classificar documentos, com base em “seed words”
             "readtext", # para ler diferentes formatos de texto 
             "spacyr", # para anotação de classes gramaticais, reconhecimento de entidades e anotação sintática (python deve estar instalado)
             "ggplot2", #para gráfico simples das frequências
             "seededlda", # para modelagem de tópico
             "stringr" # para as expressões regulares
             )

# Instalamos (se necessário) e carregamos os pacotes
package.check <- lapply(
  packages,
  FUN = function(x) {
    if (!require(x, character.only = TRUE)) {
      install.packages(x, dependencies = TRUE)
      require(x, character.only = TRUE)
    }
  }
)

Os códigos abaixo foram implementados na versão 3.2.1 do Quanteda. Utilizar uma versão diferente dessa pode resultar em erros ou resultados indesejados. Para verificar qual é a versão dos pacotes, empregamos a função packageVersion. Para verificar a versão do R, utilizamos R.version.string.

# verificar versão do quanteada
packageVersion("quanteda")
## [1] '3.2.1'

Por fim, precisamos estabelecer qual será nosso diretório de trabalho. Este será o local onde os resultados serão salvos. Para identificar qual é o diretório de trabalho selecionado, utilizamos getwd(). Esta função retorna o caminho absoluto, i.e., o endereço completo, do diretório. Para definirmos o novo local de trabalho, utilizamos a função setwd(). Arquivos salvos nesse diretório podem ser lidos apenas com a indicação do nome do arquivo. Isto porque podemos utilizar o caminho relativo, ou seja, o endereço onde o arquivo está salvo a partir do diretório em que estamos trabalhando.

1.3 Dados

Uma vez instalados os pacotes necessários, pode-se proceder à análise do corpus. Para isso, precisamos carregar o corpus no R. Se estamos trabalhando com dados armazenados localmente, isto é, disponíveis no computador onde as análises serão realizadas, basta utilizar a função readtext(), indicando o local (relativo ou absoluto) do arquivo desejado.

O livro pode ser lido como um arquivo único,

# para lermos um arquivo único com todo o conteúdo do livro
ldod_unico <- readtext("corpora/pessoa_ldod_completo.txt", encoding = "latin1")

# retorna a estrutura do objeto criado
str(ldod_unico)
## Classes 'readtext' and 'data.frame': 1 obs. of  2 variables:
##  $ doc_id: chr "pessoa_ldod_completo.txt"
##  $ text  : chr "Na casa de Saude de Cascaes\n\nInclue: -(1) Introdução, entrevista com António Mora.\n\t (2) Alberto Caeiro.\n\"| __truncated__

ou considerando cada fragmento do livro como uma unidade:

# ler todos os arquivos na pasta ldod do diretório corpora 
ldod_files <- readtext("corpora/ldod", encoding = "utf-8")

# retornar a estrutura do objeto criado
str(ldod_files)
## Classes 'readtext' and 'data.frame': 518 obs. of  2 variables:
##  $ doc_id: chr  "001_Na casa de Saude de Cascaes.txt" "002_Peristylo.txt" "003_Introducção.txt" "004_Na Floresta do....txt" ...
##  $ text  : chr  "Na casa de Saude de Cascaes\n\nInclue: -(1) Introdu" "L.doD.\n\n1. Peristylo.\n2. Bailado.\n3. O ultimo Cysne.\n4. Tecedeira...\n5. Encantamento.\n6. Apotheose do Ab"| __truncated__ "L.doD.\n\n1. Introduc" "L.doD.\n\n1- Na Floresta do Alheamento.\n2- Viagem nunca feita.\n3. Intervallo doloroso.\n4. Epilogo na Sombra."| __truncated__ ...

Os textos acima derivam da edição de Jacinto do Prado Coelho (1982) disponível no arquivo LdoD, disponíveis em UTF-8. O arquivo completo foi salvo com a codificação latin1 e informação para-textual e editorial (como notas dos editores) que pudessem interferir na pesquisa automática do software foram eliminadas.

As análises abaixo serão demonstradas utilizando os dois corpora, em diferentes momentos.

1.3.1 Limpeza

A limpeza abaixo foi aplicada apenas aos textos salvos separadamente (ldod_files). O arquivo com o livro em um único texto (ldod_unico) já havia sido limpo anteriormente.

# criamos uma cópia para recuperarmos o orignal caso haja erros na regex
ldod_clean <- ldod_files

## remoção dos elementos indesejados
# remover L.doD. no inicio dos fragmentos 
ldod_clean$text <- str_replace_all(ldod_clean$text, "^\t?+L.\\s?+do\\s?+D.", "")

# remover números no início de linhas (index) 
ldod_clean$text <- str_replace_all(ldod_clean$text, "\\n\\d", "\n")

# remover datas
ldod_clean$text <- str_replace_all(ldod_clean$text, "\\d{1,2}-(\\d{1,2}|[IVX]{1,4})-19\\d{2}", "")

1.4 Investigações com o Quanteda

Depois que os arquivos estão carregados no sistema, precisamos criar um objeto “corpus”, i.e., o formato necessário para que o Quanteda possa processar e gerar informações sobre o(s) texto(s). Para isso, basta aplicar a função corpus. Automaticamente, o texto é segmentado em tokens e frases. Tokens correspondem a todas as ocorrências (incluindo as repetições) de palavras, e outros itens como pontuação, números e símbolos. Quando investigamos o corpus com a função summary, temos a contagem das frases, tokens e dos types (o número de tokens distintos em um corpus).

# criar o corpus de vários arquivos
corpus_clean <- corpus(ldod_clean)
# ver um resumo do corpus
summary(corpus_clean)
# criar corpus do arquivo único
corpus_unico <- corpus(ldod_unico)
summary(corpus_unico)

Caso seja necessário, podemos alterar a estrutura do nosso corpus. No corpus_unico, temos um corpus feito com apenas um texto. Com corpus_reshape podemos criar um novo corpus em que cada frase seja considerada um texto, ou seja, uma unidade.

# revelar o número de textos no corpus
ndoc(corpus_unico)
## [1] 1
# remodelar o corpus, tornando cada sentença uma unidade
corpus_sents <- corpus_reshape(corpus_unico, to = "sentences")

# apresentar um resumo do corpus
summary(corpus_sents)
# número total de unidades na nova formatação do corpus
ndoc(corpus_sents)
## [1] 7542

Os exemplos acima nos mostram que um corpus é um conjunto de textos com informações sobre cada texto (metadados), do qual pode-se extrair facilmente a contagem de tokens, types e frases para cada texto. Porém, para realizar análises quantitativas no corpus, precisamos quebrar os textos em tokens (tokenização). É possível também filtrá-los, removendo elementos como pontuação, símbolos, números, urls e separadores.

# tokenizar nossos três corpora
toks_unico <- tokens(corpus_unico)
toks_sents <- tokens(corpus_sents)
toks_files <- tokens(corpus_clean)


## abaixo filtramos os três corpora de formas diversas,para demonstração
# remover pontuação (corpus limpo com regex)
toks_nopunct_files <- tokens(corpus_clean, remove_punct = TRUE)
toks_nopunct_unico <- tokens(corpus_unico, remove_punct = TRUE)

# remover números (corpus com apenas um arquivo)
toks_nonumbr <- tokens(corpus_unico, remove_numbers = TRUE)

# remover separadores (Unicode "Separator" [Z] and "Control" [C] categories)  (corpus feito por frases)
toks_nosept <- tokens(corpus_sents, remove_separators = TRUE)

# remover vários elementos ao mesmo tempo (corpus com apenas um arquivo)
toks_simples <- tokens(corpus_unico, remove_numbers = TRUE, remove_symbols = TRUE, remove_punct = TRUE)

É possível também remover tokens indesejados. Quanteda oferece uma lista de “stopwords” para diferentes línguas. Stopwords, ou palavras vazias em português, são palavras a serem removidas quando se processa textos para análises computacionais. Não existe uma lista padrão, mas geralmente as stopwords são as palavras mais frequentemente utilizadas em uma língua, como preposições e artigos. O bloco abaixo elimina as palavras incluídas na lista stopwords para português e inclui outras palavras que se repetem no corpus em questão.

# eliminar stopwords do corpus feito com um único arquivo
toks_nostop <- tokens_select(toks_unico, pattern = stopwords("pt"), selection = "remove")

# eliminar tokens específicios do corpus feito com vários arquivos e limpo com regex, após eliminação das pontuações
toks_selected_files <- tokens_select(toks_nopunct_files, pattern = c("é", "l.dod", "porqu", "ha", "ond", "tudo", "toda", "porque", "onde", "mim", "todo", "tão", "ter", "grand", "ell", "sobr", stopwords("pt")), selection = "remove")

# eliminar tokens específicios do corpus feito com um arquivo, após eliminação das pontuações
toks_selected_unico <- tokens_select(toks_nopunct_unico, pattern = c("é", "l.dod", "porqu", "ha", "ond", "tudo", "toda", "porque", "onde", "mim", "todo", "tão", "ter", "grand", "ell", "sobr", stopwords("pt")), selection = "remove")

Após a tokenização, o próximo passo é criar uma tabela com a frequência de cada token por cada texto, ou nos termos do quanteda, uma document-feature-matrix (dfm). A dfm é um pré-requisito para várias outras funções no quanteada, como é o caso da topfeatures, que retorna os tokens mais frequentes e um corpus.

Após a tokenização, o próximo passo é criar uma tabela com a frequência de cada token por cada texto, ou nos termos do Quanteda, um document-feature-matrix (dfm). A dfm é um pré-requisito para várias outras funções no quanteda, como é o caso da topfeatures, que retorna os tokens mais frequentes e um corpus.

# aqui podemos ver as 20 palavras mais frequentes quando removemos
# números, símbolos e pontuação
dfm_simples <- dfm(toks_simples)
print("com remoção de número, simbolos e pontuação")
## [1] "com remoção de número, simbolos e pontuação"
topfeatures(dfm_simples, 20)
##  que   de    a    o    e  não    é   do   da   um   se como  uma   os   em para 
## 6147 5118 4743 4181 4104 2547 1932 1846 1797 1692 1563 1328 1317 1284 1265 1142 
##  com   as   me  por 
## 1054 1020  982  786
dfm_nostop <- dfm(toks_nostop)
print("remoção de stopwords")
## [1] "remoção de stopwords"
topfeatures(dfm_nostop, 20)
##      ,      .      é   vida      ;   tudo      ?    ser porque    mim      / 
##  12988   8524   1932    763    650    570    539    534    501    487    441 
##   alma      (      )     ha   nada sempre    ter  nunca   onde 
##    412    363    362    345    334    331    315    314    301
dfm_selected_unico <- dfm(toks_selected_unico)
print("remoção de tokens selecionados no corpus previamente limpo com regex e sem stopwords")
## [1] "remoção de tokens selecionados no corpus previamente limpo com regex e sem stopwords"
topfeatures(dfm_selected_unico, 20)
##     vida      ser     alma     nada   sempre    nunca    todos   outros 
##      763      534      412      334      331      314      290      264 
##      sei    sonho    mundo    assim    sobre    ainda qualquer    outro 
##      261      250      233      220      215      210      200      198 
##    outra   grande      dia    coisa 
##      193      184      183      175
dfm_selected_files <- dfm(toks_selected_files)
print("remoção de tokens selecionados no corpus de arquivo único e sem stopwords")
## [1] "remoção de tokens selecionados no corpus de arquivo único e sem stopwords"
topfeatures(dfm_selected_files, 20)
##        n     vida   sempre    nunca        s    sonho     nada     alma 
##       93       51       30       19       18       17       17       16 
##    chuva    sensa      ser     hoje      dia    todos     dias        c 
##       15       15       15       14       14       14       14       11 
##    vezes qualquer   quanto   coisas 
##       11       11       11       11

Depois de gerar a lista de tokens, podemos então explorar o corpus. Uma das técnicas mais simples e utilizadas para investigação de corpus é através das linhas de concordâncias, ou concordance lines, ou keywords in context (kwic). As linhas de concordância mostram fragmentos do corpus onde há ocorrência do(s) termo(s) buscados. O número de palavras no contexto, pode ser estipulado pelo usuário, sendo 5 tokens a esquerda e 5 a direita o padrão. A primeira coluna indica o nome do arquivo onde a palavra buscada ocorre. Há várias opções para buscas. Elas podem ser feitas por palavras ou por fragmentos, sequências, combinações das mesmas.

# ocorrências de palavras que iniciam com “feli”.
kwic(toks_files, pattern =  "feli*")
# Podemos também procurar por mais de uma palavra ao mesmo tempo
kwic(toks_files, pattern = c("feli*", "alegr*"))
# e por sequência de mais de um token
kwic(toks_files, pattern = phrase("me fal*"))

1.4.1 N-gramas

Listas de frequência de palavras podem ser úteis para identificar elementos comuns a um texto. Porém, em muitos casos, é importante também saber em qual contexto estas palavras estão. Identificar quais palavras coocorrem frequentemente em um corpus podem nos revelar ainda mais informações sobre o texto. Por exemplo, saber que o par “estou triste” ocorre frequentemente no corpus nos diz mais sobre o corpus do que a frequência da palavra “triste” sozinha. A sequência “estou triste” é um exemplo de que chamamos de n-grams, ou neste caso específico, bigramas. N-gramas são sequências de duas ou mais palavras que ocorrem em um texto. Para gerar listas de n-grams, partimos de uma lista de tokens e delimitamos o número mínimo e máximo de tokens em cada n-grama.

# criar uma lista de 2-grama, 3-grama e 4-grama 
toks_ngram <- tokens_ngrams(toks_simples, n = 2:4)
# visualizar apenas os 30 mais frequentes
head(toks_ngram[[1]], 30)
##  [1] "Na_casa"               "casa_de"               "de_Saude"             
##  [4] "Saude_de"              "de_Cascaes"            "Cascaes_Inclue"       
##  [7] "Inclue_Introdução"     "Introdução_entrevista" "entrevista_com"       
## [10] "com_António"           "António_Mora"          "Mora_Alberto"         
## [13] "Alberto_Caeiro"        "Caeiro_Ricardo"        "Ricardo_Reis"         
## [16] "Reis_Prolegómenos"     "Prolegómenos_de"       "de_Antonio"           
## [19] "Antonio_Mora"          "Mora_Fragmentos"       "Fragmentos_Vida"      
## [22] "Vida_e"                "e_obras"               "obras_do"             
## [25] "do_engenheiro"         "engenheiro_Alvaro"     "Alvaro_de"            
## [28] "de_Campos"             "Campos_Livro"          "Livro_do"

1.4.2 Dicionário

Uma outra forma de extrair informações de um texto é com a criação de “dicionários”. A função dictionary no Quanteda nos permite agrupar tokens por categorias. Esta categorização pode então ser utilizada para buscas no corpus. Por exemplo, podemos criar as categorias “alegria” e “tristeza” contendo palavras relacionadas a esses sentimentos respetivamente. Com o dicionário criado, podemos identificar a distribuição desses termos em um corpus.

# criação de dicionário
dict <- dictionary(list(alegria = c("alegr*", "allegr*", "feli*", "content*"),
                        tristeza = c("trist*", "infeli*")))

dict_toks <- tokens_lookup(toks_unico, dictionary = dict)
print(dict_toks)
## Tokens consisting of 1 document.
## pessoa_ldod_completo.txt :
##  [1] "tristeza" "tristeza" "tristeza" "tristeza" "tristeza" "alegria" 
##  [7] "alegria"  "tristeza" "alegria"  "alegria"  "alegria"  "alegria" 
## [ ... and 389 more ]
dfm(dict_toks)
## Document-feature matrix of: 1 document, 2 features (0.00% sparse) and 0 docvars.
##                           features
## docs                       alegria tristeza
##   pessoa_ldod_completo.txt     240      161

2 Visualização e análise dos dados

2.1 Nuvem de palavras e gráfico de frequência

Em 1.4 criamos uma dfm com a frequência dos tokens. Para absorver estas frequências de uma forma mais rápida, podemos gerar visualizações com estas frequências. A nuvem de palavras é um gráfico que permite a rápida visualização dos termos mais frequentes.

# demonstração de como as frequências de palavras alteram de acordo com a preparação do corpus
set.seed(100) #para reprodução dos resultados
textplot_wordcloud(dfm_selected_unico, min_count = 6, random_order = FALSE, rotation = .25, color = RColorBrewer::brewer.pal(8, "Dark2"))

set.seed(100) 
textplot_wordcloud(dfm_selected_files, min_count = 6, random_order = FALSE, rotation = .25, color = RColorBrewer::brewer.pal(8, "Dark2"))

set.seed(100)
textplot_wordcloud(dfm_nostop, min_count = 6, random_order = FALSE, rotation = .25, color = RColorBrewer::brewer.pal(8, "Dark2"))

Outra solução é utilizar a biblioteca ggplot e representar em um gráfico com o número de ocorrências das palavras mais frequentes.

dfm_selected_unico %>% 
  textstat_frequency(n = 20) %>% 
  ggplot(aes(x = reorder(feature, frequency), y = frequency)) +
  geom_point() +
  coord_flip() +
  labs(x = NULL, y = "Frequência") +
  theme_minimal()

2.2 Topic modeling (LDA)

Uma outra função frequentemente utilizada na PLN é a modelagem de tópicos, ou topic modeling (TM). A modelagem de tópicos aplica um modelo estatístico que procura “entender” a estrutura do corpus e identificar e agrupar palavras que de alguma forma se relacionam entre si. O TM utiliza uma técnica semi ou não supervisionada para identificação desses tópicos. Ou seja, o programa aprende a reconhecer padrões nos dados sem haver a necessidade de anotá-los previamente. O códigos abaixo demonstra a aplicação do modelo Latent Dirichlet Allocation (LDA) após.

tm_nostop <- textmodel_lda(dfm_selected_files, k = 8)
terms(tm_nostop, 10)
##       topic1     topic2  topic3     topic4     topic5   topic6   topic7  
##  [1,] "n"        "nada"  "chuva"    "sempre"   "n"      "sonho"  "vida"  
##  [2,] "nunca"    "d"     "dias"     "vida"     "homem"  "cada"   "s"     
##  [3,] "c"        "vezes" "qualquer" "ser"      "cousas" "arte"   "sensa" 
##  [4,] "assim"    "luar"  "chove"    "coisas"   "j"      "sonhos" "hoje"  
##  [5,] "grandes"  "mundo" "coisa"    "tedio"    "quanto" "cidade" "n"     
##  [6,] "alma"     "rua"   "n"        "horas"    "tempo"  "vez"    "fazer" 
##  [7,] "ainda"    "desde" "voz"      "silencio" "outros" "vivo"   "sentir"
##  [8,] "noite"    "olhos" "vejo"     "duas"     "todos"  "alto"   "sempre"
##  [9,] "floresta" "m"     "outra"    "alto"     "parte"  "viver"  "f"     
## [10,] "viagem"   "l"     "calor"    "cousa"    "teem"   "novo"   "todos" 
##       topic8     
##  [1,] "vida"     
##  [2,] "dia"      
##  [3,] "sobre"    
##  [4,] "antes"    
##  [5,] "outro"    
##  [6,] "homens"   
##  [7,] "cansa"    
##  [8,] "esthetica"
##  [9,] "lisboa"   
## [10,] "inteira"

2.3 Semantic Network

O Feature co-occurrence matrix (FCM) é similar ao dfm, mas considerando as coocorrências, e apresenta um gráfico com as redes semânticas.

#criar fcm a partir de dfm
fcm_nostop <- fcm(dfm_selected_files)
# listar as top features
feat <- names(topfeatures(fcm_nostop, 50)) 
#selecionar
fcm_select <- fcm_select(fcm_nostop, pattern = feat, selection = "keep") 

size <- log(colSums(dfm_select(dfm_nostop, feat, selection = "keep"))) 

textplot_network(fcm_select, min_freq = 0.8, vertex_size = size / max(size) * 3)

Dados e repositório

Os dados e códigos estão disponíveis via github https://github.com/andressarg/analise_lit_quanteda/

O código pode ser visualizado em https://rpubs.com/gomide/quanteda_LdoD


Agradecimentos

Alguns dos códigos aqui descritos utilizaram os códigos gentilmente cedidos por Mark Alfano, utilizados em seu trabalho “Nietzsche corpus analysis”.