O Sensacionalista e Text Mining: Análise de sentimento usando o lexiconPT

De volta à ativa no blog!

Recentemente, quando precisei fazer pela primeira vez algum tipo de análise em cima de textos (o chamado Text Mining ou Mineração de Texto) em Português, senti falta de ter um acesso fácil a um léxico na linguagem. O R já tem a sua disposição vários recursos para quem quer fazer Text Mining em inglês, como os pacotes tokenizer, tidytext, tm e lexicon, além de vários blog posts sobre Sentiment Analysis que você encontra no R-bloggers. Contudo, existe uma séria escassez de material de referência na língua portuguesa.

O pacote lexiconPT, que eu lancei em 20/09/2017 no Github (e em breve no CRAN também) nasceu para resolver parte desse problema. Até o momento, o lexiconPT possui três datasets de léxicos: o OpLexicon (versões 2.1 e 3.0) e o SentiLex-PT02. Não pretendo (nem tenho competência para tal, pois sou iniciante em Text Mining - sem falsa modéstia) destrinchar como cada um deles funciona e em quê eles diferem. Para isso, sugiro ler as referências citadas na documentação dos próprios datasets (ex.: help("oplexicon_v2.1")).

Mas ter o léxico em mãos só resolve parte dos problemas: ainda faltam os textos em si para serem analisados. Algumas ideias de datasets poderiam ser notícias, letras de músicas, livros (tem vários em Domínio Público), tweets, etc. Para demonstrar um simples uso do pacote, eu decidi por analisar comentários feitos por usuários na página do Sensacionalista, uma das mais populares do Facebook. A coleta dos dados foi relativamente fácil graças ao pacote Rfacebook.

Com o pacote lexiconPT, podemos responder a perguntas como:
* Os comentários no Sensacionalista são mais negativos ou positivos?
* Qual termo está mais associado a comentários negativos? PT ou PSDB? Temer ou Dilma? Bolsonaro ou Lula?
* Qual o comentário feito por um usuário mais negativo da história do Sensacionalista (dentro da amostra coletada)? E qual o mais positivo?

Vamos ao código.

Coleta dos dados

library(Rfacebook) # usado para extrair dados do facebook
library(tidyverse) # pq nao da pra viver sem
library(ggExtra)
library(magrittr) # <3
library(lubridate)
library(stringr) # essencial para trabalhar com textos
library(tidytext) # um dos melhores pacotes para text mining
library(lexiconPT)

Este post só foi possível graças ao Rfacebook. Para aprender como ele funciona, leia a documentação presente em seu repo no Github. Para este primeiro, primeiro usei a função getPage() para extrair as últimas 5000 publicações do Sensacionalista.

# token que eu gerei com minha API key.
# Essa parte vc obviamente nao vai conseguir reproduzir.
# leia o README do Rfacebook para saber como obter seu token
fb_token <- readRDS("/home/sillas/R/data/facebook_token.Rds") 

# demora cerca de 10 min pra rodar:
pg <- getPage("sensacionalista", fb_token, n = 5000)

É necessário corrigir o encoding do corpo da publicação para o R parar de reclamar sobre isso:

# corrigir encoding do texto do post
pg$message %<>% iconv(to = "ASCII//TRANSLIT")
# remover emojis
pg$message %<>% iconv(sub="", 'UTF-8', 'ASCII')
# visualizar dataframe
glimpse(pg)
## Observations: 4,666
## Variables: 11
## $ from_id        <chr> "108175739225302", "108175739225302", "10817573...
## $ from_name      <chr> "Sensacionalista", "Sensacionalista", "Sensacio...
## $ message        <chr> "Apos liminar da Justica do DF permitir o trata...
## $ created_time   <chr> "2017-09-18T18:58:53+0000", "2017-09-18T17:25:0...
## $ type           <chr> "link", "link", "link", "link", "video", "link"...
## $ link           <chr> "http://www.sensacionalista.com.br/2017/09/18/l...
## $ id             <chr> "108175739225302_1638598099516384", "1081757392...
## $ story          <chr> NA, NA, NA, NA, "Sensacionalista shared Gshow -...
## $ likes_count    <dbl> 10172, 2162, 2503, 5793, 4676, 2585, 821, 766, ...
## $ comments_count <dbl> 230, 164, 285, 221, 329, 104, 32, 58, 493, 586,...
## $ shares_count   <dbl> 2290, 453, 410, 2751, 0, 930, 26, 36, 900, 92, ...

