Exercícios

Análise de Sentimentos n'Os Maias

library(tidyverse)   # instala ggplot2, dplyr, tidyr, readr, purrr, etc.
library(tidytext)    # para análise de textos
library(stringr)     # para manipulação de strings

Este exemplo é adaptado do livro “Tidy Text Mining with R” de Julia Silge e David Robinson.

No nosso caso vamos analisar o texto d'Os Maias de Eça de Queiroz.

os_maias <- readLines("data/senti-pt/eça.queiroz.os.maias.txt")

Conversão do texto em data frame

Vamos converter o ficheiro com o texto original numa data frame, onde cada palavra ocupa uma linha. Para além disso guardamos a que parágrafo e capítulo cada palavra pertence.

os_maias %>% 
  dplyr::data_frame(text=.) %>% 
  dplyr::mutate(paragraph=row_number()) %>%                  # adiciona número parágrafo
  dplyr::mutate(chapter=                                     # adiciona capítulo
    cumsum(str_detect(text, regex("^capítulo [\\divxlcd]", ignore_case = TRUE)))) %>% 
  dplyr::mutate(chapter=as.factor(chapter)) %>%              # capítulo é um valor discreto
  tidytext::unnest_tokens(input=text, output=word, token="words") %>% 
  dplyr::filter(!str_detect(word, "[\\d]+")) %>%             # remove números
  dplyr::mutate(nth_word=row_number()) %>%                   # adiciona numero da palavra
  dplyr::select(nth_word, paragraph, chapter, word) -> df    # reorganiza colunas

df[470:490,]
# A tibble: 21 x 4
   nth_word paragraph chapter        word
      <int>     <int>  <fctr>       <chr>
1       470         6       1        essa
2       471         6       1       gente
3       472         6       1      estava
4       473         6       1 atrapalhada
5       474         7       1       ainda
6       475         7       1         tem
7       476         7       1          um
8       477         7       1      pedaço
9       478         7       1          de
10      479         7       1         pão
# ... with 11 more rows

Remoção de stopwords

As preposições e determinantes, entre outras palavras, são normalmente neutras e podemos retirá-las. Elas costumam chamar-se stopwords.

readLines("data/senti-pt/stopwords_pt_large.txt", encoding="UTF-8") %>% 
  data_frame(word=.) -> stopwords_pt
stopwords_pt
# A tibble: 416 x 1
      word
     <chr>
1        a
2        à
3    adeus
4    agora
5       aí
6    ainda
7     além
8     algo
9  algumas
10  alguns
# ... with 406 more rows

Remoção de stopwords

Agora podemos eliminar estas stopwords fazendo um anti-join entre as tabelas:

df %>% 
  dplyr::anti_join(stopwords_pt) %>%   # remove as linhas com valores de stopwords_pt
  dplyr::arrange(nth_word) -> df2      # ordena por capítulo e parágrafo
df2
# A tibble: 108,090 x 4
   nth_word paragraph chapter       word
      <int>     <int>  <fctr>      <chr>
1         1         1       1   capítulo
2         4         3       1       casa
3         7         3       1      maias
4         8         3       1     vieram
5         9         3       1    habitar
6        11         3       1     lisboa
7        13         3       1     outono
8        16         3       1  conhecida
9        18         3       1 vizinhança
10       20         3       1        rua
# ... with 108,080 more rows

Esta manipulação reduziu uma tabela com 216k entradas para uma com 108k.

Exploração da data frame

A tabela já pode ser explorada:

df2 %>% 
  dplyr::count(word, sort=TRUE) %>% 
  dplyr::filter(n > 300) %>% 
  dplyr::mutate(word = reorder(word, n)) %>%   # reordena os factores, para visualizar
  ggplot(aes(x=word,y=n)) +
    geom_bar(stat = "identity") +              # usa os valores em y
    labs(x=NULL, y="ocorrências") + 
    coord_flip()

plot of chunk unnamed-chunk-6

Análise de sentimentos

Uma tarefa típica na análise de texto é determinar se uma frase encerra um sentimento negativo ou positivo.

Para tal, precisamos de uma classificação dos sentimentos transmitidos pelas palavras portuguesas. Felizmente, já existe um dataset, o SentiLex desenvolvido por Paula Carvalho, Mário J. Silva e João Ramalho do INESC. Vamos usá-lo aqui:

file <- readLines("data/senti-pt/SentiLex-flex-PT02.csv", encoding="UTF-8")
head(file)
[1] "à-vontade,à-vontade.PoS=N;FLEX=ms;TG=HUM:N0;POL:N0=1;ANOT=MAN"    
[2] "abafada,abafado.PoS=Adj;FLEX=fs;TG=HUM:N0;POL:N0=-1;ANOT=JALC"    
[3] "abafadas,abafado.PoS=Adj;FLEX=fp;TG=HUM:N0;POL:N0=-1;ANOT=JALC"   
[4] "abafado,abafado.PoS=Adj;FLEX=ms;TG=HUM:N0;POL:N0=-1;ANOT=JALC"    
[5] "abafados,abafado.PoS=Adj;FLEX=mp;TG=HUM:N0;POL:N0=-1;ANOT=JALC"   
[6] "abafante,abafante.PoS=Adj;FLEX=fs|ms;TG=HUM:N0;POL:N0=-1;ANOT=MAN"

Análise de sentimentos

Primeiro precisamos de o converter para uma data frame organizada:

