Anunciando o lançamento de literaturaBR

Paixão por Dados de cara nova!

O blog está de cara nova! O endereço antigo do blog começou a apresentar alguns bugs bem chatos, então tomei a decisão de finalmente migrar para uma nova plataforma, utilizando o pacote blogdown, a mesma que o pessoal do Curso-R usa no site deles. Para comemorar essa migração, anuncio o lançamento do meu terceiro pacote R: o literaturaBR.

literaturaBR, o mais novo pacote da comunidade R Brasil

Após lançar o pacote lexiconPT, senti que a carência de datasets textuais na língua portuguesa poderia restringir seu potencial de alcance de desenvolvedores e cientistas de dados interessados em usar os léxicos para fazer análise de sentimento. Apesar de ter feito um post mostrando seu uso em dados obtidos do Facebook, eu admito que é complicado ter que fazer web scraping de algum site toda vez que se deseja praticar ou ensinar mineração de texto com textos em Português.

Esse problema me inspirou a desenvolver mais um pacote R para facilitar pequenas demonstrações de Text Mining. Assim como a Julia Silge criou o janeaustenr com livros clássicos da Jane Austen, eu criei o literaturaBR, um data package criado para disponibilizar livros clássicos da literatura brasileira já prontos para serem importados e manuseados no R. Para saber quais livros estão disponíveis na versão atual do pacote e outras informações úteis, visite seu repositório.

Este post se destina a apresentar algumas exemplos simples de tarefas de Text Mining, como:
* Análise de Sentimento;
* Complexidade Léxica;
* Analise de ocorrência de palavras em específico.

Introdução

Os pacotes usados neste post são:

library(literaturaBR) # meio obvio
library(tidytext) # excelente pacote de text mining
library(tidyverse) # <3
library(stringr) # indispensavel para manipulacao de texto
library(quanteda) # otimas funcoes para analise quantitativa de texto
library(qdap) # similar ao quanteda, embo ra eu nao me lembre exatamente se eu o uso neste post
library(forcats) # manipulacao de fatores
library(ggthemes) # temas para o ggplot2
library(lexiconPT)

Vamos então importar os datasets presentes no literaturaBR na data de hoje e os transformar em um dataset só:

data("memorias_de_um_sargento_de_milicias")
data("memorias_postumas_bras_cubas")
data("alienista")
data("escrava_isaura")
data("ateneu")

df <- bind_rows(memorias_de_um_sargento_de_milicias,
                memorias_postumas_bras_cubas,
                alienista,
                escrava_isaura,
                ateneu)


# Olhando a estrutura do dataframe
glimpse(df)
## Observations: 5,149
## Variables: 5
## $ book_name        <chr> "Memórias de um Sargento de Milícias", "Memór...
## $ chapter_name     <chr> "Capítulo 1 - Origem, nascimento e batismo", ...
## $ url              <chr> "https://pt.wikisource.org/wiki/Mem%C3%B3rias...
## $ paragraph_number <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, ...
## $ text             <chr> "Era no tempo do rei.", "Uma das quatro esqui...

Todos os datasets fornecidos pelo literaturaBR possuem a mesma estrutura, onde cada linha corresponde a um parágrafo de um livro e contêm 5 variáveis:
* book_name: Nome original do livro;
* chapter_name: Nome original do capítulo do livro do parágrafo;
* url: Link para artigo do Wikisouce de onde o capítulo do parágrafo foi extraído;
* paragraph_number: Ordem do parágrafo em seu capítulo;
* text: Texto do parágrafo. Contem acentos e pontuação.

Um rápido adendo: os nomes das colunas está em inglês porque só tomei a decisão de escrever toda a documentação do pacote em Português meio tardiamente.

Análise básica

Para usar (parte) das funções do pacote quanteda, precisamos converter o dataframe dos livros em um objeto do tipo corpus.

df_corpus <- df %>% 
  # agrupar por livro
  group_by(book_name) %>% 
  # formatar o dataframe para que so tenha uma linha por livro
  summarise(text = paste0(text, sep = "", collapse = ". "))