Só esse dataset por si só já renderia (e renderá) análises interessantes, mas vou as deixar para um futuro post para não deixar este aqui grande demais.

A coluna id é a que usaremos como referência como input na função getPost() para extrair os comentários dos usuários na publicação. Infelizmente, a API do Facebook apresenta uma certa instabilidade para requests de dados muito grandes. Em várias tentativas que fiz, o máximo de dados que consegui extrair foram 200 comentários de 500 publicações da página. Portanto, vou usar esses parâmetros:

# roda em cerca de 8 minutos:
df_posts <- pg$id[1:500] %>% map(getPost, fb_token, n = 200, comments = TRUE, likes = FALSE,
                            reactions = FALSE)

A função getPost() fornece o seguinte output:

str(df_posts)
## List of 500
##  $ :List of 2
##   ..$ post    :'data.frame': 1 obs. of  10 variables:
##   .. ..$ from_id       : chr "108175739225302"
##   .. ..$ from_name     : chr "Sensacionalista"
##   .. ..$ message       : chr "Após liminar da Justiça do DF permitir o tratamento da homossexualidade como doença"
##   .. ..$ created_time  : chr "2017-09-18T18:58:53+0000"
##   .. ..$ type          : chr "link"
##   .. ..$ link          : chr "http://www.sensacionalista.com.br/2017/09/18/liminar-que-chancela-cura-gay-permite-tratar-justica-brasileira-como-doenca/"
##   .. ..$ id            : chr "108175739225302_1638598099516384"
##   .. ..$ likes_count   : num 11340
##   .. ..$ comments_count: num 248
##   .. ..$ shares_count  : num 2590
##   ..$ comments:'data.frame': 200 obs. of  7 variables:
##   .. ..$ from_id       : chr [1:200] "1437254732976789" "1445900342154501" "10209941046899919" "173469923210786" ...
##   .. ..$ from_name     : chr [1:200] "Sérgio Henrique Reis" "Renata Gil" "Lucas Ferreira" "Leonardo Wesley" ...
##   .. ..$ message       :
## Error in strtrim(encodeString(object, quote = "\"", na.encode = FALSE), : string multibyte inválida em '<a0><be>\xed<b4>\u0094  Ahh do Distrito Federal.... Quando Juscelino Kubitschek fundou Brasília, será que ele tinha a mínima ideia do que sairia de lá?"'

Para extrair os dataframes relativos aos comentários e aos metadados das publicações, o purrr é uma mão na roda:

df_comments <- df_posts %>% map_df("comments")
df_posts <- df_posts %>% map_df("post")
# repetir procedimento de consertar o encoding
df_comments$message %<>% iconv(to = "ASCII//TRANSLIT") %>% iconv(sub="", 'UTF-8', 'ASCII')
df_posts$message %<>% iconv(to = "ASCII//TRANSLIT") %>% iconv(sub="", 'UTF-8', 'ASCII')
# por questoes de anonimizacao, vou remover os dados pessoais referentes aos usuarios
df_comments %<>% select(-from_id, -from_name)

