Análise de Texto: Dados Tidy e Métodos de Dicionário

Introdução

Na aula de hoje, vamos dar nossos primeiros passos no uso de R para análise de texto. Vamos dividir a aula nos seguintes tópicos.

  • Abrindo Textos em R.

  • Texto como um banco “Tidy”.

  • Análise de Sentimentos + Modelos de Dicionários.

Recomendo fortemente aos alunos a leitura do livro Text Mining with R: A Tidy Approach. Este tutorial de hoje é fortemente inspirado neste belíssimo livro de Julia Silge e David Robinson.

Todos os dados utilizados neste tutorial podem ser baixados aqui

Revisão: Manipulação de Strings em R

Antes de iniciarmos este workshop, seria importante vocês fazerem uma revisão das nossas aulas sobre manipulação de strings em R usando o pacote stringr.

Abrindo Textos em R.

Por ser uma linguagem de programação, o R é flexível sobre quais tipos de dados podem ser importados em seu ambiente de trabalho. Ao trabalhar com textos digitais, vocês vão em geral acessá-los diretamente da internet e salvar como um objeto de R. No entanto, há algumas outras opções que cobriremos aqui.

Acessando arquivos digitais diretamente em R.

Para nosso exemplo diretamente em R, vamos acessar dados da API do Twitter utilizando o pacote rtweet.

library(rtweet)
library(tidyverse)
## ── Attaching packages ────────────────────────────────── tidyverse 1.3.0.9000 ──
## ✓ ggplot2 3.3.3     ✓ purrr   0.3.4
## ✓ tibble  3.1.0     ✓ dplyr   1.0.5
## ✓ tidyr   1.1.2     ✓ stringr 1.4.0
## ✓ readr   1.4.0     ✓ forcats 0.5.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter()  masks stats::filter()
## x purrr::flatten() masks rtweet::flatten()
## x dplyr::lag()     masks stats::lag()
bolsonaro_tweets<-search_tweets("bolsonaro", n=50, include_rts = TRUE)

# Selecionar somente o texto
bolsonaro_tweets <- bolsonaro_tweets %>%
                        select(user_id, screen_name, text)
                            
# Veja os dados
bolsonaro_tweets
## # A tibble: 50 x 3
##    user_id         screen_name   text                                           
##    <chr>           <chr>         <chr>                                          
##  1 11489501043987… mblribeiraosp "Bolsonaro cair antes de 2022 será um alívio p…
##  2 11462231216341… ThomaziMaxsu… "Acho que está muito há quem a posição de um v…
##  3 540282529       J_Gondim      "Carlos Bolsonaro interfere em licitação de ap…
##  4 57211944        sandsilva     "Por isso o comportamento envergonhado na CPI …
##  5 2613383027      OAlkymista    "Quem poderia imaginar que o Ministro Pazuello…
##  6 11834877954254… Sulamitasoar… "Ricardo Salles, Ministro do Meio Ambiente, al…
##  7 124272417       elaneide      "Além de dados telefônicos, fiscal e bancáros …
##  8 378974422       joacyotabol   "Além de dados telefônicos, fiscal e bancáros …
##  9 12964294772250… edilsondiniz… "https://t.co/6X84Uo8DiS"                      
## 10 12080721683844… Rosa80295620  "Vejo o Renan Calheiros rindo todos os dias da…
## # … with 40 more rows
# Salvar como objeto de R
save(bolsonaro_tweets,file="bolsonaro_tweets.Rdata")

Este processo serve para qualquer tipo de dados de texto acessado via API.

Acessando Dados salvos como txt.

Em uma pasta “data_txt”, eu salvei dez discursos de deputados no plenário da câmara no formato .txt. Vamos aprender a importá-los no ambiente do R.

# Ver arquivos
list.files("data_txt")
##  [1] "discurso1.txt"  "discurso10.txt" "discurso2.txt"  "discurso3.txt" 
##  [5] "discurso4.txt"  "discurso5.txt"  "discurso6.txt"  "discurso7.txt" 
##  [9] "discurso8.txt"  "discurso9.txt"
# Salvar nomes
nomes <- list.files("data_txt")

# Criar endereço completo
path <- paste0("data_txt/", nomes)

# Abrir
dados <- map_chr(path, read_lines) 

