10 Classificação: Dicionários e Análise de Sentimentos

Objetivos deste capítulo:

  • Apresentar uma introdução à análise de sentimentos utilizando dicionários
  • Apresentar como criar e utilizar dicionários/léxicos em classificação
  • Como fazer isso com diferentes pacotes do R

10.1 Classificação

Podemos classificar palavras, sentenças, textos, documentos, corpus, etc. através de diferentes métodos. A análise de sentimentos ou emoções é um sub-item particular dentre os tipos de classificação. Entre as diferentes formas de classificação, seguindo a tipologia de Welbers et al. (2017) temos:

Welbers, K., Van Atteveldt, W., & Benoit, K. (2017). Text Analysis in R. Communication Methods and Measures, 11 (4), 245-265. http://www.tandfonline.com/doi/10.1080/19312458.2017.1387238

  • Dicionários e métodos de contagem:
    • as categorias e os termos que compõem estas categorias são disponibilizados previamente pelo pesquisador.
  • Aprendizado de máquinas supervisionado, ou supervised machine learning:
    • Um (ou vários) seres humanos classificam textos com base em categorias estabelecidas por estes e o algoritmo irá tentar aprender o padrão e aplicar na classificação subsequente.
  • Aprendizado de máquinas não-supervisionado ou unsupervised machine learning.
    • Exemplo é a modelagem de tópicos (topic modeling) que tem o algoritmo Latent Dirichlet Allocation (LDA) como exemplo mais famoso. O algoritmo estabelece as categorias e os termos, cabendo muitas vezes ao pesquisador apenas estabelecer o número de categorias.
  • categorização estatística:
    • não possui categorias prévias dadas por humanos, mas também não é machine learning.

Há várias formas de analisar sentimentos. O modo mais simples e talvez mais recorrente se dá com o uso de dicionários/léxicos com termos/palavras e seu sentimento ou uma pontuação (score) correspondente. Mas há também formas mais elaboradas de análise sentimento, e também mais complicadas, que mixam outras técnicas de processamento de linguagem natural com a análise de sentimentos.

10.2 Análise de sentimentos (Sentiment Analysis) por meio de dicionário/léxico

Como sempre, o R possui diversos pacotes também para análise de sentimentos utilizando léxico de palavras, como por exemplo, o SentimentAnalysis, syuzhet, o Tidytext e o Quanteda.

De modo simplificado, a análise de sentimentos pode ser entendida como uma forma de classificação em categorias pré-definidas. Tais categorias podem ser “positivo” e “negativo” em sua versão mais simples e binária, trinaria se para além das duas se incorporar a categoria “neutro”, ou pode-se ainda utilizar de diversas outras categorias, como “espanto”, “indecisão”, etc. Pode-se ainda atribuir diferentes pesos aos termos dentro de um escopo de pontuação, por exemplo, entre -5 e +5, ou entre -10 a +10. Isto faz sentido nas situações onde queira se diferenciar termos como “ruim” de “péssimo” e de “pior”.

10.2.1 Datasets de sentimentos/emoções

Além de possibilitar criar seu próprio dicionário, com suas próprias categorias, é possível usar léxicos de sentimentos já pronto, contendo dezenas de milhares de palavras já categorizadas. Diferentes pacotes vem com diferentes dicionários, alguns com dicionários em comum. Alguns pacotes são apenas de datasets de sentimentos a serem usados.

O pacote tidytext vem com os seguintes dicionários: - Bing: palavras rotuladas em positivo e negativo - AFINN: palavras com escores entre -5 e 5. - NRC: Emoções são classificados em 8 emoções. Além de positivo e negativo: - “trust”, “fear”, “sadness”,“anger”, “surprise”, disgust”, “joy” e “anticipation” - Loughran-McDonald Sentiment lexicon. Requer licença para uso comercial.