# olhar estrutura dos dataframes
str(df_comments)
## 'data.frame':    72100 obs. of  5 variables:
##  $ message       : chr  "Entao pode faltar no servico, ligar pro chefe(a), levar atestado e dizer que acordou com vontade de chupar rola?" "Nunca o sensacionalista foi tao verdadeiro. Pq a justica brasileira se demonstrou uma verdadeira praga na socie"| __truncated__ "O unico que precisa de tratamento e o sr juiz que autorizou." "Isso viola todas os acordos internacionais e uma aberracao contra qqr liberdade individual e humana. Logo essa liminar cai." ...
##  $ created_time  : chr  "2017-09-18T19:01:21+0000" "2017-09-18T19:03:57+0000" "2017-09-18T19:01:26+0000" "2017-09-18T19:00:54+0000" ...
##  $ likes_count   : num  575 392 270 227 310 145 113 73 38 50 ...
##  $ comments_count: num  58 5 4 3 12 16 10 0 0 8 ...
##  $ id            : chr  "1638598099516384_1638600206182840" "1638598099516384_1638602159515978" "1638598099516384_1638600266182834" "1638598099516384_1638599869516207" ...
str(df_posts)
## 'data.frame':    500 obs. of  10 variables:
##  $ from_id       : chr  "108175739225302" "108175739225302" "108175739225302" "108175739225302" ...
##  $ from_name     : chr  "Sensacionalista" "Sensacionalista" "Sensacionalista" "Sensacionalista" ...
##  $ message       : chr  "Apos liminar da Justica do DF permitir o tratamento da homossexualidade como doenca" "Ja recebeu encomendas de quarteis" "Aparelho esta sendo oferecido por importadores por ate 6 mil reais" "Temer desembarcou nos Estados Unidos para jantar com Trump e participacao na Assembleia Geral da ONU" ...
##  $ created_time  : chr  "2017-09-18T18:58:53+0000" "2017-09-18T17:25:00+0000" "2017-09-18T17:05:41+0000" "2017-09-18T17:00:02+0000" ...
##  $ type          : chr  "link" "link" "link" "link" ...
##  $ link          : chr  "http://www.sensacionalista.com.br/2017/09/18/liminar-que-chancela-cura-gay-permite-tratar-justica-brasileira-como-doenca/" "http://www.sensacionalista.com.br/2017/09/18/fabricante-de-pau-de-arara-comemora-falta-de-resposta-a-general-qu"| __truncated__ "http://www.sensacionalista.com.br/2017/09/18/empresa-lanca-servico-de-escolta-armada-para-quem-comprar-o-iphone-x/" "http://www.sensacionalista.com.br/2017/09/18/coreia-do-norte-nega-ter-lancado-temer-nos-eua/" ...
##  $ id            : chr  "108175739225302_1638598099516384" "108175739225302_1638510679525126" "108175739225302_1638503796192481" "108175739225302_1638456466197214" ...
##  $ likes_count   : num  11340 2271 2826 6361 4990 ...
##  $ comments_count: num  248 167 315 242 347 106 32 58 494 590 ...
##  $ shares_count  : num  2590 478 441 3008 0 ...

Só pode ser trollagem da API do Facebook retornar aquela mensagem logo no topo do dataframe, mas enfim.

O dataframe df_comments, o objeto da nossa análise, não possui alguns dados que serão valiosos para a análise, como o link para o artigo no site do Sensacionalista. Por isso, vamos um left_join com o df_posts.

Percebeu que nas colunas df_comments$id e df_posts$id existe um traço separando dois conjuntos numéricos? Por alguma razão que beira a imbecilidade, não é possível combinar imediatamente essas duas colunas para formatar um dataframe só com o left_join. A sintaxe de identificação do Facebook funciona assim: O post é identificado IDPAGINA_IDPUBLICAÇÃO e o comentário na publicação é identificado como IDPUBLICAÇÃO_IDCOMENTÁRIO. Ou seja, para poder juntar os dois dataframes, vamos ter que combinar a sequência númerica à esquerda do underline (acho que esse traço tem algum outro nome, mas não me lembro no momento) em df_comments$id e à direita em df_posts$id.

# consertar colunas de id: no df_comments, deixar à esquerda do underline. no df_posts, deixar à direita.
df_comments$id_post_real <- df_comments$id
df_comments$id %<>% str_replace_all("_.*", "")
df_posts$id %<>% str_replace_all(".*_", "")

# juntar as duas tabelas
df_posts %<>% dplyr::select(id, post_message = message, horario_post = created_time,
                     type, post_comments_count = comments_count, post_link = link,
                     post_type = type, post_likes_count = likes_count)
df_comments %<>% rename(horario_comentario = created_time)