dados <- tibble(file=nomes, texto=dados)
dados
## # A tibble: 10 x 2
##    file          texto                                                          
##    <chr>         <chr>                                                          
##  1 discurso1.txt "\"1\" \"O SR. GONZAGA PATRIOTA  (PSB-PE. Sem revisão do orado…
##  2 discurso10.t… "\"1\" \"O SR. AUGUSTO CARVALHO  (Bloco/PPS-DF. Sem revisão do…
##  3 discurso2.txt "\"1\" \"O SR. DR. UBIALI  (PSB-SP. Sem revisão do orador.) - …
##  4 discurso3.txt "\"1\" \"O SR. DOMINGOS DUTRA  (PT-MA. Sem revisão do orador.)…
##  5 discurso4.txt "\"1\" \"O SR. GONZAGA PATRIOTA  (PSB-PE. Pela ordem. Sem revi…
##  6 discurso5.txt "\"1\" \"O SR. LEONARDO GADELHA  (PSC-PB. Sem revisão do orado…
##  7 discurso6.txt "\"1\" \"O SR. DR. UBIALI  (PSB-SP. Pela ordem. Sem revisão do…
##  8 discurso7.txt "\"1\" \"O SR. DOMINGOS DUTRA  (PT-MA. Pela ordem. Sem revisão…
##  9 discurso8.txt "\"1\" \"O SR.  LINS  (PSD-AM. Sem revisão do orador.) - Sr. P…
## 10 discurso9.txt "\"1\" \"O SR. IZALCI  (PSDB-DF. Sem revisão do orador.) - Sr.…

Acessando via csv.

Algumas vezes, vocês encontrarão arquivos de texto salvos como .csv Abri-los é tão simples como acessar qualquer outro tipo de arquivo csv. Abriremos um exemplo com discursos do plenário de deputados no Brasil e com este banco de dados vamos seguir no restante das aulas.

discursos <- read_csv("speeches.csv")
## 
## ── Column specification ────────────────────────────────────────────────────────
## cols(
##   nome = col_character(),
##   partido = col_character(),
##   uf = col_character(),
##   speech = col_character()
## )

Texto como um banco “Tidy”.

Aprendemos em aulas anteriores sobre o conceito de banco de dados tidy. As três propriedades mais importantes que definem um banco de dados tidy são:

  • Cada coluna é uma variável.

  • Cada linha é uma observação.

  • Cada valor em uma linha.

Como já discutimos diversas vezes, o tidyverse é uma linguagem própria dentro do R. Por isso, há extensões do uso de dados tidy e de pacotes do tidyverse para a mais diversas áreas da Ciência Social Computacional, includindo modelos de análise de texto.

IMPORTANTE: Um banco de dados de texto Tidy é organizado com um token por linha.

Um token é uma unidade significativa de texto, como uma palavra, que estamos interessados em usar para análise, e tokenização é o processo de dividir o texto em tokens.

Para bancos de texto tidy, um token é geralmente uma palavra, porém você pusar um n-gram, uma sentença ou até um parágrafo.

Para converter nosso banco de dados de texto para um formato tidy, vamos utilizar a função unnest_tokens do pacote tidytext

Tidytext: unnest_tokens

library(tidytext)
# Convertendo para o formato tidy. 
tidy_discursos <- discursos %>%
                  mutate(id_discursos=1:nrow(.)) %>%
                   unnest_tokens(words, speech) #(output, input)

Os dois argumentos básicos para unnest_tokens usados aqui são nomes de colunas. Primeiro temos o nome da coluna de saída (o nome da nova coluna) que será criado, e, em seguida, a coluna de entrada de onde vem o texto (speech, neste caso).

Note: pontuações são removidas e os textos são convertidos para letra minúscula.

Outras formas de textos tidy.

Sentenças

discursos %>%
 unnest_tokens(words, speech, token="sentences") #(output, input)
## # A tibble: 312,821 x 4
##    nome          partido uf    words                                            
##    <chr>         <chr>   <chr> <chr>                                            
##  1 ABEL MESQUIT… PDT     RR    o sr.                                            
##  2 ABEL MESQUIT… PDT     RR    abel mesquita jr.                                
##  3 ABEL MESQUIT… PDT     RR    (pdt-rr.                                         
##  4 ABEL MESQUIT… PDT     RR    sem revisão do orador.) - sr.                    
##  5 ABEL MESQUIT… PDT     RR    presidente, eu queria dar como lido este meu pro…
##  6 ABEL MESQUIT… PDT     RR    peço a v.exa. que receba como lido o meu pronunc…
##  7 ABEL MESQUIT… PDT     RR    muito obrigado.                                  
##  8 ABEL MESQUIT… PDT     RR    pronunciamento encaminhado pelo orador  sr.      
##  9 ABEL MESQUIT… PDT     RR    presidente, sras. e srs.                         
## 10 ABEL MESQUIT… PDT     RR    deputados, esta semana nós demos um importante p…
## # … with 312,811 more rows

