Análise de sentimento baseada em léxico classifica textos em positivo, negativo ou neutro contando palavras de listas curadas: palavras que carregam valência positiva (excelente, aprovar, conquista) contra palavras de valência negativa (péssimo, fracasso, retrocesso).
O acR usa por padrão o OpLexicon (Souza
& Vieira, 2012), o mais usado para português brasileiro, com cerca
de 30 mil palavras rotuladas.
| Cenário | Léxico | LLM |
|---|---|---|
| Sinal grosso em milhares de docs | ✅ | ❌ caro |
| Ironia, sarcasmo, contexto | ❌ | ✅ |
| Sem chave de API, sem rede | ✅ | ❌ |
| Precisa reprodutibilidade exata | ✅ | ⚠️ estocástico |
| Documentos muito curtos (tweets) | ⚠️ | ✅ |
Regra prática: use léxico para panorama rápido e
filtragem de casos extremos, depois use codificação
qualitativa (via ac_qual_code()) na amostra que
interessa.
Corpus fabricado de 14 pronunciamentos, com metadados de partido e
posição declarada. Serve para exercitar visualizações; em corpora reais,
substitua pela coleta via ac_fetch_camara() ou
ac_fetch_senado().
textos <- c(
# Favoráveis
"Excelente proposta que moderniza o sistema e protege as geracoes futuras.",
"Aprovamos com convicção esta reforma responsável e necessária ao país.",
"Vitória histórica para a economia: reforma corrige distorções antigas.",
"Reforma equilibrada garante sustentabilidade fiscal e futuro promissor.",
"Conquista importante que beneficia trabalhadores e empresas do Brasil.",
# Contrários
"Esta reforma é um retrocesso vergonhoso que prejudica os trabalhadores.",
"Voto contra: proposta desastrosa ataca os mais pobres e vulneráveis.",
"Uma tragédia nacional: estão roubando direitos duramente conquistados.",
"Rejeitamos com indignação essa proposta injusta e cruel com o povo.",
"Reforma péssima que aprofunda desigualdades e destrói a proteção social.",
# Neutros / técnicos
"O texto substitutivo altera o artigo 201 da Constituição Federal.",
"O relatório final incorporou emendas apresentadas em plenário.",
"A comissão aprovou o parecer com dez votos a favor e oito contra.",
"A proposta segue para votação na próxima sessão ordinária."
)
df <- data.frame(
text = textos,
posicao = c(rep("favor", 5), rep("contra", 5), rep("neutro", 4)),
partido = c("PT","PSD","PL","MDB","PSDB",
"PSOL","PT","PDT","PSOL","PCdoB",
"MDB","PSDB","PL","MDB"),
stringsAsFactors = FALSE
)
corpus <- ac_corpus(df)
corpus
#>
#> ── Corpus acR ──────────────────────────────────────────────────────────────────
#> • Documentos: 14
#> • Metadados: 2 colunas
#> • Idioma: "pt"
#>
#> # A tibble: 14 × 4
#> doc_id text posicao partido
#> <chr> <chr> <chr> <chr>
#> 1 doc_1 Excelente proposta que moderniza o sistema e protege a… favor PT
#> 2 doc_2 Aprovamos com convicção esta reforma responsável e nec… favor PSD
#> 3 doc_3 Vitória histórica para a economia: reforma corrige dis… favor PL
#> 4 doc_4 Reforma equilibrada garante sustentabilidade fiscal e … favor MDB
#> 5 doc_5 Conquista importante que beneficia trabalhadores e emp… favor PSDB
#> 6 doc_6 Esta reforma é um retrocesso vergonhoso que prejudica … contra PSOL
#> # ℹ 8 more rowsac_sentiment() retorna um tibble com contagens de
palavras positivas (n_pos), negativas (n_neg)
e neutras (n_neu), o score (n_pos − n_neg pelo
método padrão sum) e o rótulo categórico
sentiment.
sent <- ac_sentiment(corpus)
sent
#> # A tibble: 14 × 6
#> doc_id n_pos n_neg n_neu score sentiment
#> <chr> <int> <int> <int> <int> <chr>
#> 1 doc_1 1 0 10 1 positivo
#> 2 doc_10 0 1 9 -1 negativo
#> 3 doc_11 0 0 10 0 neutro
#> 4 doc_12 0 1 7 -1 negativo
#> 5 doc_13 1 0 12 1 positivo
#> 6 doc_14 0 1 8 -1 negativo
#> 7 doc_2 1 0 9 1 positivo
#> 8 doc_3 0 0 9 0 neutro
#> 9 doc_4 2 0 6 2 positivo
#> 10 doc_5 2 0 7 2 positivo
#> 11 doc_6 1 1 8 0 neutro
#> 12 doc_7 0 3 7 -3 negativo
#> 13 doc_8 0 0 8 0 neutro
#> 14 doc_9 0 2 9 -2 negativoCruzando com a posição declarada: o léxico “acerta”?
sent |>
dplyr::left_join(df |> dplyr::mutate(doc_id = paste0("doc_", dplyr::row_number())),
by = "doc_id") |>
dplyr::count(posicao, sentiment) |>
tidyr::pivot_wider(names_from = sentiment, values_from = n, values_fill = 0L)
#> # A tibble: 3 × 4
#> posicao negativo neutro positivo
#> <chr> <int> <int> <int>
#> 1 contra 3 2 0
#> 2 favor 0 1 4
#> 3 neutro 2 1 1Como ler esta tabela
Onde olhar. Cada linha é uma posição declarada pelos autores (
favor,contra,neutro); cada coluna é o rótulo que o léxico deu. Leia por linha: o número em cada célula é quantos discursos daquela posição receberam aquele rótulo. O acerto está nas células “favor × positivo” e “contra × negativo”.O que este exemplo mostra. Dos 5 discursos
favor, 4 saempositivo; dos 5contra, 3 saemnegativo; nenhum discurso é invertido (não há “contra” lido como positivo, nem “favor” como negativo). Os erros caem todos emneutroou nos textos técnicos. Os três casos que mais ensinam:doc_3(“Vitória histórica para a economia”) edoc_8(“Uma tragédia nacional: estão roubando direitos”) saem neutros porque nenhuma das palavras deles está no OpLexicon; edoc_14(“segue para votação na próxima sessão ordinária”) sai negativo porque o léxico lê “ordinária” como adjetivo pejorativo.Em palavras simples. O léxico acerta a direção quando o texto usa adjetivos de opinião (“excelente”, “desastrosa”), e erra de dois jeitos: quando a opinião está em substantivos e verbos que ele não conhece (cobertura) e quando uma palavra tem outro sentido no jargão legislativo (domínio).
Cuidado. Esses dois erros são os clássicos do método de dicionário (Grimmer & Stewart, 2013). O OpLexicon é formado sobretudo por adjetivos (Souza et al., 2011), e palavras mudam de polaridade conforme o domínio: Loughran & McDonald (2011) mostraram o mesmo com “liability” em relatórios financeiros. Antes de usar o léxico num corpus legislativo, liste os termos dele mais frequentes no seu corpus, corrija os que têm outro sentido e valide numa amostra humana (
ac_dictionary_validate()).
Três métodos alteram como o score é calculado a partir
das contagens:
sum (padrão):
score = n_pos − n_neg. Depende do tamanho do texto.mean: normaliza pelo número total de
palavras. Comparável entre textos curtos e longos.ratio:
(n_pos − n_neg) / (n_pos + n_neg). Ignora palavras neutras,
foca só na intensidade emotiva relativa.sent_sum <- ac_sentiment(corpus, method = "sum")
sent_mean <- ac_sentiment(corpus, method = "mean")
sent_ratio <- ac_sentiment(corpus, method = "ratio")
comparacao <- data.frame(
doc_id = sent_sum$doc_id,
sum = round(sent_sum$score, 2),
mean = round(sent_mean$score, 2),
ratio = round(sent_ratio$score, 2)
)
head(comparacao, 10)
#> doc_id sum mean ratio
#> 1 doc_1 1 0.09 1
#> 2 doc_10 -1 -0.10 -1
#> 3 doc_11 0 0.00 0
#> 4 doc_12 -1 -0.12 -1
#> 5 doc_13 1 0.08 1
#> 6 doc_14 -1 -0.11 -1
#> 7 doc_2 1 0.10 1
#> 8 doc_3 0 0.00 0
#> 9 doc_4 2 0.25 1
#> 10 doc_5 2 0.22 1ac_plot_sentiment(sent, type = "bar") +
ggplot2::labs(
title = "Sentimento por documento",
subtitle = "Contagem de palavras positivas vs. negativas (OpLexicon)",
caption = "acR - ac_plot_sentiment(type = \"bar\")"
)Como ler este gráfico
Onde olhar. Cada barra é um discurso, e o comprimento é o
score(palavras positivas menos negativas). Barra para a direita (verde) = saldo positivo; para a esquerda (laranja) = negativo; sem barra = saldo zero. As barras estão ordenadas pelo escore, não pela ordem dos documentos.O que este exemplo mostra. No topo,
doc_4edoc_5(+2, ambos favoráveis); na base,doc_7(−3, contrário: “proposta desastrosa ataca os mais pobres e vulneráveis”, três palavras negativas). Quatro discursos ficam sem barra (escore 0): só um é técnico (doc_11); os outros sãodoc_3(favorável),doc_6edoc_8(contrários). E dois textos técnicos (doc_12,doc_14) aparecem em laranja.Em palavras simples. O gráfico ordena os discursos do mais positivo ao mais negativo segundo o léxico, mas a ordem só é tão boa quanto o léxico: as barras “vazias” e as laranjas dos textos técnicos são os erros de cobertura e de domínio da seção 2.
Cuidado. A cor depende só do sinal: +1 recebe o mesmo verde que +2, embora +1 seja um caso quase neutro (uma única palavra positiva). E o escore
sumcresce com o tamanho do texto; em textos de tamanhos muito diferentes, compare commethod = "mean"ou"ratio"(seção 3).
Útil para ver a forma da distribuição: sentimento é bimodal (dois picos, favor e contra) ou tem uma cauda longa?
ac_plot_sentiment(sent, type = "density") +
ggplot2::labs(
title = "Distribuição de scores de sentimento",
subtitle = "Curvas separadas pelo sinal do escore (veja a nota abaixo)"
)Como ler este gráfico
Onde olhar. Uma curva colorida para cada rótulo (
positivo,negativo,neutro). A linha tracejada vertical marca o zero.O que este exemplo mostra. A curva verde fica toda à direita do zero, a laranja toda à esquerda, e a cinza é um pico fino em cima do zero.
Em palavras simples. Essa separação não é um achado: as cores foram definidas pelo próprio sinal do escore, então a curva “positivo” só pode ficar à direita do zero, e a “neutro” só pode ficar no zero. Aconteceria com qualquer conjunto de textos. Este gráfico não mostra polarização; ele só mostra como cada grupo de rótulo se espalha.
Cuidado. Polarização, no sentido de DiMaggio, Evans & Bryson (1996), é uma propriedade da distribuição inteira (o quanto os escores se espalham e se formam dois montes). Avalie olhando todos os escores juntos, como no histograma abaixo, ou colorindo por uma variável que não seja o próprio escore (a posição declarada, por exemplo). E com 14 documentos a estimativa de densidade é instável (Silverman, 1986).
ggplot2::ggplot(sent, ggplot2::aes(x = score)) +
ggplot2::geom_histogram(binwidth = 1, fill = "grey60", color = "white") +
ggplot2::geom_vline(xintercept = 0, linetype = "dashed", color = "grey40") +
ggplot2::labs(
title = "Distribuição de todos os escores",
subtitle = "Uma única distribuição: é aqui que se avalia polarização",
x = "Score", y = "Documentos"
) +
ggplot2::theme_minimal()Como ler este gráfico
Onde olhar. Cada barra é um valor de escore (−3, −2, …, +2), e a altura é quantos discursos tiveram aquele escore. A linha tracejada marca o zero. Procure dois montes separados (sinal de polarização) ou um monte só (sinal de consenso ou de pouca opinião).
O que este exemplo mostra. A barra mais alta é a do zero (4 discursos); −1 e +1 vêm logo depois (3 cada); as pontas são curtas (um discurso em −3, um em −2, dois em +2). É um monte só, centrado perto do zero.
Em palavras simples. Pelos escores do léxico, o corpus parece pouco polarizado, embora os autores tenham declarado posições bem opostas (favor e contra).
Cuidado. O histograma descreve os escores, não as posições reais. Parte dos discursos opinativos caiu no zero por falta de cobertura do léxico (seção 2), o que puxa a distribuição para o centro. Quando o histograma contradiz o que você sabe do corpus, a explicação costuma estar no instrumento, não nos textos.
Se seus documentos têm ordem temporal (discursos ao longo do ano), a
série mostra tendência. Atenção:
ac_plot_sentiment(type = "line") desenha o eixo X na ordem
dos levels de doc_id; se doc_id for
só texto (character), o R usa ordem alfabética de string
por padrão, não a ordem de criação nem cronológica. Com rótulos como
doc_1…doc_14, isso significa que
doc_10 apareceria logo depois de doc_1 e antes
de doc_2. Para evitar isso, transforme doc_id
num factor com os levels na ordem desejada antes
de plotar:
sent_ordenado <- sent
sent_ordenado$doc_id <- factor(sent_ordenado$doc_id, levels = paste0("doc_", 1:14))
ac_plot_sentiment(sent_ordenado, type = "line") +
ggplot2::labs(
title = "Sentimento ao longo dos documentos (ordem de entrada)",
subtitle = "doc_id reordenado explicitamente como factor (ver texto)"
)Como ler este gráfico
Onde olhar. O eixo horizontal é a ordem dos documentos (
doc_1adoc_14); cada ponto é o escore de um documento, colorido pelo rótulo; a linha tracejada é o zero.O que este exemplo mostra. Os 5 primeiros pontos ficam acima do zero ou nele, os 5 seguintes (
doc_6adoc_10) descem até −3, e os 4 últimos ficam perto do zero.Em palavras simples. A “série” sobe, cai e se estabiliza, mas isso não é tendência no tempo: os documentos foram criados em três blocos, primeiro os favoráveis, depois os contrários, depois os técnicos. O desenho só reproduz a ordem em que o exemplo foi montado.
Cuidado. Uma linha só mostra evolução se o eixo for de fato tempo. Numa análise real, ordene pela data de publicação (ou use
ac_temporal(), que agrega por período) e desconfie de oscilações ponto a ponto em amostras pequenas: costumam ser ruído.
A análise mais útil raramente é doc-a-doc: é comparar grupos. Vamos agregar por partido:
por_partido <- sent |>
dplyr::left_join(df |> dplyr::mutate(doc_id = paste0("doc_", dplyr::row_number())),
by = "doc_id") |>
dplyr::group_by(partido) |>
dplyr::summarise(
n_docs = dplyr::n(),
score_med = round(mean(score), 2),
score_dp = round(stats::sd(score, na.rm = TRUE), 2),
n_positivo = sum(sentiment == "positivo"),
n_negativo = sum(sentiment == "negativo"),
n_neutro = sum(sentiment == "neutro"),
.groups = "drop"
) |>
dplyr::arrange(dplyr::desc(score_med))
por_partido
#> # A tibble: 8 × 7
#> partido n_docs score_med score_dp n_positivo n_negativo n_neutro
#> <chr> <int> <dbl> <dbl> <int> <int> <int>
#> 1 PSD 1 1 NA 1 0 0
#> 2 PL 2 0.5 0.71 1 0 1
#> 3 PSDB 2 0.5 2.12 1 1 0
#> 4 MDB 3 0.33 1.53 1 1 1
#> 5 PDT 1 0 NA 0 0 1
#> 6 PCdoB 1 -1 NA 0 1 0
#> 7 PSOL 2 -1 1.41 0 1 1
#> 8 PT 2 -1 2.83 1 1 0Visualização (ranqueando partidos pelo score médio):
if (requireNamespace("ggplot2", quietly = TRUE)) {
ggplot2::ggplot(
por_partido,
ggplot2::aes(x = stats::reorder(partido, score_med), y = score_med,
fill = score_med > 0)
) +
ggplot2::geom_col(alpha = 0.85, show.legend = FALSE) +
ggplot2::geom_hline(yintercept = 0, linetype = 2, color = "grey40") +
ggplot2::coord_flip() +
ggplot2::scale_fill_manual(values = c("TRUE" = "#166534", "FALSE" = "#991B1B")) +
ggplot2::labs(
title = "Sentimento médio por partido",
subtitle = "Score médio agregando pronunciamentos por sigla partidária",
x = NULL,
y = "Score médio (positivo = verde, negativo = vermelho)",
caption = "acR"
) +
ggplot2::theme_minimal(base_size = 12) +
ggplot2::theme(
plot.title = ggplot2::element_text(face = "bold"),
plot.subtitle = ggplot2::element_text(color = "grey40", size = 10),
panel.grid.major.y = ggplot2::element_blank()
)
}Como ler esta tabela
Onde olhar. Uma linha por partido. Leia
score_med(média dos escores) sempre junto comn_docs(quantos discursos entraram na média) escore_dp(desvio-padrão: o quanto os discursos do partido discordam entre si).O que este exemplo mostra. PSD tem a maior média (1,00), mas com 1 discurso só. O PT aparece com média −1,00, perto do PSOL e do PCdoB, mas o desvio-padrão dele é o maior da tabela (2,83): seus 2 discursos são um favorável (+1) e um contrário (−3), e a média de posições opostas parece uma posição negativa que nenhum dos dois discursos tem.
Em palavras simples. A média resume bem um partido quando os discursos dele são parecidos; quando são opostos, ela inventa um meio-termo. O desvio-padrão alto é o aviso.
Cuidado. Com 1 a 3 discursos por partido, nenhuma dessas médias sustenta uma afirmação sobre “a posição do partido”. Reporte
n_docsescore_dpao lado da média, e quando o desvio-padrão for alto, olhe os discursos individuais antes de concluir.
ac_plot_xray() mostra onde termos-alvo
aparecem dentro de cada documento. Útil para ver se palavras negativas
se concentram no começo, meio ou fim, sinal de estrutura retórica.
ac_plot_xray(corpus, terms = c("reforma", "trabalhadores", "proposta")) +
ggplot2::labs(
title = "Onde os termos-chave aparecem no corpus",
subtitle = "Cada ponto marca uma ocorrência do termo (posição relativa no texto)"
)Como ler este gráfico
Onde olhar. Cada linha é um documento; cada marca é uma ocorrência de um dos termos buscados, colorida pelo termo. O eixo horizontal é a posição relativa dentro do texto: 0 = primeira palavra, 1 = última.
O que este exemplo mostra. reforma aparece no começo de vários discursos (posição 0 em
doc_4edoc_10, 0,11 emdoc_6); proposta também tende ao início (de 0,10 a 0,40); trabalhadores aparece no meio dedoc_5(0,50) e na última palavra dedoc_6(1,00).Em palavras simples. Nos discursos contrários e favoráveis, o objeto do debate (“a reforma”, “a proposta”) é nomeado logo no início da frase, e as consequências (“prejudica os trabalhadores”) vêm no fim.
Cuidado. O X-ray foi pensado para documentos longos, onde início, meio e fim correspondem a partes do discurso (introdução, argumento, conclusão). Aqui cada documento é uma frase só, então a posição reflete a ordem das palavras na frase, não a estrutura retórica. Use o gráfico para ver onde um termo aparece; a interpretação de por que aparece ali pede leitura dos trechos (
ac_kwic()).
# Tabela por partido pronta para colar no LaTeX
ac_export(por_partido, arquivo = "sentimento_por_partido.tex", formato = "latex")
# CSV para Excel/planilha
ac_export(sent, arquivo = "sentimento.csv", formato = "csv")
# Se quiser passar para a etapa qualitativa (LLM):
# amostrar documentos ambíguos (perto de score = 0) para revisão humana
casos_limitrofes <- sent |> dplyr::filter(abs(score) <= 1)