df_comments %<>% left_join(df_posts, by = "id")
# remover NAs (nao sao muitos casos)
df_comments %<>% filter(!is.na(horario_post))
# converter colunas de horario
df_comments$horario_comentario %<>% str_sub(1, 19) %>% str_replace_all("T", "") %>% ymd_hms()
df_comments$horario_post %<>% str_sub(1, 19) %>% str_replace_all("T", "") %>% ymd_hms()
# Como ficou:
glimpse(df_comments)
## Observations: 71,891
## Variables: 12
## $ message             <chr> "Entao pode faltar no servico, ligar pro c...
## $ horario_comentario  <dttm> 2017-09-18 19:01:21, 2017-09-18 19:03:57,...
## $ likes_count         <dbl> 575, 392, 270, 227, 310, 145, 113, 73, 38,...
## $ comments_count      <dbl> 58, 5, 4, 3, 12, 16, 10, 0, 0, 8, 69, 3, 0...
## $ id                  <chr> "1638598099516384", "1638598099516384", "1...
## $ id_post_real        <chr> "1638598099516384_1638600206182840", "1638...
## $ post_message        <chr> "Apos liminar da Justica do DF permitir o ...
## $ horario_post        <dttm> 2017-09-18 18:58:53, 2017-09-18 18:58:53,...
## $ post_type           <chr> "link", "link", "link", "link", "link", "l...
## $ post_comments_count <dbl> 248, 248, 248, 248, 248, 248, 248, 248, 24...
## $ post_link           <chr> "http://www.sensacionalista.com.br/2017/09...
## $ post_likes_count    <dbl> 11340, 11340, 11340, 11340, 11340, 11340, ...

Uso do lexiconPT

Agora temos o dataset em mãos para usar o lexiconPT. Acho muito importante ressaltar que Text Mining é uma atividade razoavelmente complexa que envolve uma extensa etapa de limpeza e tratamento de dados, como remover (ou não) acentos e corrigir palavras com letras duplicadas (tristee) ou erros gramaticais (infelismente). Para fins de simplicidade, não vou me ater muito a esses detalhes e pular direto para a utilização dos léxicos portugueses e apresentação dos resultados.

# carregar datasets
data("oplexicon_v3.0")
data("sentiLex_lem_PT02")

op30 <- oplexicon_v3.0
sent <- sentiLex_lem_PT02

glimpse(op30)
## Observations: 32,191
## Variables: 4
## $ term              <chr> "=[", "=@", "=p", "=P", "=x", "=d", "=D", ";...
## $ type              <chr> "emot", "emot", "emot", "emot", "emot", "emo...
## $ polarity          <int> -1, -1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, ...
## $ polarity_revision <chr> "A", "A", "A", "A", "A", "A", "A", "A", "A",...
glimpse(sent)
## Observations: 7,014
## Variables: 5
## $ term                    <chr> "a-vontade", "abafado", "abafante", "a...
## $ grammar_category        <chr> "N", "Adj", "Adj", "Adj", "Adj", "Adj"...
## $ polarity                <dbl> 1, -1, -1, -1, -1, 1, -1, 1, 1, -1, -1...
## $ polarity_target         <chr> "N0", "N0", "N0", "N0", "N0", "N0", "N...
## $ polarity_classification <chr> "MAN", "JALC", "MAN", "JALC", "JALC", ...

Ambos os datasets possuem colunas de polaridade de sentimento, que é a que usaremos para quantificar o quão negativo ou positivo é um comentário.

# criar ID unica para cada comentario
df_comments %<>% mutate(comment_id = row_number())
# usar funçao do tidytext para criar uma linha para cada palavra de um comentario
df_comments_unnested <- df_comments %>% unnest_tokens(term, message)

df_comments_unnested %>%
  select(comment_id, term) %>%
  head(20)
##      comment_id     term
## 1             1    entao
## 1.1           1     pode
## 1.2           1   faltar
## 1.3           1       no
## 1.4           1  servico
## 1.5           1    ligar
## 1.6           1      pro
## 1.7           1    chefe
## 1.8           1        a
## 1.9           1    levar
## 1.10          1 atestado
## 1.11          1        e
## 1.12          1    dizer
## 1.13          1      que
## 1.14          1  acordou
## 1.15          1      com
## 1.16          1  vontade
## 1.17          1       de
## 1.18          1   chupar
## 1.19          1     rola

De novo esse comentário…

Enfim, veja que foi criada uma linha para cada palavra presetnte no comentário. Será essa nova coluna term que usaremos como referência para quantificar o sentimento de um comentário.

df_comments_unnested %>% 
  left_join(op30, by = "term") %>% 
  left_join(sent %>% select(term, lex_polarity = polarity), by = "term") %>% 
  select(comment_id, term, polarity, lex_polarity) %>% 
  head(10)