read.csv2("data/senti-pt/SentiLex-flex-PT02.csv", encoding="UTF-8", header = FALSE) %>% 
  dplyr::filter(str_detect(V1, "\\.PoS=")) %>%                # filta linhas sem info
  tidyr::separate(V1, into=c("word","dummy"), sep=",") %>%    # busca palavra
  tidyr::separate(V4, into=c("_","value"), sep="=") %>%       # busca valor
  dplyr::mutate(sentiment = as.integer(value))%>%             # converte valor para inteiro
  dplyr::select(word, sentiment) -> lex                       # fica só com estas cols
head(lex)
       word sentiment
1 à-vontade         1
2   abafada        -1
3  abafadas        -1
4   abafado        -1
5  abafados        -1
6  abafante        -1

Explorar o SentiLex

Podemos, antes de continuar, explorar este dataset.

lex %>% 
  dplyr::mutate(sentiment = as.factor(sentiment)) %>% 
  dplyr::group_by(sentiment) %>% 
  dplyr::summarise(count = n()) %>% 
  dplyr::filter(abs(count)>2) %>%       # remove alguns outliers
  ggplot(aes(sentiment, count)) +
    geom_bar(stat = "identity", show.legend = FALSE) + coord_flip()

plot of chunk unnamed-chunk-9

Notamos que existem bastante mais palavras negativas que positivas. Será isto uma característica do Português?

Anexar os sentimentos às palavras do romance

Agora podemos adicionar o valor sentimental de cada palavra n'Os Maias que pertença ao léxico:

df2 %>% 
  dplyr::inner_join(lex)
# A tibble: 18,078 x 5
   nth_word paragraph chapter      word sentiment
      <int>     <int>  <fctr>     <chr>     <int>
1        44         3       1    fresco         0
2        51         3       1   sombrio        -1
3        55         3       1   severas        -1
4        71         3       1    tímida        -1
5        75         3       1 abrigadas         0
6        83         3       1 tristonho        -1
7       122         3       1     certo         1
8       125         3       1  quadrado        -1
9       141         3       1  colocado         0
10      149         3       1     atado        -1
# ... with 18,068 more rows

Exploração do data frame

Quais as palavras mais negativas no romance?

df2 %>% 
  dplyr::inner_join(lex) %>%                          # junta sentimento da palavra
  dplyr::count(word, sentiment, sort=TRUE) %>%        # conta ocorrências de cada palavra
  dplyr::ungroup() %>%                                # desagrupa a contagem anterior
  dplyr::filter(sentiment!=0) %>%                     # remove palavras neutras
  dplyr::filter(n>40) %>%                             # filtra as pouco frequentes
  dplyr::mutate(n=ifelse(sentiment==-1,-n,n)) %>%     # troca sinal das ocorr. negativas
  dplyr::mutate(sentiment=as.factor(sentiment)) %>%   # torna sentimento um valor discreto
  dplyr::mutate(word=reorder(word,n)) %>%             # reordea palavras para o ggplot
  ggplot(aes(word, n, fill=sentiment)) +
    geom_bar(alpha = 0.8, stat = "identity") +
    labs(y = "Contribution to sentiment", x = NULL) +
    coord_flip()

Exploração do data frame

plot of chunk unnamed-chunk-12

No geral, as atribuições parecem fazer sentido. Esta é uma forma de ganharmos confiança nas análises seguintes.

Trajectória sentimental d'Os Maias

Queremos mostrar como o sentimento se desenrola ao longo do livro. Para evitar tornar o gráfico excessivamente comprido, somam-se os sentimentos de cada parágrafo por capítulo:

df2 %>% 
  dplyr::inner_join(lex) %>% 
  dplyr::group_by(chapter, paragraph) %>% 
  dplyr::summarise(overall_sentiment=sum(sentiment)) -> sentiments_paragraph
sentiments_paragraph
Source: local data frame [4,296 x 3]
Groups: chapter [?]

   chapter paragraph overall_sentiment
    <fctr>     <int>             <int>
1        1         3                -5
2        1         4                 1
3        1         5                -9
4        1         6                -3
5        1         8                 0
6        1         9                -2
7        1        10                 1
8        1        11                 1
9        1        14                 3
10       1        17                -3
# ... with 4,286 more rows

Trajectória sentimental d'Os Maias

Vejamos como decorrem os 18 capítulos do romance.

sentiments_paragraph %>% 
  ggplot(aes(paragraph, overall_sentiment, fill=chapter)) + theme_bw() +
    geom_bar(stat = "identity", show.legend = FALSE) +
    facet_wrap(~chapter, ncol=3, scales="free_x")

Trajectória sentimental d'Os Maias

plot of chunk unnamed-chunk-15

Trajectória sentimental d'Os Maias

E podemos fazer o mesmo para cada capítulo (calculando o sentimento médio por parágrafo):

sentiments_paragraph %>% 
  summarise(chapter_sentiment = mean(overall_sentiment)) %>% 
  ggplot(aes(chapter, chapter_sentiment)) + theme_bw() +
    geom_bar(stat = "identity", show.legend = FALSE) +
    xlab("capítulos") + ylab("sentimento médio")

No capítulo 4, Carlos da Maia forma-se em Medicina e viaja pela Europa. Segundo a análise é o capítulo mais positivo. O capítulo 17 é onde se descobre o segredo trágico da relação entre Carlos e Maria. Este é, sem surpresas, o capítulo mais negativo da obra.

plot of chunk unnamed-chunk-17