dim(df_corpus)
## [1] 5 2
meu_corpus <- quanteda::corpus(df_corpus$text, docnames = df_corpus$book_name)
summary(meu_corpus)
## Corpus consisting of 5 documents:
## 
##                                 Text Types Tokens Sentences
##                     A escrava Isaura  9349  64929      1751
##  Memórias de um Sargento de Milícias  8703  71391      2287
##      Memórias Póstumas de Brás Cubas 11058  74223      3199
##                          O Alienista  4387  19977       770
##                             O Ateneu 15341  73063      3551
## 
## Source:  /home/sillas/R/Projetos/paixaopordados-blogdown/content/post/* on x86_64 by sillas
## Created: Wed Nov 22 20:34:35 2017
## Notes:

Como vemos, a função quanteda::corpus() identificou a quantidade de Types (número de palavras distintas em um corpus), Tokens (número total de palavras em um corpus) e Sentences (frases) em cada livro.

Vamos então criar uma document-feature matrix a partir desse corpus criado, tomando o cuidade de remover pontuações e stopwords:

corpus_dfm <- dfm(meu_corpus, remove_punct = TRUE,
                  remove = quanteda::stopwords("portuguese"),
                  groups = df_corpus$book_name)

# Analisando as 15 palavras mais comuns no geral por livro
dfm_sort(corpus_dfm)[, 1:15]
## Document-feature matrix of: 5 documents, 15 features (5.33% sparse).
## 5 x 15 sparse Matrix of class "dfm"
##                                      features
## docs                                    é casa tempo ainda tudo bem todos
##   A escrava Isaura                    424   98    81   121   78 175    78
##   Memórias de um Sargento de Milícias 305  193   207   131  192 126   139
##   Memórias Póstumas de Brás Cubas     492  107   108    89   98  65    69
##   O Alienista                          88  108    20    29   26  10    37
##   O Ateneu                            199   51    56    97   56  56    91
##                                      features
## docs                                  tão ser havia   d leonardo coisa
##   A escrava Isaura                    137  96    56  20        0    35
##   Memórias de um Sargento de Milícias  63  75   153 194      366   128
##   Memórias Póstumas de Brás Cubas      95  97    53  83        0   140
##   O Alienista                          31  25     7  44        0    23
##   O Ateneu                             47  78   101  26        0    38
##                                      features
## docs                                  disse dia
##   A escrava Isaura                       67  41
##   Memórias de um Sargento de Milícias   119 128
##   Memórias Póstumas de Brás Cubas       130  89
##   O Alienista                            30  21
##   O Ateneu                               11  77

A função dfm_sort() retorna a ocorrência de cada palavra (também chamado de token ou feature) em cada livro. Para pesquisar a ocorrência de alguma palavra específica nos documentos, use a função dfm_select():

# ocorrencias da palavra amor
dfm_select(corpus_dfm, "amor")
## Document-feature matrix of: 5 documents, 1 feature (0% sparse).
## 5 x 1 sparse Matrix of class "dfm"
##                                      features
## docs                                  amor
##   A escrava Isaura                      71
##   Memórias de um Sargento de Milícias   30
##   Memórias Póstumas de Brás Cubas       53
##   O Alienista                            3
##   O Ateneu                              45

Curioso para saber o contexto em que essa palavra aparece? Você pode usar a função kwic() para isso:

# usar a função head() para o output nao ficar mt grande
kwic(meu_corpus, "amor") %>% head()
##                                                              
##  [A escrava Isaura, 2011]    , bem pode conquistar o | amor |
##  [A escrava Isaura, 5090]    não se havia casado por | amor |
##  [A escrava Isaura, 5173]     o mais cego e violento | amor |
##  [A escrava Isaura, 6555] pudesse obter também o teu | amor |
##  [A escrava Isaura, 7215]          Ora, senhor, pelo | amor |
##  [A escrava Isaura, 7686]  disputar com o senhor por | amor |
##                         
##  de algum guapo mocetão,
##  , sentimento esse a que
##  , que de dia em        
##  !... És                
##  de Deus!..             
##  de uma escrava..

Para saber as palavras mais usadas, dentre os vários métodos possíveis para isso, pode-se usar a função topfeatures()