##    comment_id    term polarity lex_polarity
## 1           1   entao       NA           NA
## 2           1    pode       NA           NA
## 3           1  faltar        1           NA
## 4           1      no       NA           NA
## 5           1 servico       NA           NA
## 6           1   ligar       -1           NA
## 7           1     pro       NA           NA
## 8           1   chefe       NA           NA
## 9           1       a       NA           NA
## 10          1   levar       -1           NA

A amostra acima mostra que nem toads as palavras possuem uma polaridade registrada nos léxicos. Não só isso, mas algumas palavras (como faltar, ligar e levar) estão presentes no OpLexicon mas não no SentiLex. A polaridade 1 em faltar significa que, de acordo com esse léxico, a palavra está associada a comentários positivos. Saber essa diferença é fundamental, pois a escolha do léxico pode ter uma grande influência nos resultados da análise.

Vamos então manter no dataframe apenas as palavras que possuem polaridade tanto no OpLexicon como no SentiLex:

df_comments_unnested <- df_comments_unnested %>% 
  inner_join(op30, by = "term") %>% 
  inner_join(sent %>% select(term, lex_polarity = polarity), by = "term") %>% 
  group_by(comment_id) %>% 
  summarise(
    comment_sentiment_op = sum(polarity),
    comment_sentiment_lex = sum(lex_polarity),
    n_words = n()
    ) %>% 
  ungroup() %>% 
  rowwise() %>% 
  mutate(
    most_neg = min(comment_sentiment_lex, comment_sentiment_op),
    most_pos = max(comment_sentiment_lex, comment_sentiment_op)
  )

head(df_comments_unnested)
## # A tibble: 6 x 6
##   comment_id comment_sentiment_op comment_sentiment_lex n_words most_neg
##        <int>                <int>                 <dbl>   <int>    <dbl>
## 1          2                    0                     0       2        0
## 2          7                   -2                    -3       3       -3
## 3          8                    1                     1       2        1
## 4          9                    0                     0       3        0
## 5         11                   -1                    -1       3       -1
## 6         12                   -3                    -3       3       -3
## # ... with 1 more variables: most_pos <dbl>

Apresentação dos resultados

O gráfico de pontos abaixo mostra a distribuição de polaridade para cada léxico:

df_comments_unnested %>% 
  ggplot(aes(x = comment_sentiment_op, y = comment_sentiment_lex)) +
    geom_point(aes(color = n_words)) + 
    scale_color_continuous(low = "green", high = "red") +
    labs(x = "Polaridade no OpLexicon", y = "Polaridade no SentiLex") +
    #geom_smooth(method = "lm") +
    geom_vline(xintercept = 0, linetype = "dashed") +
    geom_hline(yintercept = 0, linetype = "dashed")

Existem pelo menos três outliers nos dados, todos causados pela grande quantidade de palavras do comentário, o que pode ser um indício de que simplesmente somar a polaridade de cada palavra do comentário pode não ser um bom método. Outra informação revelada pelo gráfico é que existem palavras que possuem sentimentos diferentes de acordo com o léxico usado. Ter isso em mente é fundamental para a análise.

Após remover os outliers, já é possível descobrir quais os comentários mais positivos e mais negativos da amostra coletada:

df_comments_unnested %<>% filter(between(comment_sentiment_op, -10, 10))

# comentario mais positivo da historia do sensacionalista
most_pos <- which.max(df_comments_unnested$most_pos)
most_neg <- which.min(df_comments_unnested$most_neg)