n-gram

discursos %>%
 unnest_tokens(words, speech, token="ngrams", n=2) #(output, input)
## # A tibble: 5,859,418 x 4
##    nome              partido uf    words        
##    <chr>             <chr>   <chr> <chr>        
##  1 ABEL MESQUITA JR. PDT     RR    o sr         
##  2 ABEL MESQUITA JR. PDT     RR    sr abel      
##  3 ABEL MESQUITA JR. PDT     RR    abel mesquita
##  4 ABEL MESQUITA JR. PDT     RR    mesquita jr  
##  5 ABEL MESQUITA JR. PDT     RR    jr pdt       
##  6 ABEL MESQUITA JR. PDT     RR    pdt rr       
##  7 ABEL MESQUITA JR. PDT     RR    rr sem       
##  8 ABEL MESQUITA JR. PDT     RR    sem revisão  
##  9 ABEL MESQUITA JR. PDT     RR    revisão do   
## 10 ABEL MESQUITA JR. PDT     RR    do orador    
## # … with 5,859,408 more rows

Operações Básicas com Textos Tidy

A principal vantagem de ter nossos textos em formato tidy consiste na facilidade de limpar e fazer análises básicas dos textos. Como casa linah de nosso banco de dados se refere a um token, é possível fazer operações usando as palavras como unidade de análise. Por exemplo, para eliminar “stop words”, agregar informação de dicionários, agregar sentimentos das palavra, basta conectar (“join”) diferentes bancos de dados também no formato tidy.

Estatísticas básicas

Vamos calcular algumas estatisticas básicas com base no nossos conhecimento prévios no tidyverse

tidy_discursos <- tidy_discursos %>%
                  group_by(id_discursos) %>%
                  mutate(total_palavras=n()) %>%
                  ungroup() 

# Informações sobre os discursos

partido_st <- discursos %>%
                   group_by(partido) %>%
                   summarise(n_partidos=n()) 

nome_st <- discursos %>%
                   group_by(nome) %>%
                   summarise(n_dep=n())

uf_st <- discursos %>%
          group_by(uf) %>%
            summarise(n_uf=n())

tidy_discursos <- left_join(tidy_discursos, partido_st) %>%
                  left_join(nome_st) %>%
                  left_join(uf_st)
## Joining, by = "partido"
## Joining, by = "nome"
## Joining, by = "uf"

Removendo stop words

“Stop Words” são palavras que comumente retiramos em nossas análises de texto. A idéia fundamental é de que estas palavras (artigos, preposições, pontuações) carregam pouco sentido substantivo.

library(stopwords)
stop_words <- tibble(words=stopwords("portuguese"))

# Elimina com um Join
tidy_discursos <- tidy_discursos %>%
                    anti_join(stop_words)
## Joining, by = "words"

O que mais podemos eliminar? Nome dos Estados.

estados <- tibble(words=unique(str_to_lower(tidy_discursos$uf)))

# Elimina com um Join
tidy_discursos <- tidy_discursos %>%
                    anti_join(estados)
## Joining, by = "words"

Palavras funcionais, que são marcantes do ambiente em debate, porém carregam pouco sentido.

function_names <- tibble(words=c("candidato", "candidata", "brasileira", "brasileiro", 
                                 "câmara", "municipio",
                    "municipal", "eleições", "cidade", "partido",
                    "cidadão", "deputado", "deputada", "caro", "cara", 
                    "plano", "suplementar", 
                    "voto","votar", "eleitor", "querido", 
                    "sim", "não", "dia", "hoje", "amanhã", "amigo", "amiga", 
                    "seção", "emenda", "i", "ii", "iii", "iv", 
                    "colegas", "clausula", "prefeit*", "presidente",
                    "prefeitura", 'proposta','propostas','meta',
                    'metas','plano','governo','municipal','candidato',
                    'diretrizes','programa', "deputados", "federal",
                    'eleição','coligação','município', "senhor", "sr", "dr", 
                    "excelentissimo", "nobre", "deputad*", "srs", "sras", "v.exa",
                    "san", "arial", "sentido", "fim", "minuto", "razão", "v.exa", 
                    "país", "brasil", "tribuna", "congresso", "san", "symbol", "sans", "serif",
                    "ordem", "revisão", "orador", "obrigado", "parte", "líder", "bloco", "esc", 
                    "sra", "oradora", "bloco", "times", "new", "colgano", "pronuncia", "colega", 
                    "presidenta", "pronunciamento", "mesa", "parlamentares", "secretário", "seguinte", 
                    "discurso","mato", "sul", "norte", "nordeste", "sudeste", "centro-oeste", "sul", "grosso",
                    "é", "ser", "casa", "todos", "sobre", "aqui", "nacional"))


