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()
search_tweets("bolsonaro", n=50, include_rts = TRUE)
bolsonaro_tweets<-
# 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
list.files("data_txt")
nomes <-
# Criar endereço completo
paste0("data_txt/", nomes)
path <-
# Abrir
map_chr(path, read_lines)
dados <-
tibble(file=nomes, texto=dados)
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.
read_csv("speeches.csv") discursos <-
##
## ── 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.
discursos %>%
tidy_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
discursos %>%
partido_st <- group_by(partido) %>%
summarise(n_partidos=n())
discursos %>%
nome_st <- group_by(nome) %>%
summarise(n_dep=n())
discursos %>%
uf_st <- group_by(uf) %>%
summarise(n_uf=n())
left_join(tidy_discursos, partido_st) %>%
tidy_discursos <- 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)
tibble(words=stopwords("portuguese"))
stop_words <-
# Elimina com um Join
tidy_discursos %>%
tidy_discursos <- anti_join(stop_words)
## Joining, by = "words"
O que mais podemos eliminar? Nome dos Estados.
tibble(words=unique(str_to_lower(tidy_discursos$uf)))
estados <-
# 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.
tibble(words=c("candidato", "candidata", "brasileira", "brasileiro",
function_names <-"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
tidy_discursos %>%
total_palavras <- 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
tidy_discursos %>%
palavras_partido <- count(partido, words) %>%
filter(partido%in%c("PT", "PSDB"))
# Merge
left_join(palavras_partido, total_palavras) %>%
partidos <- 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")
as_tibble(sentiLex_lem_PT02)
sent_pt <-
# -1 negative +1 positive
left_join(tidy_discursos, sent_pt, by=c("words"="term"))
tidy_discursos <-
# clean words with no sentiment
tidy_discursos %>%
tidy_discursos_sent <- 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_discursos_sent %>%
tidy_dicursos_av <- 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
discursos %>%
part_pol <- 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",
!=0) %>%
n 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!