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")
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
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
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.
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()
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"
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
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()
Notamos que existem bastante mais palavras negativas que positivas. Será isto uma característica do Português?
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
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()
No geral, as atribuições parecem fazer sentido. Esta é uma forma de ganharmos confiança nas análises seguintes.
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
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")
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.