# mais positivo
cat(df_comments$message[df_comments$comment_id == df_comments_unnested$comment_id[most_pos]])
## O mundo a esquerda e sempre melhor. A musica, o pincel, a pena e a talha. Sempre esteve para a esquerda. As grandes mentes criativas, os grandes pensadores, os humanistas, os geniais. Tudo que e original e belo e oriundo da esquerda.
## E tendencia natural do ser humano, ao passo q alcanca um minimo de consciencia critica do mundo sempre tender a esquerda.
## Todavia o mundo tambem precisa de mentes computacionais, a esse papel a direita tem seu valor. A direita funciona bem quando o objetivo e produzir o fisico, o tangivel e operacional. Embora isso tambem pudesse ser feito por maquinas, robos, ou mesmo por animais adestrados.
## A exemplo, os EUA, onde o cidadao comum e um ser vegetativo, destinado a operar, produzir e consumir. Sao seres incapazes de formular um pensamento critico e original. Uma nacao que por forca do capital tem seu vies ideologico voltado pra direita. Ainda q por vezes elejam um presidente com vies de esquerda, nunca irao evoluir sua consciencia. Sera sempre uma nacao de dementes e ignorantes.
## Nao por acaso que o capital, na sua forma economica ou politica sempre se poe sobre a direita quando tem por objetivo o ganho, financeiro ou de poder. E sobre a direita q se faz os movimentos de massa imbecilizada, pois como seres roboticos sao facilmente levados aonde se quer levar.
## A esquerda liga-se ao mundo das ideias, ao pensamento critico, a modelagem do ser humano como ser consciente. A esquerda e a construcao do pensamento, e o
## individuo pensante e critico, tudo que evolui e eleva o ser humano a um patamar de consciencia superior, provem da esquerda.
## Nacoes capitalistas, ainda q direitista, mas q sua massa possui em sua construcao um vies ideologico de esquerda serao sempre nacoes ricas financeiramente e ricas de estado de consciencia critica. Observa se isto em paises europeus, onde o
## capital existe por forca do consumo, mas coexiste com o estado de bem estar social, com a beleza das artes e com tudo aquilo e natural da esquerda, enquanto consciencia e beleza.
## A direita tem por tendencia natural, o simples, o computacional e robotizado. O argumento da direita e sempre algoritmo, linear e raso. A direita sera sempre um universo de seres ocos, massivos e imbecilizados.
## A.L.C.
# mais negativo
cat(df_comments$message[df_comments$comment_id == df_comments_unnested$comment_id[most_neg]])
## No ultimo domingo foi ao ar o programa do Silvio Santos, onde Maisa foi ridiculamente feita de idiota por duas pessoas na frente do Brasil todo. Onde mesmo ela pedindo pra parar, nao pararam. Mas eu to aqui apenas pra falar a verdade sobre uma dessas pessoas: DUDU CAMARGO. 
## Muitos o conhecem como o cara chato que apresenta o jornal da manha, mas eu o conheco como o EX ABUSIVO QUE FEZ COISAS IMPERDOAVEIS. 
## Tudo comecou quando nos conhecemos na escola, quando ele fez de tudo pra me conquistar, e eu era bobo. Cai. Comecamos a namorar e era tudo incrivel e flores, mesmo com o preconceito, era bom ter alguem com quem contar e gostar. 
## As coisas foram andando, e mesmo com todas as pessoas dizendo pra eu nao continuar esse relacionamento, eu continuei. Meus professores, meus amigos, e ate gente desconhecida tentaram abrir meus olhos. Mas ja era tarde. Eu estava apaixonado! Ia fazer de tudo pra ficar com ele!
## O terror comeca. 
## Estava tudo bem, ate ele comecar a ser agressivo e babaca. Gritava, ficava de cara feia, falava merda, e eu tambem, mas ele foi ao extremo. Ele estava vidrado em ser um astro da tv que esqueceu que tinha um relacionamento e comecou a me tratar como lixo, ate que ele me machucou feio, arranhou meu braco e deixou uma cicatriz em uma de nossas brigas. Nao sou santo, claro que tentei revidar para me defender. Mas apenas UMA vez.
## As brigas e abusos tornaram-se serios, ele cada dia era mais estourado, e nao so comigo, com todos, ate com a familia, que por sinal sempre o apoiou. Mas ele sempre queria ser mais que todos.
## Nao contente em 1, ele bateu 2, 3, 4... Varias vezes. Ja cheguei em casa com o rosto machucado e tive que mentir pra minha mae, mas ela nao era e nem e burra, sabia o que tinha acontecido. 
## Na escola, todos perguntavam e eu mentia, achando que estava tudo bem, mas nao estava. Eu estava tao apaixonado que nao conseguia sair daquele pesadelo.
## Outro ponto: ABUSO SEXUAL. Isso e bem serio, e se fosse brincadeira, eu nao estaria me expondo assim. Em um relacionamento voce consequentemente transa com a pessoa, e no comeco era bom, era normal. Mas depois das agressoes, eu fiquei tao em choque que inventava que estava doente para nao ter que fazer nada com ele. E ele reclamava, e bla bla bla, e dizia que sexo nao era nada, mas era. Ate que ele comecou a falar coisas horriveis para mim, comecou ficar tao frustrado que comecou a forcar coisas dizendo que se eu nao fizesse, era pra eu ir embora e nunca mais voltar, e que se eu voltasse, ele me trataria pior do que ja estava tratando. Mas eu fiquei, nao sei o porque, mas fiquei, com todo o medo e receio. 
## Toda vez que eu queria conversar com ele sobre algo, ele ia direto me mandando ficar quieto e me chamava pra fazer coisas... Quando ele nao estava no instagram ou implorando para alguma aparicao na tv. 
## Eu estava louco e assustado, a escola chamou minha mae pra conversar pois estavam com medo de que eu fizesse algo comigo mesmo, meu psicologico estava destruido e eu fiquei com depressao. 
## Algumas pessoas vao dizer que eu quem quis, ja que nao fui embora, mas nao e bem assim. 
## Nao tenho tudo registrado, - e acho que nem preciso - mas eu vivi, as pessoas viram e tentaram me ajudar, mas eu recusei, com medo. 
## Passei muitas coisas boas no comeco, mas depois eu vi o monstro que ele era/e. Ele pode se fazer de santo na tv, mas muita gente sabe quem ele realmente e. E eu sei o que eu sofri. 
## Vao dizer que e tudo mentira, e que eu quero fama. Talvez que sou louco, mas felizmente tenho testemunhas e nao tenho mais medo de me esconder. Depois de domingo, ele provou ser a pessoa mais falsa e artificial que existe. Engessado, segundo a Maisa. 
## Nao e porque ele e uma pessoa publica que pode fazer o que quer sem pagar. Sei das consequencias que posso ter, mas nada tira a paz que sinto sabendo que tenho pessoas incriveis que me apoiaram e apoiam ate hoje. 
## Espero que entendam e ajudem as pessoas que precisam. Se voce sabe alguem que esteja nessa vida, ajude-a a sair... Nao importa quem seja. Chega de abuso!