str(tidytext::get_sentiments("bing"))
## tibble [6,786 × 2] (S3: tbl_df/tbl/data.frame)
##  $ word     : chr [1:6786] "2-faces" "abnormal" "abolish" "abominable" ...
##  $ sentiment: chr [1:6786] "negative" "negative" "negative" "negative" ...
str(tidytext::get_sentiments("afinn"))
## spec_tbl_df [2,477 × 2] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ word : chr [1:2477] "abandon" "abandoned" "abandons" "abducted" ...
##  $ value: num [1:2477] -2 -2 -2 -2 -2 -2 -3 -3 -3 -3 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   word = col_character(),
##   ..   value = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>
str(tidytext::get_sentiments("loughran"))
## tibble [4,150 × 2] (S3: tbl_df/tbl/data.frame)
##  $ word     : chr [1:4150] "abandon" "abandoned" "abandoning" "abandonment" ...
##  $ sentiment: chr [1:4150] "negative" "negative" "negative" "negative" ...
str(tidytext::get_sentiments("nrc"))
## tibble [13,875 × 2] (S3: tbl_df/tbl/data.frame)
##  $ word     : chr [1:13875] "abacus" "abandon" "abandon" "abandon" ...
##  $ sentiment: chr [1:13875] "trust" "fear" "negative" "sadness" ...

O pacote SentimentAnalysis vem com os seguintes léxicos: - QDAP (somente seu dicionário, não o pacote QDAP), - Harvard IV, dicionário piscológico, com a lista utilizada no software General Inquirer. - Henry’s Financial dictionary (Henry 2008), dicionário focado em finanças - Loughran-McDonald Financial dictionary (Loughran and McDonald 2011)

str(SentimentAnalysis::DictionaryGI) # Harvard-IV
## List of 2
##  $ negative: chr [1:2005] "abandon" "abandonment" "abate" "abdicate" ...
##  $ positive: chr [1:1637] "abide" "ability" "able" "abound" ...
str(SentimentAnalysis::DictionaryHE) # Henry’s Financial dictionary
## List of 2
##  $ negative: chr [1:85] "below" "challenge" "challenged" "challenges" ...
##  $ positive: chr [1:105] "above" "accomplish" "accomplished" "accomplishes" ...
str(SentimentAnalysis::DictionaryLM) # Loughran-McDonald Financial dictionary
## List of 3
##  $ negative   : chr [1:2355] "abandon" "abandoned" "abandoning" "abandonment" ...
##  $ positive   : chr [1:354] "able" "abundance" "abundant" "acclaimed" ...
##  $ uncertainty: chr [1:297] "abeyance" "abeyances" "almost" "alteration" ...
SentimentAnalysis::loadDictionaryQDAP() |> str() # polarity words do QDAP
## List of 2
##  $ positiveWords: chr [1:1280] "a plus" "abound" "abund" "access" ...
##  $ negativeWords: chr [1:2952] "abnorm" "abolish" "abomin" "abort" ...
##  - attr(*, "class")= chr "SentimentDictionaryBinary"
  • O pacote corpus possui o WordNet-Affect Lexicon, o “AFINN Sentiment Lexicon”
  • O pacote lexicon possui muitos datasets, para citar alguns dos diversos:
    • lexicon::key_sentiment_jockers: Jockers Sentiment Key
    • lexicon::hash_sentiment_huliu: dataset com 6.874 termos com valores que vão de -2 a 1, baseado em Hu & Liu’s
    • lexicon::hash_emojis: com descrição de emojis
    • lexicon::nrc_emotions: 14.182 linhas em 9 variáveis/categorias: term, anger, anticipation, disgust, fear, joy, sadness, surprise e trust.
    • lexicon::profanity_* com vários datasets com palavrões, termos racistas/linguagem de ódio, ou hash_internet_slang com gírias de internet.
    • É possível realizar uma busca do que há disponível no pacote com a função lexicon::available_data('termo')

O pacote LexiconPT possui três léxicos de sentimentos em português (o utilizaremos mais à frente), dois deles mais de 30 mil termos.