tidy_discursos <- tidy_discursos %>%
                    anti_join(function_names)
## Joining, by = "words"

stringr para limpeza

Uma das vantagens de manter seus dados em formato Tidy é a possibilidade de usar as funções do stringr para manipulação de caracteres. Vamos ver alguns exemplos para limpar os dados um pouco mais.

str_remove_all

tidy_discursos <- tidy_discursos %>%
                  mutate(words=str_remove_all(words, "[[:digit:]]"), 
                         words=str_remove_all(words, "[:punct:]")) 

Remover Acentos, espacos e outros

tidy_discursos <- tidy_discursos %>%
                  mutate(words=str_trim(words), 
                         words=str_squish(words), 
                         words=stringi::stri_trans_general(words, "Latin-ASCII"))%>%
                  filter(words!="")

Palavras mais comuns

tidy_discursos %>%
  count(words, sort = TRUE) 
## # A tibble: 76,846 x 2
##    words        n
##    <chr>    <int>
##  1 estado   14790
##  2 anos     10169
##  3 grande    9753
##  4 porque    8823
##  5 ainda     7721
##  6 quero     6958
##  7 povo      6877
##  8 fazer     6772
##  9 projeto   6600
## 10 politica  6537
## # … with 76,836 more rows
# Gráfico
tidy_discursos %>%
count(words, sort = TRUE) %>%
  slice(1:25) %>%
  mutate(word = reorder(words, n)) %>%
  ggplot(aes(n, word)) +
  geom_col() +
  labs(y = NULL) +
  theme_minimal()

Comparando palavras mais comuns por partidos

Vamos separar os discursos do PT e PSDB e verificar se os principais termos destes deputados divergem.

library(scales)
## 
## Attaching package: 'scales'
## The following object is masked from 'package:purrr':
## 
##     discard
## The following object is masked from 'package:readr':
## 
##     col_factor
# Total palavras por partido
total_palavras <- tidy_discursos %>%
                  select(partido, total_palavras) %>%
                  distinct() %>%
                  group_by(partido) %>%
                  summarize(total_words_per_party=sum(total_palavras)) %>%
                  filter(partido%in%c("PT", "PSDB"))

# Soma cada palavra por partido
palavras_partido <- tidy_discursos %>%
                          count(partido, words) %>%
                           filter(partido%in%c("PT", "PSDB"))

# Merge
partidos <- left_join(palavras_partido, total_palavras) %>%
             mutate(prop=n/total_words_per_party) %>%
              #untidy
            select(words, partido, prop) %>%
            pivot_wider(names_from=partido,
                        values_from=prop) %>%
            drop_na() %>%
            mutate(more=ifelse(PT>PSDB, "More PT", "More PSDB"))
## Joining, by = "partido"
# Graph  
ggplot(partidos, aes(x = PSDB, y = PT, 
                     alpha = abs(PT - PSDB), 
                     color=more)) +
  geom_abline(color = "gray40", lty = 2) +
  geom_jitter(alpha = 0.1, size = 2.5, width = 0.3, height = 0.3) +
  geom_text(aes(label = words), check_overlap = TRUE, vjust = 1.5, alpha=.8) +
  scale_x_log10(labels = percent_format()) +
  scale_y_log10(labels = percent_format()) +
  scale_color_manual(values=c("#5BBCD6","#FF0000"), name="") +
  theme(legend.position="none") +
  labs(y = "Proportion of Words (PT)", x = "Proportion of Words (PSDB)") +
  theme_minimal()

Análise de Sentimento.

Como vocês devem imaginar, com os dados em formato tidy, fazer análise de sentimento com base em dicionário é super intuitivo. Basta um banco de dados com um dicionário de sentimentos. Há muitas opções de dicionários em inglês. Em Português é preciso procurar um pouco mais, e provavelmente fazer pequenos ajustes.

# Usaremos este dicionário.
#devtools::install_github("sillasgonzaga/lexiconPT")
library(lexiconPT)