Por incrível que pareça, tanto o comentário mais positivo quanto o mais negativo falam sobre a esquerda.

Para prosseguir com a análise, usaremos o léxico OpLexicon para a análise de sentimento:

df_comments %<>% inner_join(
  df_comments_unnested %>% select(comment_id, sentiment = comment_sentiment_op),
  by = "comment_id"
  )
# criar coluna de data (variavel da classe Date)
df_comments$data <- as.Date(df_comments$horario_post)

Agora sim podemos demonstrar uma visualização de uma análise básica de sentimento: Como tem sido o sentimento geral dos comentários no Sensacionalista ao longo do tempo?

df_comments_wide <- df_comments %>% 
  # filtrar fora palavras neutras
  filter(sentiment != 0) %>% 
  # converter numerico para categorico
  mutate(sentiment = ifelse(sentiment < 0, "negativo", "positivo")) %>% 
  # agrupar os dados
  count(data, post_link, post_type, sentiment) %>% 
  # converter para formato wide
  spread(sentiment, n, fill = 0) %>% 
  mutate(sentimento = positivo - negativo) %>% 
  ungroup() %>% 
  arrange(data)

head(df_comments_wide) %>% knitr::kable()
data post_link post_type negativo positivo sentimento
2017-04-13 https://www.facebook.com/sensacionalista/photos/a.187587037950838.39557.108175739225302/1460990897277106/?type=3 photo 7 17 10
2017-04-13 http://www.sensacionalista.com.br/2017/04/13/temer-lula-e-fhc-articulam-pacto-de-nao-rir-de-brasileiro-que-desfez-amizade-no-facebook-por-politica/ link 32 34 2
2017-04-14 https://www.facebook.com/sensacionalista/photos/a.187587037950838.39557.108175739225302/1462221330487396/?type=3 photo 42 22 -20
2017-04-15 https://www.facebook.com/sensacionalista/photos/a.187587037950838.39557.108175739225302/1463589740350555/?type=3 photo 7 27 20
2017-04-16 https://www.sensacionalista.com.br/2016/03/25/laja-jato-diz-que-lula-comprou-ovo-da-kopenhagen-em-nome-de-amigo/ link 23 35 12
2017-04-16 http://www.sensacionalista.com.br/2017/04/10/as-18-melhores-coisas-com-sentimentos-a-nova-obsessao-da-internet/ link 17 16 -1