str(lexiconPT::oplexicon_v2.1)
## 'data.frame':    30673 obs. of  3 variables:
##  $ term    : chr  "ababadar" "ababelar-se" "ababelar" "abacanar" ...
##  $ type    : chr  "vb" "vb" "vb" "vb" ...
##  $ polarity: int  0 1 -1 1 1 -1 -1 -1 -1 -1 ...
str(lexiconPT::oplexicon_v3.0)
## 'data.frame':    32191 obs. of  4 variables:
##  $ term             : chr  "=[" "=@" "=p" "=P" ...
##  $ type             : chr  "emot" "emot" "emot" "emot" ...
##  $ polarity         : int  -1 -1 -1 -1 -1 1 1 1 1 -1 ...
##  $ polarity_revision: chr  "A" "A" "A" "A" ...
str(lexiconPT::sentiLex_lem_PT02)
## 'data.frame':    7014 obs. of  5 variables:
##  $ term                   : chr  "a-vontade" "abafado" "abafante" "abaixado" ...
##  $ grammar_category       : chr  "N" "Adj" "Adj" "Adj" ...
##  $ polarity               : num  1 -1 -1 -1 -1 1 -1 1 1 -1 ...
##  $ polarity_target        : chr  "N0" "N0" "N0" "N0" ...
##  $ polarity_classification: chr  "MAN" "JALC" "MAN" "JALC" ...

Mas é sempre bom ressaltar e repetir: o melhor é ter um dicionário adequado para o contexto de aplicação. A aplicação de dicionários tem de ser validada!

“The main take-home message is, however, that a human eye is still required to guarantee the validity of measuring sentiment in content analysis… we strongly recommend that every automatic text analysis project should start with coding a validation set.” (van Atteveldt et al,2021, p.3)

10.2.2 Pacote SentimentAnalysis

Vamos iniciar criando nosso próprio dicionário de sentimentos para entender melhor o conceito de análise de sentimentos. Após instalar o pacote utilizando o comando install.packages("SentimentAnalysis")

# carregando o pacote
library(SentimentAnalysis)
# Vetor com frases a serem analisadas
documentos <- c("Café é bom", "Rúcula é ruim", "Bem mais ou menos", 
                "refrigerante é péssimo", "ruim é ter de trabalhar")

# Criando um dicionário (no caso, do tipo binário)
dict_pt <- SentimentDictionaryBinary(
                                     # vetor de palavras com conotação positiva
                                     c("bom","boa", "excelente"), 
                                     # vetor de palavras com conotação negativa
                                     c("ruim", "péssimo"))

# realizando a análise de sentimento a partir do dicionário criado
AnaliseSentimentos <- analyzeSentiment(documentos,
                              language="portuguese",
                              rules=list("pontos"=list(ruleSentiment, dict_pt)))
# visualizando
AnaliseSentimentos 
##       pontos
## 1  0.5000000
## 2 -0.5000000
## 3  0.0000000
## 4 -0.5000000
## 5 -0.3333333

# Apresentando este resultado de forma mais legível para humanos, como um data frame.
sentimentosDF <- data.frame(frase = documentos, 
                            score.sentimentos = AnaliseSentimentos, 
                            # convertendo os valores em direções: pos., neg. e neutro 
                            sentimento = convertToDirection(AnaliseSentimentos$pontos))

sentimentosDF 
##                     frase     pontos sentimento
## 1              Café é bom  0.5000000   positive
## 2           Rúcula é ruim -0.5000000   negative
## 3       Bem mais ou menos  0.0000000    neutral
## 4  refrigerante é péssimo -0.5000000   negative
## 5 ruim é ter de trabalhar -0.3333333   negative

Para criar dicionário ponderado (weighted), isto é, com valores contínuos, onde cada termo recebe pontuação, fazemos da seguinte forma: colocamos como primeiro vetor os termos e o segundo com suas respectivas pontuações.

# Opção 1:
# Léxico de sentimentos a partir de dois vetores
d <- SentimentDictionaryWeighted(c("péssimo", "ruim", "bom", "boa", "excelente", "top"),
                                 c(-10, -5, +5, +5, +10, +10),)
d
## Type: weighted (words with individual scores)
## Intercept: 0
## -10.00 péssimo
## -5.00 ruim
##  5.00 bom
##  5.00 boa
## 10.00 excelente
## 10.00 top

