library(tidyverse)
## Warning: replacing previous import by 'tidyr::%>%' when loading 'broom'
## Warning: replacing previous import by 'tidyr::gather' when loading 'broom'
## Warning: replacing previous import by 'tidyr::spread' when loading 'broom'
## ── Attaching packages ────────────────────────────────── tidyverse 1.2.1 ──
## ✔ ggplot2 2.2.1 ✔ purrr 0.2.4
## ✔ tibble 1.3.4 ✔ dplyr 0.7.4
## ✔ tidyr 0.7.2 ✔ stringr 1.2.0
## ✔ readr 1.1.1 ✔ forcats 0.2.0
## ── Conflicts ───────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
library(here)
## here() starts at /home/marianne-linhares/reclamacoes-do-gf
library(modelr)
library(broom)
##
## Attaching package: 'broom'
## The following object is masked from 'package:modelr':
##
## bootstrap
theme_set(theme_bw())
O governo recebe um grande número de reclamações e poderia gerenciar melhor seus recursos se soubesse o quão insatisfeitas as pessoas estão em relação a um certo órgão. Existem métodos para análise de sentimento aplicáveis, mas não sabemos se esses métodos são eficazes neste contexto.
Quão eficazes são métodos de análise de sentimento baseados em léxicos para estimar a insatisfação manifestada em reclamações recebidas pelo governo federal?
Para analisar a eficiência de léxicos em determinar insatisfação de uma reclamação do https://www.reclameaqui.com.br em relação ao Governo federal, comparamos o resultado dos mesmos com a avaliação de insatisfação dada por seres humanos.
Realizamos uma EDA básica que pode ser vista ao longo dos notebooks, buscando responder perguntas como: quantas palavras os léxicos conseguem de fato identificar nas reclamações?
Para calcular a insatisfação pelo léxico tentamos várias abordagens:
A implementação dessas abordagens e alguns outros tratamentos foram feitos no arquivo calcula-sentimentos.Rmd que pode ser acessado no nosso repositório git: https://github.com/mari-linhares/reclamacoes-do-gf.
Como nesta análise não obtivemos grandes diferenças, mantivemos apenas a abordagem 1 e omitimos os resultados das demais análises para simplificação (porém essas podem ser obtidas no repositório git).
Para comparar os resultados dos léxicos, que consistia de números positivos e negativos, com a nota de insatisfação dada por pessoas, que estava numa escala entre 0 e 5, precisamos normalizar os dados dos léxicos. Para tal, utilizamos duas abordagens:
Fazer com que o menor resultado obtido de um léxico equivalesse a 5 e o maior a 0, com os valores intermediários seguindo a mesma proporção
Fazer com que o primeiro quartil e os valores abaixo dele equivalessem a 5, o terceiro quartil e os valores acima dele equivalessem a 0, com os valores intermediários seguindo a mesma proporção.
A primeira abordagem se mostrou mais coerente então a maior parte das análises foi feita com ela.
Após conferir e entender alguns dados sobre a resposta dos léxicos, calculamos os erros de cada combinação de léxico e forma de normalizar. Com este erro, geramos regressões lineares com:
Ao final, conseguimos observar que o léxico não parece influenciar no erro, mas em alguns modelos o número de palavras que uma reclamação possui e alguns órgãos parecem ter uma certa relação (embora o número de palavras tenha um efeito pouco relevante).
Utilizando uma definição de acerto para os léxicos de que o léxico acertou se o erro foi menor que 1, obtivemos um acerto de 61% para o léxico sent e 58% para o léxico op30.
reclamacoes_raw = read_csv(here("data/reclamacoes-raw/reclamacoes-raw.csv"))
## Parsed with column specification:
## cols(
## orgao = col_character(),
## link = col_character(),
## titulo = col_character(),
## reclamacao = col_character()
## )
avaliacoes_raw = read_csv(here("data/avaliacoes/avaliacoes-20180222.csv"))
## Parsed with column specification:
## cols(
## mat_avaliador = col_integer(),
## id_reclamação = col_integer(),
## insatisfação = col_double()
## )
sentimentos = read_csv(here("data/sentimentos/sentimento.csv"))
## Parsed with column specification:
## cols(
## id = col_integer(),
## sentimento_op30 = col_integer(),
## palavras_op30 = col_integer(),
## sentimento_sent = col_integer(),
## palavras_sent = col_integer(),
## palavras = col_integer(),
## quant_adj_op30 = col_integer(),
## quant_adj_sent = col_integer()
## )
# sentimentos_ponderados = read_csv(here("data/sentimentos/sentimento_ponderado.csv"))
# adicionando id a reclamacoes, comprimento da reclamacao e nome do orgao
reclamacoes_raw = reclamacoes_raw %>%
mutate(id = 1:n(),
comprimento_reclamacao = str_length(reclamacao),
nome_orgao = str_split(link, "/") %>% map_chr(~ .[[5]]))
reclamacoes_l tem um formato long em vez de wide (explicado aqui).
avaliacoes = avaliacoes_raw %>%
group_by(id_reclamação) %>%
summarise(insatisfação = median(insatisfação),
avaliadores = n())
reclamacoes = reclamacoes_raw %>%
inner_join(avaliacoes, by = c("id" = "id_reclamação")) %>%
left_join(sentimentos, by = "id")
reclamacoes_l = reclamacoes %>%
# select(-palavras_op30, -palavras_sent) %>%
gather(key = "lexico",
value = "polaridade",
sentimento_op30, sentimento_sent)
Removendo reclamações em que pelo menos um dos léxicos não reconhece nenhuma palavra.
reclamacoes_l = subset(reclamacoes_l, palavras_op30>0 & palavras_sent>0)
reclamacoes = subset(reclamacoes, palavras_op30>0 & palavras_sent>0)
Antes de converter é importante saber alguns valores chaves para cada léxico, como o mínimo, máximo e os quartis.
summary(reclamacoes)
## orgao link titulo
## Length:155 Length:155 Length:155
## Class :character Class :character Class :character
## Mode :character Mode :character Mode :character
##
##
##
## reclamacao id comprimento_reclamacao
## Length:155 Min. : 2.0 Min. : 111
## Class :character 1st Qu.: 50.5 1st Qu.: 434
## Mode :character Median :127.0 Median : 635
## Mean :133.9 Mean :1064
## 3rd Qu.:205.5 3rd Qu.:1228
## Max. :318.0 Max. :9598
## nome_orgao insatisfação avaliadores sentimento_op30
## Length:155 Min. :0.750 Min. :1.000 Min. :-17.0
## Class :character 1st Qu.:2.000 1st Qu.:1.000 1st Qu.: -1.0
## Mode :character Median :3.000 Median :1.000 Median : 1.0
## Mean :2.876 Mean :1.594 Mean : 1.2
## 3rd Qu.:4.000 3rd Qu.:2.000 3rd Qu.: 3.0
## Max. :5.000 Max. :5.000 Max. : 28.0
## palavras_op30 sentimento_sent palavras_sent palavras
## Min. : 1.00 Min. :-16.0000 Min. : 1.000 Min. : 16.0
## 1st Qu.: 7.00 1st Qu.: -1.5000 1st Qu.: 2.000 1st Qu.: 68.5
## Median : 13.00 Median : 0.0000 Median : 3.000 Median : 106.0
## Mean : 21.71 Mean : -0.1226 Mean : 4.735 Mean : 176.6
## 3rd Qu.: 25.50 3rd Qu.: 1.0000 3rd Qu.: 5.000 3rd Qu.: 200.5
## Max. :190.00 Max. : 18.0000 Max. :45.000 Max. :1593.0
## quant_adj_op30 quant_adj_sent
## Min. : 0.00 Min. : 0.000
## 1st Qu.: 5.00 1st Qu.: 1.000
## Median : 8.00 Median : 2.000
## Mean : 14.41 Mean : 3.168
## 3rd Qu.: 17.00 3rd Qu.: 4.000
## Max. :135.00 Max. :32.000
min_op30 = min(reclamacoes$sentimento_op30)
max_op30 = max(reclamacoes$sentimento_op30)
first_quantile_op30 = quantile(reclamacoes$sentimento_op30, 0.25, na.rm=T)
third_quantile_op30 = quantile(reclamacoes$sentimento_op30, 0.75, na.rm=T)
min_sent = min(reclamacoes$sentimento_sent)
max_sent = max(reclamacoes$sentimento_sent)
first_quantile_sent = quantile(reclamacoes$sentimento_sent, 0.25, na.rm=T)
third_quantile_sent = quantile(reclamacoes$sentimento_sent, 0.75, na.rm=T)
Agora iremos calcular a polaridade_normalizada_usal usando a estratégia mais simples, normalizando os valores entre 0 e 5 de maneira usual.
polarizeResultUsual <- function(Min, Max){
range = Max - Min
arr = (reclamacoes_l$polaridade - Min)/range
normalized = 5 - ((arr*5) -0) # invertendo 0-> 5 e 5->0
return(normalized)
}
# reclamacoes long
reclamacoes_l$polaridade_normalizada_usual <- ifelse(reclamacoes_l$lexico=="sentimento_op30", polarizeResultUsual(min_op30, max_op30), polarizeResultUsual(min_sent, max_sent))
# reclamacoes op30
polarizeResultUsual2 <- function(Min, Max){
range = Max - Min
arr = (reclamacoes$sentimento_op30 - Min)/range
normalized = 5 - ((arr*5) -0) # invertendo 0-> 5 e 5->0
return(normalized)
}
reclamacoes$sentimento_op30_usual <- polarizeResultUsual2(min_op30, max_op30)
# reclamacoes sent
polarizeResultUsual3 <- function(Min, Max){
range = Max - Min
arr = (reclamacoes$sentimento_sent - Min)/range
normalized = 5 - ((arr*5) -0) # invertendo 0-> 5 e 5->0
return(normalized)
}
reclamacoes$sentimento_sent_usual <- polarizeResultUsual3(min_sent, max_sent)
Como os resultados dos léxicos possuem muitos valores extremos, criaremos a polaridade_normalizada_quartil que ao invés de considerar os mínimos e máximos, considera os primeiros e terceiros quartis.
polarizeResultQuartil <- function(Min, Max){
range = Max - Min
arr = (reclamacoes_l$polaridade - Min)/range
normalized = 5 - ((arr*5) -0)
normalized <- ifelse(arr<0, 5, normalized)
normalized <- ifelse(arr>1, 0, normalized)
return(normalized)
}
# reclamacoes long
reclamacoes_l$polaridade_normalizada_quartil <- ifelse(reclamacoes_l$lexico=="sentimento_op30", polarizeResultQuartil(first_quantile_op30, third_quantile_op30), polarizeResultQuartil(first_quantile_sent, third_quantile_sent))
# reclamacoes op30
polarizeResultQuartil2 <- function(Min, Max){
range = Max - Min
arr = (reclamacoes$sentimento_op30 - Min)/range
normalized = 5 - ((arr*5) -0)
normalized <- ifelse(arr<0, 5, normalized)
normalized <- ifelse(arr>1, 0, normalized)
return(normalized)
}
reclamacoes$sentimento_op30_quartil <- polarizeResultQuartil2(first_quantile_op30, third_quantile_op30)
# reclamacoes sent
polarizeResultQuartil3 <- function(Min, Max){
range = Max - Min
arr = (reclamacoes$sentimento_sent - Min)/range
normalized = 5 - ((arr*5) -0)
normalized <- ifelse(arr<0, 5, normalized)
normalized <- ifelse(arr>1, 0, normalized)
return(normalized)
}
reclamacoes$sentimento_sent_quartil <- polarizeResultQuartil3(first_quantile_sent, third_quantile_sent)
reclamacoes_l = reclamacoes_l %>%
mutate(erro_usual = (insatisfação - polaridade_normalizada_usual)**2,
erro_quartil = (insatisfação - polaridade_normalizada_quartil)**2)
reclamacoes %>%
ggplot(aes(x = sentimento_op30, y = sentimento_sent)) +
geom_abline(slope = 1, intercept = 0, color = "grey") +
geom_count(alpha = .7)
reclamacoes %>%
ggplot(aes(x = sentimento_op30_usual, y = sentimento_sent_usual)) +
geom_abline(slope = 1, intercept = 0, color = "grey") +
geom_count(alpha = .7)
reclamacoes %>%
ggplot(aes(x = sentimento_op30_quartil, y = sentimento_sent_quartil)) +
geom_abline(slope = 1, intercept = 0, color = "grey") +
geom_count(alpha = .7)
reclamacoes_l %>%
ggplot(aes(x = insatisfação, y = polaridade_normalizada_usual, group = insatisfação)) +
geom_jitter(alpha = .7) +
facet_wrap(~ lexico)
reclamacoes_l %>%
ggplot(aes(x = insatisfação, y = erro_usual, group = insatisfação)) +
geom_jitter(alpha = .5) +
# geom_boxplo() +
facet_wrap(~ lexico)
reclamacoes_l %>%
ggplot(aes(x = insatisfação, y = polaridade_normalizada_quartil, group = insatisfação)) +
geom_jitter(alpha = .7) +
facet_wrap(~ lexico)
reclamacoes_l %>%
ggplot(aes(x = insatisfação, y = erro_quartil, group = insatisfação)) +
geom_jitter(alpha = .5) +
# geom_boxplo() +
facet_wrap(~ lexico)
Aqui iremos construir vários modelos buscando entender a relação entre o léxico (e outras variáveis independentes) e o erro.
model_erro_lex = lm(erro_usual ~ factor(lexico), data=reclamacoes_l)
tidy(model_erro_lex, conf.int = TRUE, conf.level = 0.95)
glance(model_erro_lex)
Regressão simples foi utilizada para analisar se o léxico tem uma associação significativa com o erro na estimativa de instatisfação da reclamação. Os resultados da regressão indicam que um modelo com os preditores no formato Erro = X1.léxico explicam basicamente nada da variância da variável de resposta (R² = 0.0008613828).
Pelos resultados do p.value, cujo alpha está bem acima de 0.05, e do intervalo de confiança, que inclue 0, não parece haver relação entre o léxico e o erro.
model_erro_lex = lm(erro_usual ~ factor(lexico) + palavras, data=reclamacoes_l)
tidy(model_erro_lex, conf.int = TRUE, conf.level = 0.95)
glance(model_erro_lex)
Regressão múltipla foi utilizada para analisar se o léxico e o número de palavras têm uma associação significativa com o erro na estimativa de instatisfação da reclamação. Os resultados da regressão indicam que um modelo com os preditores no formato Erro = X1.léxico + X2.palavras explicam ~18% da variância da variável de resposta (R² = 0.1793).
model_erro_lex = lm(erro_usual ~ factor(lexico) + palavras + factor(nome_orgao), data=reclamacoes_l)
tidy(model_erro_lex, conf.int = TRUE, conf.level = 0.95)
glance(model_erro_lex)
Regressão múltipla foi utilizada para analisar se o léxico, o número de palavras e o órgão têm uma associação significativa com o erro na estimativa de instatisfação da reclamação. Os resultados da regressão indicam que um modelo com os preditores no formato Erro = X1.léxico + X2.palavras + X3.órgão1 + X4.órgão2 + … + XN.órgãoN explicam ~26% da variância da variável de resposta (R² = 0.2638).
léxico: medida como fator (0 ou 1) tem uma relação não significativa com o erro (b = [-0.602515183; 0.326581443], IC com 95%). Não podemos afirmar se um léxico é “melhor” que o outro para estes dados usando este modelo.
palavras: medida como unidade tem uma relação significativa com o erro (b = [0.003598035; 0.006104555], IC com 95%). Adicionar mais uma palavra ao texto faz com que o erro cresça ligeiramente, mais especificamente cerca de 0.005.
Para está regressão iremos considerar cada léxico separadamente a fim de estudar os efeitos do número de palavras (palavras), quantidade de palavras conhecidas pelo léxico na reclamação (palavras_
lexico_op30 = filter(reclamacoes_l, lexico=="sentimento_op30")
model_erro_lex_adj_op30 = lm(erro_usual ~ quant_adj_op30 + palavras + palavras_op30, data=lexico_op30)
tidy(model_erro_lex_adj_op30, conf.int=T, conf.level=0.95)
glance(model_erro_lex_adj_op30)
O resultado explica ~22% da variância da variável de resposta (R²=0.2248284).
lexico_sent = filter(reclamacoes_l, lexico=="sentimento_sent")
model_erro_lex_adj_sent = lm(erro_usual ~ quant_adj_sent + palavras + palavras_sent, data=lexico_sent)
tidy(model_erro_lex_adj_sent, conf.int=T, conf.level=0.95)
glance(model_erro_lex_adj_sent)
O resultado explica ~19% da variância da variável de resposta (R²=0.1994397).
Para responder tal pergunta, definimos que um léxico “acerta” o nível de insatisfação quando seu erro é igual ou menor que 1. Com isso, calculamos a porcentagem de acerto de cada léxico com forma de normalização.
total_op30 = reclamacoes_l %>%
filter(lexico == "sentimento_op30") %>%
count()
hits_usual_op30 = reclamacoes_l %>%
filter(lexico == "sentimento_op30") %>%
filter(erro_usual <= 1) %>%
count()
hits_quart_op30 = reclamacoes_l %>%
filter(lexico == "sentimento_op30") %>%
filter(erro_quartil <= 1) %>%
count()
total_sent = reclamacoes_l %>%
filter(lexico == "sentimento_sent") %>%
count()
hits_usual_sent = reclamacoes_l %>%
filter(lexico == "sentimento_sent") %>%
filter(erro_usual <= 1) %>%
count()
hits_quart_sent = reclamacoes_l %>%
filter(lexico == "sentimento_sent") %>%
filter(erro_quartil <= 1) %>%
count()
sprintf("Porcentagem de hits OP30 (Usual): %f", hits_usual_op30/total_op30)
## [1] "Porcentagem de hits OP30 (Usual): 0.580645"
sprintf("Porcentagem de hits OP30 (Quartil): %f", hits_quart_op30/total_op30)
## [1] "Porcentagem de hits OP30 (Quartil): 0.296774"
sprintf("Porcentagem de hits Sent (Usual): %f", hits_usual_sent/total_sent)
## [1] "Porcentagem de hits Sent (Usual): 0.580645"
sprintf("Porcentagem de hits Sent (Quartil): %f", hits_quart_sent/total_sent)
## [1] "Porcentagem de hits Sent (Quartil): 0.348387"
Com tais resultados concluímos duas coisas: A primeira que a utilização da normalização usual foi bem melhor que as dos quartis para os nossos dados. A segunda coisa foi que o léxico acerta o nível de insatisfação em cerca de 60% das reclamações, acertando cerca de 3 a cada 5 reclamações.
obs: por algum motivo no knit hits sent = hits op30, porem rodando localmente vimos que hits sent = 0.610687. Devido ao tempo não conseguimos investigar a fundo o motivo da diferença.
No gráfico abaixo verificamos um histograma da porcentagem de palavras identificadas pelos léxicos.
ggplot(data=lexico_op30, aes(lexico_sent$palavras_sent/palavras)) +
geom_histogram(bins = 30, color="darkblue", fill="lightblue") +
labs(x="Porcentagem de palavras identificadas pelo lexico", y="Reclamações com esse número") + scale_y_continuous(breaks=seq(0, 250, 10)) +
scale_x_continuous(breaks=seq(0, 0.5, 0.05))
ggplot(data=lexico_sent, aes(lexico_op30$palavras_op30/palavras)) +
geom_histogram(bins = 30, color="darkblue", fill="lightblue") +
labs(x="Porcentagem de palavras identificadas pelo lexico", y="Reclamações com esse número") + scale_y_continuous(breaks=seq(0, 250, 10)) +
scale_x_continuous(breaks=seq(0, 0.5, 0.05))
A partir dos gráficos acima observamos que o léxico op30 para a maioria das reclamações identifica cerca de 15% das palavras, já o ĺéxico sent na maioria das vezes identifica apenas 5% das palavras, mas mesmo assim ambos obtiveram uma taxa de acerto bastante similar e os modelos não identificaram os léxicos como sendo significativos para o erro.