topfeatures(corpus_dfm, groups = df_corpus$book_name)
## $`A escrava Isaura`
##       é  isaura  senhor leôncio  álvaro escrava     bem     tão     pai 
##     424     344     243     205     188     180     175     137     127 
##   ainda 
##     121 
## 
## $`Memórias de um Sargento de Milícias`
## leonardo        é    maria  comadre    tempo    porém        d     casa 
##      366      305      231      209      207      205      194      193 
##     tudo    major 
##      192      179 
## 
## $`Memórias Póstumas de Brás Cubas`
##        é virgília    coisa    olhos    disse     nada    outro    outra 
##      492      199      140      138      130      125      122      116 
##     vida   porque 
##      116      113 
## 
## $`O Alienista`
##      casa alienista         é     verde bacamarte  barbeiro    câmara 
##       108       106        88        77        59        54        52 
##   itaguaí     simão  evarista 
##        49        49        45 
## 
## $`O Ateneu`
##         é     sobre aristarco   diretor     havia     ainda    ateneu 
##       199       167       148       104       101        97        93 
##     todos       ser      dois 
##        91        78        77

Análise comparativa entre os livros

Algo interessante a se fazer é quantificar a similaridade e a dissimilaridade ou distância entre os livros. As funções textstat_simil e textstat_dist implementam diversas técnicas e algoritmos para isso. Sugiro ler a documentação completa das funções e ler as referências indicadas para conhecer melhor os métodos de cálculo.

Vamos então calcular a similaridade entre os livros presentes no pacote:

# normalizar os livros pelo seu tamanho
corpus_dfm_norm <- dfm_weight(corpus_dfm, "relfreq")
corpus_simil <- textstat_simil(corpus_dfm_norm, method = "correlation",
                               margin = "documents", upper = TRUE,
                               diag = FALSE)
# ver os resultados individualmente para cada livro
round(corpus_simil, 3)
##                                     A escrava Isaura
## A escrava Isaura                                    
## Memórias de um Sargento de Milícias            0.524
## Memórias Póstumas de Brás Cubas                0.625
## O Alienista                                    0.431
## O Ateneu                                       0.512
##                                     Memórias de um Sargento de Milícias
## A escrava Isaura                                                  0.524
## Memórias de um Sargento de Milícias                                    
## Memórias Póstumas de Brás Cubas                                   0.642
## O Alienista                                                       0.502
## O Ateneu                                                          0.550
##                                     Memórias Póstumas de Brás Cubas
## A escrava Isaura                                              0.625
## Memórias de um Sargento de Milícias                           0.642
## Memórias Póstumas de Brás Cubas                                    
## O Alienista                                                   0.585
## O Ateneu                                                      0.631
##                                     O Alienista O Ateneu
## A escrava Isaura                          0.431    0.512
## Memórias de um Sargento de Milícias       0.502    0.550
## Memórias Póstumas de Brás Cubas           0.585    0.631
## O Alienista                                        0.450
## O Ateneu                                  0.450

Alguns resultados são bem interessantes. O Alienista é mais diferente de todos, tendo uma correlação superior a 0,51 apenas com o livro Memórias Póstumas de Brás Cubas, coincidentemente ou não ambos do mesmo autor. A maior correlação pertence aos livros Memórias Póstumas de Brás Cubas e Memórias de um Sargento de Milícias.

Passemos então para a análise da dissimilaridade ou distância entre os livros, com o auxílio de um dendograma:

corpus_dist <- textstat_dist(corpus_dfm_norm, method = "euclidean",
                               margin = "documents", upper = TRUE,
                               diag = FALSE)
# ver os resultados individualmente para cada livro
plot(hclust(corpus_dist))

Análise de Sentimento

Como já publiquei no blog um post sobre Análise de Sentimento, não vou me alongar em repetir conceitos sobre o tema. Segue o código comentado:

# Criar um dataframe em que cada linha corresponda a uma unica palavra
df.token <- df %>%
  unnest_tokens(term, text)

glimpse(df.token)
## Observations: 252,299
## Variables: 5
## $ book_name        <chr> "Memórias de um Sargento de Milícias", "Memór...
## $ chapter_name     <chr> "Capítulo 1 - Origem, nascimento e batismo", ...
## $ url              <chr> "https://pt.wikisource.org/wiki/Mem%C3%B3rias...
## $ paragraph_number <int> 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, ...
## $ term             <chr> "era", "no", "tempo", "do", "rei", "uma", "da...
# importar lexico de sentimentos
data("oplexicon_v3.0")
df.token <- df.token %>%
  inner_join(oplexicon_v3.0, by = "term")

Um pergunta muito interessante a se fazer é se o sentimento varia ao longo dos capítulos dos livros. Os livros ficam mais (ou menos) positivos a medida em que se aproximam do final?