# Opção 2
# Montando o léxico de sentimentos a partir de uma tabela
tabela <- read.table(header = FALSE, text = '
péssimo -10
ruim -5
ok 0
bom +5
boa +5
excelente +10
top +10')

d <- SentimentDictionaryWeighted(tabela[,1], tabela[,2]) 
d
## Type: weighted (words with individual scores)
## Intercept: 0
## -10.00 péssimo
## -5.00 ruim
##  0.00 ok
##  5.00 bom
##  5.00 boa
## 10.00 excelente
## 10.00 top

Também é possível utilizar dicionários/léxicos de sentimentos prontos em português. É sempre bom lembrar que certos termos em um contexto, podem bem significar outra coisa em outros contextos. Por isso há diversidade de dicionários, e sempre deve-se procurar, sempre que possível, utilizar um dicionário que tenha sido criado num contexto parecido ao que vamos utilizar, quando não, criarmos nosso próprio léxico para a ocasião, ou adaptar algum. Além disso, o ideal é sempre validarmos o dicionário. O pacote LexiconPT disponibiliza alguns desses dicionários/léxicos na língua de Camões, alguns com mais de 30 mil termos já catalogados. Para padronização, os termos se encontram em minúsculo e sem acentuação, alguns até com emoticons catalogados ;). Após a instalação do pacote com install.pacakges("lexiconPT"), vamos carregar a biblioteca/pacote com:

library(lexiconPT)

Para checar se uma palavra específica está presente nos léxicos do LexiconPT e quais pontuações recebe:

lexiconPT::get_word_sentiment("comemorar")
## $oplexicon_v2.1
##           term type polarity
## 6622 comemorar   vb        0
## 
## $oplexicon_v3.0
##           term type polarity polarity_revision
## 7298 comemorar   vb        0                 A
## 
## $sentilex
## [1] "Word not present in dataset"
lexiconPT::get_word_sentiment("gritar")
## $oplexicon_v2.1
##         term type polarity
## 16646 gritar   vb        1
## 
## $oplexicon_v3.0
##         term type polarity polarity_revision
## 17567 gritar   vb        1                 A
## 
## $sentilex
## [1] "Word not present in dataset"
lexiconPT::get_word_sentiment("espernear")
## $oplexicon_v2.1
## [1] "Word not present in dataset"
## 
## $oplexicon_v3.0
## [1] "Word not present in dataset"
## 
## $sentilex
## [1] "Word not present in dataset"
lexiconPT::get_word_sentiment("confrontar")
## $oplexicon_v2.1
##            term type polarity
## 7049 confrontar   vb       -1
## 
## $oplexicon_v3.0
##            term type polarity polarity_revision
## 7736 confrontar   vb       -1                 A
## 
## $sentilex
##            term grammar_category polarity polarity_target
## 1582 confrontar                V        1           N0:N1
##      polarity_classification
## 1582                     MAN
lexiconPT::get_word_sentiment("detestar")
## $oplexicon_v2.1
##           term type polarity
## 10573 detestar   vb        1
## 
## $oplexicon_v3.0
##           term type polarity polarity_revision
## 11314 detestar   vb        1                 A
## 
## $sentilex
##          term grammar_category polarity polarity_target polarity_classification
## 2353 detestar                V        0           N0:N1                     MAN
lexiconPT::get_word_sentiment("odiar")
## $oplexicon_v2.1
##        term type polarity
## 22371 odiar   vb        1
## 
## $oplexicon_v3.0
##        term type polarity polarity_revision
## 23549 odiar   vb        1                 A
## 
## $sentilex
##       term grammar_category polarity polarity_target polarity_classification
## 5215 odiar                V        0           N0:N1                     MAN

Para podermos trocar de dicionário de modo fácil, vamos criar um vetor com estes léxicos disponíveis no pacote LexiconPT

# Escolhendo manuamente um léxico vamos usar. 
lex <- oplexicon_v2.1

# Criando o vetor com léxicos
lex <- c("oplexicon_v2.1", "oplexicon_v3.0", "sentiLex_lem_PT02")

# para que captemos não o nome, mas o conteúdo do dataframe, rodamos o seguinte 
# comando, que pega o segundo item do vetor
lexico <- eval(parse(text = lex[2]))
str(lexico)
## 'data.frame':    32191 obs. of  4 variables:
##  $ term             : chr  "=[" "=@" "=p" "=P" ...
##  $ type             : chr  "emot" "emot" "emot" "emot" ...
##  $ polarity         : int  -1 -1 -1 -1 -1 1 1 1 1 -1 ...
##  $ polarity_revision: chr  "A" "A" "A" "A" ...