# Ver Dicionario
data("sentiLex_lem_PT02")
sent_pt <- as_tibble(sentiLex_lem_PT02)

# -1 negative +1 positive

tidy_discursos <- left_join(tidy_discursos, sent_pt, by=c("words"="term"))

# clean words with no sentiment
tidy_discursos_sent <- tidy_discursos %>%
                        mutate(polarity=ifelse(is.na(polarity), 0, polarity)) %>%
                        filter(polarity!=7)
          
tidy_discursos_sent
## # A tibble: 2,865,805 x 13
##    nome   partido uf    id_discursos words total_palavras n_partidos n_dep  n_uf
##    <chr>  <chr>   <chr>        <int> <chr>          <int>      <int> <int> <int>
##  1 SIMÃO… PP      RJ               1 sess…            301        493    43   836
##  2 SIMÃO… PP      RJ               1 pp               301        493    43   836
##  3 SIMÃO… PP      RJ               1 gost…            301        493    43   836
##  4 SIMÃO… PP      RJ               1 regi…            301        493    43   836
##  5 SIMÃO… PP      RJ               1 torc…            301        493    43   836
##  6 SIMÃO… PP      RJ               1 prof…            301        493    43   836
##  7 SIMÃO… PP      RJ               1 educ…            301        493    43   836
##  8 SIMÃO… PP      RJ               1 rio              301        493    43   836
##  9 SIMÃO… PP      RJ               1 jane…            301        493    43   836
## 10 SIMÃO… PP      RJ               1 greve            301        493    43   836
## # … with 2,865,795 more rows, and 4 more variables: grammar_category <chr>,
## #   polarity <dbl>, polarity_target <chr>, polarity_classification <chr>
# sentimento por discursos
tidy_dicursos_av <- tidy_discursos_sent %>%
                          group_by(id_discursos) %>%
                          summarize(polarity=mean(polarity)) %>%
                          arrange(polarity)

Temos portanto uma medida dos sentimentos por discursos. Vamos gerar três gráficos com esta informação:

  • Nuvem de Palavras com sentimentos

  • Distribuição dos sentimentos no decorrer dos anos

  • Distribuição dos sentimento de acordo com partidos.

Palavras Mais Negativas e Positivas

library(reshape2)
## 
## Attaching package: 'reshape2'
## The following object is masked from 'package:tidyr':
## 
##     smiths
library(wordcloud)
## Loading required package: RColorBrewer
tidy_discursos_sent %>%
  filter(polarity!=0) %>%
  mutate(polarity=ifelse(polarity==1, "Positiva", "Negativa")) %>%
  count(words, polarity, sort = TRUE) %>%
  acast(words ~ polarity, value.var = "n", fill = 0) %>%
  comparison.cloud(colors = c("gray20", "gray80"),
                   max.words = 200)

Sentimento Over Time

part_pol <- discursos %>%
  mutate(id_discursos=1:nrow(.)) %>%
  left_join(tidy_dicursos_av) %>%
  mutate(polarity_binary=ifelse(polarity>0,"Positivo", "Negativo"),)%>%
  count(partido, polarity_binary) %>%
  mutate(n=ifelse(polarity_binary=="Negativo", -1*n, n)) %>%
  filter(partido!="\n", 
         n!=0) %>%
  arrange(polarity_binary, n) %>%
  mutate(partido=fct_inorder(partido))
## Joining, by = "id_discursos"
# Graph
ggplot(part_pol,
       aes(x = partido, y = n, fill = polarity_binary)) + 
    geom_col(alpha=.6, color="black") +
    coord_flip() +
    scale_fill_manual(values=c("#5BBCD6","#FF0000"), 
                       name="Polaridade em \n Discursos Legislativos") +
    labs(x="Partidos", y="Numero de Discursos") +
  theme_bw() +
  theme(legend.position = "bottom") 

Outras formas de analisar texto em R.

Existem diversos outros pacotes para fazer análise de texto em R. O mais famoso e mais útil de todos é o quanteda. O Quanteda é muito completo e permite que você faça análise muito complexas, e rode modelos estatísticos em dados de texto de forma bastante intuitiva.

Porque então não aprendemos quanteda? Porque o Quanteda possui uma forma própria de organizar os dados (corpus e document feature matrices) e como estamos aqui dando nossos passos iniciais usando tidy, minha opção foi por manter nosso aprendizado consistente.

No entanto, eu recomendo fortemente que vocês aprendam quanteda. O site do pacote é super intuitivo e tem vários tutoriais. Vale a pena praticar durante as férias!