Para isso, primeiro precisamos normalizar os livros, visto que eles apresentam tamanhos e quantidades de capítulos diferentes.

# extrair capitulos de cada livro
df_chapter_number <- df.token %>%
  distinct(book_name, chapter_name) %>%
  group_by(book_name) %>%
  # normalizar capitulo de acordo com sua posicao no livro
  mutate(chapter_number_norm = row_number()/max(row_number()))

glimpse(df_chapter_number)
## Observations: 252
## Variables: 3
## $ book_name           <chr> "Memórias de um Sargento de Milícias", "Me...
## $ chapter_name        <chr> "Capítulo 1 - Origem, nascimento e batismo...
## $ chapter_number_norm <dbl> 0.02083333, 0.04166667, 0.06250000, 0.0833...

Agora calculamos o sentimento de cada capítulo, que corresponde à soma da polaridade de suas palavras:

df.sentiment <- df.token %>%
  # calcular sentimento por capitulo
  group_by(book_name, chapter_name) %>%
  summarise(polarity = sum(polarity, na.rm = TRUE)) %>%
  ungroup() %>%
  # retornar posicao relativa (ou normalizada) do capitulo de cada livro
  left_join(df_chapter_number) %>%
  arrange(book_name, chapter_number_norm)

# grafico
df.sentiment %>%
  ggplot(aes(x = chapter_number_norm, y = polarity)) +
    geom_line() +
    facet_wrap(~ book_name, ncol = 5, labeller = label_wrap_gen(20)) +
    labs(x = "Posição relativa no livro", y = "Sentimento") +
    theme_bw()

Os resultados são muito interessantes: De acordo com esse método, A Escrava Isaura e O Ateneu são verdadeiras montanhas-russas de emoções, enquanto que livros os “dois Memórias” são mais estáveis. O Alienista apresenta um sentimento que tem uma tendência decrescente até a metade e crescente após ela.

Complexidade léxica e ocorrência de palavras

A função textstat_lexdiv traz várias métricas de complexidade e diversidade léxicas. Novamente, recomendo a leitura de sua documentação para conhecer as métricas disponíveis.

# aplicando a funcao no objeto sem stopwords e pontuação
lexdiv <- textstat_lexdiv(corpus_dfm, measure = "TTR")
lexdiv
##                    A escrava Isaura Memórias de um Sargento de Milícias 
##                           0.3062233                           0.2517707 
##     Memórias Póstumas de Brás Cubas                         O Alienista 
##                           0.3139269                           0.4291334 
##                            O Ateneu 
##                           0.4134223
#grafico
lexdiv %>% 
  as.data.frame() %>% 
  magrittr::set_colnames("TTR") %>% 
  tibble::rownames_to_column("livro") %>% 
  mutate(livro = forcats::fct_reorder(livro, TTR)) %>% 
  ggplot(aes(x = livro, y = TTR)) + 
    geom_col(fill = "cadetblue4") +
    coord_flip() + 
    labs(x = NULL, y = "TTR") +
    theme_minimal()

O livro que apresenta a maior diversidade léxica, de acordo com a métrica Type-Token Ratio (TTR), é O Alienista. Memórias de um Sargento de Milícias vem bem atrás dos demais.

De todas os gráficos que apresentei neste post, o mais legal vem a seguir. É possível plotar a ocorrência de uma determinada palavra ao longo dos livros analisados. Por exemplo, a ocorrência da palavra amor é uniformemente distribuída nos livros?

kwic(meu_corpus, "amor") %>% textplot_xray(scale = "relative")

Com o gráfico acima, é possível ver que a palavra amor aparece, no livro Ateneu, muito mais frequentemente na primeira metade do que na segunda. Nos demais livros, a ocorrência da palavra é mais uniforme.

Outro exemplo interessante surge com a busca da palavra fogo. Quem já leu O Ateneu já consegue imaginar como serão os resultados:

kwic(meu_corpus, "fogo") %>% textplot_xray(scale = "relative")

Conclusão

Eu realmente torço para que o literaturaBR e o lexiconPT atinjam seus potenciais e sejam dois grandes marcos em pesquisas em Text Mining com textos em português. Toda sugestão ou pedido de melhorias serão muitíssimos bem-vindos.

 
comments powered by Disqus