Os dicionários do LexiconPT possuem vários termos duplicados, e isto impossibilita de usá-los diretamente no pacote SentimentAnalysis. Para identificar duplicados usamos o comando duplicated(), que retorna um vetor de mesmo tamanho, com valores lógicos.

# "True" indica quantos valores duplicados nós temos
summary(duplicated(lexico$term))
##    Mode   FALSE    TRUE 
## logical   31951     240
# Se quisermos ver quais os termos duplicados:
duplicados <- lexico$term[duplicated(lexico$term)]
duplicados[1:35]
##  [1] ";)"            ";D"            ";p"            ":("           
##  [5] ":)"            ":@"            ":*"            ":P"           
##  [9] ":x"            "aberrantes"    "academica"     "academicas"   
## [13] "academico"     "academicos"    "acoitar"       "acoutar"      
## [17] "adultera"      "adulteras"     "adultero"      "adulteros"    
## [21] "afinar"        "anacronica"    "anacronicas"   "anacronico"   
## [25] "anacronicos"   "anemica"       "anemicas"      "anemico"      
## [29] "anemicos"      "anomalo"       "antiquissima"  "antiquissimas"
## [33] "antiquissimo"  "antiquissimos" "apedrar"

Uma solução rápida seria retirar estes termos duplicados, ficando apenas com os termos que apareceram uma única vez. Fazemos isto com a função unique().

EM EXPANSÃO Este trecho sobre uso do LexiconPT será expandido em breve

A função lexiconPT::compareDictionaries() compara dicionários de dois em dois, mostrando total de palavras únicas e as compartilhadas.

dic1 <- SentimentAnalysis::SentimentDictionary(c("incerto", "possível", "provável", "talvez"))
dic2 <- SentimentAnalysis::SentimentDictionary(c("preferencialmente", "pretender", "provável"))

# Comparando os dicionários
cmp <- SentimentAnalysis::compareDictionaries(dic1, dic2)
## Comparing: wordlist vs wordlist
## 
## Total unique words: 6
## Matching entries: 1 (0.1666667%)
cmp
## $totalUniqueWords
## [1] 6
## 
## $totalSameWords
## [1] 1
## 
## $ratioSameWords
## [1] 0.1666667

10.2.3 Pacote Tidytext

Para ver exemplos de análise de sentimentos com princípios Tidy, em inglês:

  • ver o capítulo 2 de “Text Mining with R: A Tidy Approach” da Julia Silge link aqui.
  • Um exemplo de análise de sentimentos de Harry Potter (há um pacote no Github com sete livros da saga Harry Potter) com o Tidytext, ver este link.

10.2.4 Pacote Quanteda

O Quanteda torna fácil o uso de dicionários, e a análise de sentimentos é um caso de dicionário. Veja aqui a sessão “Grouping words by dictionary or equivalence class” para se ter uma ideia.

EM EXPANSÃO Este trecho será expandido

10.2.5 Pacote syuzhet

O pacote syuzhet e seu vingette.

Carregando o pacote

library(syuzhet)

Criando um dicionário personalizado

texto <- "É comum adorar esta definição. 
        Mas é fácil odiar esta outra. 
        A beleza está nos detalhes"
# tokenizando
token_frases <- get_sentences(texto)

# criando um data frame com sentimentos, usando vetores
meu_lexicon <- data.frame(word=c("adorar", "odiar", "beleza", "feio"), 
                          value=c(1,-1,1, -1))
# aplicando
valores_frases <- get_sentiment(token_frases, 
                                  method = "custom", 
                                  lexicon = meu_lexicon)
valores_frases
## [1]  1 -1  1

EM EXPANSÃO Este trecho será expandido

Por fim, vale dizer que a análise de sentimentos não se resume ao uso de dicionários prontos, havendo toda uma miríade de outras técnicas/ técnicas mistas utilizadas ali, como word embedding, topic models, etc.

10.3 Dicas finais: Bibliografia indicada

Dicas: Análise de Sentimentos