--- title: "Analise de sentimento" output: rmarkdown::html_vignette: toc: true toc_depth: 2 vignette: > %\VignetteIndexEntry{Analise de sentimento} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( echo = TRUE, eval = TRUE, collapse = TRUE, comment = "#>", fig.width = 8.5, fig.height = 5.5, fig.align = "center", out.width = "100%", dpi = 140 ) library(acR) ``` ## O que é análise de sentimento com léxico 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. ### Quando usar léxico vs. LLM? | 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. ```{r pkg} library(acR) library(dplyr) ``` ## 1. Corpus: pronunciamentos sobre uma reforma polêmica 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()`. ```{r corpus} 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 ``` ## 2. Sentimento por documento `ac_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`. ```{r sentiment} sent <- ac_sentiment(corpus) sent ``` Cruzando com a posição declarada — o léxico "acerta"? ```{r validacao} 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) ``` Léxico funciona bem para **posições explícitas** (favor/contra) com vocabulário emotivo. Falha em textos técnicos-neutros, que tipicamente misturam palavras positivas e negativas em proporção baixa. ## 3. Comparar métodos de agregação 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. ```{r metodos} 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) ``` ## 4. Visualização: barras por documento ```{r plot-bar, fig.height=6} ac_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\")" ) ``` Padrão claro: os 5 primeiros documentos (favoráveis) verdes/positivos, os 5 seguintes (contrários) vermelhos/negativos, os 4 últimos (neutros/técnicos) cinza. ## 5. Visualização: distribuição (densidade) Útil para ver **a forma da distribuição** — sentimento é bimodal (dois picos, favor e contra) ou tem uma cauda longa? ```{r plot-density, fig.height=5} ac_plot_sentiment(sent, type = "density") + ggplot2::labs( title = "Distribuição de scores de sentimento", subtitle = "Bimodalidade sugere polarização; unimodal sugere consenso" ) ``` ## 6. Visualização: série ordenada Se seus documentos têm ordem temporal (discursos ao longo do ano), a série mostra tendência. ```{r plot-line, fig.height=5} ac_plot_sentiment(sent, type = "line") + ggplot2::labs( title = "Sentimento ao longo dos documentos (ordem de entrada)", subtitle = "Substitua a ordem por data em corpora reais" ) ``` ## 7. Agregação por grupo (partido) A análise mais útil raramente é doc-a-doc: é comparar **grupos**. Vamos agregar por partido: ```{r 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 ``` Visualização (ranqueando partidos pelo score médio): ```{r plot-partidos, fig.height=6} 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() ) } ``` ## 8. Termos-chave dentro do texto (X-ray) `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. ```{r plot-xray, fig.height=5} ac_plot_xray(corpus, terms = c("reforma", "trabalhadores", "aprovar", "vergonha")) + ggplot2::labs( title = "Onde os termos-chave aparecem no corpus", subtitle = "Cada ponto marca uma ocorrência do termo (posição relativa no texto)" ) ``` ## 9. Exportar para artigo/relatório ```{r exportar, eval=FALSE} # 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) ``` ## Referências * Souza, M.; Vieira, R. (2012). Sentiment Analysis on Twitter with Portuguese Language. *4th Workshop on Computational Approaches to Subjectivity, Sentiment and Social Media Analysis*, PUCRS. * Pang, B.; Lee, L. (2008). Opinion Mining and Sentiment Analysis. *Foundations and Trends in Information Retrieval*, 2(1-2), 1-135. * Silge, J.; Robinson, D. (2017). *Text Mining with R*. O'Reilly. * Sampaio, R. C.; Lycarião, D. (2021). *Análise de conteúdo categorial: manual de aplicação*. ENAP.