Por exemplo, o primeiro link coletado na amostra, uma foto, teve 9 palavras contadas como negativas e 13 como positivas. O score geral dos comentários nessa publicação foi 13 - 9 = 4.

Qual a publicação do Sensacionalista com maior nível de “positividade”? E o de “negatividade”?

df_comments_wide %>% 
  arrange(sentimento) %>% 
  filter(row_number() == 1 | row_number() == nrow(df_comments_wide)) %>% 
   knitr::kable()
data post_link post_type negativo positivo sentimento
2017-09-11 http://www.sensacionalista.com.br/2017/09/11/projeto-do-mbl-pretende-vestir-obras-de-arte-em-museus-ao-redor-do-mundo/ link 95 39 -56
2017-06-05 http://www.sensacionalista.com.br/2017/06/05/festa-se-nada-der-certo-em-colegio-debocha-de-garis-e-faxineiras-e-mostra-que-ja-deu-tudo-errado/ link 29 86 57

A publicação que mais recebeu comentários negativos (não tenho certeza se é essa a interpretação mais correta dos resultados, mas enfim) é um link sobre o MBL, enquanto o mais positivo é sobre o famoso caso do “E se der errado”.

O gráfico abaixo mostra a evolução do sentimento dos comentários nas publicações do Sensacionalista ao longo do tempo:

df_comments_wide %>% 
  mutate(index = row_number()) %>% 
  ggplot(aes(x = index, y = sentimento)) +
    geom_col(aes(fill = post_type)) +
    scale_y_continuous(breaks = seq(-60, 60, 20), limits = c(-60, 60)) +
    labs(x = "Índice da publicação", y = "Sentimento",
         fill = NULL, title = "Evolução do sentimento em publicações do Sensacionalista")

Uma possível interpretação do gráfico é que a série temporal não possui uma clara tendência, apesar de os picos de negatividade serem bem mais frequentes que os de positividade.

Outra análise que dá para fazer é investigar o nível de sentimento de comentários associados a determinadas palavras. Por exemplo, o quão negativo costuma ser um comentário quando ele menciona a palavra bolsonaro?

# qual o sentimento mais associado a palavras em especifico
df_comments %>% 
  mutate(
    temer = str_detect(str_to_lower(message), "temer"),
    lula = str_detect(str_to_lower(message), "lula"),
    pmdb = str_detect(str_to_lower(message), "pmdb"),
    psdb = str_detect(str_to_lower(message), "psdb"),
    pt = str_detect(str_to_lower(message), "pt"),
    dilma = str_detect(str_to_lower(message), "dilma"),
    doria = str_detect(str_to_lower(message), "doria"),
    governo = str_detect(str_to_lower(message), "governo"),
    bolsonaro = str_detect(str_to_lower(message), "bolsonaro")
  ) %>% 
  gather(termo, eh_presente, temer:bolsonaro) %>% 
  filter(eh_presente) %>% 
  group_by(termo) %>% 
  summarise(sentiment = mean(sentiment)) %>% 
  ggplot(aes(x = termo, y = sentiment)) + 
    geom_col(fill = "#C10534")

Temer e Dilma, os dois presidentes com os piores níveis de popularidade de República, estarem associados a comentários positivos é bem surpreendente. Na verdade, isso ocorre porque a própria palavra temer possui polaridade positiva. Para consultar a polaridade de uma palavra nos datasets presentes no lexiconPT, use a função lexiconPT::get_word_sentiment().

get_word_sentiment("temer")
## $oplexicon_v2.1
##        term type polarity
## 28711 temer   vb        1
## 
## $oplexicon_v3.0
##        term type polarity polarity_revision
## 30160 temer   vb        1                 A
## 
## $sentilex
##       term grammar_category polarity polarity_target
## 6546 temer                V       -1           N0:N1
##      polarity_classification
## 6546                     MAN

Conclusão e chamada para futuros trabalhos

O pacote lexiconPT, apesar de simples, tem um enorme potencial para enriquecer o conteúdo de Text Mining em Português na comunidade brasileira de R. O exemplo dado nesse post pode ser considerado deveras simplório. Muitas etapas foram puladas ou desconsideradas com o intuito de fornecer a você uma rápida introdução às possibilidades criadas pelo pacote. Espero que o leitor deste post tenha se sentido motivado a fazer suas próprias análises de sentimento. As possibilidade são incontáveis.

 
comments powered by Disqus